Why do we need monads?
The answer is, of course, "We don't". As with all abstractions, it isn't necessary.
Haskell does not need a monad abstraction. It isn't necessary for performing IO in a pure language. The IO
type takes care of that just fine by itself. The existing monadic desugaring of do
blocks could be replaced with desugaring to bindIO
, returnIO
, and failIO
as defined in the GHC.Base
module. (It's not a documented module on hackage, so I'll have to point at its source for documentation.) So no, there's no need for the monad abstraction.
So if it's not needed, why does it exist? Because it was found that many patterns of computation form monadic structures. Abstraction of a structure allows for writing code that works across all instances of that structure. To put it more concisely - code reuse.
In functional languages, the most powerful tool found for code reuse has been composition of functions. The good old (.) :: (b -> c) -> (a -> b) -> (a -> c)
operator is exceedingly powerful. It makes it easy to write tiny functions and glue them together with minimal syntactic or semantic overhead.
But there are cases when the types don't work out quite right. What do you do when you have foo :: (b -> Maybe c)
and bar :: (a -> Maybe b)
? foo . bar
doesn't typecheck, because b
and Maybe b
aren't the same type.
But... it's almost right. You just want a bit of leeway. You want to be able to treat Maybe b
as if it were basically b
. It's a poor idea to just flat-out treat them as the same type, though. That's more or less the same thing as null pointers, which Tony Hoare famously called the billion-dollar mistake. So if you can't treat them as the same type, maybe you can find a way to extend the composition mechanism (.)
provides.
In that case, it's important to really examine the theory underlying (.)
. Fortunately, someone has already done this for us. It turns out that the combination of (.)
and id
form a mathematical construct known as a category. But there are other ways to form categories. A Kleisli category, for instance, allows the objects being composed to be augmented a bit. A Kleisli category for Maybe
would consist of (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
and id :: a -> Maybe a
. That is, the objects in the category augment the (->)
with a Maybe
, so (a -> b)
becomes (a -> Maybe b)
.
And suddenly, we've extended the power of composition to things that the traditional (.)
operation doesn't work on. This is a source of new abstraction power. Kleisli categories work with more types than just Maybe
. They work with every type that can assemble a proper category, obeying the category laws.
- Left identity:
id . f
=f
- Right identity:
f . id
=f
- Associativity:
f . (g . h)
=(f . g) . h
As long as you can prove that your type obeys those three laws, you can turn it into a Kleisli category. And what's the big deal about that? Well, it turns out that monads are exactly the same thing as Kleisli categories. Monad
's return
is the same as Kleisli id
. Monad
's (>>=)
isn't identical to Kleisli (.)
, but it turns out to be very easy to write each in terms of the other. And the category laws are the same as the monad laws, when you translate them across the difference between (>>=)
and (.)
.
So why go through all this bother? Why have a Monad
abstraction in the language? As I alluded to above, it enables code reuse. It even enables code reuse along two different dimensions.
The first dimension of code reuse comes directly from the presence of the abstraction. You can write code that works across all instances of the abstraction. There's the entire monad-loops package consisting of loops that work with any instance of Monad
.
The second dimension is indirect, but it follows from the existence of composition. When composition is easy, it's natural to write code in small, reusable chunks. This is the same way having the (.)
operator for functions encourages writing small, reusable functions.
So why does the abstraction exist? Because it's proven to be a tool that enables more composition in code, resulting in creating reusable code and encouraging the creation of more reusable code. Code reuse is one of the holy grails of programming. The monad abstraction exists because it moves us a little bit towards that holy grail.
Why do we need monads?
- We want to program only using functions. ("functional programming (FP)" after all).
Then, we have a first big problem. This is a program:
f(x) = 2 * x
g(x,y) = x / y
How can we say what is to be executed first? How can we form an ordered sequence of functions (i.e. a program) using no more than functions?
Solution: compose functions. If you want first
g
and thenf
, just writef(g(x,y))
. This way, "the program" is a function as well:main = f(g(x,y))
. OK, but ...More problems: some functions might fail (i.e.
g(2,0)
, divide by 0). We have no "exceptions" in FP (an exception is not a function). How do we solve it?Solution: Let's allow functions to return two kind of things: instead of having
g : Real,Real -> Real
(function from two reals into a real), let's allowg : Real,Real -> Real | Nothing
(function from two reals into (real or nothing)).But functions should (to be simpler) return only one thing.
Solution: let's create a new type of data to be returned, a "boxing type" that encloses maybe a real or be simply nothing. Hence, we can have
g : Real,Real -> Maybe Real
. OK, but ...What happens now to
f(g(x,y))
?f
is not ready to consume aMaybe Real
. And, we don't want to change every function we could connect withg
to consume aMaybe Real
.Solution: let's have a special function to "connect"/"compose"/"link" functions. That way, we can, behind the scenes, adapt the output of one function to feed the following one.
In our case:
g >>= f
(connect/composeg
tof
). We want>>=
to getg
's output, inspect it and, in case it isNothing
just don't callf
and returnNothing
; or on the contrary, extract the boxedReal
and feedf
with it. (This algorithm is just the implementation of>>=
for theMaybe
type). Also note that>>=
must be written only once per "boxing type" (different box, different adapting algorithm).Many other problems arise which can be solved using this same pattern: 1. Use a "box" to codify/store different meanings/values, and have functions like
g
that return those "boxed values". 2. Have a composer/linkerg >>= f
to help connectingg
's output tof
's input, so we don't have to change anyf
at all.Remarkable problems that can be solved using this technique are:
having a global state that every function in the sequence of functions ("the program") can share: solution
StateMonad
.We don't like "impure functions": functions that yield different output for same input. Therefore, let's mark those functions, making them to return a tagged/boxed value:
IO
monad.
Total happiness!