loading...
Cover image for The Maybe data type in JavaScript

The Maybe data type in JavaScript

aminnairi profile image Amin ・6 min read

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

Posted on by:

Discussion

pic
Editor guide
 

Using Maybe like that has no sense. If you will use nullable (value | null) then withDefault is just x ?? 0 what means if x is null then fallback to 0. There is no value in Maybe here, only additional abstraction instead of idiomatic construct.

Maybe starts to have sense when you totally work with it in functional way. When u use maps, binds and ap.

If you write statements (and if is a statement) then I would say you don't need Maybe, Nullable is idiomatic and fully ok. If you use TS then compiler will inform you when you need to check if value is there, so win win. Additionally you don't loose latest added feature of optional chaining which works with null values.

But if you go fully functional then many things become harder with idiomatic JS, and then such constructs has a sense.

Below using Nullable.

function divide(numerator, denominator) {
    if (denominator === 0) {
        return null
    }
    return numerator / denominator;
}
// using
const x = divide(1,2) ?? 0

If we would type arguments as numbers then TS would automatically say the function returns number | null and dev would be enforce to or check the value or set the fallback

 

Very good point! Could you please describe when using Maybe make sens in JavaScript/TypeScript?
And also do you know some libraries that provides Maybe types?

 

Will write the whole article about that. So watch out for it :)

 

If we want to mix Maybe/Either with asynchronous API fetches, there's a functional data type that's perfect for that, Task (also known as Futures). Tasks are the functional version of Promises, and can be used to wrap Promise-based apis in order to make them functional (which for our purposes in Javascript, means: coherently interoperable with other functional data types).

Here's the simplest implementation of a javascript Task:

const Task = fork => ({fork});

