The use of monads can be thought of as a design pattern that allows us to redefine how function composition works. Before we define what a monad is, let’s develop our intuition with some motivational examples.
Example 1: exception handling (a.k.a. “Maybe”)
To get our feet wet, let’s use a purely functional subset of Python to describe a function which computes the quotient of 100 and some divisor. In the special case where the divisor is zero, we’ll return None:
Example 2: vector operations (a.k.a. “List”)
We know that, mathematically, positive numbers have two square roots. Let’s modify sqrt to return a list of values:
It works! But our original goal was to find the square roots of one number. We could always write bind([x], sqrt) where x is a number, maybe it would be better to use a function to abstract the representation of our input. Let’s call this function unit:
Cool, we can now intelligently compose functions that might return several values! That’s basically Haskell’s List monad.
Example 3: debug output (a.k.a. “Writer”)
Suppose we have some functions u and v, and each function takes a number and returns a number. It doesn’t really matter what these functions are, so let’s define them as follows:
Awesome! Not only do we get a numerical result, but we also get a complete stack trace. Notice how we passed (4, ''), when all we cared about was the 4. The empty string means that no functions had been called at that point, but that’s an implementation detail. It would be better to write a unit function to construct this tuple for us, so we don’t have to worry about what goes in the string, the ordering of the elements of the tuple, etc.
Still works! We’ve just implemented Haskell’s Writer monad!
In each example, we had some functions which we wanted to compose, but their type signatures were incompatible. Our functions took a value of some type and returned a value in the corresponding monadic type. All we did was write a wrapper function (bind(x, f)), which defines the logic for how such functions can be chained together. We also defined unit(x) (which is often called return because of the way it is used) to convert a value into a monadic type, so it can be used with our bound functions.
A monad is a type constructor, a bind operation, and a unit function. In Example 1, the monadic type was the union of float and NoneType (we didn’t need a unit function because the monadic type was a superset of the input type). In Example 2, the monadic type was list (of floats), and unit simply put values into a list. Finally, the monadic type for Example 3 was the product (i.e., tuple) of int and str, so unit constructed that tuple from a value and the empty string.So the use of monads is basically a design pattern, and it’s been used to implement several concepts that are normally associated with imperative programming, such as exception handling (much like in Example 2), parsers, state, collections, and I/O (as we will see shortly).
Proper monads must obey three axioms so they behave the way we expect.
(Here, equality should be taken to mean the two expressions represent the same computation.)
The first two axioms mean that unit and bind roughly do inverse things: unit puts a value into a monadic container for binding and bind takes it out to apply a function. The last axiom means we can compose functions together without worrying about how composition precedence works. I’ll leave it as an exercise for the reader to prove that the versions of unit and bind that we wrote obey the three monad laws.
But what about side effects?
It’s probably not obvious how we can use monads to do side effects in a pure way. But by now you understand what a monad is and how to use it to do some useful things, like exception handling, error propagation, and stack traces. We’ll talk about I/O in the next post, but keep in mind that I/O is just one of many uses of monads.
* Monads, part 2: impure computations