BattlefyBlogHistoryOpen menu
Close menuHistory

Monads lurking in your code

Ronald Chen January 2nd 2023

You are already using monad and just don't know it. Simply using undefined in Javascript is a monad. Let me show you an example in Typescript as types will make it more clear.

type Maybe<T> = T | undefined;

function maybeSix(): Maybe<number> {
    if (Math.random() > 0.5) {
        return 6;
    }
    return undefined;
}

function maybeDouble(maybeNumber: Maybe<number>): Maybe<number> {
    if (maybeNumber !== undefined) {
        return 2 * maybeNumber;
    }
    return undefined;
}

let maybeNumber: Maybe<number> = maybeSix();
const maybeDoubled: Maybe<number> = maybeDouble(maybeNumber);

console.log(maybeDoubled); // half the time 12, half the time undefined

The definition of a monand comes in 2 parts. First is to create a monad, another is to map a monad to another.

We "create the Maybe monad" by setting a variable of type Maybe<number> with either a number or undefined. We "map the Maybe monad" with maybeDouble.

We can write rewrite maybeDouble to a more general form.

function mapMaybe<T, U>(maybe: Maybe<T>, map: (t: T) => Maybe<U>): Maybe<U> {
    if (maybe !== undefined) {
        return map(maybe);
    }
    return undefined;
}

function maybeDouble(maybeNumber: Maybe<number>): Maybe<number> {
    return mapMaybe(maybeNumber, (number) => 2 * number); // number can no longer be null
}

What is the point of all this? We can see by using mapMaybe, we don't need to do null-checks explicitly anymore. The null-check is implemented once in mapMaybe and never again.

Another reason is to avoid the Pyramid of doom. Code without monads can get too nested.

const maybeNumber: Maybe<number> = ... // could be undefined

if (maybeNumber !== undefined) {
    const maybeString: Maybe<string> = maybeNumber % 2 == 0
        ? String(maybeNumber)
        : undefined;

    if (maybeString !== undefined) {
        const maybeBoolean: Maybe<boolean> = maybeString.length > 4
            ? true
            : undefined;

        if (maybeBoolean !== undefined) {
            // 3 levels deep? Oh the humanity!
            console.log(maybeBoolean);
        }
    }
}

But we can flatten the code with monads.

const maybeNumber: Maybe<number> = ... // could be undefined

const maybeString: Maybe<string> = mapMaybe(maybeNumber, (number) => maybeNumber % 2 == 0
    ? String(maybeNumber)
    : undefined);

const maybeBoolean: Maybe<boolean> = mapMaybe(maybeString, (string) => maybeString.length > 4
    ? true
    : undefined);

mapMaybe(maybeBoolean, (boolean) => console.log(boolean));

Once one understands monad is simply two operations (create and map), one can see monads in other parts of the code.

Promise is a monad

// create a Promise monad
const promise = Promise.resolve(42);

// map a Promise monad
const doubledPromise = promise
    .then((x) => 2 * x);

And since async/await is just syntanic sugar over Promises, they too are monads. It is just less obvious at first.

async function foo() {
    return 42;
}

async function doubleFoo() {
    // A Promise monad is created when foo() is called
    const x = await foo();
    return 2 * x;
}

The Promise is mapped using the async/await keywords. Since doubleFoo is an async function, it returns a Promise. async/await made it easy to access the resolved value.

Do you want to to learn how to see more than just monads? You're in luck, Battlefy is hiring.

Closures are weird
November 28th 2022

2024

  • MLBB Custom Lobby Feature
    Stefan Wilson - April 16th 2024

2023

2022

Powered by
BATTLEFY