What about an asynchronous Either?
How the JS Promise design overlaps with the Either abstraction
Today, a few years after Promises took over our JavaScript day-to-day life, we look back and miss the good old callback days.
Remember how easy it was to just connect to a database, then reading something from it only to make a few calculations and save it back? Those three indentation levels on your last pseudo error handling bit. Those loyal if statements checking the truthiness of a solemn error variable, only it doesn't know exactly what to do when it invariably comes loaded with some bad news.
Yeah, no one misses those days. Callbacks are fine as a low-level construct, but when you want to coordinate many of them efficiently and safely, you'll most likely end up in the bad place everyone kept telling you about.
Enter Promises
Yeah, we had them in our JS lives way before the official inclusion in our beloved ECMAScript 2015. Libraries like bluebird and q had already implemented their versions of this amazing abstraction that would change our lives forever and everyone who was 'in the know' was using them before it was cool.
Nowadays Promises are ubiquitous and we dread the minutes we spend writing callbacks on that legacy code we have to support once a week or even using that renegade library with Peter Pan syndrome. (Even setTimeout
seems weird to me, to be honest.)
We all know what Promises look like in the wild, but what I find particularly interesting are the similarities they have with the more formal Either
type. If you know yourself a Haskell, you are most likely familiar with it. For Rustacians, it's ever-present but with another name. If you use Elixir, you are probably used to a kind of it, using tuples, atoms, and some syntax. But if you write some JavaScript, you live and breathe Either without even knowing.
Either… what?
Either is just a type that represents one of two things. That's it. It could be an int
or a string
. It could be an apple or a signed copy of your favorite album. It could Either
be something or another something.
If it seems too simplistic, that's because it is.
The formal definition you'll most likely see is something along the lines of Either a b = Left a | Right b
. The a
and b
can be anything, they're parameters. The |
means it's either one or the other. It's either the Left
one with a value like a
, or the Right
one with a value of type b
. Can it be both? Can it be neither? Can it be Left
with b
? Right
with a
? No. (:
Ok, Either then. So what?
You might have realized this definition fits the Promise
semantics pretty well. It has two kinds of result, two pipelines of processing, and it only ends up in one of them. Never none and never both.
In this sense, the resolve
and reject
functions on the Promise
module are just simple constructors. Might we even call them… Left
and Right
?
I know, I know. It's a rather weak parallel and it doesn't mean much alone, but bear with me, there's more.
'map' isn't just for lists
Either
works as functors, which means you can apply a transformation on the underlying data without touching the containing structure. In other words, you can map
values into other values, without losing the Either
container.
Since you don't know which of the two possible values the Either
has, you usually have two mapping functions: one that operates on the left value and one that operates on the right one. Thankfully, the common names are helpful in this case: mapLeft / mapRight.
How does this translate to JS Promise? When stuff goes right, you can chain transformations on the data calling then
. When stuff goes wrong, you can deal with the error chaining catch
calls.
Now if you're following me on this, you might be thinking: you could map to other Promises in JS. What happens to the Either
then?
Since you're used to promises, you know you don't end up in situations like this one. Nested promises aren't a thing, you don't have to worry about both of them. They are collapsed into one, right?
'flatMap' isn't just for lists either
Turns out that's a monadic property and it can be found in the Either
type as well. There's a function that also maps the values, but, instead of mapping them just inside the container, it will let you map to another container and will merge those two containers into one.
In Either
terms, you can map a value into another Either
. The function will leave you with just one Either
that resulted from the combination of both. In Promise
terms, you can map a value to another Promise
, and you'll be left with one Promise
that combines both of them.
This (very powerful) function is usually called flatMap
or andThen
. In JS, it's called then
(or catch
). The same function that works like mapRight
works as flatMap
, depending on what you return. So the above code's semantics would actually be:
Why isn't it called Either instead of Promise then?
Fair question. I'm making a case that the Promise
semantics we see in current JavaScript borrows a lot from the semantics of the Either
abstraction. They aren't quite the same though, especially because of a simple overlooked fact until now: Either
has nothing to do with asynchronous workflows, and that's all the JS Promise
is about.
They came and took the land once ruled by callbacks. Synchronous workflows are easy and we needed no promises for that. They came to help with the hard bit, which is async workflows!
Yes, that's right. The thing is, Promise
is a known abstraction as well. It just isn't as sophisticated as the JS implementation. Promise
(in general) is a simple abstraction that represents a delayed value. In its simplest form, it doesn't support mapping, chaining computation or even error handling. It is just a value that will be there at some point but isn't right now. Whoever needs the value, will have to wait for it.
Waiting usually means dereferencing it or blocking the thread until the value is ready. In modern JS, we have a handy dereferencing function coded into a keyword many people love: await
.
A JS Promise
is, in fact, aPromise
.
Thing is, it isn't just a Promise
, it is something more. It's almost like they are a Promise
that resolves to an Either
. Or an Either
with a Promise
on each end.
JS Promises rock!
All in all, I really love the JS Promise
abstraction and implementation, and it's not just because everyone was traumatized by working with callbacks.
The concept takes the simplistic and powerful Promise
async primitive, throws in some error handling mechanics, and spices it up with powerful mapping functions that can be chained together. It then gets packaged with a ridiculously simple API, and there you go.
What we end up with is much cleaner code that makes asynchronous computations a lot easier to read while also making errors easier to manage and harder to ignore.
It's classy and precise.