resolve()/reject() on Promise subclasses and @@species

# Boris Zbarsky (8 years ago)

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.

# Claude Pache (8 years ago)

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 and Promise.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

# Kevin Smith (8 years ago)

Should it be corrected before @@species is widely implemented? I think so.

I agree, if feasible.

# Boris Zbarsky (8 years ago)

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.

# Allen Wirfs-Brock (8 years ago)

+1

# C. Scott Ananian (8 years ago)

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 and Promise.race use Promises in two different ways: (1) every element of the array argument has C1.resolve(elem).then(....) applied to it, and (2) the result is constructed with NewPromiseCapability(C2). It's not entirely clear to me that C1 and C2 should be the same. Consider the TimeoutPromise: 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 a WeakPromise 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 and Promise.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.

# Domenic Denicola (8 years ago)

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 and Promise.race use Promises in two different ways: (1) every element of the array argument has C1.resolve(elem).then(....) applied to it, and (2) the result is constructed with NewPromiseCapability(C2). It's not entirely clear to me that C1 and C2 should be the same. Consider the TimeoutPromise: 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 a WeakPromise 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 and Promise.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.

# C. Scott Ananian (8 years ago)

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.

# C. Scott Ananian (8 years ago)

(Sorry, that should have been Promise.resolve, not Promise#resolve.)

# Boris Zbarsky (8 years ago)

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.

# C. Scott Ananian (8 years ago)

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.

# Boris Zbarsky (8 years ago)

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 for Promise.resolve(x).all() in ES7, and so Promise.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.

# C. Scott Ananian (8 years ago)

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.

# Boris Zbarsky (8 years ago)

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.

# C. Scott Ananian (8 years ago)

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.

# Domenic Denicola (8 years ago)

All uses of @@species are to be removed from Promise.race and Promise.all. The committee achieved consensus on this, without you.

# C. Scott Ananian (8 years ago)

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.