Since React took off, Functional Programming has really taken off and is being used in more places. Not one to get left behind I tried learning it and applying it in as many places as I could 5 or 6 years ago but quickly realized it wasn’t for me. I’m not going to get into all the reasons for that since most of them are just preference but the big reason why I keep my usage of Functional concepts to somewhat of a minimum is that it makes things overly complicated in many cases.

For example, one of the foundational concepts in Functional Programming is the Monad structure. To understand it (at least to understand it well) you need to learn a whole bunch of related concepts none of which are plainly laid out for newbies. However the use of Monads can really make your code easier to understand in many cases. To that end I’m writing up a super simplified, no frills description of what a Monad is, why they’re useful, and some examples of them.

What is a monad?

According to the massive page on Monads in the Haskell docs, a Monad is a structure (they make it clear it doesn’t have to be a data structure) which has something akin to the following interface (note that this is in Typescript parlance which is slightly different).

interface Monad<T> {
    constructor(value: T): Monad<T>;

    bind<ReturnType>(callable: (value: T) => Monad<ReturnType>) => Monad<ReturnType>;
}

So basically there’s three parts to the interface:

  • Some generic type T which is the data type the Monad works with (or sometimes contains)
  • A constructor (or a static method of) which takes a value of type T and returns a Monad<T>
  • A bind method which can be used to apply some function to the data represented by the Monad

On the surface this doesn’t seem all that interesting but let’s look at a few examples and see if we can make things clearer of why this interface is useful.

Examples

Promises

One really good example from the JavaScript/Browser API is the Promise type.

The Promise type provides 3 ways to create a new instance: new Promise(callback), Promise.resolve(value), and Promise.reject(value). This fulfills the first part of the interface.

It also has the following methods: .then(callback), .catch(callback), and .finally(callback) which are equivalent to the .bind method but with specialized usage.

So now that we’re pretty sure that a Promise is a Monad, let’s look at why it’s useful.

Without a Promise, if you wanted to perform some asynchronous operation you would need to do something like this:

function remoteOperation(callback: (value: string) => void): void {
    // snip...
}

function anotherRemoteOperation(callback: (value: SomeType) => void): void {
    // snip...
}

remoteOperation((value) => {
    anotherRemoteOperation((anotherValue) => {
        // and so on...
    })
});

With Promise we can rewrite this as:

function remoteOperation(): Promise<string> {
    // snip...
}

function anotherRemoteOperation(): Promise<SomeType> {
    // snip...
}

remoteOperation.then(anotherRemoteOperation)

It’s much easier to chain a Promise than it is to keep doing callbacks.

Another nice feature of this particular Monad is that the functions that are given in the .then, .catch, and .finally methods don’t do anything until the Promise actually resolves which makes the whole thing lazy in a good way. And if the Promise never resolves (this might be a good thing) then the cost of running those functions is never incurred. This leads us into the next useful Monad.

Maybe/Optional

A Maybe (or sometimes called Optional) is a special type that you can think of as an array that con only zero or one element in it any any time. Again this seems silly but let’s look at a super trivial example.

function tryGetName(): undefined | string {
    // snip trying to get the name, return `undefined` otherwise
}

const maybeName = tryGetName();
if (maybeName === undefined) {
    console.log("Hello, anonymous user!");
} else {
    console.log(`Hello, ${maybeName}!`);
}

Now sure, this could be condensed down in a couple ways but the logic would still be following this pattern and it’s a bit clearer to read it this way.

Thanks to Typescript we can be pretty confident we handled all the possible values of maybeName (in other languages you may not have type checking which would make this riskier). However we’re having to put the fact that the variable might have multiple types of values front and center which can be a bit annoying the more frequently the value is used.

Let’s look at an alternative way this could be written.

function tryGetName(): Maybe<string> {
    // snip trying to get the name
}

maybeName
    .map((name: string) => `Hello, ${name}!`)
    .orElse(() => "Hello, anonymous user!")
    .bind(console.log);

It’s basically the same number of lines of code but at no point can you accidentally dereference an undefined value and there are no visible branches in the code which can be useful.

It’s also not very hard to write this particular type.

class Maybe<T> {
    public static of<T>(value: T): Maybe<T> {
        return new Maybe(value);
    }

    public static none<T>(): Maybe<T> {
        return new Maybe(undefined);
    }

    private readonly maybeValue: undefined | T;

    private constructor(value: undefined | T) {
        this.maybeValue = value;
    }

