//------------------------------------------------------------- | |
// | |
// Hypothesis: | |
// | |
// Promises/A is a Monad | |
// | |
// To be a Monad, it must provide at least: | |
// - A unit (aka return or mreturn) operation that creates a corresponding | |
// monadic value from a non-monadic value. | |
// - A bind operation that applies a function to a monadic value | |
// | |
// And it must satisfy the 3 Monadic Laws: | |
// 1. unit a >>= f == f | |
// 2. m >>= unit == m | |
// 3.(m >>= f) >>= g == m >>= (\x -> f x >>= g) | |
// | |
// See: | |
// http://en.wikipedia.org/wiki/Monad_(functional_programming) | |
// http://www.haskell.org/haskellwiki/Monad_Laws | |
// http://mvanier.livejournal.com/3917.html | |
// http://mvanier.livejournal.com/4305.html | |
// | |
// Author: Brian Cavalier (brian@hovercraftstudios.com) | |
// | |
//------------------------------------------------------------- | |
var when, PromiseMonad, a, b, leftResult, rightResult; | |
// Promise manager | |
// Used to create Promises/A promises and to help in implementing | |
// unit and bind for Promises/A | |
when = require('when'); | |
//------------------------------------------------------------- | |
// | |
// Promise Monad | |
// | |
// This defines unit and bind, the two fundamental Monad operations | |
// | |
//------------------------------------------------------------- | |
PromiseMonad = { | |
// Given a value, return a corresponding monadic value | |
// In this case, given a value, return a promise for that value | |
unit: when.resolve, | |
// Apply a function f to the underlying value of a monadic value | |
// In this case, given a promise, apply f to that promise's resolved value | |
bind: when, | |
// Two promises are equivalent if their resolved values are equivalent | |
// This helper observes two promises and compares their underlying | |
// values, and is used to validate the monadic laws for Promises/A below. | |
compare: function(left, right, msg) { | |
return when.all([left, right], function(results) { | |
var eq = results[0] === results[1]; | |
console.log(msg + ': ' + eq); | |
return eq; | |
}).otherwise(function(e) { | |
console.error(e); | |
throw e; | |
}); | |
} | |
}; | |
// Other helpers and setup | |
// Simple helper to produce a monadic value *without* using unit() | |
function m(x) { | |
var d = when.defer(); | |
d.resolve(x); | |
return d.promise; | |
} | |
// Expected values for validation | |
a = { name: 'a' }; | |
b = { name: 'b' }; | |
// Simple test functions | |
function f(x) { return a; } | |
function g(x) { return b; } | |
//------------------------------------------------------------- | |
// | |
// Proof | |
// | |
// Promises/A satisfies the 3 Monadic Laws | |
// | |
//------------------------------------------------------------- | |
//------------------------------------------------------------- | |
// Law 1 | |
// unit a >>= f == f | |
// f bound to unit(a) == f(a) | |
leftResult = PromiseMonad.bind(PromiseMonad.unit(a), f); | |
rightResult = f(a); | |
// A little strange here in that only leftResult is a promise | |
// so technically only have to observe leftResult, but we'll | |
// just reuse PromiseMonad.compare anyway. | |
PromiseMonad.compare(leftResult, rightResult, 'Law 1: unit a >>= f == f'); | |
//------------------------------------------------------------- | |
// Law 2 | |
// m >>= unit == m | |
// unit bound to promise value == promise value | |
leftResult = PromiseMonad.bind(m(a), PromiseMonad.unit); | |
rightResult = m(a); | |
PromiseMonad.compare(leftResult, rightResult, 'Law 2: m >>= unit == m'); | |
//------------------------------------------------------------- | |
// Law 3 | |
// (m >>= f) >>= g == m >>= (\x -> f x >>= g) | |
// Or, perhaps more simply: (f >=> g) >=> h == f >=> (g >=> h) | |
// promise function composition is associative | |
leftResult = PromiseMonad.bind(PromiseMonad.bind(m(a), f), g); | |
rightResult = PromiseMonad.bind(m(a), function(x) { | |
return PromiseMonad.bind(f(x), g); | |
}); | |
PromiseMonad.compare(leftResult, rightResult, 'Law 3: (m >>= f) >>= g == m >>= (\\x -> f x >>= g)'); |
This comment has been minimized.
This comment has been minimized.
I've also had another colleague suggest that a Promise is effectively an Identity monad. Here's a quick take on leftResult = PromiseMonad.unit(PromiseMonad.unit(a)); // M(M(x)) ?
rightResult = PromiseMonad.unit(a); // M(x)
// Logs: Join: M(M(x)) == M(x): true
PromiseMonad.compare(leftResult, rightResult, 'Join: M(M(x)) == M(x)'); |
This comment has been minimized.
This comment has been minimized.
var nested = Promise.unit(Promise.unit(a));
Promise.bind(nested, function (inner) {
// inner is also a Promise
Promise.bind(inner, function (val) {
assert(val === a);
});
}); In your example, you'd want to show that I'm also no expert in monads, but when I saw when and Promises I instantly thought it was monad-ish, which is how I ended up at this gist. It seems like a nesting of the error monad in the continuation monad, in that it handles the plumbing for errors and for CPS, but that's just based on my intuition. Plus some rules are broken to make it more convenient (for instance, |
This comment has been minimized.
This comment has been minimized.
@jonahkagan Ah, thank you for clarifying. I've been debating whether
Which seems intuitive to me, but maybe not to others. The idea is that rejected promises must be explicitly handled or the rejection propagates. That sounds like it may fit with your thoughts about Error+Continuation monads. If you have more thoughts on that, I'd be very interested to hear! |
This comment has been minimized.
This comment has been minimized.
Did you know that Douglas Crockford thinks he is the first to have found exactly that, Promises are a monad. The code for this talk is here. Personally, I strongly believe that the concept (as opposed to Promises/A or a particular implementation like I know that C#'s Tasks are a comonad (a comonad is the categorical dual of a monad; here the difference is roughly that between a "push" model [Promises] and a "pull" model [Tasks]). Re the Continuation monad I'm not sure. That is because what Promises encapsulate / let you abstract from is the _delay_ or _time consumed_ by a computation. Note that this NOT only pertains to parallel (aka "async") computations. Sure, you pass callback functions that can legitimally be viewed as continuations. But hey, every monad has that. The 2nd argument to Re the Identity monad: this is really only a wrapper, nothing else. In a statically typed language as eg Haskell it can (and actually is) simply "compiled away". So in comparison to Promises it at least lacks the Error aspect (dispatching either to Re
It is important to keep in mind that all 3 are meant to have type
Again, notice that the right-hand-side type is Now for
Well, pretty clear which promise
So in that I slightly disagree with
As you said, it's with rejection where things get interesting (and different).
Well, that's just what
From that you can see that
In fact this is true for any monad, because defining |
This comment has been minimized.
This comment has been minimized.
I wrote a blog about this last year: http://kybernetikos.com/2012/07/10/design-pattern-wrapper-with-composable-actions/ |
This comment has been minimized.
This comment has been minimized.
I'd like to see similar proof for Promises/A+. Notice the case when onFulfilled returns promise. |
This comment has been minimized.
This comment has been minimized.
I don't get why the spec doesn't require a promise to be returned from the then/catch callbacks. Then you wouldn't need to have this annoyingly-complicated 'promise resolution procedure', and you could wrap promises in promises. Seems like they're just overcomplicating and restricting things just for the sake of being able to write Oh, and to replace the behaviour of passing a thenable to Unless there is another reason, such as performance? |
This comment has been minimized.
This comment has been minimized.
You can also alias the more common method names (resolve -> of, then -> chain), but here's a quick proof for ES6 Promises.
Promise.resolve won't work quite like a standard MType.of/return/point, unfortunately, so if you wanted to create an .of method, it wouldn't be a simple alias and you'd have to do something like:
And as noted, .then is overloaded/too-smart because it checks the result and doubles as .map if it's just a value, instead having explicit map/flatMap methods. The laws don't really care about map though (since it can be derived from flatMap/chain as long it obeys those laws). It's just that doing so with the overloaded .then becomes a bit silly/pointless:
|
This comment has been minimized.
@briancavalier
This is a cursory assessment, but it looks like
PromiseMonad
is just a functor from some category representing regular JavaScript objects and morphisms to thePromise
category where the objects are promises and the morphisms are from promise to promise (lifted bywhen
).The reason I say that is that there's no
join
equivalent ( see http://en.wikibooks.org/wiki/Haskell/Category_theory#Monads ). That is no way to wrap a Promise in a Promise.