What is a monad? It’s not that complicated…

Adam B. Csapo
11 min readJul 29, 2022

--

A growing number of blog posts and videos are dedicated to the topic of what monads are and why they are really cool. And yet, readers and viewers often complain that the monadic curse still hasn’t been broken for them.

In this post, my goal is to convince you that actually, monads are not that arcane. Here’s my crack at explaining what challenges they solve and why you should just use them, without worrying too much about their definition. The main point I want to make is that from a practical perspective, monads offer a clean and elegant way to manage side effects (including change of state) in your code. To show this, let’s first take a detour into the world of functors, and then return to our main topic.

Imperative style — Functors — Monads

First of all, what do I mean by client side code? In this post, I refer to the code that makes use of some pre-defined data structures to achieve some concrete functionality as client side code. This is in contrast to the part of the code that defines the data structures being used (which would be the server side code).

Some problems with the imperative style

Now, to motivate both functors and monads, let’s take a look at how one might approach writing down a series of transformations — first, using an imperative style. For the sake of simplicity, we will consider some very simple arithmetic transformations in Javascript ES6 — which should be understandable to you even if you’ve never ever done any programming in Javascript:

It should be clear that we are computing a rather simple algebraic expression; however, one really needs to read through the code with careful attention to understand how the value of each variable changes through time and what the end result is. That’s because in this way of programming, there are no constraints between the lines of code that follow each other — neither in terms of the variables on the left-hand side (and how they relate to each other), nor in terms of the variables (values) referenced on the right-hand side of each assignment. The main point is not that it’s impossible to write more elegant code in the imperative style; but that even if you do, you will still have to pay careful attention to dependencies between variables and the operations that are being carried out at the same time.

The case for functors

To see why this is inelegant, let’s take a look at how might one approach the same problem in a more functional way. One idea that’s often useful is to encapsulate (or *lift*) the value we are *”working on”* inside a higher-order structure (which would constitute the server side of your code), and to use *map()* to perform the transformations:

How is this better? Notice that on the client side at least (the part of the code starting with let w, that makes use of the pre-defined data structure — NumberContainer*— to compute the result), it is much easier to see what is going on. Instead of having to trace, through time, which variables are overwritten when, and how those writes influence subsequent operations, map() will guarantee under the hood that it is always the previous result that is being operated on, and that interim results are never mutated. Now, the only task remaining for the programmer is to consider what transformations are being carried out — in this case: Math.pow, times 3, and plus 2. Thus, we have cleanly separated the data management aspects from the functional (operational) aspects of the computations we are carrying out.

It took me quite some time to realize that the map() function is this generic. For the longest time, I thought it was just a way to carry out the same transformation on all elements of a container like a List or an Array (my thinking went that in this case, you’d be mapping the same function over all elements of the container). But in a more abstract sense, structures that provide a map() function really just provide a way to chain transformations in a functional way without the programmer having to concentrate on lower-level details, such as how different lines of code influence each other or how variables are initialized and kept from being mutated. In other words, the defining feature of map() is not that it operates on multiple elements, but that it can be called multiple times (i.e., chained) to reap the above-mentioned benefits. (Think of a sequence of map() operations called on a list, instead of the transformation carried out on each element of the list in each call to map().)

Data structures that support map() in this way are known as functors in the functional programming paradigm. We could say that a functor is a Functor F of type T if the type of the value that it can wrap is of type T (in our earlier example, based on the client code at least, T was a numeric type). Functors are really great, but one limitation they have is that using map(), the only kinds of functions that you can chain together are the kinds of functions that take a single input of this type T, and return a value of the same type (f: T -> T).

Of course, you will always need to stick to at least some rules if you want to chain anything together. Nevertheless, it is still the case that map() will only ever allow you to chain together functions (transformations) of one type. A different kind of transformation we might want to consider is one that takes as input some type T, and returns a Functor F of type T (instead of just type T). For example, you couldn’t use map in this way:

That’s because from the second call to map() onwards, each mapped function is expecting to receive as its input a numeric value… not a NumberContainer object created in the previous step. If we wanted to chain functions like these (for some reason), we would need to use a different kind of map(), which is often referred to as bind() or flatmap().

But why would we want to do this in the first place?

A glimpse into what bind() has to offer

To see why the distinction between map() and bind() is crucial, let’s consider a wallet application that can keep track of denominations in different currencies. We can define the Denomination and WalletState classes as follows:

A Wallet, in turn, is just an object that holds any number of denominations, and also stores a history of transactions. In addition, let’s say that the wallet can have a type, such as basic or premium:

Based on the comments in the code, you might already see that the question of whether to use map() and bind() will hinge on whether you want to apply functions to the Wallet object that focus only on transformations to its state, or instead operate at a higher level of generating new Wallet objects.

Specifically, when using map(), the client side code will defer the creation of the new Wallet to the code inside the map() function based on its own logic; whereas in the case of bind(), it’s the client side that gets to determine the way in which the new Wallet is constructed (for example, it can even change the type of the wallet, which is simply kept at its previous value in map()).

The point is not that an implementation of map() couldn’t — in theory — support the use of functions that also return a wallet type and then use the returned value to instantiate the new Wallet; the point is that there can always exist some aspects (i.e., parameters) of the newly created object that are better specified on the client side. Even if our implementation of map() allowed for the function being mapped to specify, through its return value, all parameters of the newly created Wallet, it could still be the case that multiple constructors for the Wallet type exist, and the decision of which one to call is better handled on the client side.

