Interested in working with us? We are hiring!

See open positions Written by Leonardo Farroco, November 11, 2022

I know that I’m some years (decades?) late, but here’s my take on writing a monad tutorial.

When you start learning about Functional Programming, sooner or later, you will hear about “monads” - the secret handshake for functional programmers.

For those coming from imperative languages, there’s a considerable learning curve to conquer before venturing into dealing with monads.

Here we will see how the sausage is made in a mainstream language, JavaScript. I hope that it will help you visualize how the data plumbing works. After we understand the concept, we can also see how monads look in functional languages.

Before we start, a warning: this is a more practical guide. We are going to skip some of the math involved. If the concepts involved happen to light your interest, the book “Category Theory for Programmers” is beneficial.

# Not just lists

One of the mistakes I see people making when they try concisely explaining monads is comparing them to lists.

It is very tempting to use this example because lists and arrays are common data structures and programmers coming from imperative languages are familiar with at least one of these. The argument is quite appealing.

This line of thinking can make newcomers believe that if they `.flatMap` a list, they explored all that monads offer. It goes pretty beyond that.

Imagine someone asking what a machine is and then getting the response: “a car is a machine.” While this is true, one must be aware that there are many other types of machines: rockets, phones, clocks, etc.

# Maybe JavaScript

For this tutorial, instead of lists, we will implement a data type called `Maybe`. This data type helps when working with values that may or may not exist, avoiding common JS errors like `Cannot read properties of null`. It is pretty common in functional languages - but if this is your first time seeing it, don’t worry - we will use just a basic implementation. Here are the minimal utility functions that we need to work with this data type:

``````const maybe = (value) => {
if (value === null)
return {
tag: 'nothing',
};
else
return {
tag: 'just',
value,
};
};

const exists = (someMaybe) => someMaybe.tag === 'just';
``````

The code is pretty self-explanatory: some value comes in, and we check if it is `null` and then we return a wrapper with a `tag` property that labels which kind of data is being held. The `exists` function allows us to check the value of the `tag`.

We can imagine that this code receives some nullable thing and returns a maybe thing, where thing could be `string` or `number` or whatever.

When talking about the types involved in this operation, we can replace the `string` or `number` part with `a`, and the maybe part with `m`. A function returning something can be described as `->`. By doing this, we can say that the type of the `maybe` function is `a -> m a`.

Working with values wrapped in `Maybe` will look like this:

``````const sum = (a, b) => a + b;

/** Receives two maybes (ma and mb) */
const process = (ma, mb) => {
if (exists(ma) && exists(mb)) {
return sum(ma.value, mb.value);
} else {
return null;
}
};

process(2, 3);
// 5;

process(2, null);
// null;
``````

As you can see, we are not gaining much by using `Maybe` instead of checking if a value equals `null` - we are just calling `exists` all the time. Another code smell is that we are returning `null` in the “failure” path. We threw away our safety harness and now we are letting `null`s wander inside our app without adult supervision.

# Map all the way!

This can be improved by using `map`, enabling us to provide the hypothetical `m a` value to a function, if said function accepts an `a`:

``````const map = (fn) => (ma) =>
exists(ma) ? maybe(fn(ma.value)) : ma

const addThree = a => a + 3

// {
//   tag: "just",
//   value: 6
// }
``````

So far, so good. Now we can “inject” a function to operate on values that might not exist. We also get a `Maybe` back, instead of a raw value. It might be tempting to “extract” the value after we are done, but keeping it “wrapped” ensures that consumers of our code will be able to `map` their functions safely on it. This also makes more sense than returning `null`: if we are adding a number to something that might not exist, then the result might not exist as well. Tip: once you step into `Maybe` land, stay there (otherwise you are lying to yourself and not handling all possible states properly).

# Let’s make it harder

In the real world, things are usually more complicated than this example. We are probably going to have multiple “maybe” values coming in. Let’s see how we can sum two nullable values - for that, we can use a nested `map` to access both values:

``````const process = (ma, mb) =>
map((a) =>
map((b) =>
sum(a, b)
)(mb)
)(ma);

