-
-
Save briancavalier/3296186 to your computer and use it in GitHub Desktop.
| //------------------------------------------------------------- | |
| // | |
| // 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 ([email protected]) | |
| // | |
| //------------------------------------------------------------- | |
| 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)'); |
I'd like to see similar proof for Promises/A+. Notice the case when onFulfilled returns promise. Then works then as flatMap. If returned value is not promise then it works as map.
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 return value; instead of return Promise.resolve(value);.
Oh, and to replace the behaviour of passing a thenable to Promise.resolve, new Promise(thenable.then.bind(thenable)).
Unless there is another reason, such as performance?
You can also alias the more common method names (resolve -> of, then -> chain), but here's a quick proof for ES6 Promises.
//Left Identity
(function(){
var value = 6;
var p_func = x => Promise.resolve(value+6);
var left = Promise.resolve(value).then(p_func);
var right = p_func(value);
Promise.all([left, right]).then(([lVal,rVal]) => console.log(lVal === rVal, {lVal,rVal}));
}());
//Right Identity
(function(){
var value = 6;
var left = Promise.resolve(value);
var right = Promise.resolve(value).then(x=> Promise.resolve(x));
Promise.all([left, right]).then(([lVal,rVal]) => console.log(lVal === rVal, {lVal,rVal}));
}());
//Associativity
(function(){
var f = a => Promise.resolve(a * a);
var g = a => Promise.resolve(a - 6);
var m = Promise.resolve(7);
var left = m.then(f).then(g);
var right = m.then( x => f(x).then(g) );
Promise.all([left, right]).then(([lVal,rVal]) => console.log(lVal === rVal, {lVal,rVal}));
}());
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:
Promise.prototype.of = x => Promise.resolve(x);
Promise.of = Promise.prototype.of;
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:
Promise.prototype.map = function(fn){
return this.then( x=> Promise.resolve(fn(x)) );
};
I wrote a blog about this last year: http://kybernetikos.com/2012/07/10/design-pattern-wrapper-with-composable-actions/