resolve()/reject() on Promise subclasses and @@species
Le 29 oct. 2015 à 03:51, Boris Zbarsky <bzbarsky at MIT.EDU> a écrit :
I was just implementing subclassing of Promise in Gecko, when I realized that given a Promise subclass MyPromise these two calls:
MyPromise.race([]) MyPromise.all([])
will take MyPromise[@@species] into account when creating the return value, but these two calls:
MyPromise.resolve() MyPromise.reject()
will not; they will invoke MyPromise itself, not MyPromise[@@species].
This is because www.ecma-international.org/ecma-262/6.0/#sec-promise.all and www.ecma-international.org/ecma-262/6.0/#sec-promise.race do the whole @@species thing but www.ecma-international.org/ecma-262/6.0/#sec-promise.reject and www.ecma-international.org/ecma-262/6.0/#sec-promise.resolve do not.
Is this behavior intentional? If so, I'd really like to understand the reason for it.
-Boris
Relevant discussion: esdiscuss.org/topic/performpromiseall, esdiscuss.org/topic/performpromiseall
As I understand, the specified behaviour is an accident of history. Previously, Promise.resolve()
and Promise.reject()
used the species pattern in order to determine the constructor to be used, just as Promise.all()
and Promise.race()
. Then, at the last minute, it was decided that it wasn’t the best semantics for Promise.resolve()
, and it was corrected. For Promise.all()
and Promise.race()
, the motivation wasn’t stronger than the lack of time.
Should it be corrected before @@species is widely implemented? I think so. The arguments I give are:
-
Overall consistency in the language. Except for the two offending Promise static methods, all uses of
@@species
in the ES2015 spec (15 uses) are for the following pattern: Starting from one instance, one constructs a derived object for that instance. (The effective lookup of the @@species property is factored in the SpiecesConstructor and ArraySpeciesCreate abstract operations.) -
Also, in static methods like
Promise.all
andPromise.race
, a constructor is explicitly provided by the user: simply use it. Compare with what is done for arrays:Array.from(someArray) // use the Array constructor explicitly provided. Array.of(x, y, z) // ditto
someArray.slice(0) // compute the constructor to be used through some algorithm involving @@species. [].concat(x, y, z) // ditto
Should it be corrected before @@species is widely implemented? I think so.
I agree, if feasible.
On 10/30/15 1:19 PM, Claude Pache wrote:
Should it be corrected before @@species is widely implemented?
Or implemented at all? Chrome doesn't implement it yet. My Firefox tree does for Promise as of today, but I could just as easily drop it from all/race if people prefer that.
+1
As referenced in the cited thread
(esdiscuss.org/topic/performpromiseall#content-6) there are
actually two uses of the Promise constructor in the definition of
all
and race
. To quote myself:
[N]ote that the implementations given for
Promise.all
andPromise.race
use Promises in two different ways: (1) every element of the array argument hasC1.resolve(elem).then(....)
applied to it, and (2) the result is constructed withNewPromiseCapability(C2)
. It's not entirely clear to me that C1 and C2 should be the same. Consider theTimeoutPromise
: do we want to apply a timeout to every element individually and to the result as a whole?It seems that C1 might use species (to avoid the timeout/weak reference), but C2 ignore it (to apply the timeout/weak reference to the final result)
[...] [I]f we decided that
WeakPromise.all()
should return aWeakPromise
then I'd suggest that we change step 6 from:Let promiseCapability be NewPromiseCapability(C). to Let promiseCapability be NewPromiseCapability(this). in both
Promise.all
andPromise.race
, but leave the definition of C otherwise alone, so that internally-created promises honor the species.
The subtly of this detail of the spec was (for me at least) one of the
reasons I didn't push for a change to Promise.all
and Promise.race
at the time. And, as I wrote then, the current semantics do have a
plausible consistency, given future Promise.prototype.all
and
Promise.prototype.race
methods.
I'm certainly willing to reopen this for discussion, but I'd like to see some hard thought given to the two separate uses in the definition.
I still think everything should go through @@species. I don't understand the utility of @@species if it is not uniformly applied to constructing new instances (in both instance and static methods). But it seems that ES15 decided to only use @@species for instance methods so I guess that's where we're stuck.
From: "C. Scott Ananian" <ecmascript at cscott.net>
Sent: Nov 5, 2015 3:20 AM To: Allen Wirfs-Brock Cc: es-discuss Subject: Re: resolve()/reject() on Promise subclasses and @@species
As referenced in the cited thread
(esdiscuss.org/topic/performpromiseall#content-6) there are
actually two uses of the Promise constructor in the definition of
all
and race
. To quote myself:
[N]ote that the implementations given for
Promise.all
andPromise.race
use Promises in two different ways: (1) every element of the array argument hasC1.resolve(elem).then(....)
applied to it, and (2) the result is constructed withNewPromiseCapability(C2)
. It's not entirely clear to me that C1 and C2 should be the same. Consider theTimeoutPromise
: do we want to apply a timeout to every element individually and to the result as a whole?It seems that C1 might use species (to avoid the timeout/weak reference), but C2 ignore it (to apply the timeout/weak reference to the final result)
[...] [I]f we decided that
WeakPromise.all()
should return aWeakPromise
then I'd suggest that we change step 6 from:Let promiseCapability be NewPromiseCapability(C). to Let promiseCapability be NewPromiseCapability(this). in both
Promise.all
andPromise.race
, but leave the definition of C otherwise alone, so that internally-created promises honor the species.
The subtly of this detail of the spec was (for me at least) one of the
reasons I didn't push for a change to Promise.all
and Promise.race
at the time. And, as I wrote then, the current semantics do have a
plausible consistency, given future Promise.prototype.all
and
Promise.prototype.race
methods.
I'm certainly willing to reopen this for discussion, but I'd like to see some hard thought given to the two separate uses in the definition.
Without rehashing the previous discussion, I totally disagree. Having
Promise#resolve()
go through @@species
made it totally useless. It's
also not in the spirit of the smalltalk species, which used very narrowly
for instance methods like map
, not for constructors.
(Sorry, that should have been Promise.resolve
, not Promise#resolve
.)
On 11/4/15 9:36 PM, Domenic Denicola wrote:
But it seems that ES15 decided to only use @@species for instance methods
Except it also uses it for Promise.all and Promise.race.
Again, the reasoning at the time was that Promise.all(x)
could be
considered sugar for Promise.resolve(x).all()
in ES7, and so
Promise.all
was "really" an instance method after all. Again, I could
support a change, I'm just saying it wasn't totally crazy, and no one made
any objection at the time.
But this is really rehashing the previous discussion.
On 11/4/15 10:29 PM, C. Scott Ananian wrote:
Again, the reasoning at the time was that
Promise.all(x)
could be considered sugar forPromise.resolve(x).all()
in ES7, and soPromise.all
was "really" an instance method after all.
OK, but note that the behavior of the two is different (or at least the behavior of @@species is different between Promise.prototype.then and Promise.all). Specifically if I have:
class MyPromise extends Promise { static get Symbol.species { return undefined; } }
then MyPromise.all() will return a MyPromise but MyPromise.resolve(x).then() will return a Promise, because SpeciesConstructor falls back to "defaultConstructor", not "C", if Get(@@species) returns null-or-undefined, while Promise.all()/race() fall back to "C".
I'm just saying it wasn't totally crazy, and no one made any objection at the time.
Sure. Also, no one has implemented any of this stuff yet. ;) I was just implementing it and was confused by the various inconsistencies. I can implement it as written, of course, but I wanted to double-check that I wasn't misunderstanding something.
The spec has been implemented as is by both es6-shim
and core-js
, and
there are plenty of users of these. So it's not correct to say that it
hasn't been implemented yet; folks aren't waiting for the browsers.
The differing behaviors in the null-or-undefined case should probably be remedied, at least.
But I am in favor of making a change to all/race to move them away from @@species, see my previous message for the exact wording proposal which changes step 6. I'd appreciate comments on that.
On 11/4/15 11:22 PM, C. Scott Ananian wrote:
see my previous message for the exact wording proposal which changes step 6.
esdiscuss.org/topic/performpromiseall#content-6 you mean?
I don't have a strong opinion on it, honestly. The existence of the intermediate promises in all/race seems mostly to be a specification device to make it easier to deal with people passing in not-thenable-at-all values, from my point of view, so I'm not sure I have a principled opinion on how they should be constructed.
Unfortunately, it's not just a specification device, it has a real effect on the behavior of Promise subclasses. In the case of TimeoutPromise, for instance, it affects whether the timeouts apply to each promise in the array or just to the final result Promise.
I'm hearing through the grapevine ( tc39/ecma262#151) that it has been decided to change the behavior of Promise.all/race. Have you decided to adopt the spec language I proposed above? There has been precious little actual discussion of it, certainly not enough for me to claim any sort of consensus has been reached (except perhaps by default, since no one has advocated for alternatives). --scott On 11/4/15 11:22 PM, C. Scott Ananian wrote:
see my previous message for the exact wording proposal which changes step 6.
esdiscuss.org/topic/performpromiseall#content-6 you mean?
I don't have a strong opinion on it, honestly. The existence of the intermediate promises in all/race seems mostly to be a specification device to make it easier to deal with people passing in not-thenable-at-all values, from my point of view, so I'm not sure I have a principled opinion on how they should be constructed.
All uses of @@species are to be removed from Promise.race and Promise.all. The committee achieved consensus on this, without you.
On Wed, Nov 18, 2015 at 6:50 PM, Domenic Denicola <d at domenic.me> wrote:
All uses of @@species are to be removed from Promise.race and Promise.all. The committee achieved consensus on this, without you.
To be precise, steps 3-5 are to be removed entirely from Promise.all (25.4.4.1) and Promise.race (25.4.4.3)?
So for WeakPromise.all([ p1, {} ])
a weak reference would be held to p1's
value, the new {} object, and the array result of the all
.
Similarly, TimeoutPromise.all([ p1, p2 ])
would put timeouts on p1 and p2
and on the array result, so the semantics are the same as:
TimeoutPromise.resolve(TimeoutPromise.all([ TimeoutPromise.resolve(p1), TimeoutPromise.resolve(p2) ]))
.
I'm not objecting to this semantics, necessarily, I'm just surprised it wasn't proposed or discussed on the public mailing list before adoption.
I was just implementing subclassing of Promise in Gecko, when I realized that given a Promise subclass MyPromise these two calls:
MyPromise.race([]) MyPromise.all([])
will take MyPromise[@@species] into account when creating the return value, but these two calls:
MyPromise.resolve() MyPromise.reject()
will not; they will invoke MyPromise itself, not MyPromise[@@species].
This is because www.ecma-international.org/ecma-262/6.0/#sec-promise.all and www.ecma-international.org/ecma-262/6.0/#sec-promise.race do the whole @@species thing but www.ecma-international.org/ecma-262/6.0/#sec-promise.reject and www.ecma-international.org/ecma-262/6.0/#sec-promise.resolve do not.
Is this behavior intentional? If so, I'd really like to understand the reason for it.