process(maybe(2), maybe(3));
// {
//   tag: 'just',
//   value: {
//     tag: 'just',
//     value: 5,
//   },
// };
``````

FP folks are probably screaming “Use apply!!”, but let’s restrict the scope of this tutorial.

These nested `map`’s are hard to read - and there’s also an unfortunate effect: We got a nested `Maybe` as a result! If we add more variables (as in `map (map (map ...))`), more nested levels will be added as well. Working with this type of result is possible but burdensome:

``````const mma = process(maybe(2), maybe(3));

if (exists(mma)) {
if (exists(mma.value)) {
console.log(mma.value);
}
}

//or by mapping:

map((ma) =>
map((a) =>
console.log(a);
)(ma);
)(mma);
``````

# Crunching data

Let’s create a function that will help ease the pain of working with nested values, as this nesting got out of hand quickly.

``````const join = (mma) => (!exists(mma) ? mma : mma.value);
``````

This is a very simple function:

• If we are in the “nothing” case, return it (as “nothing” can have no children).
• Otherwise, ignore the outer layer and return the inner, wrapped value.

The type of `join` is `m (m a) -> m a`.

Now, for each variable that we add, a `join` call needs to be added as well:

``````const process = (ma, mb, mc) =>
join(map((a) =>
join(map((b) =>
map((c) =>
a + b + c
)(mc)
)(mb))
)(ma));

// Notice that we have two `join(map(` segments
// and one `map(` segment. This will be revelant below.
``````

This is getting even worse to read! Let’s try refactoring this.

First, let’s replace the innermost, solitary, `map(` segment with `join(map`. The result looks like this:

``````const process = (ma, mb, mc) =>
join(map((a) =>
join(map((b) =>
join(map((c) =>
a + b + c
)(mc))
)(mb))
)(ma));

process(maybe(1), maybe(1), maybe(1));
// 3;
``````

This works, but the resulting value is not wrapped inside a `Maybe`. If our inputs may not exist, coherence dictates that the result might not exist as well. To fix this, we can add a `maybe` around the innermost expression (`a + b + c`):

``````const process = (ma, mb, mc) =>
join(map((a) =>
join(map((b) =>
join(map((c) =>
maybe(a + b + c)
)(mc))
)(mb))
)(ma));

process(maybe(1), maybe(1), maybe(1));
// {
//   tag: 'just',
//   value: 3,
// };
``````

Now we have a working solution, but it looks like a letter soup. Let’s try combining (composing) the repeated pattern `join(map` into a new function:

``````const bind = (ma) => (fn) => join(map(fn)(ma));
``````

As you can see `bind` equals “`map`, then `join`”.

Now let’s replace all `join(map` expressions with `bind`.

``````const process = (ma, mb, mc) =>
bind(ma)((a) =>
bind(mb)((b) =>
bind(mc)((c) =>
maybe(a + b + c)
)
)
);

process(maybe(2), maybe(2), maybe(2));
// {
//   tag: 'just',
//   value: 6,
// };
``````

Now even if we add more variables, they will all collapse until a single, flat maybe value, because we call `join` once per level.

The `bind` function allowed our nested structure to be executed as a sequence of steps.

By the way, `bind`’s type signature is quite popular: `m a -> (a -> m b) -> m b`.

The final code doesn’t look much pretty, as those nested functions can be confusing to work with. Other languages have features that make working with this type of expression easier.

# Entering the Big M: Generalizing Plumbing

Now you learned how to create a system that performs a sequence of computations involving `Maybe`.

But now imagine Matrix’s Morpheus coming into the scene and saying:

“What if I told you that this pattern can be generalized to other data types?”

Remember when we saw `bind`’s type? That `m` doesn’t stand for `Maybe`. It’s `Monad`. When we say `m a`, we are not talking about a `Maybe a`, but any “`m`onadic” type that holds an `a`. In the FP world there are many such types: a list (`List a`), a side effect `IO a`, something that may contain an error (`Either err a`), etc.

