Fixing `Promise.resolve()`

# C. Scott Ananian (9 years ago)

Hopefully everyone has had a little bit of time to think over the issues with Promise.resolve().

Last week I proposed three different reasonable semantics for Promise.resolve, none of which involve the [[PromiseConstructor]] internal field: esdiscuss.org/topic/subclassing-es6-objects-with-es5-syntax#content-50

I continue to feel that the "no species" proposal is the best alternative, reasoning that Promise.resolve is more like an explicit constructor than an species-using instance-transformation method. Here's that proposal again:

Promise.resolve(x)

  1. Let C be the this value.
  2. If IsPromise(x) is true, a. Let constructor be the value of Get(x, "constructor"). b. ReturnIfAbrupt(constructor) c. If SameValue(constructor, C) is true, return x.
  3. Let promiseCapability be NewPromiseCapability(C).
  4. ReturnIfAbrupt(promiseCapability).
  5. Let resolveResult be Call(promiseCapability.[[Resolve]], undefined, «x»).
  6. ReturnIfAbrupt(resolveResult).
  7. Return promiseCapability.[[Promise]].

All mentions of [[PromiseConstructor]] should then be garbage-collected from the spec.

This simplifies the semantics and fixes the "hidden new.target" brokenness which affects interoperability with code written in ES5 syntax. Eliminating "species" here also yields more useful behavior from P.resolve if an subclass sets P[Symbol.species] !== P.

It's my understanding that Mark Miller volunteered to champion the changes to Promise.resolve at the next TC39 meeting. (Thanks, Mark!).

I'll update es6-shim and core-js/babel if/when TC39 reaches consensus on this.

# Allen Wirfs-Brock (9 years ago)

At the May 27-29 TC39 meeting we agreed to make this change.

# C. Scott Ananian (9 years ago)

Thanks! It looks like core-js has already patched in the new spec: zloirock/core-js#75

I've opened paulmillr/es6-shim#344 on es6-shim, and I'll see if I can get a patch together for it.

I've filed bugzilla.mozilla.org/show_bug.cgi?id=1170742 against Mozilla. I've filed code.google.com/p/v8/issues/detail?id=4161 against v8.

Allen: Will this be an errata to ES6, part of ES7, or "something else"? --scott ​

# Mark S. Miller (9 years ago)

If the change is as simple as it appears, it seems it will go into ES6 itself!

Thanks for pushing this forward.

# Allen Wirfs-Brock (9 years ago)

On Jun 2, 2015, at 2:01 PM, Mark S. Miller wrote:

Hi Scott,

If the change is as simple as it appears, it seems it will go into ES6 itself!

The fix has already been made to the production copy that will be released in a couple weeks when we have ECMA GA approval

# Axel Rauschmayer (9 years ago)

Is there any way to find out how this works now? I can’t find the May 2015 meeting notes, either.

Do all static Promise methods now just use this (e.g. Promise.all()this.promiseResolve(···))? Or only Promise.resolve()?

# Allen Wirfs-Brock (9 years ago)

Revised definition of Promise.resolve:

25.4.4.5 Promise.resolve ( x ) The resolve function returns either a new promise resolved with the passed argument, or the argument itself if the argument is a promise produced by this constructor.

  1. Let C be the this value.
  2. If Type(C) is not Object, throw a TypeError exception.
  3. If IsPromise(x) is true, a. Let xConstructor be Get(x, "constructor"). b. ReturnIfAbrupt(xConstructor). c. If SameValue(xConstructor, C) is true, return x.
  4. Let promiseCapability be NewPromiseCapability(C).
  5. ReturnIfAbrupt(promiseCapability).
  6. Let resolveResult be Call(promiseCapability.[[Resolve]], undefined, «x»).
  7. ReturnIfAbrupt(resolveResult).
  8. Return promiseCapability.[[Promise]].

So far, no changes to any other Promise static methods

# Domenic Denicola (9 years ago)

Allen, that change seems wrong. I thought we were only changing the IsPromise steps. The actual construction should still go through species. If nothing else, it should do so for consistency with reject.

The motivation of @@species, as I understood it, was to allow alternate subclass constructor signatures (originally for Array and TypedArray, but Promise can benefit as well). It’s understandable to not involve @@species when doing weak type-tests. But any time you construct a promise instance, you should go through @@species, instead of the constructor directly.

