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)