Composing Maybes in Javascript

Modelled on monadic comprehension in Scala and Haskell

Dan Hood
AnyJunk
6 min readApr 23, 2018

--

In my previous article Using Maybes with React-Redux I discussed the use of Maybes in javascript. If you’re not familiar with what a Maybe is then I urge you to take a look at it and then come back to this article as it will all make more sense then.

A problem when using Maybes is that it is awkward to work with many at the same time:

This works, but it’s not very easy to read as it involves several layers of indentation and nested brackets when chaining them. What we’d like is a sensible way of combining and working with multiple Maybes at once. Languages that make heavier use of constructs like Maybe have a way to handle this: in Scala it’s the for keyword and in Haskell it’s do.

This is a lot neater than nesting but javascript doesn’t offer any equivalent by itself so we’re going to have to make one. I will take you through my process for creating it as I believe it could be helpful for understanding the end result, but if you just want to see the final result then please feel free to skip to the end.

We’ll start by working out what we want from the end result. As a side note, since for and do are both protected words in javascript we decided to call this mFor at AnyJunk. This stands for monadic for comprehension, which is actually a generalisation of a for loop. We want something that will accept as many Maybes as we need and will combine them all together with flatMap.

But what if the calculation for one Maybe needs the value from another one? In that case we’re going to need to collect the values as we go and offer them to the next step, and we’re also going to need the things we’re passing in to be functions from the previous values to the next Maybe.

In the particular example we’re using this just made it more confusing but I hope to demonstrate why this is an important feature later. Also because of the way javascript evaluates functions it’s possible to omit any arguments you aren’t using.

Now that we know what we want it’s time to work out how to achieve it.

Since we’ve defined our interface for the function first it makes sense to write that out as a type so that we can check that we’re sticking to our objectives as we go.

At first glance it may not be obvious how we got there from our above requirements so I’ll lead you through it. The easiest bit to understand is the final result. We need to get a Maybe out at the end so the final type must be a Maybe. Ignore the T for now.

The section just before that, ((…_: Array<*>) => T), is about the last section of brackets where we take the values we’ve collected from the Maybes and define how to combine them into a single value. The ellipsis is just the array spread notation which means that you can pass in separate arguments and they’re treated as an array of arguments. So (a, b, c, d) becomes ([a, b, c, d]) — this is important when asking for an arbitrary number of arguments, and I prefer it over passing them in as an array. So this will accept any function from an arbitrary number of arguments to a single value. The T in both of these sections says that the type returned by this function will also be the type in the final Maybe we get out as a result.

Next, the outer part, (…_: Array<MaybeFunction>), just says that we accept an arbitrary number of whatever this MaybeFunction is. The MaybeFunction is then defined as a function from an arbitrary number of arguments (the previous values) to a Maybe of a new value.

You might have noticed that the type does not constrain the types of the functions to taking the same types as the previous values, nor does it constrain us to the correct numbers of arguments. This is an unfortunate limitation of the type system as we have used it here — if you can find an improvement that restricts the types further I would be very interested to learn about it.

Now we know our type it’s time to start actually writing the function.

We know our type so we know what the arguments are going to look like. We also know that since we’re taking an array of objects and returning one object it’s probably going to require the use of reduce. The final thing that’s easy to see is that the last thing we do will have to be map using the last argument.

As we are collecting values we’re going to need to come out of the reduce with a Maybe holding an array. We want to combine the Maybes with flatMap and we want to pass the current values in as arguments and add the new value to that array of arguments for the next one. That leads us to the following function.

And we could stop here. We have a function that could be used as we have specified. But there’s actually one further step that we could take.

A Maybe is actually a particular instance of something called a monad. I won’t get into the exact properties of a monad, but for our purposes it is something that has a flatMap, to combine them and change the value, and a pure, to create a new one. In the case of Maybe the pure is Maybe.Some(x). From those two functions you can also always create map as well since it is just map(f) = flatMap(pure(f)). So we can also assume that every monad has a map function.

(Note that flatMap is sometimes known as bind, and pure is sometimes known as unit or identity.)

Some examples of other monads include Result, which contains either the value or an error; List, and by extension array if you have access to flatten with which you can define a flatMap using map; and actually Promise as well if you were to rename resolve to pure and then to flatMap.

This means that if we know the pure function we can generalise our mFor to allow us to use it for any monad. The pure function usually exists as a static method on the class so I find it more readable to pass in the class rather than the pure function directly, but if you prefer to pass in the function then hopefully it is obvious how to change it.

I also assume in the type that our monads are all children of a Monad class. This works because we have defined our monads as such and so we’ve constrained the type this way to denote that we need all the monads to be the same kind of monad, and we expect the same kind out at the end. You can always just define them as any object that has the properties we listed above.

We do need the monads to be the same type because it is not possible to use flatMap to combine two different types of monad.

So how about a real world example of how to use this mFor? Here is an example we use when we have a route on a map and we need to extrapolate to the rest of the route. Our map component (which will feature in a future blog post) expects a list of the origin and destination of each route.

Hopefully you can see how this neatly handles protecting against undefined values, that can come from a range of different sources, with the Maybes and uses the mFor to make it significantly more readable than it would otherwise be.

If you were to add those renamed methods to Promise as I mentioned above you could do a similar thing for multiple asynchronous API calls as part of a load function. The difference between the mFor and Promise.all in this case is that mFor executes the Promises one after the other and can use the results of one in the next. It can be combined with Promise.all where appropriate.

Both of these functions do the same thing but the second version will execute both loadCollections and loadCustomer simultaneously, while the first does them one at a time. This can only be done if they don’t depend on each other so it still has to wait for loadBooking to finish since the subsequent calls use the result.

I hope this further encourages you to make use of Maybes in your code now that there is an easier way of managing then. And if you are using them already, and/or are using other monads, I hope this helps you improve the maintainability and readability of your code.

--

--