Promises in JavaScript are almost monads. With ordinary values, they behave like monads. However, they have some edge behaviour that disqualifies them from attaining monadic status.
#Enter The Monad
In order for something to be considered a monad, two operations must be defined1. Scala calls them unit
and flatMap
, whereas Haskell calls them return
and >>=
.
def unit[A](a: A): F[A]
def flatMap[A,B](a: F[A], f: A => F[B]): F[B]
return :: Monad m => a -> m a
(>>=) :: Monad m => m a -> (a -> m b) -> m b
Additionally, these two operations must fulfill certain laws:
#Left Identity
flatMap(unit(a), f) == f(a)
def f(x: Int) = Some(x + 1)
Some(1).flatMap(f) == f(1)
#Right Identity
flatMap(a, x => unit(x)) == a
Some(1).flatMap(x => Some(x)) == Some(1)
#Associativity
flatMap(flatMap(a, f), g) == flatMap(a, x => flatMap(f(x), g))
def f(x: Int) = Some(x + 1)
def g(x: Int) = Some(x + 2)
Some(1).flatMap(f).flatMap(g) == Some(1).flatMap(x => f(x).flatMap(g))
These three laws must hold for all values of a
, f
, and g
.
#Almost Famous Monadic
The analogues of unit
and flatMap
in JavaScript promises are Promise.resolve
and Promise.then
.
As you can see below, promises do behave like monads for most ordinary values.
For left identity:
const f = x => Promise.resolve(x + 1);
Promise.resolve(1).then(f); // Promise { 2 }
f(1); // Promise { 2 }
For right identity:
Promise.resolve(1).then(x => Promise.resolve(x)); // Promise { 1 }
Promise.resolve(1); // Promise { 1 }
And lastly for associativity:
const g = x => Promise.resolve(x + 2);
Promise.resolve(1).then(f).then(g); // Promise { 4 }
Promise.resolve(1).then(x => f(x).then(g)); // Promise { 4 }
So where’s the catch? Remember the Monad laws should hold, even if the value of a
is itself already a Monad instance. In Scala, this could be represented by a value like Option[Option[A]]
. It’s up to you to assign semantic meaning to a value like this, but an example could the result of 2 sequential network calls, either of which has a significant chance of failing.
Some(Some(2)).flatMap(x => x.flatMap(y => Some(y + 2))) == Some(Some(4))
This is where JavaScript promises fall flat. Promises don’t like being nested. They will automatically resolve whatever is inside if it happens to a thenable. To demonstrate this, see below:
> Promise.resolve(Promise.resolve(2))
Promise { 2 }
Or an equivalent thenable:
> Promise.resolve({ then: (x) => x(2) })
Promise { 2 }
So how does this break the Monad laws?
const obj = { then: x => x(2) };
const f = x => Promise.resolve(x.then);
f(obj).then(res => console.log(res));
Promise.resolve(obj)
.then(f)
.then(res => console.log(res));
If you run the above, The first line prints x => x(2)
, as expected, but the second line prints undefined
. This is because instead of passing a “nested” promise to .then(f)
, Promise.resolve
“unwraps” the thenable, passing only a singly-wrapped promise. So what f
ends up receiving in its argument is the function x => x(2)
, instead of the entire { then: x => x(2) }
object.
If you refer back to the laws, you’ll see that the left identity law is violated.
#Footnotes
#Footnotes
-
There is more than one way to specify this combination of operations, and they are all equivalent. Instead of implementing
unit
andflatMap
, one could also implementunit
,map
, andjoin
, orunit
andcompose
.join
’s function signature isF[F[A]] => F[A]
, whereascompose
’s signature is(A => F[B], B => F[C]) => (A => F[C])
. ↩