Interested in working with us? We are hiring!

See open positions

Exploring Monads with JavaScript

Leonardo Farroco 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 nulls 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

map(addThree)(maybe(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:

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 “monadic” 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 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, binding 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:

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

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.

Why Monads?

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 Maybes using bind, you should be able to replace the Maybes 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.

Now go ahead and write your monad tutorial as well!

Other resources

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