Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft D #5

Closed
ForbesLindesay opened this issue Feb 5, 2013 · 28 comments
Closed

Draft D #5

ForbesLindesay opened this issue Feb 5, 2013 · 28 comments

Comments

@ForbesLindesay
Copy link
Member

Promieses/A+ Extension: Synchronous Inspection

This proposal extends the Promises/A+ specification to cover synchronous inspection of a promise's fulfillment value or rejection reason.

It is not expected that all Promises/A+ implementations will include this extension. If the features of this extension are desired, you should test for them:

if (typeof promise.asap === 'function') {
  // Use the `asap` method, assuming it conforms to the contract below.
}

Motivation

TODO

Requirements: the asap method

A promise's asap method accepts two arguments:

promise.asap(onFulfilled, onRejected)
  1. Both onFulfilled and onRejected are optional arguments:
    1. If onFulfilled is not a function, it must be ignored.
    2. If onRejected is not a function, it must be ignored.
  2. If onFulfilled is a function:
    1. it must be called after promise is fulfilled, with promise's fulfillment value as its first argument.
    2. it must not be called more than once.
    3. it must not be called if onRejected has been called.
  3. If onRejected is a function,
    1. it must be called after promise is rejected, with promise's rejection reason as its first argument.
    2. it must not be called more than once.
    3. it must not be called if onFulfilled has been called.
  4. asap may be called multiple times on the same promise.
    1. If/when promise is fulfilled, respective onFulfilled callbacks must execute in the order of their originating calls to asap.
    2. If/when promise is rejected, respective onRejected callbacks must execute in the order of their originating calls to asap.
  5. asap returns undefined and allows errors to be thrown synchronously.

Note

This deliberately makes the API for synchronous inspection similar to that of asynchronous inspection to discourage anti-patterns like polling for completion. It is also optimized for the common use case of a promise that may or may not be completed already.

@ForbesLindesay
Copy link
Member Author

The advantage of this over Draft C is that this avoids the creation of an extra (frequently wasted) promise.

@timjansen
Copy link

I must admit that I don't understand the purpose of asap(). Unlike then(), it does not create a new promise and the return values of the callback functions will also be ignored. But the callback handlers will be called asynchronously, right? So how is this a synchronous extension?
(Aside from that, I love the short name "asap()". The earlier drafts used very long property names that were difficult to spell)

@domenic
Copy link
Member

domenic commented Feb 5, 2013

asap is a complete no-go. It makes it too easy. "Of course I want my values ASAP!" It also mixes synchronous and asynchronous behavior, sometimes calling synchronously and sometimes asynchronously.

Aside from that, I love the short name "asap()". The earlier drafts used very long property names that were difficult to spell

That's the point: synchronous value retrieval should be as difficult as possible, to discourage it. An API's ease of use should be proportional to how often it is expected to be used, and synchronous value retrieval is rare.

@briancavalier
Copy link
Member

I agree with @domenic. If we do end up specifying sync inspection, it should be entirely un-ergonomic. It should make you think twice before doing it.

@ForbesLindesay
Copy link
Member Author

@timjansen It calls its callbacks synchronously if it's already resolved (fulfilled or rejected) thus allowing synchronous inspection.

The following works fine:

Q.isPending = function (promise) {
  var isPending = true;
  promise.asap(function () { isPending = false; },
               function () { isPending = false; });
  return isPending;
};

Q.isFulfilled = function (promise) {
  var isFulfilled = false;
  promise.asap(function () { isFulfilled = true; });
  return isFulfilled;
};


Q.isRejected = function (promise) {
  var isRejected = false;
  promise.asap(undefined, function () { isRejected = true; });
  return isRejected;
};

It's doesn't return a promise (unlike then) for 2 reasons:

  1. To make it un-ergonomic in the common use case of promises, so people don't use it when they should use then
  2. To make it perform faster in the correct use cases of either all(promise1, promise2, ...) or assimilation

If you're doing assimilation you don't need the synchronous resolution.

@domenic

I've made a deliberate attempt to discourage the over-use of asap by not returning a promise in this spec.

The following won't work:

doSomething()
  .asap(function (res1) {
    return doSomething(res1);
  })
  .asap(function (res2) {
    return doSomething(res2);
  });

And to discourage polling (which wouldn't work well with any of the solutions). I don't have a problem with giving it an alternative much longer name if that would be preferred, but I do think synchronous inspection is needed. I'll talk more on that in #3 though. We can debate how much it should be used along with motivation for it.

@jkroso
Copy link

jkroso commented Feb 6, 2013

just a thought. It may as well return whatever the on* function returns. That way you can just reuse an identity function if you know for sure the promise is already fulfilled.
A bit of topic but I'll ask anyway, can someone enlighten me on the thinking behind why everything must be async? I'll state as much as I know, and that is, the reason for this is to make it safe to write sync code which your handlers depend on after the call to then. Is there more to it or are my scales just balanced differently?

@timjansen
Copy link

@ForbesLindesay Thanks. I see, for synchronous polling it is definitely un-ergonomic. Unless, of course, you provide a method like Q.isPending() :)

