Skip to content

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 method f on some object obj of class C, f may return some value, but it may also alter the internal state of obj, the data it stores. This alteration of the state of obj is a side effect of f. The behaviour of f, including f's return value may also depend on the state of obj before f is called. Again, this dependence of f on obj's state is a side effect, as it can only be implemented by reading some of the memory where obj is stored, and thus relies on information not explicitly provided as an argument to f.

    To model this in functional terms, we define a type s that reflects the entire state of obj. For example, if obj stores an integer and a string, then s would be (Int, String) (or any other data type storing an Int and a String). Our Haskell version of f takes all the same arguments as our object-oriented version of f, but it also takes a value of type s that represents the current state of obj. The return value of f becomes a pair consisting of the return value that the object-oriented version of f would have returned, plus the updated state of obj. The dependence of f on obj's state is now explicitly reflected in the arguments passed to f, and the modifications to obj's state made by f are reflected in f'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.


  1. 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 the IO 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 type RealWorld, which represents but does not store the state of the entire universe. The IO monad is then a state monad that threads a value of type RealWorld through the computation. When working with the IO monad, this implementation detail is unimportant. The practical consequence of the IO monad modelling the state of the real world is that we have no way of converting a computation in the IO 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.