MonadicPromises, revisited
And, if you like the division between then
and chain
:
class MonadicPromise extends Promise {
constructor(exec) {
class MP extends MonadicPromise {
constructor(exec) {
Promise.call(this, (f,r) => exec( v => f([v]), r));
}
}
return new MP(exec);
}
chain(f, r) {
return super.then(v => f(v[0]), r);
}
then(f, r) {
return super.then(v => Promise.resolve(v[0]).then(f, r), r);
}
// See https://github.com/domenic/promises-unwrapping/issues/95
resolve(v) {
return new MonadicPromise(function(r) { r(v); });
}
};
You can put MonadicPromises in your async maps if you wish to be able
to use chain on the values; otherwise they are indistinguishable from
standard promises. There is a bit of extra cost for
MonadicPromise.resolve
, since it always creates a wrapper -- but the
common-case non-monadic Promise doesn't need to pay for this.
On 14 February 2014 00:32, C. Scott Ananian <ecmascript at cscott.net> wrote:
For your consideration, here is an implementation of Monadic Promises, as a Promise subclass:
Since promises are expressible in JS you can always create a new class that does what you want. It's also well-understood that you can do that using the existing class by wrapping all resolution values.
The fundamental problem, however, is that existing producers will not give you instances of the new class, which makes it rather useless in most cases.
Whether the new class is a subclass or not is mostly immaterial regarding that problem. In fact, I would argue that it is incorrect to implement it via subclassing, since you are effectively using a different internal representation (a wrapped value) that the base class methods do not recognize. If, for example, somebody was to add a method like
value() {
if (this.status != resolved) throw TypeError;
return this.value;
}
to the base class then it would not work properly with subclass instances. In other words, this form of subclassing violates substitutability.
My goal was simply to see how feasible it was to write a "autoboxing" Promise subclass that could be used for async maps, the Service Worker API, etc. It turns out that the subclass is quite small and straightforward, although Promise.all() and Promise.race() won't work as-is on these "monadic" promises. The grisly details are mostly at domenic/promises-unwrapping#95 (and in cscott/prfun/blob/monad-wip-1b/lib/index.js#L579-L599).
Anyway, I learned a lot, and I recommend the exercise. It turns out
that enforcing a monadic semantics causes a lot of extra
wrapping/unwrapping throughout the implementation (for example, an
extra .then
call per element in Promise.all
and Promise.race
).
(Again, see the prfun monad-wip-1b branch for details.)
I also implemented bluebird's Promise.bind
using promise subclasses.
I'm pretty happy with how subclasses work for the current Promise
spec. (There are some corner cases, see
domenic/promises-unwrapping#94, but I didn't
run into them in practice.)
For your consideration, here is an implementation of Monadic Promises, as a Promise subclass:
(This version is inexpertly hand-rewritten into ES6; see gist.github.com/cscott/b1966d485807d9a8cc39 for an ES5 version which has been tested to work against paulmillr/es6-shim#215)
class MonadicPromise extends Promise { constructor(exec) { // the promise constructor needs to be new each time, // in order to thwart the optimization in Promise.resolve // (formerly Promise.cast) class MP extends MonadicPromise { constructor(exec) { Promise.call(this, (f,r) => exec( v => f([v]), r)); } } return new MP(exec); }, then(f, r) { return super.then(v => f(v[0]), r); }, // XXX Note that I wouldn't have to override this if it weren't for the // check in step 8 of CreatePromiseCapabilityRecord. resolve(v) { return new MonadicPromise(function(r) { r(v); }); } }; MonadicPromise.prototype.monadic = true; // just for demonstration purposes // Let's test it out! MonadicPromise.resolve(5).then(function(x) { console.log('x is', x); // 5, of course. }); var resolve; new MonadicPromise(function(r) { resolve = r; }).then(function(x) { if (x.monadic) { console.log('this is another promise!'); return MonadicPromise.resolve(x); // wrap it again } }).then(function(x) { console.log('got', x); if (x.monadic) { console.log('still monadic'); return x; } }).then(function(x) { console.log('finally resolved', x); }); resolve(MonadicPromise.resolve(5));
I hope this demonstrates that with the current spec you can, in fact, have your Monadic Promise cake, if that's your preference.
I would suggest that the check in step 8 of CreatePromiseCapabilityRecord be rewritten to reassign the result of the constructor to
capability.promise
; that will make subclasses like this one a bit more straightforward to implement (without having to closely read the spec to figure out why TypeErrors are being thrown).