That sounds nice in theory, but real programs have multiple channels of IO going on: std IO, logging, network, database, file system. I follow discussions in Haskel groups sometimes, and combining and untangling multiple monads is a persistent problem that doesn't have a good solution yet.
It's debatable how much one is willing to describe historical solutions like MTL style to be a "good" solution to this problem (I explained the downsides in my talk "A History of Effect Systems"[1] at Zurihac 2025), but I certainly have no hesitation in describing my capability-based effect system Bluefin[2], as a good solution.
For the channels you describe above the type signature would look like this
Six different capabilities that give you access to six different channels of IO. A "Yield" to which you yield strings to stdout, an "Await" from which you can await strings from stdin, a "Yield" to which you yield log messages, and three abstract effects that allow you to do network, database and file system operations (abstract because I'm not sure exactly how you want them defined, but there's an example of the implementation of a file system effect at https://hackage-content.haskell.org/package/bluefin-0.6.0.0/...).
No problem with combining or untangling monads whatsoever. This is the natural conclusion of galaxyLogic's point: instead of dividing your program into two parts (IO and not IO) you divide it into six parts (or maybe 2^6 parts?) according to which fine grained effects you have in scope.
If you (or anyone) has any questions about Bluefin I'm happy to help. Please feel free to ask here or on the issue tracker[3].
Interesting. Those channels seem to all manage side-effects that persist after program execution. What about state-changes that are made and persist into the memory of the running program? Or is there any need for that?
Yes, there's a need for it, and Bluefin provides Modify which allows you to modify a reference to a value in a way that is not observable outside the scope of the capability.
You should try actually doing some Haskell programming. There are some anti-patterns but the RIO pattern is very coherent and flexible. One IO base monad, one readerT that holds an IORef that gives you access to every other effect you need.
What? I have to assume you mean combining and untangling multiple effects in an effect system. And the solution is simple: don't. IO is the only monad you use for all IO: logging, networking, databases, and filesystems included. Adding a layer of ReaderT on top is about the most complicated you do. Every other monad is pure. Effect systems are a great way to build abstractions that aren't usually worthwhile. It is the same kind of mentality that leads to AbstractWumbleProviderFactoryBean-oriented programming in Java.
That is not the only reason to have an IO monad. PureScript and Idris are both strict languages that still reify IO effects. Its still useful to know which functions are pure.
I'm actually not a fan of Idris' implementation of the IO monad, it really should have been a free monad with a cofree comonad so you can enforce invariants against the world without a bunch of extra machinery. It seems kind of important for a dependent type system.
I quite like Mercury's non-monadic linear IO + determinism semantics. You can subtype the world with insts and unify against the world-state pretty easily.
I'm not a Haskeller but I would like to use a language that indeed separates all code into two areas, one in which I can write non-pure functions and one in which I can not.
That would obviously make it easier to analyze and test and verify the program by first focusing on the pure part of it, then on the impure part. I'm not sure if that is possible in "strict languages" but perhaps.