The JavaScript Promise is a tool for asynchronous operation. However, it is a whole lot more powerful than that.
The promise's then
method can be thought to act both like map and flatMap.
Arrays, map, flatMap, Functors, and Monads
Recall that in JavaScript arrays, map
allows you to take an array, and get a totally new array, with each elements entirely transformed. In other words, map
takes an array (implicitly), a function, and returns another array.
So, for instance, if you wanted to derive an array of strings from an array of numbers, you would invoke the map
method, by supplying a function.
Here's an example.
const nums = [ 1, 2, 3, 4, 5 ];
const strs = nums.map(n => n.toString());
// Should be:
// [ '1', '2', '3', '4', '5' ]
Because arrays implement a map
method, you can think of arrays as functors.
Arrays also implement a flatMap
method. Like map
, it is also used to derive an entirely new array. But the key difference here is that rather than the supplied function returning the transformed value, it can return it wrapped inside an array.
const nums = [ 1, 2, 3, 4, 5 ];
const strs = nums.flatMap(n => [ n.toString() ]);
// Note: we're returning an ARRAY with a single string!
// Should be:
// [ '1', '2', '3', '4', '5' ]
In case your wondering: yes, the returned array can absolutely have more than one element in it. Those values will simply be concatenated into the final result.
Because arrays implement flatMap
, you can think of arrays as Monads.
About Functors and Monads
Functors and monads are two constructs that hold value.
Functors implement map
, and monads implement flatMap
.
Functors and monads can be defined to hold any number of values, whether it be strictly one, two, three, or unlimited.
Promises as Functors and Monads
The JavaScript promise represents a construct that holds a single value.
A promise's then
method acts as both map
, and flatMap
.
The method then
, like map
, and flatMap
, will always return a promise.
With then
, you can have the function return a non-promise value. This will have then
act like an array's map
method. Or, you can have that function return a promise. This will have then
act like an array's flatMap
method.
Here is then
acting like map
.
promise.then((x) => {
return x + 42;
});
Here is then
acting like flatMap
.
promise.then((x) => {
// Note: Promise.resolve will return a promise.
return Promise.resolve(x + 42);
});
Monad laws with promise
Monads have laws. Think of them like Newton's three laws of motion.
These are:
- left-dentity
- right-identity
- associativity
Because promises can be interpreted as monads, you most certainly can use then
to follow the three laws.
Let's demonstrate. First, let's assume that the functions f
and g
accept a value and returns a promise, and p is a promise.
Left-identity
Promise.resolve(x).then(f)
// Is equivalent to
f(x)
Right-identity
p.then(Promise.resolve)
// Is equivalent to
p // I'm serious. that's all there is to it.
Associativity
p.then(x => f(x).then(g))
// Is equivalent to
p.then(f).then(g)
Monadic error handling in Promise
Traditionally flatMap
(the then
in promises) is very instance-specific. After all, you can substitute the name flatMap
with whatever name you want, so long as the instance behaves like a monad. And in the case of promises, flatMap
is called then
.
Other than the name (then
instead of flatMap
), the way it is implemented can be different from instance to instance.
And in the case of Promises, it can be implemented so that then
does not evaluate if the Promise holds no value other than an error.
For example
Promise.reject(new Error('Some error'))
.then(() => {
console.log('Wee!');
// Trust me. Nothing will happen here.
});
In order to do anything with the promise, you will need to invoke the catch
method. The catch
method will return a promise, just like then
.
However, while then
will only evaluate the function if the promise holds a value, catch
will evaluate the function if the promise holds an error.
Promise.reject(new Error('Some error'))
.then(() => {
console.log('Wee!');
// Trust me. Nothing will happen here.
return Promise.resolve(1);
})
.catch(() => {
console.log('Caught an error!')
return Promise.resolve(42);
})
.then(x => {
console.log(x);
// Will log 42, not 1.
});
Interestingly enough, the monad laws will also work with catch
, as well as then
.
Conclusion
So this article went over what a monad is, and how promises can be thought of as monads. To put it in simple terms, an object can be thought of as a monad, as long as it implements some method that looks like flatMap
.
Top comments (0)