promises-aplus / constructor-spec

Discussion and drafts of a possible spec for creating and resolving promises
10 stars 4 forks source link

Extended promise objects #6

Open juandopazo opened 11 years ago

juandopazo commented 11 years ago

Promise objects have only the then method and maybe a few other utility methods. But using promises for chaining asynchronous operations require more complex objects. For instance, jQuery's animate method should return a promise with an animate method so you can do $(selector).animate(opts1).animate(opts2).

I see two basic approaches to this: prototypical inheritance or "parasitic" inheritance (copying properties). How have you folks been dealing with this? What have you learned so far? Would you do anything differently?

domenic commented 11 years ago

I've been doing object-side prototypal inheritance in Chai as Promised. Basically:

function createExtendedPromise(promise) {
  var extendedPromise = Object.create(promise);
  extendedPromise.extraMethod = function () { };
  return extendedPromise;
}

This was somewhat necessary due to Q promises being frozen (and thus non-extensible).

ForbesLindesay commented 11 years ago

I've sometimes used promises as a mixin:

function FluentAPI() {
  let {promise, resolver} = defer();
  this.then = function () {
    //start request
    //then delegate to real then
    return promise.then.apply(promise, arguments);
  };
  //done calls this.then anyway
  this.done = promise.done;
}

Which is necessary if you want a fluent API:

Request()
  .post(url)
  .with(data)
  .then(function (res) {
  });
ForbesLindesay commented 11 years ago

I'm leaning towards saying we should go with Promise(fn) as a constructor, partly because it makes it really easy to do sane inheritance and extension of promise objects. With that in mind, we need to be very careful in the Promise constructor. This, for example, doesn't work:

function PromiseA(fn) {
  fn('RESOLVER_GOES_HERE');
  this.then = ...;
}

function PromiseB(fn) {
  PromiseA.call(this, fn);
}

PromiseB.prototype = new PromiseA();
PromiseB.prototype.constructor = PromiseB;
new PromiseB(function () {});

If we add a check for typeof fn === 'function' in the PromiseA constructor everything works great. We could then add all our extensions to B's prototype.

I think if we think it's desirable to have our promises frozen (as Q does) we should probably require people to instead do something like:

function PromiseA(fn) {
  fn('RESOLVER_GOES_HERE');
  this.then = ...;
}

function PromiseB(fn) {
  PromiseA.call(this, fn);
}

PromiseB.prototype = Object.create(PromiseA.prototype);
PromiseB.prototype.constructor = PromiseB;
new PromiseB(function () {});

Two key points to note about this:

  1. it breaks on ie older than 9. Personally I don't care and think we should do our best to never write websites that work on ie older than 9.
  2. Setting the constructor of PromiseB.prototype is essential in order for #5 to be possible.

This also lets you extend the resolver object by wrapping fn in something which extends the resolver.

lsmith commented 11 years ago

@ForbesLindesay Breaks because of lack of Object.create()? There's the Object.create() polyfill for that. Though not fully feature compatible, it seems pragmatically appropriate here. I haven't done Y.prototype = new X(); in years :)

ForbesLindesay commented 11 years ago

Yes, Object.create can be polyfilled sufficiently as:

function create(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}