Pretty crazy simple (and let's not talk about why we used the word "fork" just yet)! On the surface, we're literally just setting up a way to call a variable, and then get back an object with that variable stored in a key called "fork". But what we're reaaaaaally doing here is setting up a way to delay the execution of some function (while happening to call that operation "fork"). And here's an example operation where we're fetching a resource at the api endpont, 'some-api.com/'...

Task( ( left, right ) => { fetch('http://some-api.com/').then(right).catch(left) });

Now, we could have called "left" and "right"... "error" and "success" but if we wanted to better understand how Tasks are similar to the Either Type, let's go with this naming convention.

What we get back from the above, isn't a Promise of a result, or even itself an actual call to an api, but rather: a stored proceedure that COULD make a call to that api. And, if that's ever done, this particular construct would not return a result or throw an error directly: it'd instead return the results or an errors to two specific functions: left, and right.

What are those functions? Well, most people would think of them as callbacks. In fact, a lot of imperative programming for asynchronous code used to use some version of this idiom:

makeCall( CALL_FUNCTION, ON_SUCCESS, ON_ERROR ); where all the shouty words are functions, and the last two are "callback" functions (that is, the functions that we'd call once a result or error was returned).

In our "Tasks," Left and Right are basically just ON_ERROR and ON_SUCCESS respectively (in functional programming, the error condition, by convention, is usually specified first, in part to remind us all that the error condition is easily forgotten, but needs to be handled).

Why is this Task pattern superior to the imperative version? Well, some would argue that it's not! But if you're dipping your toes into the world of functional programming, and you've been sold on exploring the Maybe or the Either data type, you might already be sold on why, and so let's assume you're sold, and move forwards:

The Task pattern is pure: it causes no side effects UNTIL it's explictly called, if ever. And that means that, despite tackling messy, potentially error-prone asynchronous operations, Tasks can still live and operate and speak the same language as all the other types that live in a synchronous world: Lists (Arrays), Maybes, Eithers... all the rest. You can .map over a Task in exactly the same way you can .map over an List (Array) or a Maybe or an Either.

The Task pattern is also, unlike its more familiar cousin, Promises, LAZY. In practice, that means that you can reason and plan and speak about Tasks without that very act of reasoning then directly causing the potentially dangerous side-effects that a commitment to purity avoids. And this paragraph is in some ways just restating the previous paragraph: Tasks are pure. It just that, because they deal with operations that are not timeless and unary in their effects, that purity must necessarily also imply laziness.

We're leading up to this: remember that fist Task operation we described? Here it was:

const A_TASK = Task( ( left, right ) => { fetch('http://some-api.com/').then(right).catch(left) });

So, here's how you'd use it:

A_TASK.fork( logError, doSuccess )

Just define whatever logError and doSuccess do to handle api results or errors for yourself: all we care about right now is that they're functions: functions in exactly the same way as are Nothing and Just, Left and Right. They basically only exist in order to continue a functional chain (and indeed, this power of Tasks/Futures is known in computer programming as a "continuation") rolling on along, all potential side-effects carefully bottled and managed. All discrete operations named and broken down in to careful atomic parts that can be then joined up again to build up a complete program that's entirely secured from errors.

But if you feel ridiculous after all of this, all for this seemingly trivial result, don't worry! The proof is in the functional pudding. Here's a glimpse of that pudding:

egghead.io/lessons/javascript-leap...

 

Thanks Drew for your answer. Indeed your example looks interesting.

Do you happen to have any more resources online on the subject? Like maybe some more open-source code and videos? I'm afraid most of the readers out there won't be able to pay the price necessary to unlock the whole course and grab the sources out of this video.

 

The course (called Professor Frisby Introduces Composable Functional JavaScript) is actually free, you just need to enter your email address. Yes, you have to pay for the source code, but if you follow the course from the beginning it might not even be necessary, you can code while watching.

I'd recommend watching the whole course, even without coding along, because it's awesome, really.

Brian has also published a book about functional programming in JS, and it is freely available on Gitbook.

Thanks for your answer. I'll look into that. The book looks great.

 

I wrote a couple of Medium articles way back on this stuff. For instance, here's my piece on Maybe:
medium.com/@dtipson/getting-someth...

And this gist extends some of the Task stuff (using a constructor with a prototype is still the way to go with these, but using just straight functions is often a simpler way to explain the concepts, imo) gist.github.com/dtipson/01fba81f3b...

 

Isn't Nothing just the concept of undefined? What's stopping me from just comparing to undefined

 

It's a valid question as this post doesn't put Maybe into context. It's very un-clean to litter your code with null checks etc; moreover, it's not very mathematical to have functions that are that unpredictable. Another reason is inversion of control. The ecosystem around Maybe/Either etc in functional JS is enables the caller of a function to do error management independently, outside of that function, i.e. no more try-catches, no more checking types of returned values, just pattern matching on the returned type if it's an Either.

I've been using Ramda long before I started using Eithers and Maybes and ever since I switched to using ADTs my Ramda composition pipelines have become more succinct, more "clean" and more predictable.

Maybe I don't make any sense, But if you read the replies to Drew's comments and go through Brian Lonsdorf's linked tutorial; it will help you discover a new paradigm of making predictable apps with JS that are easier to reason about.

 

Also, no, nothing is not the concept of undefined.

  1. You might expect output as an undefined and do a check on it, but things change and later you realize you have to check for a null, an empty object, an empty array.........everywhere your function is called in the code base you'll keep having to add type checks. Either and Maybe let you decide what is your idea of Nothing or Left; and the code all over your app is already prepared for either outcome. Predictable, maintainable, clean. Your function can return a Nothing(/Left) when the output of itself is going to be can be "not a number", "not a string", "not an object of required shape", more power to it!
  2. You might expect input to be a certain way. But the API developer makes a mistake and the app gets weird data; and bam; things break. With ADTs you can make your functions "safe" and have them do Nothing if the input is weird. This is powerful because you don't have to hope anymore; you can be confident things will always be as you expect.
 
  1. Yes, I will have to add checks. But I have to add checks for this too, the instance of Nothing will have to be returned too for all the empty array, string cases. I get this is more easier to read and understand. But I could as simply check with isUndefined. And if I'm going to wrap every result in an Nothing/Just I could maintain consistency with returning undefined too.
  2. That doesn't happen automatically, have to validate the input. If there's validation logic already, then undefined can be a safe choice too as long my own code knows to expect it.

I'm not against the idea, I just think this is maybe better as a Type in Typescript then a class. No need to complicate which can be just:
Maybe = T | undefined

Or maybe I haven't understood this completely, will have a look at some more articles.

Yeah I don't think I'm able to explain the idea to you well enough; I hope you find the right resources online! Just to set the right context; no; you will not have to add any checks if you're using ADTs and mapping over them. The reason to use them is to remove all these unnecessary checks.

 

I think is more about returning a meaningful "empty" value, one that doesn't stop execution in unexpected ways. That's half the story, like many patterns in functional programming, this one encourages extension through function composition. Maybe a more practical example could help you see it, I wrote this one last year.

 

If you ever use Python, we also have Maybe, Either, and other monads (compatible with mypy and type-hints): github.com/dry-python/returns

GitHub logo dry-python / returns

Make your functions return something meaningful, typed, and safe!

Returns logo


Build Status Coverage Status Documentation Status Python Version wemake-python-styleguide Checked with mypy


Make your functions return something meaningful, typed, and safe!

Features

  • Provides a bunch of primitives to write declarative business logic
  • Enforces better architecture
  • Fully typed with annotations and checked with mypy, PEP561 compatible
  • Has a bunch of helpers for better composition
  • Pythonic and pleasant to write and to read 🐍
  • Support functions and coroutines, framework agnostic
  • Easy to start: has lots of docs, tests, and tutorials

Installation

pip install returns

You might also want to configure mypy correctly and install our plugin to fix this existing issue:

# In setup.cfg or mypy.ini:
[mypy]
plugins =
  returns.contrib.mypy.decorator_plugin

We also recommend to use the same mypy settings we use.

Make sure you know how to get started, check out our docs!

Contents

 

Whoa! That's so cool!!!

I'm nowhere near a good Python developer unfortunately, but I'll keep that repo in case I start getting really into Python for my next big project.

Thanks!

 

My Either implementation:

type Either<T> = [Left, Right<T>]

Using it:

/**
   * Finds the first Document that matchs the params
   * @param params.filter Object used to filter Documents
   * @param params.fieldsToShow Object containing the fields to return from the Documents
   * @param params.databaseName Set this to query on another database in the current mongo connection
   * @param params.throwErrors Enable classical try/catch way of handling errors
   * @returns A Promise with a single Document
   */
  findOne(params: IFindOneParams<Interface, false>): Promise<Either<Interface>>;
  findOne(params: IFindOneParams<Interface, true>): Promise<Interface>;
  findOne(params: IFindOneParams<Interface, boolean>): Promise<Either<Interface> | Interface> {
    return new Promise(async (resolve, reject) => {
      try {
        const _model = this.getModel(params.databaseName);
        const result = (await _model.findOne(params.filter, params.fieldsToShow).lean(true)) as any;
        if (params.throwErrors) {
          resolve(result);
        }
        resolve([null, result]);
      } catch (e) {
        const exception = new GenericException({ name: e.name, message: e.message });
        if (params.throwErrors) {
          reject(exception);
        }
        resolve([exception, null]);
      }
    });
  }

In another place using express

/** some code **/
const [insertError, insertSuccess] = await this.promotionService.insert({
      entity: promotion,
      databaseName: validationSuccess._id.toString(),
    });
    res.status(insertError ? insertError.statusCode : CREATED).send(insertError ? insertError.formatError() : insertSuccess);
 

I didn't know you could overload your function signature like that in TypeScript... Looking dope!

In my opinion, since you need to keep track of your errors as well as your result in an Either type, it would have been like that in my understanding:

type Either <A, B> = Left<A> | Right<B>

What do you think?

 
Sloan, the sloth mascot Comment marked as low quality/non-constructive by the community View code of conduct

Oh man... When I see something like this... It really knocks out all of the fuses...
This is how you create a solution for a non-existent problem and over-complicate things without good reason. This is the typical abuse of OOP and shows what is the main problem with this paradigm. This is what happens when beginners totally misunderstand what is OOP for. This is the exact case of over-engineering. The kind of problem you try to solve was solved even 30-40 years ago inlanguages like Fortran, so I advise you to look into what those people did back in time and what they are doing since then instead of re-inventing the wheel especially this way that is worse than possible. There is nothing wrong to look into the history of this industry and learn from that like what happens in any other profession. Do not think that Javascript is the language that has the solution that people before never were able to solve. It is actually quite the opposite. If you want to solve in a "programming language" like Javascript if I can even call it like that then try to look into something more serious to understand the solution. I advise you to look into for example how this is handled in C, javascript actually mostly implemented in C and guess what they handle the division of zero situation somehow without even having OOP support, so look into that. I tell you a big secret the solution is not in creating a hierarchy of un-needed classes and object and then think oh what I nice OOP program, this is how you do things... In real life large systems actually it is a problem when you have a lot of un-needed code and classes plus objects and garbage like that especially just to handle what can be handled without them. People who create code like this need to spend a few years working on large real-life systems written in a serious compiled program language most preferably without OOP support and then they might start to understand the basics of real-life software development with a real use case. Other than that I can't argue that this is a working solution but there is a huge difference between the value of different solutions. I think 20 years ago, when program languages did not even have construct to create this kind of "solution" was a better era to learn somebody to think logically. What makes me particularly mad when I see something like this is that the industry nowadays are full of beginners who create code like this especially using "programming languages" like javascript. My advise is that if you want any reliable math in your code then start to use a program language... /and not a script language built-in a browser/ Or if you use this kind of embededd script language then try to think in terms of memory usage if you know how to do that, and then people might save re-booting their machine after opening a webpage full of javascript like the above as it eats up their 12 GB ram in a second... /also even if you do node then think about the memory in your server eaten up by your un-needed classes and objects../ It is true that the division by zero can not really handled in javascript nicely but this is the fault of the language itself, and you still have to handle it the way it is supported instead of creating these class hierarchy, It does not make anything better only worse... The big secret here is that division by zero must be handled by the language level using only what is supported by basic language constructs built-in into the language. Creating a hierarchy of classes to handle whatever is supported by the language differently is never a solution, you need to live with the fact that javascript is like this and instead of re-inventing the wheel and filling up the computer memory of the user's machine, start to look into other program languages and start to understand and gain experience in this industry and then one day you might be able to just go into the javascript interpreter's source code and build something nice into the language that handle problems in a better and resource efficient way and then if you have done that you might start to call yourself a developer.