Jimmy Breck-McKye

A lazy programmer

Another Crack at Explaining Monads

The internet heaves with monad tutorials. Very few of them really help…

Null checking is annoying

Consider the following function.

1
2
3
4
5
6
7
function getSurname() {
    var userNameInput = document.querySelector('#userId');
    var userId = userIdInput.value;
    var user = getUsers(userId);
    console.log(user.name);
    return user.name;
}

This function

  1. There might be no element, so the value lookup will fail
  2. The value might be blank, so getUsers will fail
  3. If user.name is null, we don’t want the console to log anything.

Normally, we’d handle this with some kind of null checking, like the following:

1
2
3
4
5
6
7
8
9
10
11
12
function getUserSurname() {
    let userIdInput = document.querySelector('#userId');
    if (userIdInput) {
        let userId = userIdInput.value;
        if (userId) {
            let user = getUsers(userId);
            if (user && user.name) {
                console.log(userName);
            }
        }
    }
}

As you can see, this approach gets very unwieldy, very quickly.

What would be ideal is if there were some kind of construct that could take each step of our function, and perform null checking at each stage. If any of the steps returned a null, the actions would no longer be called.

This would allow us to write the above in a very different style:

1
2
3
4
5
6
nullChecker
.apply( ()=> document.querySelector('#userId') )
.apply( input => input.value )
.apply( id => getUsers(id) )
.apply( user => user.name )
.apply( name => console.log(name) )

This is where the Maybe monad steps in.

Maybe you need a monad

This is Maybe. Before explaining how it is a monad, let’s try and understand it in its own right. Maybe can be written in a few lines of JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Maybe(value) {
    // Get the unwrapped value, if needed
    this.unit = ()=> value;

    // Applies an action if value isn't null
    // Returns the result or null wrapped in a maybe
    this.bind = action => {
        if (value === null) {
            return new Maybe(null);
        } else {
            let result = action(value);
            return new Maybe(result);
        }
    };
}

When I apply an action to bind, I always get a new Maybe, which lets me chain up further calls to bind. Because each result is wrapped in a Maybe, any action can return a null, but my null-safety will propagate. And because bind returns a new Maybe each time, I can re-use any I create.

Let’s write our example code again, using Maybe:

1
2
3
4
5
6
7
function getSurname() {
    let userNameInput = new Maybe(document.querySelector('#userId'));
    let userId = userNameInput.bind( input => input.value );
    let user = userId.bind(getUsers);
    user.bind( user => user.name)
        .bind( name => console.log(name) );
}

Understanding monads is much harder than it needs to be. Tutorials on the web either

Programmers often struggle with monad tutorials. When they finally grasp the idea, they often try and write their own, hoping to do a better job than the those who came before them. Rarely do they succeed. Either they focus on type definitions, which readers find too abstract, or they stick to examples, which leaves readers too little to join up into a real definition. This is my own attempt. I can’t guarantee you it’ll do much better, but I can promise you I’ve tried.

Monads

A monad is a wrapper around some data.

+--- MONAD ---+
|    value    |
+-------------+

The monad has a unit method, which returns the unwrapped value.

+--- MONAD ---+
|    value    |   .unit = ()=> value
+-------------+

The monad also has a bind method which takes an action. Bind applies the action to the value, then returns a new monad wrapping the result.

+--- MONAD ---+                     +--- MONAD 2 ---+
|    value    |   .bind (action)=>  | action(value) |
+-------------+                     +---------------+

That’s not very useful by itself – but if you insert more logic into the bind, things become more interesting.

Bind can translate the value to work with the action

When bind applies the action to the value, it can decide whether the action and the value are compatible, and it can act accordingly. How it acts depends on the monad. For example, a monad could decide that if its value is null, all calls to bind will just return a monad containing null. This means the action doesn’t have to test for null input.

This is called the maybe monad. A maybe might contain a non-null… or maybe it won’t.

+--- MAYBE ---+                          +--- MAYBE 2 ---+
|    'foo'    |   .bind (toUpperCase)=>  |     'FOO'     |
+-------------+                          +---------------+

+--- MAYBE ---+                          +--- MAYBE 2 ---+
|    null     |   .bind (toUpperCase)=>  |     null      |
+-------------+                          +---------------+

Because bind returns a new Maybe, I can keep chaining together actions, and my monad takes care of performing null checks between each one. If any of my actions can return null themselves, this becomes really handy.

+- MAYBE -+                        +- MAYBE 2 -+                       +- MAYBE 3 -+
|   99    |  .bind (minus100)=>    |    -1     |  .bind(squareRoot)=>  |   null    |  .bind...
+---------+                        +-----------+                       +-----------+

+- MAYBE -+                        +- MAYBE 2 -+                       +- MAYBE 3 -+
|  'foo'  |  .bind (minus100)=>    |   null    |  .bind(squareRoot)=>  |   null    |  .bind...
+---------+                        +-----------+                       +-----------+

+- MAYBE -+                        +- MAYBE 2 -+                       +- MAYBE 3 -+
|  null   |  .bind (minus100)=>    |   null    |  .bind(squareRoot)=>  |   null    |  .bind...
+---------+                        +-----------+                       +-----------+

Both minus100 and squareRoot can return nulls, but neither does null checking. The Maybe monad makes it very easy to chain these functions safely.

RECAP:

Comments