    public map<ReturnType>(transform: (value: T) => ReturnType): Maybe<ReturnType> {
        if (this.maybeValue === undefined) {
            return Maybe.none();
        }

        return Maybe.of(transform(this.maybeValue));
    }

    public flatMap<ReturnType>(transform: (value: T) => Maybe<ReturnType>): Maybe<ReturnType> {
        if (this.maybeValue === undefined) {
            return Maybe.none();
        }

        return transform(this.maybeValue);
    }

    public orElse(otherValue: T): Maybe<T> {
        if (this.maybeValue !== undefined) {
            return this;
        }

        return Maybe.of(otherValue);
    }

    public bind(operation: (value: T) => void): void {
        if (this.maybeValue === undefined) {
            return;
        }

        operation(this.maybeValue);
    }

    public unwrap(): undefined | T {
        return this.maybeValue;
    }
}

It’s a very simple type but it allows for easy chaining of operations without having to continuously check for potentially undefined values. It’s also hopefully pretty clear how it’s similar to the Promise type.

Result

A slight variation on the Maybe type is the result type. This type represents the result of some operation. Just like with Maybe it only holds a single value but the type of that value is dependent on what the result was of the operation it came from. For example, maybe you are creating some remote resource. If the creation succeeded, Result would contain the newly created resource, otherwise it would contain some error information about what went wrong.

This can prevent users of your API from being able to ignore errors while doing development which is super easy to do in many languages which allow for “throwing” errors. If you don’t wrap the operation in a try/catch block then the error could be ignored until it happens at runtime which isn’t great. With a Result type it’s clear you must handle the possibility of failure before using the success value.

Here’s an example of how such a type could be used:

interface User {
    id: string;
    username: string;
}

type UserCreationRequest = Omit<User, "id">;

interface UserCreationError {
    message: string;
}

function createUser(
    creationRequest: UserCreationRequest,
): Result<User, UserCreationError> {
    // snip...
}

createUser({ username: "bob.builder" })
    .bind((user: User) => {
        console.log(`Successfully created new user "${user.username}"!`)
    })
    .bindErr((err: UserCreationError) => {
        console.error(`Unable to create user: ${err.message}`);
    })
;

At every point the result of createUser has a defined value and you can’t just try and access the User that was created until you decide on how you’re going to handle the error.

And here’s the possible implementation of such a type.

type ResultContents<T, E> = { value: T } | { err: E };

class Result<T, E> {
    public static ofValue<T, E>(value: T): Result<T, E> {
        return new Result<T, E>({ value });
    }

    public static ofError<T, E>(err: E): Result<T, E> {
        return new Result<T, E>({ err });
    }

    private readonly contents: ResultContents<T, E>;

    private constructor(contents: ResultContents<T, E>) {
        this.contents = contents;
    }

    public map<ReturnType>(transform: (value: T) => ReturnType): Result<ReturnType, E> {
        if ("err" in this.contents) {
            return Result.ofError<ReturnType, E>(this.contents.err);
        }

        return Result.ofValue(transform(this.contents.value));
    }

    public mapErr<ReturnErrType>(transform: (err: E) => ReturnErrType): Result<T, ReturnErrType> {
        if ("value" in this.contents) {
            return Result.ofValue<T, ReturnErrType>(this.contents.value);
        }

        return Result.ofError(transform(this.contents.err));
    }

    public bind(operation: (value: T) => void): Result<T, E> {
        if ("err" in this.contents) {
            return;
        }

        operation(this.contents.value);

        return this;
    }

    public bindErr(operation: (err: E) => void): Result<T, E> {
        if ("value" in this.contents) {
            return;
        }

        operation(this.contents.err);

        return this;
    }

    public unwrap(): T {
        if ("err" in this.contents) {
            throw new Error("cannot call unwrap on error result");
        }

        return this.contents.value;
    }

    public unwrapErr(): E {
        if ("value" in this.contents) {
            throw new Error("cannot call unwrapErr on success result");
        }

        return this.contents.err;
    }
}

Just slightly more complicated than the Maybe type but very much the same in terms of idea.

📝 I could have implemented this with the Maybe type being in charge of the error and value conditions but I think in this case that would have cluttered things up since the implementation is already pretty small and I was able to leverage typing better with this solution.

Wrap-up

There are tons of other monads out there but honestly most of them make things too complicated in my mind and I stick with these. They solve the majority of problems I encounter, are relatively easy to understand, take little to work into existing code, and don’t require a PHD or mastery of Haskell to explain to someone.