Practical Intro to Monads in JavaScript: Either
My simple and practical Intro to Monads in JS, where I covered basics of Identity and Maybe monads, seemed to be helpful for a lot of folks, so I’ve decide to continue the topic. Now it's time for Either - a tool for fast-failing, synchronous computation chains. A tool that may increase readability and quality of code while reducing error proneness. The best part is that we can still ignore the category theory!
An ounce of practice is generally worth more than a ton of theory.
Ernst F. Schumacher
All examples are based on monet.js - a tool bag that assists Functional Programming by providing a rich set of Monads and other useful functions. I'll use arrow functions introduced in ES6. They are much more readable than regular functions when used as single operation "callbacks". Also in most examples TypeScript type definitions will be added to enhance overall readability.
Do you remember the Maybe monad? The one that could have been Some(value) or Nothing? This is great when we have a function that can fail in one way - i.e .pop() from an array. If there's no element return Nothing. However, the Maybe monad loses expressivity when we have many things that can go wrong. E.g. getting some value from the encoded JSON - a corrupted JSON, an empty JSON or an empty field that should hold the value. We need a way to communicate multiple failures, or a success, enter Either.
It can have a value on either Right(value) or Left(error) side. For example Some(value) on the Right or an Error corresponding with Nothing on the Left (never both at the same time).
Behold the Either monad!
Either fail or success
Let's take a function from the previous article:
If we cannot get the current user, there must be a reason. So we should get an information why this exceptional situation have happened - an Error object. Look at our new declaration:
It will return either Right(User) or Left(Error). Of course Either can be parameterized with any pair of values, but it's often used as a container for a value (right) or an exception info (left). As monet.js docs state the most common is right biased either (success on the right) which means that .map() and .flatMap() will operate on the right side of the either.
Either in practice
Generally, the use of exceptions for ordinary control flow is considered an anti-pattern. We can choose a real life computation, which fails or returns a value, and decorate it so it returns an Either. For example, it can be an enhanced JSON.parse() which returns Either<Error, any>. It means that if .parse() fails we will get Left(Error) or if it succeeds we'll get Right<any>:
The any type here is because we actually don't know what type of data is serialized as a JSON string. But we can expect it to be a some type T, so our function can look like this:
So now we should implement what we have declared:
Great. So what can we do with that?
I. Parse that stringy JSON
We can for example parse some JSON serialized information, take some value from it and write it into a HTML element. Or log error - why we failed. We can now for e.g. try to parse our JSON and write "title" into <h1/>. So step by step:
Starting point
Let's say we have a JSON data that has a form of:
and it's already stored as a string in const json: string;. So we assume that our parsed JSON should fit this declaration:
Get data
The eitherData value here will be of type Either<Error, Config>. If it's Right(Config) we are pretty sure that it was valid JSON, but what if it's empty?
Catch empty data
Let's make sure that we proceed only if it's concrete:
Why .flatMap()? Remember how we have searched for a name of a Maybe user? This method is useful for converting value of initial Either (and it will work only if it's Right) to another Either - Left or Right. Analogically .map() will do anything with the value of Either only if the Either is Right. So if we have many methods that return Either<E,A> we want to be able to call them sequentially. With Either, the chain between these methods is linked only on the Right side. If any method in the chain returns a Left, the computation fails at that point returning that Left value.
The eitherData is still Either<Error, Config>, but we are now sure that it's not Right(null).
Extract valuable information
Next we need to get a "title" field. And handle the lack of title error:
Note that the current output is eitherTitle of type Either<Error, string>. So now we have Either a Right("Title text") or Left(Error("Failure information")).
Final Error handling
I've used Error objects on the Left which gives almost no information for the type system. It would be much better to introduce the enumeration to carry specific information about what has gone wrong. For example:
…and then we can:
…and fix parseJson so it also uses our custom Err enum value instead of Error object:
Enhance display value
Now, when we are sure we have a "title" string or an exception, we can produce the final display value. Let's map our title to a version prefixed with 'Title: ' and wrapped with ":
Where `Title: "$ Practical Intro to Monads in JavaScript: Either "` is ES6 interpolation.
Note that if parseJSON fails, the whole computation stops. We go straight to end of chain and get Left(Error("Some info). Our computation chain will also stop at any further .flatMap() that returned Left and go to the end. This superpower is called fail-fast error handling.
OK. The first part is done. We have the display information. Now we have to render it.
Cata… clysm once more
Remember wtf is catamorphism? So here we'll use it once more. As a final point of our computations so we can handle either final value or exception. Of course we can do that without .cata():
Pretty verbose. And imperative. Try a more awesome version:
Functional freaks will burn at the stake. So here is more kosher version - move side effects as far away as possible:
The big picture - part I
So now we have a complete form raw JSON to simple view chain:
But that is only a half of what we wanted to achieve.
II. Grab the node
How can we fill element with text?
But where did the element came from? We have to grab it. Some possible ways are .querySelector(selector), .querySelectorAll(selector) or .getElementById(id). I'll use first one to grab a heading and fill it with text:
So what if there is no <h1/> on page? It will throw. So maybe we can check:
Or maybe we can create own element getter that will return a Maybe<HTMLElement>?
The big picture - part II
Now we have working example:
But it ain't no good. We have side effect inside catamorphism. Also flow can stuck in a dead end - if there is no <h1/> then nothing will happen. No action and no information - debugging nightmare. So…
III. Wire it up
First of all, to upgrade developers experience we can do one of the two things:- find a good fallback (.orSome()) for our element getter- provide information about lack of elements corresponding to provided selectorI do not see a good fallback - I'll take more informative approach. Let's make getElem return eitherRight(element) or Left(Err.NoTag) instead of Maybe<HTMLElement>.
Now we can:
Working. But what a mess!
The right path
Let's create something that will take our ingredients and give us a dish. It sounds like a function:
I think it is not enough. We need to pass these ingredients one-by-one and get a factory rather than ready meal. To make it let's add some curry.
Currying
What? Why? Currying is an idea that a function can collect its arguments one-by-one (not all at once) and will do the final computation (and return the final value) when all arguments are in. In the JavaScript world it can be achieved with a function taking the first argument and returning a function taking the second argument which returns the third one… etc. etc. The function that is supposed to take the last value does the computation and returns a value. Our needed value here is no-args-no-return function that will do an I/O operation - will display text. So here it goes:
Of course thanks to ES6 and TypeScript we can simplify it with => syntax sugar:
Now we are able to collect the ingredients step by step in our computation chain, and postpone any I/O operations as long as possible.
Currying in practice
At some point in the article we had such situation:
Now we can go one step further and collect wrapped title into our curried function:
As title => fillElem(title) actually does the same as the fillElem alone, we can:
What do we need it for? Now we can grab element and pass it to our curried function. Let's say we already have const title: string; which is proper "title" extracted successfully from JSON. Now we can pass another ingredient:
Putting few last steps together:
Either render title or log error
So we finally have either render() function or an Error to log:
The big picture - part III (final)
With prepared helpers:
Here is a final, working example:
Visual explanation:And a plunker playground covering this article.
What else?
As you see, using monads (and other beings from the monadic world) requires a little intellectual exercise. But not that much. And at the end of a day you increase readability and quality of code. Application becomes less error prone. And Haskell freaks may even notice you around. This was quite simple example but that's ok - monads are good tools for simple problems. They are even better tools for large, complex problems. I'd say they are Just(awesome).
Consider other use cases:
unpacking Response object
res.ok ? Left(res) : Right(res)
nested relationships can be either Left("id") or Right(Resource)
complicated computations where you need fail
fast error handling
I have covered basics of Identity, Maybe and Either. In next article you can expect meat and potatoes of an error accumulation with the Validation (which is not exactly a monad, but shares a lot with monads).
Further reading:
A Monad in Practicality: First-Class Failures at robotlolita.me
monet.js Either documentation
Maybe monad vs exceptions at StackExchange