Why are Promises Monads?

2019-02-06 12:33发布

问题:

I've been learning about functional programming and have come across Monads, Functors and Applicatives.

From my understanding the following definitions apply:

a) ( A=>B ) => C[A] => C[B] | Functor

b) ( A=>C[B] ) => C[A] => C[B] | Monad

c) ( C[A=>B] ) => C[A] => C[B] | Applicative

(reference: https://thedet.wordpress.com/2012/04/28/functors-monads-applicatives-can-be-so-simple/)

Furthermore, I understand a Monad is a special case of a Functor. As in, it applies a function that returns a wrapped value to a wrapped value and returns a wrapped value.

When we use Promise.then(func), we are passing the Promise(i.e. C[A]) a function which normally has signature A => B and return another Promise (i.e. C[B]). So my thinking was that a Promise would only be a Functor and not a Monad as func returns B and not C[B].

However, googling I found out that a Promise is not only a Functor, but also a Monad. I wonder why, as func does not return a wrapped value C[B] but just B. What am I missing?

回答1:

Promise is neither a Functor nor an Applicative nor a Monad

It is not a functor, because the composition preservation law (sending compositions of functions to compositions of their images) is violated:

promise.then(x => g(f(x))) is NOT equivalent to promise.then(f).then(g)

What this means in practical terms, it is never safe to refactor

promise
  .then(x => f(x))
  .then(y => g(y))

to

promise
  .then(x => g(f(x))

as it would have been, were Promise a functor.

Proof of the functor law violation. Here is a counter-example:

//Functor composition preservation law:
// promise.then(f).then(g)  vs  promise.then(x => g(f(x)))

// f takes function `x` and saves it in object under `then` prop:
const f = x => ({then: x})

// g returns the `then` prop from object 
const g = obj => obj.then

// h = compose(g, f) is the identity
const h = x => g(f(x))

// fulfill promise with the identity function
const promise = Promise.resolve(a => a)

// this promise is fulfilled with the identity function
promise.then(h).then(res => {
    console.log("then(h) returns: ", res)
})
// => "then(h) returns: " a => a

// but this promise is never fulfilled
promise.then(f).then(g).then(res => {
    console.log("then(f).then(g) returns: ", res)
})
// => ???

// because this one isn't
promise.then(f).then(res => {
    console.log("then(f) returns: ", res)
})

Here is this example on Codepen: https://codepen.io/dmitriz/pen/QrMawp?editors=0011

Explanation

Since the composition h is the identity function, promise.then(h) simply adopts the state of promise, which is already fulfilled with the identity a => a.

On the other hand, f returns the so-called thenable:

1.2. “thenable” is an object or function that defines a then method.

To uphold the functor law, .then would have to simply wrap into promise the result f(x). Instead, the Promise Spec requires a different behavior when the function inside .then returns a "thenable". As per 2.3.3.3, the identity function id = a => a stored under then key is called as

id(resolvePromise, rejectPromise)

where resolvePromise and rejectPromise are two callback functions provided by the promise resolution procedure. But then, in order to be resolved or rejected, one of these callback functions must be called, which never happens! So the resulting promise remains in the pending state.

Conclusion

In this example, promise.then(x => g(f(x))) is fulfilled with the identity function a => a, whereas promise.then(f).then(g) remains in the pending state forever. Hence these two promises are not equivalent and therefore the functor law is violated.


Promise is neither a Monad nor an Applicative

Because even the natural transform law from the Pointed Functor Spec, that is part of being Applicative (the homomorphism law), is violated:

Promise.resolve(g(x)) is NOT equivalent to Promise.resolve(x).then(g)

Proof. Here is a counter-example:

// identity function saved under `then` prop
const v = ({then: a => a})

// `g` returns the `then` prop from object 
const g = obj => obj.then

// `g(v)` is the identity function
Promise.resolve(g(v)).then(res => {
    console.log("resolve(g(v)) returns: ", res)
})
// => "resolve(g(v)) returns: " a => a

// `v` is unwrapped into promise that remains pending forever
// as it never calls any of the callbacks
Promise.resolve(v).then(g).then(res => {
    console.log("resolve(v).then(g) returns: ", res)
})
// => ???

This example on Codepen: https://codepen.io/dmitriz/pen/wjqyjY?editors=0011

Conclusion

In this example again one promise is fulfilled, whereas the other is pending, therefore the two are not equivalent in any sense, violating the law.



回答2:

Promise is (a lot like) a monad because then is overloaded.

When we use Promise.then(func), we are passing the Promise(i.e. C[A]) a function which normally has signature A => B and return another Promise (i.e. C[B]). So my thinking was that a Promise would only be a Functor and not a Monad as func returns B and not C[B].

this is true for then(Promise<A>, Func<A, B>) : Promise<B> (if you'll excuse my pseudocode for javascript types, I'll be describing functions as though this were the first argument)

the Promise API supplies another signature for then though, then(Promise<A>, Func<A, Promise<B>>) : Promise<B>. This version obviously fits the signature for monadic bind (>>=). Try it out yourself, it works.

however, fitting the signature for a monad doesn't mean that Promise is a monad. it also needs to satisfy the algebraic laws for monads.

the laws a monad must satisfy are the law of associativity

(m >>= f) >>= g ≡ m >>= ( \x -> (f x >>= g) )

and the laws of left and right identity

(return v) >>= f ≡ f v
m >>= return ≡ m

in JavaScript:

function assertEquivalent(px, py) {
    Promise.all([px, py]).then(([x, y]) => console.log(x === y));
}

var _return = x => Promise.resolve(x)
Promise.prototype.bind = Promise.prototype.then

var p = _return("foo")
var f = x => _return("bar")
var g = y => _return("baz")

assertEquivalent(
    p.bind(f).bind(g),
    p.bind(x => f(x).bind(g))
);

assertEquivalent(
    _return("foo").bind(f),
    f("foo")
);

assertEquivalent(
    p.bind(x => _return(x)),
    p
);

I think anyone familiar with promises can see that all of these should be true, but feel free to try it yourself.

because Promise is a monad, we can derive ap and get an applicative out of it as well, giving us some very nice syntax with a little ill-advised hackery:

Promise.prototype.ap = function (px) {
    return this.then(f => px.then(x => f(x)));
}

Promise.prototype.fmap = function(f) {
    return this.then(x => f(x));
}

// to make things pretty and idiomatic
Function.prototype.doFmap = function(mx) {
    return mx.fmap(this);
}

var h = x => y => x + y

// (h <$> return "hello" <*> return "world") >>= printLn
h.doFmap(_return("hello, ")).ap(_return("world!")).bind(console.log)


回答3:

Promises Are Not Monads Over Objects Containing a Then Property

Promises treat objects containing a then property which is a function as a special case. Because of this, they violate the law of left identity as below:

//Law of left identity is violated
// g(v) vs Promise.resolve(v).then(g)

// identity function saved under `then` prop
const v = ({then: x=>x({then: 1})})

// `g` returns the `then` prop from object wrapped in a promise
const g = (obj => Promise.resolve(obj.then))

g(v).then(res =>
          console.log("g(v) returns", res))
// "g(v) returns" x => x({ then: 1 })


Promise.resolve(v).then(g)
  .then(res =>
        console.log("Promise.resolve(v).then(g) returns", res))
// "Promise.resolve(v).then(g) returns" 1

example on codepen

This happens because resolve treats the function under the then property as a callback, passing the continuation of the then chain in as the argument rather than creating a promise containing it. In this way, it does not function like unit and causes a violation of the monad laws.

However, over values which do not contain a then property, it should function as a monad.



回答4:

According to me, Promises are Functors, Applicative Functors and Monads since they obey the functor and monads laws.

Ok, lets study the functor case. For Promises to be an instance of Functor we must define an fmap function (a -> b) - f a -> f b for Promises and fmap shall pass the Functor laws. What are functor laws?

fmap id      = id
fmap (p . q) = (fmap p) . (fmap q)
  • id is identity function. We can simply implement it in JS like var id = x => x
  • The . in (p . q) is the composition operation just like in Math. It's essentially var dot = p => q => x => p(q(x)) in JS.

The problem in JS is that the objects, including the functions are reference types which means unlike in Haskell, every time you partially apply a function, you will get a different function doing the same thing. So just the equity checks in the following laws will fail but they will pass if you check the resulting values.

var id   = x => x,
    dot  = f => g => x => f(g(x)),
    fmap = f => p => p.then(v => f(v)),
    pr1 = Promise.resolve(1);
    
fmap(id)(pr1) === id(pr1); // false since objects are mutable
fmap(id)(pr1).then(v => console.log(v));
id(pr1).then(v=> console.log(v));

fmap(dot(x => x*2)(y => y+5))(pr1).then(v => console.log(v));
dot(fmap(x => x*2))(fmap(y => y+5))(pr1).then(v => console.log(v));

So yes Promises are Functors and if you check the Monad laws you can easily tell that they are also Monads.