A Motivating Example
To explain what a monad is, let us focus on the Writer w
functor mentioned in
the introduction. Here's a simplified version of it, where we fix
w = String
:
newtype Writer a = Writer { runWriter :: (a, String) }
Remember, we can view this type as a functor—it stores a value of type a
—or as
a decorator—in addition to storing an a
, it stores additional information of
type String
. You can verify that the following instance definition satisfies
all the functor laws, so Writer
is indeed a functor:
instance Functor Writer where
fmap f (Writer (x, s)) = Writer (f x, s)
The decoration provided by Writer
is rather explicit: it's an additional value
of type String
, and a value of type Writer a
is always an a
bundled
together with a String
. In the case of other functors, the decoration is less
explicit.
A value of type Maybe a
doesn't store any additional data. The decoration
provided by the Maybe
functor is the ability of a value of type a
to be
present or not. This clearly makes Maybe a
different from a
because a value
of type a
is just that, a value of type a
. It cannot be absent. Maybe
doesn't decorate its argument type in the sense that it stores additional
information but in the sense that Maybe a
is a richer type than a
, one that
has the ability to express the absence of a value.
A value of type [a]
once again stores only a
s, possibly many of them. And
that's the decoration—the enrichment—of the type a
provided by the list
functor. It's the ability to have an arbitrary number of values of type a
; not
exactly one of them, as with the type a
; or one or none of them, as with the
type Maybe a
.
Remember, the Reader r
functor:
newtype Reader r a = Reader { runReader :: r -> a }
The decoration provided by Reader r
is the ability of a value of type a
to
depend on some context of type r
. Conceptually, a value of type Reader r a
is a collection of many values of type a
, indexed using values of type r
.
A monad is a functor equipped with two operators return
and (>>=)
. When
programming using a monad, we use the monad to decorate the return values of
functions. Let's call functions that do this decorated functions. Since
functional programming is programming by function composition, we need a
composition operator for decorated functions. This is what the (>>=)
operator of the monad provides.
Let's look at a simple example of these ideas in action, using our Writer
functor. We start with two functions that use Writer
to decorate their return
values:
double :: Int -> Writer Int
double x = Writer (2 * x, "Doubling " ++ show x ++ "\n")
add1 :: Int -> Writer Int
add1 x = Writer (x + 1, "Adding 1 to " ++ show x ++ "\n")
Each function simply doubles its argument or increases it by one, but it also decorates the result with a string that explains how the return value was produced from the function argument. You can imagine that such functions can be useful for debugging purposes or when writing systems code1 that should add log messages to the system's log files.
Now, if we had undecorated versions of double
and add1
,
pureDouble :: Int -> Int
pureDouble x = 2 * x
pureAdd1 :: Int -> Int
pureAdd1 x = x + 1
then we could define
pureDoublePlus1 :: Int -> Int
pureDoublePlus1 x = pureAdd1 (pureDouble x)
or, more succinctly,
pureDoublePlus1 :: Int -> Int
pureDoublePlus1 = pureAdd1 . pureDouble
If we wanted to build a function doublePlus1
that combines double
with
add1
in the same way, then we would probably want it to concatenate the log
messages of double
and add1
. After all, this is what it does. It first
doubles its argument and then adds one to the result. Our log should allow us to
trace these steps. We can build this function manually like this:
doublePlus1 :: Int -> Writer Int
doublePlus1 x = Writer $ let (y, msgDouble) = runWriter (double x)
(z, msgAdd1) = runWriter (add1 y)
in (z, msgDouble ++ msgAdd1)
This does exactly what we expect:
>>> newtype Writer a = Writer { runWriter :: (a, String) }
>>> double x = Writer (2 * x, "Doubling " ++ show x ++ "\n")
>>> add1 x = Writer (x + 1, "Adding 1 to " ++ show x ++ "\n")
>>> :{
| doublePlus1 x = Writer $ let (y, msgDouble) = runWriter (double x)
| (z, msgAdd1) = runWriter (add1 y)
| in (z, msgDouble ++ msgAdd1)
| :}
>>> runWriter $ doublePlus1 3
(7,"Doubling 3\nAdding 1 to 6\n")
>>> runWriter $ doublePlus1 10
(21,"Doubling 10\nAdding 1 to 20\n")
But we have lost the ability to simply pass the result of one function to the next. Neither of the following would compile:
doublePlus1 :: Int -> Writer Int
doublePlus1 x = add1 (double x)
or
doublePlus1 :: Int -> Writer Int
doublePlus1 = add1 . double
That's because the types don't match: add1
expects a value of type Int
, but
the return type of double
is Writer Int
, a completely different type. To
compose add1
and double
, we had to remove the decoration—the log
message—from the return value of double x
, call add1
on the result, and
finally combine the decorations.
We don't have any of this trouble in imperative languages, say in Python:2
log = ""
def double(x):
global log
log += f"Doubling {x}\n"
return 2 * x
def add1(x):
global log
log += f"Adding 1 to {x}\n"
return x + 1
def doublePlus1(x):
return add1(double(x))
When we run doublePlus1
, the log holds the expected log messages afterwards:
>>> import logging
>>> logging.doublePlus1(5)
11
>>> print(logging.log)
Doubling 5
Adding 1 to 10
In our Python implementation, we are able to just compose add1
and double
because the effect of producing the log is exactly this, an effect. It happens
when the function runs, but the effect is not reflected in the function's return
value. Since Haskell doesn't allow us to have such hidden effects, we need to
resort to something like our Writer
functor to decorate function return
values, and then it is up to us to combine the results and log messages
correctly, as we did in our definition of doublePlus1
. Programming that way is
a pain though.
What Haskell does give us is the ability to define our own functions and
operators. Hopefully, this will let us come up with a prettier implementation of
doublePlus1
. The key to the succinct definition of pureDoublePlus1
was the
function composition operator:
(.) :: (b -> c) -> (a -> b) -> (a -> c)
(f . g) x = f (g x)
Can we come up with an operator that allows us to compose decorated functions?
Let's call this operator (>=>)
. We want this operator to take
g :: a -> Writer b
and f :: b -> Writer c
and produce from it a composed
function g >=> f :: a -> Writer c
which removes the decoration from g
's
return value, calls f
on the undecorated value, and then combines the
decoration removed from g
's return value with f
's decoration of its return
value. This looks a lot like pure function composition using the (.)
operator,
only the return values of all functions involved are decorated using Writer
:
f :: b -> c and g :: a -> b ==> f . g :: a -> c
f :: b -> Writer c and g :: a -> Writer b ==> g >=> f :: a -> Writer c
The order of the arguments is reversed: Instead of f >=> g
, we write
g >=> f
, but that will make sense soon because we should think about something
like f >=> g >=> h >=> k
as a sequence of steps to be executed in this order.
The definition of (>=>)
practically writes itself and looks remarkably similar
to our definition of doublePlus1
:
(>=>) :: (a -> Writer b) -> (b -> Writer c) -> (a -> Writer c)
(f >=> g) x = let (y, msgG) = runWriter (g x)
(z, msgF) = runWriter (f y)
in (z, msgG ++ msgF)
With this in hand, we can now define doublePlus1
more succinctly:
doublePlus1 :: Int -> Writer Int
doublePlus1 = double >=> add1
We seem to have gained very little. Sure, our definition of doublePlus1
is
short and very similar to the definition of pureDoublePlus1
now, but we had to
write the (>=>)
operator that does exactly the same as our original
implementation of doublePlus1
. What we have gained is the benefit of
refactoring: the (>=>)
operator allows us to compose functions whose return
values are decorated by Writer
anywhere in our code; we don't have to manually
implement the composition of Writer
-decorated functions again and again and
again.
Moreover, once we turn Writer
into a proper monad, this composition operator
will work quietly under the hood of Haskell's do
-notation,
which allows us to write code that looks a lot like imperative code. The Python
implementation of doublePlus1
,
def doublePlus1(x):
y = double(x)
return add1(y)
becomes
doublePlus1 :: Int -> Writer Int
doublePlus1 x = do
y <- double x
add1 y
So let's discuss the general concept of a monad next.
-
Not that you'd ever write systems code in Haskell. That's what C, C++, Rust, and scripting languages are for. ↩
-
This Python code uses global variables for something that could be done much more nicely using a
Log
class and by passing an object of this type to any function that would like to add to the log, but in the interest of keeping the example short, I opted for a quick and dirty solution with a global variable as a log. ↩