Monad is interesting in retrospect. After you "get" X, Y, and Z you come to realize that they're all monads and then maybe you get some mileage out of the mental compression.
What's often happening in PL theory, though, is that people start off with axioms/syntax and then mutilate the actual problem until it fits the syntax (still badly). That's how you get ridiculous stuff like "everything is an object" or "everything is a file". In PL theory, people will usually first make up some syntax/theory and then search for models. If physicists worked the same way, they'd write down random formulae and would then go out to find phenomena described by them. You'd probably read this comment on a cave wall.
You don't get everything is an X because of axiomatic formulation. You get it for the same sort of reason that instead of spending ages describing the configurations of atoms in front of you, you throw all that out and call it a screen. Is everything still atoms? Yes! But we can call it a screen and that makes things massively easier to reason about and so we do it, because we like to reason well. But there are still benefits to thinking the other way, thinking from the very foundations. They are just different benefits, which is why we choose to think of problems from more than one perspective.
For example, the notion of elementary topos has been invented because its creators wanted to capture the way Grothendieck toposes kind of behave like constructive sets. This I find very useful, and also the axiomatizations of elementary toposes. On the other hand, Martin-Löf type theory didn't have a formal semantics at first, then an erroneous one, and finally ~20 years later a kind of acceptable one. And its category of models is... not really interesting. Except for categories of assemblies, I don't know of a single model of ML type theory that's not also an elementary topos. And the interesting thing about assemblies is that they can be turned into objects of the associated topos... so yeah.
So, String Theory?
The IO monad is noncommutative, and thus controls sequencing of operations, because of data dependencies, more or less hidden.
A "Haskell wizard" tried to tell me the ordering of operations was implemented through low-level magic a few weeks ago. The fact is that whatever ultimate form GHC might compile your code to, the sequencing is expressible in the semantics of Haskell code. Getting sequencing into the semantics is one of the problems which monads solve.
Without that magic the compiler may inline everything, drop the 'uselss' state token and happily move your buffer freeing before the allocation - or drop it entirely.
Alternatively you could mark every function as noinline and end up with fantastically slow code.
fma a b c = (a * b) + c
The sum depends on the product but this doesn't sequence anything: it may be compiled into a single fma machine instruction. More generally inlining, CSE, etc. can break apparent data dependencies.It's simpler and cleaner to merely state that the Haskell runtime is written to execute IO actions in sequence.
fma a b c = (a * b) + c
forces evaluation order: you need the result of `a * b` in `fma` in order to add `c` to it. One can argue whether both of these, or neither of these, have the evaluation order forced, but I don't think that it is reasonable to argue that one does and the other doesn't. (There is a slight artefact from the fact that addition is commutative, so that it makes no difference whether you compute `c` first and then add `a * b` to it or vice versa, but I think the idea should remain.)These details of evaluation order are important to know, and they are not something to be argued about: they are well-specified and the compiler doesn't just casually make lazy code into strict code or vice versa. That would cause chaos. And what instruction set Haskell is being compiled to is totally irrelevant to this level of the language's semantics.
Sequencing using the IO monad predictably involves strictness. There is nothing in the type of >>= that forces strictness. That is why, for example, you can usefully bind infinite lists in the list monad. So in e.g. [1..] >>= return . (+ 1), the list is not evaluated before the call to (+).
In fact, binding is not strict in the IO monad either (that's why lazy IO works). It's only the special magic world parameter that is, in effect, evaluated strictly.
The idea of using a data dependency to force a certain order of evaluation predates monads and it predates laziness. So it is implemented accordingly, and any hoops that need to be jumped through in the implementation to make it work aren't magic—they are just necessary to express a long-established pattern.
What monads offer is a way of discreetly passing the state from one operation to the next (the State monad is the model for the IO monad). It removes the need for the programmer to think about that aspect of the plumbing, without creating magic elements in the language.
The hoops are magic in the sense that they can't be expressed in Haskell 98 or Haskell 2010. A Haskell compiler is perfectly entitled to optimize out any dummy values which aren't used. That includes dummy world variables.
More generally, it simply isn't possible to use data dependencies -- even real data dependencies -- to control the order of side effects. The Haskell standards just don't provide the guarantees necessary to make that workable, for the reasons that millstone gave.
Yes, you are certainly right. I misread and thought incorrectly that the discussion was about whether certain constructs forced data dependencies in any theoretical language, not in the particular language Haskell.
> These details of evaluation order are important to know, and they are not something to be argued about: they are well-specified ….
While I take your meaning, I think that you don't mean literally what you said: they got to be well specified precisely by being argued about, and will hopefully get to be even better specified by continuing argument. They are certainly not for me to argue about, though.
do { x <- return [1..]; return 7 }
That evaluates to 7, and the infinite list is never evaluated. So the 'data dependency' that monadic bind gives you doesn't, in general, force evaluation order. It's only because the implementation details of the IO monad ensure that the dummy world parameter is always 'evaluated' that you get guarantees about evaluation order. Note that the dummy world parameter is just that -- a dummy -- and no computation actually depends on its value. It's pure compiler magic.
Well, yes, that's the point. You don't need to understand anything about monads to understand how the implementation of >>= for IO forces the correct order of side effects.
But I do agree with the general idea that Haskell as a language is still a leaky abstraction, you still need to understand a lot of behind-the-scenes stuff, like how the RTS interacts with the OS ABI, how concurrency is implemented, the consequences of laziness and so on. Just knowing the denotational semantics of the language is not enough.
Well, that's in theory at least. In GHC Haskell the ordering is not denoted by neither data dependency ot the operator semantics, but by a complex set of rules that change from one version of the compiler to the other.
1 - Although it is sufficient for any monad that lacks real-world effects.
2 - Well, technically nothing is enough for really ordering IO operations, but those two properties give single-threaded semantics to IO.
No, there is nothing in the type of >>= which guarantees sequencing of side effects. So for example:
a >>= (\b -> c)
There is nothing in the type of >>= that guarantees that a will be completely evaluated before c is evaluated.
I suspect this misconception is what makes many people think that Haskell's way of handling IO has something crucially to do with monads.
Saying that lists are an instance of Monad in Haskell says much more about novel ways you can use a list in Haskell than about properties of a monad.