Subclassing ES6 objects with ES5 syntax.

C. Scott Ananian ecmascript at cscott.net
Thu Apr 30 17:57:16 UTC 2015


On Wed, Apr 29, 2015 at 6:32 PM, Allen Wirfs-Brock <allen at wirfs-brock.com>
wrote:

> The "another test" that you propose seems to be exactly the fix which
> Brandon proposed and I codified, to wit:
>
> 2. If IsPromise(x) is true,
>>     a. Let constructor be the value of SpeciesConstructor(x, %Promise%)
>>     b. If SameValue(constructor, C) is true, return x.
>
>
> That can't right.  x could be an object that is not a C but whose
> SpeciesConstructor is C.  In that case, a new C promise on x still has to
> be created.  But your code would just return x.  I would use a 'construtor'
> test in step 2. But if 'constructor' is not C and SpeciesConstructor is
> also not C then an extra level of C wrapping is needed.
>

Let's call the Brendan proposal above the "early test" proposal.

It seems to me that if `P.@@species` is `Q` then `P.resolve` should always
return an object `x` with `x.constructor==Q`, which I don't think any of
the proposed tweaks yet do.  So here's that variant, which I'll call the
"late test" proposal:

Promise.resolve(x)
> 1. Let C be the this value.
> 2. If Type(C) is not Object, throw a TypeError exception.
> 3. Let S be Get(C, @@species).
> 4. ReturnIfAbrupt(S).
> 5. If S is neither undefined nor null, let C be S.
> 6. 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.
> 7. Let promiseCapability be NewPromiseCapability(C).
> 8. ReturnIfAbrupt(promiseCapability).
> 9. Let resolveResult be Call(promiseCapability.[[Resolve]], undefined,
> «x»).
> 10. ReturnIfAbrupt(resolveResult).
> 11. Return promiseCapability.[[Promise]].


This moves the test down from step 2 to step 6, and ensures that we test
`x.constructor` against the same species that we would use to create the
capability.  The returned promise thus always has a consistent value for
its constructor property.

It's hard for me to determine which of these is actually preferred,
although both are more consistent that the current [[PromiseConstructor]]
semantics.

It would help if the ES2015 proposal actually had any objects where
`C[Symbol.species] !== C`, but it does not appear to.  So let's look at the
smalltalk usage of species.

To quote an arbitrarily-chosen smalltalk glossary (
http://www.mimuw.edu.pl/~sl/teaching/00_01/Delfin_EC/Glossary.htm):

> *species*: The generic class of an object. This is normally the same as
> the class of an object, otherwise it is a class which describes the generic
> group of classes to which a particular class belongs. Note that this does
> not necessarily have to be class from the same branch of the hierarchy.


With this meaning for `@@species`, when one sets
`MyPromise.constructor[Symbol.species] = Promise;`  then one is saying that
even though this is a `MyPromise` it's still generically a `Promise` (of
the `Promise` species).  Then the "early test" `Promise.resolve` is
entirely consistent in passing through anything whose species is
`Promise`.  The "late test" semantics are a bit harder to describe in terms
of species (even though they seem more consistent if you are just looking
at the `constructor` property of the result).

The smalltalk motivating example usually given for species is the
equivalent of:
```
WeakArray.constructor[Symbol.species] = Array;
```
so that the result of `WeakArray.filter(...)` isn't itself a weak array and
so won't mysteriously disappear in the hands of the caller.  Moving this
example to `Promise`s we'd have:
```
WeakPromise.constructor[Symbol.species] = Promise;
```
and `WeakPromise` would hold a weak reference to some value, but it
wouldn't cause the promise returned by `Promise#then` to also be a weak
reference.

Moving from the reference domain to the time domain, we can imagine that:
```
TimeoutPromise.constructor[Symbol.species] = Promise;
```
could also be used to ensure that a given promise is rejected if not
resolved within a certain time period, but the "timeout" functionality is
not intended to be inherited by promises chained with `then`.

The "early test" and "late test" proposals give different results and
interpretations for:
```
p = Promise.resolve(weakPromise);
p = Promise.resolve(timeoutPromise);
```
In the "early test" proposal, both the `weakPromise` and the
`timeoutPromise` are both already of the `Promise` species, and so don't
need to be recast.  In the "late test" proposal these would both be recast
to new `Promise`s -- possibly creating a new strong reference to the value
in the `weakPromise` case.

On the other hand, both "early test" and "late test" give awkward semantics
for:
```
pp = WeakPromise.resolve(p);
pp = TimeoutPromise.resolve(p);
```
Presumably these invocations are intended to ensure that `pp` holds a weak
reference to the value or times out, respectively, even if `p` is already a
generic `Promise` ("belongs to the Promise species").

But both "early test" and "late test" semantics potentially return generic
`Promise` objects here, although the "early test" semantics do pass through
a `WeakPromise`/`TimeoutPromise` if it is passed as an argument.

I think what this is saying is that `Promise.resolve` is more like a
constructor than an instance method, and it shouldn't consult `@@species`
at all.

So here's one more proposal.  We'll call this the "no species" proposal:

> 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]].


At this point I think "no species" is the correct thing to do.  It also
happens to be the simplest. If someone can come up with some alternative
use cases for Promise.@@species I could perhaps be persuaded to change my
mind.
  --scott
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20150430/a4f85504/attachment.html>


More information about the es-discuss mailing list