That said, I still think passing the world in and out of every function is too onerous for most real world programs, and functional programming definitely takes a too-extreme stance to be pleasant.
I think a better approach is a traditional language but with a functional subsystem where you can mark functions as pure.
This is a bit of a strawman. At least in my limited experience, functional programmers think so too. Part of what monads let you do is a kind of inversion of control: instead of changing the world directly (as you would if you were seriously threading the world through your domain logic), you return a description of what you're doing, and let the runtime (in the case of IO) or the interpreter for the monad (in other cases) actually perform the changes.
Similarly, lenses allow you to describe local mutations and lift them to the level of a whole structure.
Explicitly threading your state is the first refactoring, not the final destination. If you have a code smell, the first thing to do is draw it out into the open so you can wrangle it. In languages that don't conveniently support functional strategies like monads or lenses, it's tempting to stop there. But that doesn't mean FP is about stopping there.
It's rigorously correct, but I feel like it's a step too far for most programs.
Yes, but no. Again, there are functional patterns you can apply.
First, functional programs tend to be more horizontally composed than vertically. Side effects usually happen at the same "level" of the application, rather than occuring at multiple depths. So the need to thread logging apparati is already reduced, simply because logging only happens higher up in the application.
Second, because side-effects only really occur at this thin layer, you can capture logging as a monadic action, avoiding the need to explicitly thread the logger state around. (This is the same situation as the "thread the world" problem we discussed earlier.)
Third, because so much more of your program is pure, it is much easier to perform unit tests and check assertions in other ways. At that level, logging is a bandaid over poor testing and assurance, not a goal in itself.
Finally, if you do find yourself deep down-stack with a need to perform some kind of side-effect, you can apply logical methods (a la LVars, CRDTs) to give a stateful interaction between separated parts of your program. The Haxl paper [0] is a good example of this: most of the design is pure functional, but for the part that involve an interaction not easily captured by the fundamental call-return / request-response pattern, they utilize monotonic state to maintain a registry of fulfilled and unfulfilled requests.
[0] https://simonmar.github.io/bib/papers/haxl-icfp14.pdf
> It's rigorously correct, but I feel like it's a step too far for most programs.
I feel you're being too dismissive. There is an entire community of developers -- many of whom operate in industry -- who can and do effectively solve the problems you are raising.
"Rigorously correct" is one of those things non-FP folks think FP is about. You can write buggy code in any paradigm. What people like about FP is that it's easier to reason about, and it's generally easier to find precise abstractions that support that kind of reasoning. "Rigorously correct" is missing the point.