The Maybe data type in JavaScript

The Maybe data type in JavaScript

Originally posted on dev

JavaScript is not the only language that can be used to do Web development. Some other languages built upon other programming paradigms like Elm or PureScript are available as well. They rely on functional programming and most of the time have similar concepts.

And one of these concepts is the Maybe data type.

You could of course read the documentation for these languages to try and grasp this new concept, or we could together see how the Maybe data type operates by writing it in JavaScript!

cat nailing

So today, we’ll be designing our own version of the Maybe data type and see a very simple example of how to use it.

The problem

Maybe is a data type that helps to represent either the value or its absence. Let’s take a look at a division function.

function divide(numerator, denominator) {
    return numerator / denominator;
}

Simple enough. As you know, there is a special case of division where it all goes boom! If we try to divide by zero, we go a division error. Let’s handle that case.

function divide(numerator, denominator) {
    if (denominator === 0) {
        return... hm... what again?
    }

    return numerator / denominator;
}

Yeah. That’s the problem. We don’t really know what to return. Of course we could throw an exception that will be left to our users to handle.

function divide(numerator, denominator) {
    if (denominator === 0) {
        throw new Error("second argument cannot be zero");
    }

    return numerator / denominator;
}

Or we could also use another parameter to provide with a default value.

function divide(numerator, denominator, defaultValue) {
    if (denominator === 0) {
        return defaultValue;
    }

    return numerator / denominator;
}

But we will see yet another way of handling this with the Maybe data type.

Maybe

In reality, Maybe is just a container. It hides its real assets which are Just and Nothing. Just is a data construct that will help us represent the presence of a value, and Nothing the absence of it. Let’s take a look at how we could implement this.

This, of course, is my take at representing the Maybe data type in JavaScript. There can be other implementations. I’ll let you do your researches if you need to see some other alternatives.

class Maybe {}

class Just extends Maybe {
    constructor() {
        super();
    }
}

class Nothing extends Maybe {
    constructor() {
        super();
    }
}

For now, it’s just two child classes that extends from a parent one. This will help us, especially if we are using TypeScript. Our functions will always return a Maybe instance. And it’s up to the implementation of the function to return either a Just instance (when there is a value) or a Nothing instance (when there is no value to return).

And the final implementation of our divide function could look like that.

function divide(numerator, denominator) {
    if (denominator === 0) {
        return new Nothing();
    }

    return new Just(numerator / denominator);
}

Again here, we are sure that we get an instance of Maybe. But whether it is a Just or a Nothing instance is up to the person who implemented the function.

person with a hat saying maybe maybe not

And again, if we test it, we’ll know that the return value of this function is indeed a Maybe value.

function divide(numerator, denominator) {
    if (denominator === 0) {
        return new Nothing();
    }

    return new Just(numerator / denominator);
}

const numerator     = 1;
const denominator   = 0;
const result        = divide(numerator, denominator);

console.log(result instanceof Maybe); // true

Great! But that’s not very useful. We should be able to do something with this instance. Like maybe get a default value like the second definition of the divide function we saw earlier. Let’s add that.

class Maybe {
    static withDefault(value, maybe) {
        if (maybe instanceof Just) {
            return maybe.getValue();
        }

        if (maybe instanceof Nothing) {
            return value;
        }

        throw new TypeError("second argument is not an instance of Maybe");
    }
}

class Just extends Maybe {
    constructor(value) {
        super();

        this.value = value;
    }

    getValue() {
        return this.value;
    }
}

class Nothing extends Maybe {
    constructor() {
        super();
    }
}

What we did there was:

  • Add a static function to our Maybe class. This will be responsible for handling the case where a maybe instance is a Just instance (and return the value contained in this container) or a Nothing (since there is no value attached to the Nothing container, return a default value passed as a parameter).
  • Add a value to our Just constructor. This is how we can make any value an instance of Maybe. And then, of course a method to get this value.
  • Our Nothing class remains untouched, lucky you!

