7 ms·
The reason that the Monad structure is interesting, in my view, is that it's simple and neatly captures the notion of a "computation" that we can compose in dif
by tikhonj 11y ago
The reason that the Monad structure is interesting, in my view, is that it's simple and neatly captures the notion of a "computation" that we can compose in different ways.
The core useful operator for monads in Haskell is >>=. It has the following type:
m a -> (a -> m b) -> m b
If you squint, this is like function application with a few extra m's thrown in. Here's normal function application for comparison:
a -> (a -> b) -> b
So what is this extra m useful for? It's like a hole where we get to plug in some custom logic. In a sense, it lets us change what it means to "apply" a "function". This turns out to be useful for a whole bunch of things, not just IO. (Honestly, from a pedagogical standpoint, I think IO is a bit of a distraction!)
For example, take the Maybe type. It's Haskell's nullable: a Maybe a means you either have an a or Nothing.
data Maybe a = Just a | Nothing
Remembering the signature of >>= above, what's the most natural way to implement "application"? If we break the function into cases, it becomes pretty straightforward. Here's the specialized function signature we want to implement:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
x >>= f = ...
If x is Nothing then we don't have anything to pass into f so the whole result has to be Nothing. If x has a value, we can just get that value out and pass it into f normally, returning the final result.
Nothing >>= f = Nothing
Just x >>= f = f x
(In case you're not familiar with Haskell syntax, the above is actually a valid definition of (>>=) for Maybe!)
For Maybe, being a monad gives us a standard way of working with values while automatically dealing with Nothing. It abstracts over repetitive null checking and lets us easily build up Maybe values based on other Maybe values.
Other examples of monads are the same in spirit. The list monad, for example, lets us handle any number of inputs in a way that's similar to Maybe. The State monad similarly lets us combine values while carrying along an implicit state internally.
The rest of the Monad structure (namely the return function and the laws) are just a formal way of codifying behavior behavior that's already intuitive.
So how does this all apply to doing input and output?
Well, the problem in Haskell is that it's a language of evaluating expressions at heart: executing effects makes no sense any more than it would in arithmetic. To work with effects we instead have a special, opaque type IO; normal expressions get evaluated to IO actions that can be run to produce the desired effect—namely the IO type.
Critically, the IO type does not have to be a monad. It could be completely self-contained and have custom functions for doing one action after the other. We could imagine something like:
after :: IO a -> IO b -> IO b
which would let you run an IO statement then run a second one and only return the value of the last one. It's like an imperative block of code!
However, we would also like some way of using the results of an IO statement, perhaps assigning them to a name. We can't do this normally because the IO statements are run separately from expression evaluation. We'd have to have some sort of function that could take an IO value, unwrap it and do something with it. And how would we express an interface like this? With a normal function!
doSomething :: IO a -> (a -> IO b) -> IO b
Hey, doesn't that look familiar? It's exactly (>>=)!
I'm hand-waving a bit again, but the rest of the monad structure comes up when you try to make sure after behaves consistently and intuitively.
So IO being a monad emerges naturally from the desire to be able to compose actions and depend on their results in a way that's separate from normal variable bindings and expression evaluation.
The causation here is important: it's not that IO is a monad, but rather the IO type (which could exist on its own) happens to naturally and usefully form a monad. But it does a lot of other things too, including some specific capabilities (like spawning threads) that are hard to generalize.
So my point, I suppose, is twofold: monads are useful for combining some notion of computation and IO happens to be an interesting example, but the fact that we wrap statements with external effects in a custom type called IO does not inextricably depend on the idea of a monad.
Did that explanation help? I wrote a blog post on a similar topic that might be interesting too: http://jelv.is/blog/Haskell-Monads-and-Purity http://jelv.is/blog/Haskell-Monads-and-Purity
- godDLL 11y agoWhy doesn't the English language have a Super Thank You in it, some way of saying "Holy Shit Was That Uniquely And Exquisitely Useful" ?! My man tikhonj... thanks.
- Guthur 11y agoI appreciate your explanation but again it fails to actually show it being useful outside of the context of working around Haskell's strict type system. What the OP and myself would like to see is concrete examples why this is a better approach than the way something would be done without explicitly caring about monads (I say explicitly because I know there is a tendency to say that some given structure is a monad and we didn't realise it; but that still doesn't really prove that it is all that useful in the general sense) One problem I suppose is that not everyone speaks the abstract language of monads and so it does not serve much usefulness, yet, and maybe there is a little chicken and the egg with that.
- endgame 11y agoI'd say STM (software transactional memory) is a compelling example. Your computations that mess with shared variables can only mess with shared variables, the type system enforces this, and the only way you can actually run your shared-memory-changing transaction is through a function `atomically :: STM a -> IO a` (this gives a nice effect: `atomically $ do ...`)
- tel 11y agoYou know how ES6 has this big hoorah coming because they're finally getting something to make callback hell a little less hellish, async/await? Well. It's a monad. It's about 30s of work to implement it if you know what you're doing. Of course, you could implement it as a monad in Javascript as well. It'd be a neat trick, but you'd never love it. The reason is simple: you can't benefit from monads unless you use them so pervasively that everything will integrate together. And if you do, that integrate together bit works fantastically. People often complain about monads not composing---but I think they're often just misinterpreting a rather technical result. Actually, imo, monads compose incredibly well and it's astounding when you get used to it. When you use them pervasively, monads mean that you get to "pick your own semantics" on the fly, whenever you want. You can mix and match semantics as is interesting and work with your custom mixes as if they were built into the language. So people, e.g., talk about how it was easy to write STM because of monads. It wasn't "because of monads". Of course STM is a monad and anything which looks even halfway like it in any language will also be a monad no matter how hard you try to avoid it. They're "just a pattern". But when you've got a language which allows you to cut into the "STM monad" exactly and whenever you want, when you've got a hold of the root of the semantics of your language, when you've got programmable semicolons, then there's something really special. And without that you've got a couple of years of bickering between standards committees to fix what end up being trivial looking problem.s