Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are Promises Monads?

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?

like image 465
Jack Spar Avatar asked Aug 16 '17 11:08

Jack Spar


People also ask

What is the purpose of a monad?

A monad is an algebraic structure in category theory, and in Haskell it is used to describe computations as sequences of steps, and to handle side effects such as state and IO. Monads are abstract, and they have many useful concrete instances. Monads provide a way to structure a program.

Is a functor a monad?

A functor is a data type that implements the Functor typeclass. An applicative is a data type that implements the Applicative typeclass. A monad is a data type that implements the Monad typeclass. A Maybe implements all three, so it is a functor, an applicative, and a monad.

Is an array a monad?

Yes, arrays are monads.

How do monads work?

So in simple words, a monad is a rule to pass from any type X to another type T(X) , and a rule to pass from two functions f:X->T(Y) and g:Y->T(Z) (that you would like to compose but can't) to a new function h:X->T(Z) . Which, however, is not the composition in strict mathematical sense.


2 Answers

UDATE. See this new library providing functor and monad operators for plain callback-based functions that do not have the issues with theneables:

https://github.com/dmitriz/cpsfy


The JS 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 `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.


UPDATE.

What does exactly "being a Functor" mean?

There seems to be a confusion between Promise being a Functor/Applicative/Monad as it is, and ways to make it such by changing its methods or adding new ones. However, a Functor must have a map method (not necessarily under this name) already provided, and being a Functor clearly depends on the choice of this method. The actual name of the method does not play any role, as long as the laws are satisfied.

For the Promises, .then is the most natural choice, which fails the Functor law as explained below. None of the other Promise methods would make it a Functor either in any conceivable way, as far as I can see.

Changing or adding methods

It is a different matter whether other methods can be defined that conform to the laws. The only implementation in this direction that I am aware of is provided by the creed library.

But there is a considerable price to pay: not only entirely new map method needs to be defined, but also the promise objects themselves need to be changed: a creed promise can hold a "theneable" as value, while the native JS Promise can't. This change is substantial and necessary to avoid breaking the law in the examples as one explained below. In particular, I am not aware of any way to make the Promise into a Functor (or a Monad) without such fundamental changes.

like image 54
Dmitri Zaitsev Avatar answered Sep 30 '22 17:09

Dmitri Zaitsev


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) 
like image 43
colinro Avatar answered Sep 30 '22 18:09

colinro