As an example for both of these use cases, consider the following listing:

Monads — functors with bind()

Data structures that support both map() and bind() in the way described above are known as monads.

Based on the above examples, we can formulate the difference between functors that provide a map() function and monads that provide both map() and bind() functions more generally as follows: In the case of a functor, the creation of the next functor in the map() chain is hard-coded inside the map() function within the functor; whereas in the case of a monad, it is always the function that you are binding (which is external to the monad) that will create the new monad. What this means is that in the monad pattern, we get to choose whether it’s the server side (map()), or the client side (bind()) that should be given more flexibility in determining how the next monad in each step is created; whereas in the case of functors, the client side can have nothing to do with the higher-level concepts used by the functor (and are only concerned with values of some type that are wrapped by the functor).

This latter point is an often neglected aspect of monads. Often, it is claimed that monads are useful because they automatically flatten the monad that would result from calling map() (hence the name flatmap(), which is often used instead of bind())… however, the flip side of this is that now, in the case of monads, the internal monad that gets flattened is also created elsewhere in the code (on the client side). This makes for some very flexible ways to modify the behavior of the monad.

A good example is the Promise type that is used in many languages to represent asynchronous operations that will complete sometime in the future. In Javascript, when you call .then(…) on a Promise object, what you are actually doing is binding a function (passed as a parameter to then()) to the Promise monad. However, since it is the function that is being bound that will create the next Promise in the chain, it has the flexibility of specifying the next asynchronous operation to be carried out; all the rest (e.g., waiting for the previous asnyc calls to return) is handled by then().

Epilogue: Using monads to manage side effects cleanly

Now that we understand the above points, we’re basically done! The bottom line is that in order to make your client code more functional, more elegant, it is often a good idea to lift your basic data types into higher-level functors or monads, and to carry out transformations or operations on them using map() and / or bind().

But before we go out to celebrate, there is one other big advantage of using monads that I’d like to point out to you. This advantage is that monads also incidentally offer a way to work with side effects (like state change, user input, database read operations, etc.) in a controlled way.

You see, in functional programming, the goal is to keep one’s code side-effect free at (almost) all costs. At first, I wanted to say ‘at all costs’, because side effects are yucky: they cause your code to behave differently even if you give it the same input, and therefore, they make your code harder to test. Also, very often, once you decide to allow side effects, calls to those side effects get scattered all over the code base (think of a logging function being called from all over the place). Note, however, that I wrote ‘at (almost) all costs, because in practice, any interesting program has to have side effects (like interact with user input, with a database or a network).

The way that this conundrum is often solved in functional programming is to lift the data types of interest into higher-level constructs (objects) that also represent side effects. The key point is that instead of considering side effects as an action — as something that is carried out — in functional programming, they are often reified as objects. For example, instead of printing something to the console, we can keep binding to a print object (whose bind() function does the printing). In this way, at least the types we are using offer a strong clue that something fishy is going on.

Let’s take a look at an example (a simple variation of our initial example) to illustrate this point. If you wanted to log all of the interim results in a series of calculations, you could write:

As you can see, since it is always the function that we are binding to the interim results that is creating the new monad instance, we can pass a unique string in each case, making the use of VerboseArithmetics as a monad type more flexible (than as a functor type). At the same time, the fact that we are calling wrap() each time — which in turn creates a new VerboseArithmetics object, it should be clear based on the type alone what kinds of side effects will occur. Since the implementation of the wrap() function is the only place in the code that references the VerboseArithmetics type, we would just have to change the implementation of wrap() function if we wanted to instantiate a type other than VerboseArithmetics to get a different kind of side effect, or no side effect at all, each step along the way. Based on this example, the benefits compared to having to search for console.log() calls throughout the code base, or even compared to changing the functions passed to each call to bind() should be obvious.

Before I leave you with that thought, let’s take a look at one final example of how monads can be used to encapsulate state changes in a generic way. A classic example is the trampoline, which allows one to implement recursive functions without having to deal with stack overflow issues. The key idea is that instead of executing the recursive call in each step immediately, the fact that the call needs to be made — along with its parameters — can be stored as part of the state of the monad, obviating the need for new data to be placed on the stack each step along the way. Here is one possible way to compute the Fibonacci function using a trampoline monad:

As you can see, the Trampoline object itself is independent of the Fibonacci function or any specific operation, so we can use it to implement any recursive function in general: it’s the function that we are binding to the Trampoline object that is doing all the heavy lifting. At the same time, the (near) infinite loop is carried out by the bind() function, so seemingly there is no recursion inside the function we are binding. All of this results in a very clean structure and also for an effective implementation. Note that by adding memoization to the Trampoline class, it would be possible to make the computations even more efficient.

So in the end, what exactly are monads?

As we’ve seen, there are many ways to view monads, and many kinds of patterns in which they can be applied. If someone asked me to define monads in one sentence, perhaps I’d say something like this: Monads are functional wrapper types that allow the programmer to put some existing data into a context in which composable operations with specific side effects can be applied on the data.

I hope that clarifies monads for you!

Thanks to Sandor Dargo and Agoston Torok for their valuable comments.

--

--