Note that these apply mostly to Promise<NonPromiseType>
. Some properties do not hold due to the "unwrapping" of nested promises.
FP definition
Based on Functional Programming definitions all we need are:
- a type constructor:
Promise<T>
for anyT
- a
unit
/ type converter:Promise.resolve(value)
- a
bind
/ combinator:Promise#then
- have unit be left identity of bind (i.e.
unit(x) >>= f <=> f x
):Promise.resolve(x).then(f)
is functionally the same asf(x)
- have
unit
be right identity of bind (i.e.p >>= unit <=> p
):myPromise.then(Promise.resolve)
is functionally the same asmyPromise
-
bind
is associative (i.e.ma >>= λx → (f(x) >>= g <=> (ma >>= f) >>= g
):promise.then(x => f(x).then(g))
is functionally the same aspromise.then(f).then(g)
Thus, we have proven <Promise, Promise.resolve, Promise#then>
is a monad.
Where it blocks
JS promises have one cool trick that makes them viable instead of all the alternatives we ever had (e.g. jQuery's deferred): they unwrap themselves.
For instance: Promise.resolve(2).then(x => Promise.resolve(x * x)
is a Promise<int>
and not a Promise<Promise<int>>
. There are two ways to look at this:
- Nested promises cannot exist as a type
- Nested promises are aliases to their non-nested equivalents
The problems with the proof above is as follows:
const x = Promise.resolve(2);
const f = px => px.then(x => x * 2);
f(x) !== Promise.resolve(x).then(f);
Because Promise#then
is a continuation that gets a value as input (and not a promise), we cannot hold the left identity of unit
of Promises for promises: we can only hold the left identity of Promises for values. That's because Promise#then
plays the role of both map
and flatMap
.
Obviously this is the only situation this will bite you in the ass if you consider Promises monadic, otherwise you're absolutely free to treat them as that and start rewriting non async/await code to proper clean promise-based code:
promise.then(x => {
return fetch(x).then(f);
});
// <=>
promise
.then(fetch)
.then(f);
Top comments (0)