Some example usages:

  • Creating a LabeledPromise subclass (for better debugging, like RSVP's promises + Ember promise inspector) whose constructor signature is new LabeledPromise(label, executor)
  • Creating a SaneArray subclass whose constructor signature is new SaneArray(...elements) without the special-case for a single argument.
  • A more complicated usage in a proposed Element/HTMLElement/MyCustomElement hierarchy 1, to allow custom elements to have custom constructor signatures but still work well with various element-creating parts of the platform.

The LabeledPromise case will, as currently specified, work great. LabeledPromise has a custom LabeledPromise[Symbol.species](executor) which basically does return new this("<derived promise>", executor). Then it doesn't have to override .resolve, .reject, .all, or .then. However, with your change, .resolve will no longer work correctly, even though .reject will.

However, the SaneArray case actually will only work for the instance methods, which use ArraySpeciesCreate. In contrast, Array.of and Array.from use Construct(C, <<len>>). That seems like a bug in the spec?

# Allen Wirfs-Brock (9 years ago)

I don't think we discussed the possibility of Promise subclasses with different promise signatures at the May meeting; we mainly focused on the expectation that SubPromise.resolve(x) should yield an instance of SubPromise. But I see your logic, indirecting through species provides a way for subclasses to to change their constructor signature and still work correctly with the other inherited Promise static methods.

# C. Scott Ananian (9 years ago)

@Domenic: please see the previous discussion at esdiscuss.org/topic/subclassing-es6-objects-with-es5-syntax#content-50 since there is much more discussion back-and-forth there.

And you are correct about Promise.reject; Allen is proposing to remove the @@species from that method as well.

The changes to Promise.resolve are needed to make other use cases work properly (see that other thread). It seems that your particular use case could be better handled by overriding LabelledPromise.resolve to take an additional "label" argument.

Your understanding of @@species seems to be different from the use cases envisioned by the smalltalk concept named species, although it's an interesting idea in its own right. You should probably call it something like "custom constructors", though, to differentiate it from the smalltalk "species", which is used in the smalltalk world to describe two related things (1) the "generic type" of the object, for type tests, and (2) the "derived type" which instance-transformation methods should yield.[*] In smalltalk (as I understand it) custom constructors are not part of the design. --scott

[*] ""Answer the preferred class for reconstructing the receiver. For example, collections create new collections whenever enumeration messages such as collect: or select: are invoked. The new kind of collection is determined by the species of the original collection. Species and class are not always the same. For example, the species of Interval is Array." forum.world.st/species-td3374278.html

# Domenic Denicola (9 years ago)

From: Allen Wirfs-Brock [mailto:allen at wirfs-brock.com]

I don't think we discussed the possibility of Promise subclasses with different promise signatures at the May meeting; we mainly focused on the expectation that SubPromise.resolve(x) should yield an instance of SubPromise.

Yeah, and to be clear, I generally agree with this. My LabeledPromise example adheres to this. Other examples in related threads have been setting @@species to an entirely new constructor (e.g. to Promise itself), which is very strange to me. If you do that, you deserve whatever weirdness you might get, IMO. From the beginning, @@species has been about constructor signature modification.

But I see your logic, indirecting through species provides a way for subclasses to to change their constructor signature and still work correctly with the other inherited Promise static methods.

Any thoughts on Array.of and Array.from?

# Domenic Denicola (9 years ago)

Yes, I read that thread and still stand by my position.

# C. Scott Ananian (9 years ago)

I don't agree that @@species is useful at all for changing constructor signatures, since there is no closure argument.

If we had dynamically scoped variables, then:

  LabelledPromise[Symbol.species] = function() { return
LabelledPromise.bind(label/*dynamically scoped*/); };
  function() {
    let label = "foo";
    return LabelledPromise.resolve(x);
  }

would indeed be very interesting. But in the absence of some sort of closure, the only way you can make @@species into a custom constructor is for odd special cases where you are just rearranging deck chairs.

Why not:

class LabelledPromise {
 constructor(exec, label) {
   super(exec);
   this.label = label === undefined ? "<derived promise>" : label;
 }
}
# Domenic Denicola (9 years ago)

Regardless of whether or not you agree, that was the original motivation for its introduction.

# C. Scott Ananian (9 years ago)

On Wed, Jun 10, 2015 at 11:46 AM, Domenic Denicola <d at domenic.me> wrote:

Regardless of whether or not you agree, that was the original motivation for its introduction.

twitter.com/awbjs/status/535962895532584960 says:

ES6 final tweaks #8: Smalltalk-like species pattern used in Array methods, etc. to determine constructor for derived objects.

esdiscuss.org/notes/2014-11-18 discusses species in the context of Array.prototype methods. And esdiscuss.org/notes/2014-11-19 says:

Allen Wirfs-Brock: Smalltalk uses an abstract above that has a species property to determine what to create.

That's all I know of it. I wasn't there, obviously, and you were. But this is the first I've ever heard of using @@species for constructor signature modification. It's hard to reconstruct reasoning from meeting notes and tweets.

# Mark Miller (9 years ago)

Independent of the current issue, I just want to go on record stating that dynamically scoped variables are a "cure" worse than any disease we might think to apply them to. Avoid at all costs.

# Allen Wirfs-Brock (9 years ago)

On Jun 10, 2015, at 9:16 AM, C. Scott Ananian wrote:

On Wed, Jun 10, 2015 at 11:46 AM, Domenic Denicola <d at domenic.me> wrote: Regardless of whether or not you agree, that was the original motivation for its introduction.

twitter.com/awbjs/status/535962895532584960 says: ES6 final tweaks #8: Smalltalk-like species pattern used in Array methods, etc. to determine constructor for derived objects. esdiscuss.org/notes/2014-11-18 discusses species in the context of Array.prototype methods. And esdiscuss.org/notes/2014-11-19 says: Allen Wirfs-Brock: Smalltalk uses an abstract above that has a species property to determine what to create.

That's all I know of it. I wasn't there, obviously, and you were. But this is the first I've ever heard of using @@species for constructor signature modification. It's hard to reconstruct reasoning from meeting notes and tweets.

That's correct. species is intended for use when a subclass constructor wants to use something other than itself as the constructor for objects derived from the subclass instances. Using species to select a constructor higher in a class hierarchy for derived instances is a fine thing to so. For example, a "SortedArray" might choose that operations like map should produce the more general Array instances rather than SortedArray instances.

Using species to modify constructor signatures was not one of its intended use-cases, however, it might be possible to use it for that purpose.

Note that the uses of species in the ES6 spec all assume that the signature of the returned constructor is the same as the base class constructor.

However, a way around that might be to have a species method that was an adaptor of the expected constructor signature to some other signature pattern.

For example,

  LabelledPromise[Symbol.species] = function() {
       ctor = this;
       return function(executor) {
            if (new.target === undefined) throw TypeError("Can't call a class constructor");
            return Reflect.construct(ctor, [ctor.defaultLabel, executor], new.target)
       }
  };
LabelledPromise.defaultLabel = "default label";