The Identity Monad

Pure functions are great. They are clear, easy to test and easy to debug. But once you have more than a handful, composing them together becomes cumbersome and hard to read.

// confusing backwards maths
(x) => add10(divideByNine(multiplyByFour(x)))

// bizzare double barrel functions
multiply(2)(x)

// ugly hoc exports in react
export default withState(withRouter(MyComponent))

Enter the Monad.

Identity is the simplest monad and is a good place to learn the core concept of monads. That is: monads are function composers.

A monad holds a value and then lets you pass it through functions. While each type of monad has different underlying logic, the identity has no logic and just composes functions together in order.

The above examples can be rewritten using the identity monad.

// Logically forwards maths
(x) => new Identity(x)
    .map(multiplyByFour)
    .map(divideByFour)
    .map(addTen)
    .value();

// No double barrels
new Identity(x)
    .map(multiply(2))
    .value();

// Clearly ordered hock exports in react
export default new Identity(MyComponent)
    .map(withRouter)
    .map(withState)
    .value();

While each example may have slightly more code, it is now clear how each function is executed in sequence. Similar to how a promise chain can minimise callback hell, monads minimise nested function madness.

How does it work.

First let's make a class that can store a value.

class Identity {
    constructor(value) {
        this.valueue = value;
    }
}

Now we can add a map method. This takes a function as it's argument, passes this.value through it, and places the result into a new monad.

class Identity {
    constructor(value) {
        this.value = value;
    }
    map(fn) {        return new Identity(fn(this.value))    }}

At the heart of the monad is a contract to always return a monad. With each call to map the monad updates the value via our function and places that inside another monad. So when given a monad we can continue to chain map calls confident of their execution.

Side note: that's one of the reasons people who love monads also love types. If you know for certain that a function will also return a monad. You know for certain you can always call map.

What about flatMap?

You may have noticed a possible problem with our Identity. If fn returned an Identity we'd end up with a value in and identity in an identity. Sometimes that might be exactly what you want, but if that's not the case monads have a flatMap function. flatMap as the name suggests is like map but it flattens two monads into one. Or put another way it ignores its own monad and accepts the monad that fn returns.

class Identity {
    constructor(value) {
        this.value = value;
    }
    value() {
        return this.value;
    }
    map(fn) {
        return new Identity(fn(this.value));
    }
    flatMap(fn) {        return fn(this.value);    }}

Now if we restructure our pure functions to each return an Identity it doesn't matter what order we call them in.

const addTen = (value) => new Identity(value + 10);
const divideByNine = (value) => new Identity(value / 9);

addTen(80)
    .flatMap(divideByNine) // Identity(10)

divideByNine(18)
    .flatMap(addTen) // Identity(12)

Who needs map?

What's interesting about flatMap is that in combination with the unit function we can recreate map. Even though we started with map in monad land flatMap and unit are the core functions and map is just useful sugar on top.

class Identity {
    constructor(value) {
        this.value = value;
    }
    value() {
        return this.value;
    }
    map(fn) {
        return this.flatMap((val) => new Identity(fn(val)))    }
    flatMap(fn) {
        return fn(this.value);
    }
}

This may seem unnecessary on a simple monad like Identity. But as the logic inside flatMap gets more complicated it's nice to only have it in one place.

Ok. What's next?

The identity monad is great for composing functions in sequence, but where monads really shine is when there is logic in flatMap. Based on extra information stored on the monad you can choose whether or not to call the function given to flatMap.

  • Maybe checks for null values before calling
  • Either does a boolean check.

Allan Hortle

Repos

  • Normalized state management
  • A TUI for coverage reports
  • Monads with beginner-friendly naming
  • Front-end Methodology

Posts