But couldn't you implement isPending() like this, using then() instead of asap()?

Q.isPending = function (promise) {
var isPending = true;
promise.then(function () { isPending = false; }, function () { isPending = false; });
return isPending;
};

The only difference between asap() and then() that I can see is that asap() is a tiny little bit faster, depending on the Promises implementation's overhead, because asap() does not need to create a new promise and evaluate the handler's return value.

So in my understanding, synchronous inspection would be a a feature that improves the performance of polling somewhat, but does neither extend functionality nor provide convenience.

@jkroso
Copy link

jkroso commented Feb 6, 2013

@timjansen according to the promiseA+ spec then should always be async even if it doesn't have to be. asap is being discussed because in some cases sync behaviour becomes desirable enough that people consider it a good idea. Personally I consider it a good idea all the time and thats why I placed the comment above yours :)

@timjansen
Copy link

@jkroso thanks, I see, my isPending() version does not work because then()'s callbacks are called from the event loop..

@ForbesLindesay
Copy link
Member Author

@jkroso as I understand it the only thing is the fact that it makes that kind of code more consistent.

promise.then(function () {
  //assume thing has happend
});
//do thing

promise.then(function () {
  //assume thing2 has NOT happend
});
//do thing2

Both the above can have weird bugs in the sometimes async case. The overhead, as long as you're creating relatively small numbers of promises, tends to be minimal in comparison to the time saved debugging those issues.