This pattern is useful because we can generalize the act of “Work with this data, running the functions in sequence. If something goes wrong, short circuit”.

The necessary elements to create this protocol are:

• the capability of “flattening” our data structure with `join`: `m (m a) -> m a`;
• when calling our sum (`a + b + c`) function, we need to place the result in the same data structure that we are using. In our code we called it `maybe`, but in the protocol, it has a different name: `return`. Its type is `a -> m a`. Depending on the context, `return` can also be called `pure`.

The catch is that each datatype handles “wrapping”, “collapsing” and “short circuiting” differently, depending on what these actions mean for that type. This is very important for FP, because this enables the `IO` monad to run actions with side effects, then wrap the results back into lazy functions.

# How this looks outside JS-land

I think that we pushed JavaScript as far as it can go. Working with this kind of expression is easier for functional languages, as they eat “wrapped values” for breakfast. (We can go further if we use a JS-library like fp-ts or Sanctuary).

In functional languages, you can either call `bind` directly or use it as an operator `>>=`.

Our example from above:

``````const process = (ma, mb, mc) =>
bind(ma)((a) =>
bind(mb)((b) =>
bind(mc)((c) =>
maybe(a + b + c)
)
)
);
``````

In a language like Haskell, the code becomes:

``````process ma mb mc =
ma >>= \a ->
mb >>= \b ->
mc >>= \c ->
return (a + b + c)
``````

By the way, check out haskell.org and notice how the logo is similar to `bind`. Those folks take `>>=` seriously.

Now let’s see how the same code looks like when using `do` notation, which is syntax sugar over `bind`:

``````process ma mb mc = do
a <- ma
b <- mb
c <- mc
return (a + b + c)
``````

The final result looks like an imperative language - one might argue that when you type `;` in JS, you are, in fact, `bind`ing monadic code (I first learned about this on Bartosz Milewski’s course Parallel and Concurrent Haskell.

The interesting thing about this snippet is that the same syntax for `process` will work for other data types beyond `Maybe`. The only requirements for that are:

• the data type (the `m` in `m a`) “knows” how to `bind` (and to do that, the data type needs to have some sort of `map` and `join`);
• the data type “knows” how to wrap a value with `return`/`pure`.

And as we are performing a sum, there’s an additional requirement:

• the contained data (the `a` in `m a`) “knows” what to do with `+`.

As we are using JavaScript, this means that we can use `+` to sum numbers or concatenate strings. In the FP world, a function “knowing” what to do with a given type is handled by type classes, let’s focus on this monadic stuff for now.

So this is the power that this abstraction provides: all the data plumbing and small details handling are abstracted with the same interface for multiple data types.

In Haskell, here’s how the definitions would look like (with types included):

``````process :: Monad m => Num a => m a -> m a -> m a
process ma mb = do
a <- ma
b <- mb
return (a + b)

-- Adding things that might not exist
processMaybe :: Maybe Integer
processMaybe  = process (Just 3) Nothing

-- Adding values that might contain errors
processEither :: Either String Integer
processEither = process (Right 2) (Left "an error happened!")

-- Adding something that was obtained with a side effect
processIO = do
ma <- getStuffFromDB
mb <- getStuffFromFile
process ma mb
``````

If your function only cares about adding two numbers, let it just sum. The error handling can be pushed outside of sum, allowing you to separate the pure and impure parts of your code even further.

This abstraction also helps to test (free monads), as you can replace the monad you are using during testing (like stubbing or mocking in some testing frameworks). This allows you to replace functions that obtain their values with side effects with an `Either` or `Maybe`, among others.

Monads also make refactoring easier - if you have a set of functions that handle some `Maybe`s using `bind`, you should be able to replace the `Maybe`s with `Either`, if the necessity arises.

And that’s it! I tried to keep it short, but the topic is quite extensive and I barely scratched the surface. Now even if you don’t end up using monads, you better know what kind of problems they help solve.

If you are interested in using these data structures in JavaScript, alongside their useful `map`, `bind` functions, some libraries that can help you: