The argument for avoiding promises for promises is fairly simple. A promise is a representation of a value that will exit in the future. If that value is still a promise, you haven't really got to the final value yet. Conceptually having the promise doesn't really provide anything other than a means to obtain the final value, as such it's of no additional use to be able to get the intermediate nested promises: You might as well just jump straight to the value.
This problem is explained in more detail by @erights here. The currently proposed [[Resolve]]
algorithm only dictates this recursive flattening for unrecognised thenables
, not for promises. THe advantage of this behavior is that a user can pick up a Promises/A+ library and return a deeply nested mess of foreign promises objects (all of which were broken enough not to do any unwrapping), and in a single step get back to the actual value being represented.
In the following example, you would ideally expect Q
to be equivallent to Q.then(identity)
var p = new Promise(resolve => resolve(5));
var q = new Promise(resolve => resolve.fulfill(pFor5));
var P = p.then(x => x); // P is a promise for 5, just like p
var Q = q.then(x => x); // oops! Q is a promise for 5, *unlike* q.
Promises are meant to make it easier to make synchronous code into asynchronous code. With this in mind we have to ask ourselves what the synchronous analog of any new feature would be. This is easiest to see in the case of a promise-for-a-rejected-promise.
A rejected promise from a function call represents that the function threw (asynchronously). What does a return value of a promise-for-a-rejected-promise represent? It seems to represent having called another internal function, that itself threw. This has no synchronous analog.
Similarly, of course, with return values: if f(x)
returns g(x + 1)
, and g(y)
returns y * y
, then f(x)
returns (x + 1) * (x + 1)
; it does not return some wrapped representation of g(x + 1)
that must then be unwrapped to get (x + 1) * (x + 1)
.
Another way of viewing the synchronous analog issue is that chaining a new .then
call is like adding another frame to the stack, this is easier to reason about if the promises are always fully flattened, rather than having a separate flatten operation. This argument does not really cause a problem for having then
callbacks that only unwrap one layer, it just makes clear that they must unwrap at least one layer.
The Parametricity argument in favour of promises for promises is essentially completely circular. It can be boiled down to:
- Promises for Promises should be allowed for everything because we should allow Promises for anything and not restrict the values a Promise can represent.
This isn't really an argument since it depends on it's conclusion to be true.
@domenic . How do promise libraries recognize their own promises as their own? Using
instanceof
or so?And do I infer correctly from the two quotes above that a promise library won't eagerly call
then
on its "own" promises?If that's the case, and the flattening is a side-effect from attempting to normalize the promise behavior into something that's A+ compliant, then another option would be to make a new spec for recognizing A+ promises, even if they're not created by the library which is doing the recognition. There could be a property
isPromiseAPlusCompliant
on the prototype or constructor function of such promises. This would require some level of trust between library authors though. The library can't guarantee behavior of (all) its returned promises anymore, and has to trust in theisPromiseAPlusCompliant
label of the promises it passes on unchanged.This lends further assurance to my interpretation of what you said. I'll dig into RSVP to get some clue. :)
In RSVP I can't really see its own promises being special-cased. At least not with
instanceof
. So maybe NOT eagerly calling then on a libraries own promise is not specified in the spec, bu it is isn't ruled out either? Left up to the implementor?Is there any library which currently special-cases its own promises?