Now, let’s see an example at how we can use this static method.

function divide(numerator, denominator) {
    if (denominator === 0) {
        return new Nothing();
    }

    return new Just(numerator / denominator);
}

const numerator     = 1;
const denominator   = 0;
const result        = Maybe.withDefault(0, divide(numerator, denominator));

console.log(result); // 0

Yay! Working. Let’s see with some other values.

function divide(numerator, denominator) {
    if (denominator === 0) {
        return new Nothing();
    }

    return new Just(numerator / denominator);
}

const numerator     = 5;
const denominator   = 2;
const result        = Maybe.withDefault(0, divide(numerator, denominator));

console.log(result); // 2.5

See what happened? We only changed the numerator & denominator. The value is now 2.5, which is expected since it is not a zero division. Our default value did not trigger.

Why

That’s it! We now have completed this implementation of the Maybe data type. But why all that amount of code only for a default value?

Consistency.

You see, in JavaScript and some more languages, you have a plethora of ways of saying that the function will not return the expected value. For instance, we saw two ways of terminating our function when there was a division error. But we could also just return zero (even if this is not mathematically correct). Or even return null (more correct, but have to handle that particular case).

Chances are that if you use someone’s code that is a divide function, and that you read that this function returns a Maybe data type, you will probably never have to go to the documentation and read all the particular cases to handle because you know that whatever happens, your Maybe value can only have two values: either the result of the division (Just) or Nothing.

And here is the definition of the Maybe data type in Haskell which is yet another functional programming language.

data Maybe a
    = Just a
    | Nothing

This reads as follow: The Maybe data type of an a value is either Just the a value or Nothing. We could replace a with value in this case.

data Maybe value
    = Just value
    | Nothing

I particularly prefer this version since a is not really self-explanatory opposed to value.

Another use case

If you ever happen to use the Fetch API to send data to an API server for instance, you’ll know that you have to handle all the cases of the response. But we could do that using the Maybe data type as well.

async function update(user) {
    const NO_CONTENT = 204;

    const response = await fetch("https://api.website.com/users", {
        method: "PUT",

        headers: {
            "Content-Type": "application/json"
        },

        body: JSON.stringify(user)
    });

    if (response.status === NO_CONTENT) {
        return new Nothing();
    }

    const updatedUser = await response.json();

    return new Just(updatedUser);
}

Now, if we update our user, we’ll be able to enhance our user interface by sending a little toast notification by saying “Informations updated” or “Nothing to update”.

const updatedUser = Maybe.withDefault(false, await update({ email: "amin@javascript.com" }));

if (updatedUser) {
    window.localStorage.setItem("user", JSON.stringify(updatedUser));

    window.alert("Informations updated");
} else {
    window.alert("Nothing to do");
}

Conclusion

Now that we understood the inner foundation of the Maybe data type by implementing it in JavaScript, we can now approach this data type in other functional languages with more ease.

Altough very used, this data type lacks some important information. It is perfect for cases where there is no need to treat each error independently, but when you have to treat each case separately, you can’t use the Maybe data type anymore.

So what? We ditch this concept and go back to throw exceptions or return string as errors? No! There is another data type that can be used which is the Either data type.

data Either a b
    = Left a
    | Right b

This is left as an exercise for the reader: propose an implementation of the Either data type (JavaScript or TypeScript) and use it in the context of sending some data to an API. There is no wrong answers. It is just a challenge I throw out of curiosity to share with the community.

soldier saying I'm doing my part

Thanks for reading and if you didn’t entirely understand the concept, don’t hesitate to ask in the comment section. I’m also open to criticism to help me improve this article. If you see some errors, please tell me in the comment section as well!

Now, if you’ll excuse me, I’ll go do some pushups. If there is nothing interesting to watch on Netflix.

data Maybe netflix
    = Just netflix
    | Pushups Source: dev