Monads in 10 minutes!

I challenged myself to explain Monads whilst keeping it within a Medium “10 min read”

Brian Candler
6 min readMar 24, 2023

In lockdown, I wrote a rambling series of articles, where in reality I was just trying to get my own head around monads once and for all. Now I’m going to try to explain the concept more succinctly.

Function application

I’m going to assume you understand two concepts already. The first one is applying a function f to a value v. In mathematics, and many programming languages, it’s written like this:

f(v)

If f is a pure function, then the returned value depends only on its internal logic and the supplied argument value v. It is not affected by any other state in the system, nor does it have any side-effects which modify the state of the system. Applying the same f to the same v always gives the same result.

Apart from that, you can perform whatever calculation you like inside f.

Higher order functions

The second concept is a higher-order function: one which accepts another function as a value.

The simplest, non-trivial¹ higher-order function looks like this:

g(f, v)

Function g accepts another function (f) and a value (v), does some calculations, and returns a value.

As part of its internal logic, g may choose to apply function f to some values. Note that g doesn’t necessarily have to apply f at all: for example, it might choose to apply it only for certain values of v but not for others. Or it might apply it repeatedly, with the number of applications depending on the value of v. It’s all down to what you decide to write into the behaviour of g: again, that’s entirely up to you.

OK, that’s it!

What’s a monad then?

A monad consists of:

  • A Type (or Class) of values: let’s call that type T
  • A function which acts on values of that type, of the form:
bind(f, v)

…where v is of type T, and f is some other function you pass in. You can see that bind is just a simple higher-order function like g I showed above.

The bind function is part of the implementation of a particular monad. If the class was written by someone else, and you are just using it, then you don’t touch it: it’s already been written for you. You have to provide your own function f at each place where bind is invoked though.

When I say “Class”, yes, that does correspond to what object-oriented programmers think of as a “class”, where every v is an instance of that class. Since the bind function accepts a single instance of type T as one of its arguments, this could also be written in OOP form as an instance method like this:

v.bind(f)

Alternatively, if your language supports operator overloading, then you can make this an operator instead: if bind is renamed to be the operator >>= (as in Haskell) then it becomes:

v >>= f

Choose whichever representation makes most sense to you.

That’s almost it. There are a few additional constraints that must be followed to make this type+function qualify as a monad.

An important one is that whatever function f you supply, the value it returns must be of type T, in other words the same type as v. (However, the argument passed to f can be of any plain type)

There needs to be a constructor function:

unit(x)

takes some plain value x and returns a corresponding value of type T.

There are a few more rules, but I’ll skip those for now².

In short, you can see that a monad is fundamentally nothing more than a class of values, and a supplied higher-order function which acts on those values and follows a few rules.

How can you use monads?

In lots of ways. But the one you are most likely to come across first is performing I/O in a functional language.

A pure function cannot have any side-effects, which definitely excludes things like writing to the terminal, or executing SQL statements.

If you think about it, there’s only one possible way to approach this with a pure function: it has to return a value which is a request to perform an impure action, such as “please output this particular string to the terminal”, or “please execute this SQL statement”. The request is just a value, so the function is still pure when it returns such a value.

Some of those actions, when executed, may in turn yield results (for example, reading a line of text from the terminal results in a string that the user typed), and those values will then have to be passed as the arguments to some other function.

Since monads already existed as a mathematical concept, they were co-opted into implementing this behaviour in functional languages.

  • The IO Monad is a class
  • Each value (each instance of that class) represents a request to do something, like “please print this string to the terminal”, or “please read a line of text from the terminal”
  • The bind function/method actually performs the requested action.
  • It then passes the result of that action to some other function f, which is the function you passed as the other argument to bind.
  • The value returned by f, which is another instance of the IO Monad, is passed back as the return value of bind.

All the impure behaviour is encapsulated in IO’s bind function. Everything else is just values being passed around.

So, let’s say that getLine is an instance of the IO Monad which represents the request “please read a line of text from the terminal”. If you write, in a Python-esque style:

getLine.bind(f)

then the bind function will perform the action of reading a line from the terminal, and pass the string value as the argument to f. The return value from bind is the value which f itself returned.

Here, f represents “the rest of the program” which consumes the value. You can write the body of f inline, as a “lambda” or anonymous function like this:

getLine.bind(lambda text:
... rest of program goes here ...
)

which means that the argument text receives the line which was typed.

In Haskell, with its operator overloading, that could be written:

getLine >>= (\text -> ......)

But Haskell also provides some syntactic sugar, so you more commonly see it written as:

text <- getLine

Underneath it’s exactly the same though.

When you want to print text to the console, you have to construct a value saying specifically what you want to print, using a helper function. Reverting to a Python-esque syntax:

putStrLn("Hello, world!")

That function doesn’t actually print anything though: it just creates an IO value wrapping the request. You have to bind that value to perform the action.

A program with multiple actions ends up as a series of nested functions:

putStrLn("What's your name?").bind(lambda _:
getLine.bind(lambda name:
putStrLn("Hello " + name).bind(lambda _:
... rest of program goes here ...
)
)
)

That’s where the syntactic sugar really helps, but the messy form above shows what’s really going on.

Other uses of monads

Given that f(v) is direct function application, then you can think of v.bind(f) as indirect function application. It does use f and v, but depending on how the bind function is written, it could also do any of the following:

  • Pre-processing: preparing the value to be passed as an argument to f
  • Post-processing: taking the return value from f and performing additional computation, such as combining it with the initial value of v, before returning the final answer
  • Conditional behaviour: deciding whether or not to call the function f, or calling it multiple times with different arguments and combining the results.

This makes it a flexible building block. Examples of other monads include:

  • the State monad (wrapping a value which is modified and carried forward between successive invocations of bind)
  • the Maybe monad (wrapping a value which may be present or absent, and skipping computation if it is absent)

Further reading

If you want to see the nuts and bolts of exactly how I/O is performed using monads, see part 5 of my series.

If you want to know more about the structure of a monad and the rules it must follow, see part 4. It also goes into some more detail around the Maybe monad, and has links to other recommended reading.

¹I could have written g(f) but it’s not that useful. g would have to apply f to a hard-coded static argument, say 0; or return another function value.

²One of the laws that a monad must follow is unit(x).bind(f) is the same as f(x). Another is that v.bind(unit) is the same as v. Finally there’s an associativity rule. These are mathematical properties of a monad that you can rely on, in a similar sense that x+0 and 0+x are always x for addition.

--

--

No responses yet