Pure Representations of Effectful Computations: Monads
So far we have focused on functors as containers. Lists, Maybe
, binary trees,
etc. are all functors. There is a different view of some functors as
decorators: They decorate plain values with additional information. The
functor where this decoration is easiest to see is the Writer w
functor, which
we will meet in this chapter. It's defined as
newtype Writer w a = Writer { runWriter :: (a, w) }
It's a thin wrapper around a pair storing values of types a
and w
. This is
clearly a functor in the sense that it stores an a
, but it is also a decorator
because it stores additional information of type w
. To see how lists, Maybe
,
binary trees, Reader
, and other functors decorate the type a
of the values
they store, we may have to squint a bit harder, but the decoration is there in
each case, as we will dicuss.
Our discussion of monads in this chapter will mostly focus on functors as such
decorators but, especially for Maybe
and lists, will switch between the
decorator and container view, whichever is more helpful for the discussion. In a
nutshell, a monad is a functor—a decorator—equipped with two operations,
return
and (>>=)
. By choosing the right kind of decoration, that is, the
right kind of functor and defining these two operations appropriately, we can
model computations with side effects, which functional programmer call
effectful computations.
Remember, imperative programming is programming by side effects: Modifying a memory location is a side effect that is not reflected in the return type of a function that performs this modification. So are printing text on screen, rendering some graphics, reading a file, connecting to a database over a network, etc, etc, etc.
Such effects are everywhere. Indeed, given that reading input is a side effect and writing output, on screen or to disk, is a side effect, a program completely free of side effects would be utterly useless. It consists of a whole lot of functions that would be able to do truly amazing stuff if only it could read some input to which to apply these functions, but it can't.
This puts functional programmers into a bit of a pickle. How do we express such effectful computations in purely functional terms? Three schools of thought emerged:
-
Early functional languages, such as LISP and Scheme, weren't really functional languages. They were multi-paradigm languages that merely encouraged you (quite strongly) to program functionally, but you had the freedom to program imperatively in them if you had to to do things like reading input and writing output. OCaml is a modern language that also follows this approach.
-
Clean is a language quite similar to Haskell in both semantics and syntax that uses the notion of linear types to ensure that pure functions interact cleanly with functions that have side effects. There aren't many languages that use this approach, so I won't talk about it further here.
-
Finally, Haskell and its descendants use monads to capture effectful computations in a purely functional fashion. This sounds like a contradiction. Pure functions shouldn't have effects. The idea is simple though: Whatever the effect of the function is will be added to the return value of the function, so the effect is no longer actually an effect but simply affects the return value of the function. The function becomes pure again. Adding this information to the return value is a decoration, which we provide using a functor. Let me explain:
Assume we have some class
C
in an object-oriented language. When calling some methodf
on some objectobj
of classC
,f
may return some value, but it may also alter the internal state ofobj
, the data it stores. This alteration of the state ofobj
is a side effect off
. The behaviour off
, includingf
's return value may also depend on the state ofobj
beforef
is called. Again, this dependence off
onobj
's state is a side effect, as it can only be implemented by reading some of the memory whereobj
is stored, and thus relies on information not explicitly provided as an argument tof
.To model this in functional terms, we define a type
s
that reflects the entire state ofobj
. For example, ifobj
stores an integer and a string, thens
would be(Int, String)
(or any other data type storing anInt
and aString
). Our Haskell version off
takes all the same arguments as our object-oriented version off
, but it also takes a value of types
that represents the current state ofobj
. The return value off
becomes a pair consisting of the return value that the object-oriented version off
would have returned, plus the updated state ofobj
. The dependence off
onobj
's state is now explicitly reflected in the arguments passed tof
, and the modifications toobj
's state made byf
are reflected inf
's return value: our function is pure again.It is perfectly feasible to model stateful computations using this approach of threading the state of the program through all function calls in the form of explicit arguments and return values, but feasible doesn't equal fun, elegant or productive. We would really like to do this threading of the program's state through our computation implicitly. That's where monads come into play.1
You may or may not have heard about monads in functional programming before. If you have, you have probably been told that they are extremely difficult to understand and that you really shouldn't program in Haskell because lots of things that you "just do" in imperative languages require the use of this difficult tool in Haskell. This is a misunderstanding that results from people trying to teach you how to use monads for things like I/O, stateful computations, non-determinism, exception handling, continuation passing style, parsing, etc, etc, etc without ever explaining what monads really are.
As stated above, a monad is simply a functor equipped with two operations
return
and (>>=)
that must satisfy that rather natural properties. It's a
really simple concept. What's amazing is that this concept can be used to
express so many different things. On the surface, stateful computations,
exception handling or non-determinism have little to do with each other. It
turns out, they simply are different ways of sequencing steps in a
computation. And that's what monads do in programming: by defining the (>>=)
operator for a monad, we can choose what it means to carry out two steps in
sequence. In imperative languages, this notion is built right into the language:
f(); g(); h();
means call f
, then g
, then h
. When writing something
similar in Haskell, do {f; g; h}
, it is possible that g
and h
are not
called at all, are called many times, or behave differently, all depending on
what f
does and how the (>>=)
operator is implemented.
You may still not be convinced that being able to define custom monads, and actually having to do so to express effectful computations, is worth the trouble. It's a little early in our discussion of monads to try to convince you that it is. I hope that by the end of this chapter, you will see that being able to define what it means to sequence steps is a powerful tool, and that having to do so when the standard sequencing behaviour of imperative languages would suffice is a useful means to communicate the intent of a piece of code to the reader of our code.
-
There is a monad whose behaviour can be thought of in purely functional terms, but which we cannot actually express using pure functions in Haskell: The
IO
monad allows us to perform input and output, to talk to the outside world. If we wanted to model this purely functionally using the trick just described, then we would have to have a type that represents the state of the entire universe, and again thread the value of such an object through the entire computation as we just discussed. Conceptually, this is exactly what theIO
monad does, but of course we cannot possibly define a data type that stores the state of the entire universe. The Haskell runtime system has a typeRealWorld
, which represents but does not store the state of the entire universe. TheIO
monad is then a state monad that threads a value of typeRealWorld
through the computation. When working with theIO
monad, this implementation detail is unimportant. The practical consequence of theIO
monad modelling the state of the real world is that we have no way of converting a computation in theIO
monad into a computation built from pure functions. This is different from other monads, which have a mechanism to do this and which are in fact merely convenient constructs to express otherwise purely functional computations more elegantly. ↩