promises-aplus / promises-spec

An open standard for sound, interoperable JavaScript promises—by implementers, for implementers.
https://promisesaplus.com/
Creative Commons Zero v1.0 Universal
1.84k stars 164 forks source link

Assimilation #73

Closed ForbesLindesay closed 11 years ago

ForbesLindesay commented 11 years ago

I would like a repository to discuss Assimilation. I have made the following draft to get the ball rolling:

Assimilation/A+ - Draft A

This spec aims to specify a method for converting a thenable into a promise in the users preferred form (for libraries). A good use case for this is that of the utility function timeout:

function timeout(promise, time) {
  return promise.assimilate({
    then: function (onFulfilled, onRejected) {
      promise.then(onFulfilled, onRejected);
      setTimeout(onRejected.bind(null, new Error('Operation Timed Out')), time);
    }
  })
}

It is not expected that all Promises/A+ promises will include this extension so if you want to support promise assimilation you should check for its support:

if (typeof promise.assimilate === 'function') {
  //use `assimilate` assuming it conforms to the contract below
}

Motivation

Simple utility functions like timeout should ideally be written once and be usable independently of the users personal preference for promise library. As such, utility functions like timeout need to return functions of the same type that they take in as arguments. The simplest method for doing this is to allow libraries to assimilate their own promises into the correct promise type.

Requirements: the assimilate method

A promise implementing this specification must have an assimilate method. The assimilate method must act as a function. i.e. the following are equivalent

//as a function
var assimilate = promise.assimilate;
assimilate(thenable);
//as a method
promise.assimilate(thenable);
  1. The assimilate method takes a single argument: thenableArg
  2. If thenableArg does not have a then method (i.e. thenableArg && typeof thenableArg.then === 'function' is false)
    1. It returns a promise (call it result)
    2. result is fulfilled with thenableArg
  3. If thenableArg has a then method (i.e. thenableArg && typeof thenableArg.then === 'function' is true)

    1. It returns a promise (call it result)
    2. It calls thenableArg.then providing two arguments, onFulfiled and onRejected
      1. If onFulfilled is called, result is fulfilled with the first argument of onFulfilled
      2. If onRejected is called, result is rejected with the first argument of onRejected
      3. It may optionally provide a third argument, onProgress, which should be used to fire progress handlers in a library specific way.
    3. thenableArg.then must not be called until the call to promise.assimilate has returned.

      Notes

    Optionally, if the thenableArg has other methods such as cancel these can be used as appropriate.

ForbesLindesay commented 11 years ago

A fuller example for the timeout method that supports optional extensions like cancel might look like:

var Promise = require('promise');
function timeout(promise, time) {
  var assimilate = typeof promise.assimilate === 'function' ? promise.assimilate : Promise.from;

  var res = Object.create(promise);
  res.then = function (onFulfilled, onRejected) {
    promise.then.apply(promise, arguments);
    setTimeout(onRejected.bind(null, new Error('Operation Timed Out')), time);
  };

  return assimilate(res);
}
domenic commented 11 years ago

That's a pretty ugly timeout compared to https://github.com/promises-aplus/resolvers-spec/issues/15#issuecomment-13362619; why?

ForbesLindesay commented 11 years ago

Because yours doesn't handle:

Mine has a fallback if the input promise does not support assimilation, propagates cancellation because Object.create means we fall through to cancelling the input promise, passes all the arguments to the promises then method, thereby ensuring that both rejection and progress are handled.

If you don't need any of these things then:

function timeout(promise, time) {
  var Promise = promise.constructor;

  return new Promise({ then: function (resolve, reject) {
    promise.then(resolve, reject);
    setTimeout(function () {
      reject(new Error('Operation timed out.'));
    }, time);
  } });
}

seems to me no prettier than:

function timeout(promise, time) {
  return promise.assimilate({ then: function (resolve, reject) {
    promise.then(resolve, reject);
    setTimeout(function () {
      reject(new Error('Operation timed out.'));
    }, time);
  } })
}

And is functionally equivalent provided that promise supports promise.assimilate and promise.constructor

domenic commented 11 years ago

It's weird to use promise.assimilate to produce a result that isn't related to promise. In other words, it's weird to attach non-method functions to a promise.

ForbesLindesay commented 11 years ago

I think assimilation is probably a (rare) use case that merits it though. If I'm a user of Q then an assimilate method on a Q promise seems odd because it has nothing to do with that promise. If I'm an author of a (promise library agnostic) library then an assimilate method on a Q promise seems fine, because it is related to that promise in that it's the way you create a promise of the same type as that promise.

bergus commented 11 years ago

Am I wrong or can assimiliate completely replaced (shimmed?) with a then call:

Promise.prototype.assimilate = function(maybeThenable) {
      return this.then(function() {
            return maybeThenable;
      });
 };
ForbesLindesay commented 11 years ago

No, it can't be, because the promise this refers to may never become fulfilled (or rejected) so that method is completely useful for building something like timeout

domenic commented 11 years ago

I think this is probably taken care of by a combination of the resolution procedure and the upcoming promise-creation spec. E.g.

var assimilated = new promise.constructor(resolve => resolve(valueToAssimilate));

Agree/disagree?

briancavalier commented 11 years ago

@domenic that looks right to me.

ForbesLindesay commented 11 years ago

yep, that's fine with me.