As for making asap return the values that's specc'd in Draft C (#4) so best not to discuss too extensively here. In brief:

  • It's not as simple as you make it sound
  • If ASAP returns something it MUST always return a promise because it's sometimes asynchronous
  • ASAP isn't really intended for use in cases where you know something is already resolved, if you were frequently coming across that situation you'd prefer one of Draft A #1 or Draft B #2. In my experience you know whether something's a promise or not ahead of time, but if it's a promise you almost never know it has already been resolved.

@jkroso
Copy link

jkroso commented Feb 6, 2013

@ForbesLindesay thanks for helping me out again! I guess it is just a case of my scales being weighted differently though unless someone else has something to add. @timjansen has the same intuition as me at least which makes me feel less weird. lol sorry tim

@ForbesLindesay
Copy link
Member Author

I should say that Draft D remains my firm favorite, I think we should encourage users to use .then but libraries that do lots of promise manipulation to use .asap

@jkroso
Copy link

jkroso commented Feb 6, 2013

+1 from me too. Though since it really doesn't matter what .asap returns I think it should be left up to implementers to return whatever they like. I can't think of any inter-rope problems that might cause. A suggested return value of undefined is a perfectly good idea though

@domenic
Copy link
Member

domenic commented Feb 6, 2013

asap in any form has been vetoed by at least two members of the Promises/A+ organization, as well as MarkM (whose opinion, as part of TC39, should not be discounted). Closing to prevent confusion from those thinking this is actively being considered for standardization under the Promises/A+ banner, but of course discussion is still welcome.

@domenic
Copy link
Member

domenic commented Feb 6, 2013

Argh wait I can't close this O_o @briancavalier or @ForbesLindesay mind doing the honors?

@domenic
Copy link
Member

domenic commented Feb 6, 2013

OK, I'm starting to feel bad about trying/asking to close this, so that probably means it's the wrong thing to do. Request withdrawn. I don't want to be the guy squashing discussion by closing issues. (Or, in this case, impotently trying.)

Still, I think asap has zero chance of standardization. As we can see from @jkroso and @timjansen, if you provide this, it will be abused. Furthermore, the issue of thrown exceptions inside the callbacks is unaddressed; I assume you'd rethrow them next-tick (like done), turning asap into a weird sometimes-synchronous version of done.

In short, there is no gain from aping the then/done signature besides some questionable conceptual elegance, which isn't even a desired goal of a synchronous inspection mechanism. And there's a lot of potential confusion and room for abuse.

@ForbesLindesay
Copy link
Member Author

The advantage is in the code for something like all, imagine we have an all method that only accepts promises (not values):

function all(arr) {
  return new Promise(function (resolve, reject) {
    var pending = arr.length;
    for (let i = 0; i < arr.length i++;) {
      let method = arr[i].asap ? 'asap' : arr[i].done ? 'done' : 'then';
      arr[i][method](onFulfilled(i), reject);
    }
    function onFulfilled(i) {
      return function (val) {
        arr[i] = val;
        if (0 == --pending) resolve(arr);
      };
    }
    if (0 == pending) resolve(arr);
  });
}

Sharing a signature with then makes it much easier to re-use code here. If I was doing the same thing using #2 I'd probably do:

function all(arr) {
  return new Promise(function (resolve, reject) {
    var pending = arr.length;
    for (let i = 0; i < arr.length i++;) {
      asap(arr[i], onFulfilled(i), reject);
    }
    function onFulfilled(i) {
      return function (val) {
        arr[i] = val;
        if (0 == --pending) resolve(arr);
      };
    }
    if (0 == pending) resolve(arr);
  });
}

function asap(promise, cb, eb) {
  if (promise.inspect && "fulfillmentValue" in promise.inspect()) {
    cb(promise.inspect().fulfillmentValue);
  } else if (promise.inspect && "rejectionReason" in promise.inspect()) {
    eb(promise.inspect().rejectionReason);
  } else if (promise.done) {
    promise.done(cb, eb);
  } else {
    promise.then(cb, eb);
  }
}

@domenic
Copy link
Member

domenic commented Feb 6, 2013

Your second code example has the benefit of not exposing a dangerous asap function to the world.

@briancavalier
Copy link
Member

@ForbesLindesay TBH, I want people to have to jump through that extra, ugly hoop. I want a synchronous inspection to look like brute force, synchronous code, and not like then() at all.

@briancavalier
Copy link
Member

I don't see enough support for this approach currently, so I'm closing as per @domenic's request. If we find reasons to reconsider it, we can reopen or create a new draft if that time comes.

@jkroso
Copy link

jkroso commented Feb 6, 2013

As we can see from @jkroso and @timjansen, if you provide this, it will be abused.

@dominic Can you explain? Its not clear to me what your baseing this off.

I assume you'd re-throw them next-tick (like done), turning asap into a weird sometimes-synchronous version of done

What would be the advantage of async throwing over a sync throw?

@domenic
Copy link
Member

domenic commented Feb 6, 2013

@dominic Can you explain? Its not clear to me what your baseing this off.

Sure. You guys seem keen to use asap as a "faster" then. (Your scales are weighted differently, etc.) This is an anti-goal of the synchronous inspection spec. We must guarantee high integrity for promises to the best of our ability; indeed, the introduction of synchronous inspection at all is tricky in light of such a desire.

What would be the advantage of async throwing over a sync throw?

Consistency: if the promise is not already fulfilled or rejected, you're going to have to async-throw anyway.

@jkroso
Copy link

jkroso commented Feb 6, 2013

Consistency: if the promise is not already fulfilled or rejected, you're going to have to async-throw anyway

Ok I think you guys should start calling Promises/A+, Futures or something. Since the word promise is always going to be interpreted as a metaphor for what they do. If I ask for something at a shop and they they refused to give it to me right now because the next customer might have to wait for the item to be ordered in ... well thats silly. And while they are being called promises the logic you guys describe seems silly too.
There is enormous value in staying sync whenever possible as I have discovered with my own implementation. And its not just the speed aspect. I find debugging easier too since stack traces are left in tact and I can more easily step through code. I never get stung by the reason @ForbesLindesay described since I don't think of a promise as being guaranteed async by way of the "promise" metaphor.

We must guarantee high integrity for promises to the best of our ability

Again I can't see what you see. Can your explain how async preserves integrity?

@domenic
Copy link
Member

domenic commented Feb 6, 2013

Promises have a long and storied history in JavaScript and in other languages as asynchronous control flow primitives. We go by that, not by any real-world analogies. Sorry to disappoint, but your unreliably-synchronous things need to be the ones going by a different name.

I'm done with this thread; feel free to have the last word.

@jkroso
Copy link

jkroso commented Feb 6, 2013

@domic sorry just trying to get something more out of the guys who work with promises the most. Nothing I've got has made any sense.

@timjansen
Copy link

you guys seem keen to use asap as a "faster" then.

No, not at all. Frankly I am still trying to find out how the proposal(s) can help me, and performance is just the only benefit I have found so far. At least as long the extension is optional.
But that does not mean that I care about performance.

@domenic
Copy link
Member

domenic commented Feb 7, 2013

Right then, sorry to be a bit short with you both. I apologize.

As to why the effects of promises should always become asynchronously apparent, instead of sometimes-synchronously, see promises-aplus/promises-spec#4, including the links therein.

The reason for this proposal will become more apparent when we fill out #3, but in short: there are specific rare use cases for performance, interoperability, and even debugging that would be helped by synchronous inspection. They are rare and intended only to be used by library authors.

For example, if you are writing a templating library (or perhaps a "model" library as part of a MVC trio), you may wish to support promises as attributes of your template data. In many cases, the promises will already be fulfilled or rejected by the time you want to render things to the DOM. If so, it's much better to insert the data at the same time as the initial render, than to re-render once for each time a template attribute becomes fulfilled or rejected, or to find the location of the metamorphs you inserted and update each of them.

As another example, certain convenience methods, like Q.all (as @ForbesLindesay points out above), would benefit from synchronous inspection. In the case of being passed solely already-fulfilled/rejected promises, they would delay a single tick, instead of one for each promise.

Finally, certain utility methods, like Q.isFulfilled and friends, could be made to work with all types of third-party promises, instead of with just those from the implementation that provides them (in this case Q). Right now Q.isFulfilled/Q.isRejected/Q.isPending and friends consider third-party promises to be pending, always; this could be made accurate if synchronous inspection were available.

@domenic domenic mentioned this issue Feb 7, 2013
@timjansen
Copy link

@domenic Thank you for your explanation, it helped me understand the reasoning!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants