Ross Esmond

Code, Prose, and Mathematics.

portrait of myself, Ross Esmond
Written — Last Updated

Monads

Monads are Functors, which means they have a map function, but they come with extra functions and guarantees which make them more useful than functors.

Monads are required to provide a constructor function which turns any value into that Monad. I will refer to this constructor as the Application function. In Haskell, this function is return, but it (understandably) takes different forms in other languages. In fantasy-land (monad library for Javascript) the Application function takes the form Type.of(x).

Monads also have a chain function, which is similar to a map, except the computation returns another monad. That makes three monads involved in one computation, if you haven’t been counting. Say I had a maybe monad which represents a persons full name. If I needed to compute the persons last name, I could attempt to do that by splitting the name into pieces, and grabbing the last piece. We’re dealing with a maybe monad, so the name could be missing, but we could also fail to find a last name, even if the maybe monad contained a string. In the case of an empty string, there is no last name, and the resulting maybe monad should be empty.

I can call chain Maybe monad with chain.

const lastName = name.chain(str => {
    const parts = str.split(' ')
    if (parts >= 2) {
        return Maybe.Just(last(parts))
    } else {
        return Maybe.Nothing()
    }
})

The Monad Laws

The monad laws can be difficult to interpret and internalize—a byproduct of their mathematical nature. What they are trying to achieve and how they go about enforcing it are two entirely different beasts. They’re trying to force the chain call to be a flat map from a monad to itself. Chain is passed a function which maps the values inside the starting monad into another monad of values. The values inside the returned Monad(s) are then merged into the ultimately returned, singular Monad. In the case of a List Monad, the argument function is called for each value inside the starting Monad, and each time the argument function returns a new List Monad. In the end, the values inside these Lists are merged into a single List—the final return value of the chain call. This flat map is extremely versatile, and could be used to filter values, expand values, or simply map values, all in a single call.

Left Identity

Calling chain on a monad of a single value must simply return the result of the provided function. Say we have some function f, which takes in a number and returns a monad of type Box, then Box.of(x).chain(x2 => f(x2)) must be equivalent to f(x). This is referred to as left identity, as in, if the Application (identity) function is on the left, most of the function calls are unnecessary. This rule ensures some basic assumptions about monads. Primarily, the application function must store the provided value in the monad such that it can be accessed later. Monads must allow for all values, and are not allowed to reject or alter the value under any circumstances. This law also ensures that chain returns the result of the provided function in these simple cases. There are, however, some cases where this is impossible. For example, when using a List monad—a monad capable of storing multiple values—chain would need to be called on each of the stored values, and the results would need to be combined in some way.

Right Identity

When passed its own Application function, chain must return a monad equivalent to itself. box.chain(Box.of) must be equivalent to box. This is referred to as right identity, as in, if the Application (identity) function is on the right, no change is made. In the simple case where box is the directly applied to a value—as in, Box.of(x).chain(Box.of)—this law is already covered by Left Identity. Box.of(x).chain(f) must be the result of f, and f(x) in this case is Box.of, so the result is still just Box.of(x). Things get more interesting when the starting monad is more complex. Say we have a List monad containing two values. list.chain(List.of) would need to result in list. This creates some restrictions on how List’s are recombined after a chain. When I implement chain, I can’t combine the result as a List of List’s, because then list.chain(List.of) would wrap each element in its own single value list, which is not the same as the original list. In fact, the only sane way to implement chain, given this rule, is to flat map the values.

Associativity

The order of execution of chain is irrelevant; only the left-to-right order of chain matters. Therefor

const lastNameLength = name
    .chain(getLastName)
    .chain(getLength)

must be equivalent to

const lastNameLength = name
    .chain(str => (
        getLastName(str).chain(getLength)
    ))

Rule 3 is almost a byproduct of 1. If chain must return a monad equivalent to the result of the passed function, then calling chain in or outside that function shouldn’t effect the outcome.