@@new
+1.
I think you could also remove the 'construct' handler in Proxy (people.mozilla.org/~jorendorff/es6-draft.html#sec-construct-internal-method step 4), as the @@new
method can be handled with the usual proxy method dispatch mechanism. I'll leave it to someone who better understands Proxies to give the details.
As I've mentioned before, I like the fact that this refactoring reduces the amount of magic syntax in the language; new X
is now just sugar. The fact that it gets rid of the 'new super' syntax is even better (and demonstrates the virtue of shrinking the language in
this way).
On Tue, Jun 17, 2014 at 3:21 PM, Jason Orendorff <jason.orendorff at gmail.com>
wrote:
Allen asked me to fill out what @@new would mean. Here it is.
... snip ...
The "super Arguments" call syntax in the ES6 drafts would be constrained to appear only at the top of a constructor, as in Java:
class PowerUp { constructor(name, price) { ... } } class RocketPack extends PowerUp { constructor() { super("Rocket Pack", 1000); this.fuel = FULL_TANK; } }
The RocketPack constructor would behave like this:
static [Symbol.new]() { var obj = super[Symbol.new]("Rocket Pack", 1000); obj.fuel = FULL_TANK; return obj; }
How would
constructor() {
if (rand() > 0.5)
super("A");
}
behave? What about
constructor() {
if (0)
super();
}
?
This is a common trick in some languages that silently insert a super(); call if they didn't appear somewhere in the constructor to make sure that super() is always called.
We could prevent this behavior by making sure that super(); must be the first statement in a constructor, but that means that the subclass can't really influence the parent constructor execution at all.
The other option is to ask users to always chain up in their constructor, like they need to do in any other overridden method.
Remember that
class C {
constructor() {}
}
assert(C === C.prototype.constructor);
What would C.prototype.constructor
look like with your proposal? Is C === C[@@new]
?
On Tue, Jun 17, 2014 at 2:49 PM, Jasper St. Pierre <jstpierre at mecheye.net> wrote:
How would
constructor() { if (rand() > 0.5) super("A"); }
behave?
SyntaxError.
We could prevent this behavior by making sure that super(); must be the first statement in a constructor, [...]
That is what I proposed.
[...] but that means that the subclass can't really influence the parent constructor execution at all.
Any class can explicitly define a static [Symbol.new]()
method, if desired.
Or the constructor method can return another object, so that the base class @@new method is called, but the object it created is thrown away. I should have mentioned that -- I would retain this behavior, which is already in ES6, but none of the examples used it so I forgot to say so.
Anyway --- skipping a base class constructor is not a normal thing to do. It shouldn't be the default.
On Tue, Jun 17, 2014 at 2:55 PM, Erik Arvidsson <erik.arvidsson at gmail.com> wrote:
What would
C.prototype.constructor
look like with your proposal?
ES6 already achieves C.prototype.constructor === C
, for both
functions and classes, simply by defining the two properties that way
(in 1 steps 6 and 7). I wouldn't change that.
Is
C === C[@@new]
?
Good question. I think calling C(...args)
should be the same as
calling new C(...args)
. How best to specify that, I'm not sure.
I don't think C === C[@@new]
needs to be a goal, since it wouldn't
hold for regular functions, or for classes that don't define a
constructor (those inherit their @@new method from the base class), or
for classes that contain a static [Symbol.new]()
method. But as an
implementation detail, if that's the easiest way to specify it, OK.
-j
On Tue, Jun 17, 2014 at 1:02 PM, Jason Orendorff <jason.orendorff at gmail.com>
wrote:
On Tue, Jun 17, 2014 at 2:49 PM, Jasper St. Pierre <jstpierre at mecheye.net> wrote:
How would
constructor() { if (rand() > 0.5) super("A"); }
behave?
SyntaxError.
Ouch.. Don't know, there are several valid semantics. I don't know about Java (as you say it's enforces calling parent constructor automatically, and allows only on top of the child constructor), but there are other valid semantics (Python, Ruby, etc), that don't force this restriction.
Does it make implementation's simpler? I think the main driver should be developers life, not the implementation.
We could prevent this behavior by making sure that super(); must be the first statement in a constructor, [...]
That is what I proposed.
[...] but that means that the subclass can't really influence the parent constructor execution at all.
Any class can explicitly define a
static [Symbol.new]()
method, if desired.Or the constructor method can return another object, so that the base class @@new method is called, but the object it created is thrown away. I should have mentioned that -- I would retain this behavior, which is already in ES6, but none of the examples used it so I forgot to say so.
Anyway --- skipping a base class constructor is not a normal thing to do. It shouldn't be the default.
That's the question. I guess there could valid cases when you wanna call parent constructor conditionally as was shown, or to call it after prologue initialization (which probably would set some default props needed for the parent ctor) in the child constructor.
Dmitry.
I like this proposal overall. Another benefit is that your super class constructors can return objects and things will just work (I think?).
I'm of the opinion that new should be required. Perhaps for class C { }
, C is a function as if created by the expression function () { throw('Class must be instantiated with new'); }
.
In addition to the super-must-be-first restriction, we might also want a 'cant reference this before super' restriction since this will not mean anything useful until after you super.
On Tue, Jun 17, 2014 at 4:24 PM, Dmitry Soshnikov <dmitry.soshnikov at gmail.com> wrote:
Anyway --- skipping a base class constructor is not a normal thing to do. It shouldn't be the default.
That's the question. I guess there could valid cases when you wanna call parent constructor conditionally as was shown, or to call it after prologue initialization (which probably would set some default props needed for the parent ctor) in the child constructor.
That's fair. Developers who need that can write a static [Symbol.new]()
method, of course.
I proposed what I did because I think it's more common to want to control the arguments passed to the base class constructor than to want to do other work before calling it, and it's very rare indeed to intentionally skip base class initialization. But all three are possible with this proposal.
constructor() { if (rand() > 0.5) super("A"); }
behave?
SyntaxError.
We could prevent this behavior by making sure that super(); must be the first statement in a constructor, [...]
That is what I proposed.
Sorry, but I can't get with this. The current class semantics works and I don't see a particularly compelling reason here to muck with it.
On Jun 17, 2014, at 1:02 PM, Jason Orendorff wrote:
On Tue, Jun 17, 2014 at 2:49 PM, Jasper St. Pierre <jstpierre at mecheye.net> wrote:
How would
constructor() { if (rand() > 0.5) super("A"); }
behave?
SyntaxError.
We could prevent this behavior by making sure that super(); must be the first statement in a constructor, [...]
That is what I proposed.
We discussed issues like here early in the ES6 class design and the consensus at the time was that we didn't want to be restricted to a super first design.
Also, it's not clear what it really buys you because if you can say:
class RocketPack extends PowerUp { constructor() { super("Rocket Pack", 1000); this.fuel = FULL_TANK; } }
You can presumably also say:
class RocketPack extends PowerUp { constructor() { super(this.foo(), doSomethingWith(this)); this.fuel = FULL_TANK; } }
so you still have to deal with the issue of the meaning of 'this' before and after the super call in
The RocketPack constructor would behave like this:
static [Symbol.new]() { var obj = super[Symbol.new]("Rocket Pack", 1000); obj.fuel = FULL_TANK; return obj; }
which reminds me that @@new is a class-side method, not an instance method. So, its natural 'this' value is the class object and and access to that 'this' value is important (for example, accessing the 'prototype' property in the default @@new, access class side constants and methods, etc.).
I guess, Jason is arguing that if you need to do any of these things, you need to explicitly code a @@new such as
class RocketPack extends PowerUp { static Symbol.new { let obj = super(this.foo(), doSomethingWith(this)); //this is normally RocketPack or a subclass of it obj.fuel = FULL_TANK; } }
BTW, it's not clear to me in this case what gets called for RocketPack();
Related, if an @@new method is explicitly provided in a class definition, is a constructor method definition forbidden?
So far, I prefer your proposal to draft ES6 by a lot -- especially since I missed the hideous Number special-casing spread around in the draft!
Jason Orendorff wrote:
Is
C === C[@@new]
?Good question. I think calling
C(...args)
should be the same as callingnew C(...args)
. How best to specify that, I'm not sure.
Isn't the question of whether C(...args) is allowed as short for new C(...args) a separable "option" from the rest of your proposal (and from draft ES6)?
I'd hate for your proposal to founder on this somewhat controversial issue.
I don't think
C === C[@@new]
needs to be a goal,
Should be specified not to be === in my view because:
it wouldn't hold for regular functions, or for classes that don't define a constructor (those inherit their @@new method from the base class), or for classes that contain a
static [Symbol.new]()
method.
See Allen's latest followup on this -- is it a static error to have both constructor and the static Symbol.new method?
But as an implementation detail, if that's the easiest way to specify it, OK.
See above. LMK if you disagree with anything, or anything's unclear. Thanks again for writing up your counter-proposal!
I realized I'm not sure how my ClassMix library will work with this proposal, if at all. Clearly the subclass constructor in the example would have to change to account for the fact that super must be called, along with the default constructor created by the library, but I'm not sure how. Seems like I'd have to invoke super constructors and explicitly mix the instances returned. At any rate this code is definitely much harder to implement with the @@new proposal.
On Jun 17, 2014 7:44 PM, "Brendan Eich" <brendan at mozilla.org> wrote:
So far, I prefer your proposal to draft ES6 by a lot -- especially since
I missed the hideous Number special-casing spread around in the draft!
I don't.
How does this work with legacy classes?
function B() { this.x = 1; } class C extends B {}
I just do not see how this solves any issues. As far as I can tell we will still need to call the constructor after @@new is called. We need the two phase initialization. One function for creating the instance and one to initialize it.
Jason Orendorff wrote:
Is
C === C[@@new]
?Good question. I think calling
C(...args)
should be the same as callingnew C(...args)
. How best to specify that, I'm not sure.Isn't the question of whether C(...args) is allowed as short for new
C(...args) a separable "option" from the rest of your proposal (and from draft ES6)?
I'd hate for your proposal to founder on this somewhat controversial
issue.
I don't think
C === C[@@new]
needs to be a goal,Should be specified not to be === in my view because:
it wouldn't hold for regular functions, or for classes that don't define a constructor (those inherit their @@new method from the base class), or for classes that contain a
static [Symbol.new]()
method.See Allen's latest followup on this -- is it a static error to have both
constructor and the static Symbol.new method?
But as an implementation detail, if that's the easiest way to specify it, OK.
See above. LMK if you disagree with anything, or anything's unclear.
Thanks again for writing up your counter-proposal!
On Tue, Jun 17, 2014 at 6:55 PM, Erik Arvidsson <erik.arvidsson at gmail.com> wrote:
How does this work with legacy classes?
function B() { this.x = 1; } class C extends B {}
That works!
new C
desugars to C[@@new]()
. C doesn't have a @@new method of
its own, so it inherits Function.prototype[@@new]
.
The algorithm for that method is given in the proposal. Step 1 selects the right prototype here (C.prototype), and step 4 calls B as desired.
From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Brendan Eich
See Allen's latest followup on this -- is it a static error to have both constructor and the static Symbol.new method?
IMO it shouldn't be, because it'd be weird to get an error for constructor
+ static [Symbol.new]()
, but to not get an error for constructor
+ static [Symbol["n" + "ew"]]()
and similar.
Another way of guiding the decision: I don't quite recall where the spec landed { x: 1, ["x"]: 2 }
, but we should probably be consistent with that.
On Jun 17, 2014, at 4:44 PM, Brendan Eich wrote:
... especially since I missed the hideous Number special-casing spread around in the draft!
could you be a bit more specific about what you are referring to?
On Tue, Jun 17, 2014 at 4:02 PM, Jason Orendorff <jason.orendorff at gmail.com>
wrote:
On Tue, Jun 17, 2014 at 2:49 PM, Jasper St. Pierre <jstpierre at mecheye.net> wrote:
How would
constructor() { if (rand() > 0.5) super("A"); }
behave?
SyntaxError.
We could prevent this behavior by making sure that super(); must be the first statement in a constructor, [...]
That is what I proposed.
What does:
class B {
constructor(a) {
this._a = a;
}
}
class C extends B {
constructor(b) {
this._b = b;
}
}
do? Some kind of compiler error?
On Jun 17, 2014, at 5:22 PM, Allen Wirfs-Brock wrote:
On Jun 17, 2014, at 4:44 PM, Brendan Eich wrote:
... especially since I missed the hideous Number special-casing spread around in the draft!
could you be a bit more specific about what you are referring to?
Never mind, I think I know what you mean. You mean, the already initialized checks which fall out of objects having distinct allocation and initialization functions. Those checks only occur in functions that directly access private internal state of built-in oibjects.
Such methods typically have a prologue that look something like this:
• 1. Let M be the this value.
• 2. If Type(M) is not Object, then throw a TypeError exception.
• 3. If M does not have a [[MapData]] internal slot throw a TypeError exception.
• 4. If M’s [[MapData]] internal slot is undefined, then throw a TypeError exception.
The fourth step could probably be eliminated under Jason's proposal, the other would still be needed. One of the things we need to explore is whether we loose would loose any important subclass flexibility by tightly couple allocation and initialization again. The 'super' must be first issue is at the forefront of that question.
SyntaxError.
We could prevent this behavior by making sure that super(); must be the first statement in a constructor, [...]
That is what I proposed.
Sorry for my previous gut reaction. This looks like a good proposal, but it makes a break with the evolutionary design of ES classes. I think sticking to an evolutionary path with classes has played a big part in their success and I think we should stick to that.
Furthermore, when we were hashing out classes, several people (including me
I think) tried to suggest ways to maintain the C() <=> new C()
equivalence established by the built-ins, and it never really took off. I think maybe the reason is that that equivalence isn't very useful.
On 6/17/14, 8:47 PM, Allen Wirfs-Brock wrote:
You mean, the already initialized checks which fall out of objects having distinct allocation and initialization functions. Those checks only occur in functions that directly access private internal state of built-in oibjects.
I should note that for typical "DOM" (not just nodes; pretty much every non-ES-builtin in the web platform) this is every single function. And that the current separation of allocation and initialization means that for these objects we have the following choices (probably on a per-object-class basis):
- Make the object not subclassable.
- Make the object subclassable and have WebIDL this value handling perform some sort of "is initialized" checks.
- Make the object subclassable and have specification prose for all its methods/properties perform "is initialized" checks.
.#1 is generally undersirable, #3 requires a level of understanding of ES guts on the part of web spec authors that I suspect is not present and might be hard to achieve (and in particular requires them to explicitly opt in to making things subclassable).
Even #2 is a bit of a pain in terms of requiring extra work on every single thing you do, which is not all that great from a performance standpoint.
If we can have a world in which web platform objects don't exist in constructed-but-not-initialized states, I personally would much prefer that....
Domenic Denicola wrote:
From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Brendan Eich
See Allen's latest followup on this -- is it a static error to have both constructor and the static Symbol.new method?
IMO it shouldn't be, because it'd be weird to get an error for
constructor
+static [Symbol.new]()
, but to not get an error forconstructor
+static [Symbol["n" + "ew"]]()
and similar.
Silly me -- strike that "static" before "error" -- it still could be a strict error, but see latest meeting minutes where MarkM changed his position (cited below).
Another way of guiding the decision: I don't quite recall where the spec landed
{ x: 1, ["x"]: 2 }
, but we should probably be consistent with that.
Mark Miller: I am ok with removing the constraint that duplicate dynamic object properties throw (in strict mode) with the caveat that we also remove the same constraint for duplicate static properties.
from esdiscuss.org/notes/2014-06-06#rest-properties-and-spread-properties-sebastian-markb-ge-.
Still, in this case we have a ClassElement special form, constructor(){}. This distinction adds a choice not present in the ObjectLiteral case: to have a strict (dynamic) error on duplicate Symbol.new-equivalent name.
Boris Zbarsky wrote:
If we can have a world in which web platform objects don't exist in constructed-but-not-initialized states, I personally would much prefer that....
+¶
Kevin Smith wrote:
Sorry for my previous gut reaction. This looks like a good proposal, but it makes a break with the evolutionary design of ES classes.
How so? @@create or @@new, either way something in ES6 is new that was not in classes-desugared-to-functions.
Furthermore, when we were hashing out classes, several people (including me I think) tried to suggest ways to maintain the
C() <=> new C()
equivalence established by the built-ins, and it never really took off. I think maybe the reason is that that equivalence isn't very useful.
I think Jason's proposal leaves this C ==== C[Symbol.new] condition a separate choice from everything else, and choosing the way you prefer does not break the rest of the design. Jason?
On Tue, Jun 17, 2014 at 11:11 PM, Brendan Eich <brendan at mozilla.org> wrote:
I think Jason's proposal leaves this C ==== C[Symbol.new] condition a separate choice from everything else, and choosing the way you prefer does not break the rest of the design. Jason?
Yes, that's correct.
I really like this proposal.
2014-06-17 21:32 GMT+02:00 C. Scott Ananian <ecmascript at cscott.net>:
+1.
I think you could also remove the 'construct' handler in Proxy ( people.mozilla.org/~jorendorff/es6-draft.html#sec-construct-internal-method step 4), as the @@new method can be handled with the usual proxy method dispatch mechanism. I'll leave it to someone who better understands Proxies to give the details.
That's what Jason meant by getting rid of the construct
trap.
Your proposal is appealing but I haven't convinced myself it works. There are a couple bits I haven't quite grokked.
The special
constructor
method in ClassDeclaration/ClassExpression syntax would desugar to a static @@new method. This class:class Point { constructor(x = 0, y = 0) { this.x = x; this.y = y; } }
would amount to this:
class Point { static [Symbol.new](x = 0, y = 0) { var obj = superSymbol.new; obj.x = x; obj.y = y; return obj; } }
You're inlining the method body into the @@new body, but I want to make sure I understand what the specification of the Point function itself would be. You've said Point !== Point[@@new] but calling Point(...args) should behave the same as new Point(...args). So then would the specification of the Point function's behavior just be to delegate to Point@@new?
The "super Arguments" call syntax in the ES6 drafts would be constrained to appear only at the top of a constructor, as in Java:
This part is what I'm the most unclear about. What invariant are you trying to maintain? It seems like you're using this to attempt to guarantee that all superclasses have had the chance to initialize their internal fields before user code starts running, so that built-in classes can have consistent internal representations and not have to worry about guarding against reading from uninitialized internal slots. But this is a tricky invariant to guarantee. First of all I think you'd have to put syntactic restrictions in place to disallow referring to this
in the super call, to avoid e.g.:
class Sneaky extends Date { constructor(x) { super((console.log(this.valueOf()), x)); } }
But that's not enough, because you also have to worry about the superclass constructors calling overloaded methods:
class Dupe extends Date { constructor(x) { super(x); initDupe(); } initDupe() { ... } }
class Sneaky extends Dupe { constructor(x) { super(x); } initDupe() { console.log(this.valueOf()); super.initDupe(); } }
(These are common pitfalls in languages that try to provide hard guarantees about field initialization. They come up, for example, in typed OO languages that try to provide guaranteed non-nullable fields.)
But maybe I'm misunderstanding what invariant you're aiming at?
- Base class constructors are always called.
Are you saying you would not only restrict the super call to the top, but require one to always be there? Or would an absence of a super call imply an implicit super()
?
These productions would be dropped from the ES6 grammar:
MemberExpression : new super Arguments NewExpression : new super
Clearly with your design super[Symbol.new](...args)
is equivalent to new super
. Is it also emulate-able in the @@create semantics also? If so it seems like the question of whether to keep or drop the new super
syntax is 100% orthogonal to your proposal.
On Tue, Jun 17, 2014 at 12:21 PM, Jason Orendorff <jason.orendorff at gmail.com
wrote: ...
Benefits
As with Allen's proposal, we would drop [[Construct]] and the
construct
trap.@@create can be dropped entirely, but we retain the benefit to implementers that all Maps (for example) can have the same in-memory layout, and other objects can't become Maps at runtime.
These are pure implementation details, and only implementation improvements, that are not that interesting to actual developers.
This would reduce the number of `super` forms to these 3: super.IdentifierName // allowed in all methods super[Expression] // allowed in all methods super(arguments) // only allowed in constructors
I missed that: so we got another restriction, and devs won't be able to use
convenient concise super()
calls from other methods? I don't think it's
the best direction.
Overall, we got three restrictions:
(1) super only on top of the constructor, (2) want it or not: parent constructor is always called, (3) you can't use concise super calls from methods.
- bonus (4) you have to use some verbose syntax of static Symbol.new {} to do needed hacks.
What all these for? To exclude [[Construct]]/@@create form the implementation? But developers are not interested in the implementation. Developers want to make convenient super-calls, do not call parent constructors if it's not needed (yes, it's rare, but can be), or to call it whenever they want after predefined prologue.
Personally I think two traps are more intuitive and convenient (and are
present in many other languages: C++, Ruby, etc) than this
"spec/implementation-optimization", and users would appreciate the new
and constructor
traps. The former can be used in rare cases when they'll
need custom allocation (and this
value is not available there yet), and
the constructor
is a normal initialiser of the allocated instance.
I mean, there should be a very strong reason to change an intuitive semantics to this approach. "Spec/implementation optimizations" is not the best reason, and that "parent constructor should always be called" is a subjective, and just one of the semantics.
P.S.: as for the libraries, some "libraries" (actually my tests) were also coupled to this allocate/initialize pair hooks approach: gist.github.com/DmitrySoshnikov/1367025#file-r-proto-class-class-js-L23-L41
Dmitry
On 18 June 2014 09:40, Dmitry Soshnikov <dmitry.soshnikov at gmail.com> wrote:
On Tue, Jun 17, 2014 at 12:21 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
Benefits
As with Allen's proposal, we would drop [[Construct]] and the
construct
trap.@@create can be dropped entirely, but we retain the benefit to implementers that all Maps (for example) can have the same in-memory layout, and other objects can't become Maps at runtime.
These are pure implementation details, and only implementation improvements, that are not that interesting to actual developers.
The main part of this point is getting rid of @@create, which is far more than an implementation detail. If we can actually pull this off -- in particular, getting rid of the ability to observe half-constructed objects -- then I'd +100 it. It's still worth trying, but as David points out, it's not so easy.
On 6/18/14, 3:40 AM, Dmitry Soshnikov wrote:
Personally I think two traps are more intuitive and convenient (and are present in many other languages: C++, Ruby, etc)
C++ has the sorts of restrictions on calling the superclass constructor (have to do it up front, can't skip doing it) that the @@new proposal has, no?
and that "parent constructor should always be called" is a subjective, and just one of the semantics.
While true, I'd like to see a concrete proposal about how we actually proceed with making the web platform subclassable in the @@create world. Whereas in the @@new world subclassibility by default is at least somewhat viable...
From: Jason Orendorff<mailto:jason.orendorff at gmail.com>
Sent: 2014-06-17 15:22 To: Allen Wirfs-Brock<mailto:allen at wirfs-brock.com>
Cc: EcmaScript<mailto:es-discuss at mozilla.org>; Mark Miller<mailto:erights at gmail.com>
Subject: @@new
Allen asked me to fill out what @@new would mean. Here it is.
How new X
works
new X(...args)
==== X[@@new](...args)
How it works for ES5 builtin constructors
Number(value)
is specified in one section.
Number[@@new](value)
is specified in another section.
To support subclassing, Number[@@new]()
, unlike ES5 1, would set the
new object's prototype to this.prototype
rather than Number.prototype
.
Otherwise it all works just like ES5.
The same goes for Object, Array, Function, Date, and the other builtins.
How it works for regular functions
Function.prototype[@@new](...arguments)
is called. It works like this:
-
Let proto =
this.[[Get]]("prototype", this)
. -
If proto is not an object, throw a TypeError.
-
Let obj = ObjectCreate(proto).
-
If the this value or any object on its prototype chain is an ECMAScript language function (not a class), then
a. Let F be that object.
b. Let result =
F.[[Call]](obj, arguments)
.c. If Type(result) is Object then return result.
-
Return obj.
For regular functions, this behaves exactly like the [[Construct]]
internal
method in ES5 2. Step 4 is there to support regular functions and ES6 classes
that subclass regular functions.
How it works for ES6 classes
The special constructor
method in ClassDeclaration/ClassExpression syntax
would desugar to a static @@new method. This class:
class Point {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
would amount to this:
class Point {
static [Symbol.new](x = 0, y = 0) {
var obj = super[Symbol.new]();
obj.x = x;
obj.y = y;
return obj;
}
}
As in the current drafts, ES6 would have to do something mildly fancy (see 3 and step 8 of 4) to perform the desugaring.
If a class has no constructor
method, it inherits the base class's static
@@new method.
The "super Arguments" call syntax in the ES6 drafts would be constrained to appear only at the top of a constructor, as in Java:
class PowerUp {
constructor(name, price) { ... }
}
class RocketPack extends PowerUp {
constructor() {
super("Rocket Pack", 1000);
this.fuel = FULL_TANK;
}
}
The RocketPack constructor would behave like this:
static [Symbol.new]() {
var obj = super[Symbol.new]("Rocket Pack", 1000);
obj.fuel = FULL_TANK;
return obj;
}
This has different runtime semantics compared the super()
syntax in the
current ES6 draft, but it serves the same purpose.
And that's all.
Benefits
-
As with Allen's proposal, we would drop [[Construct]] and the
construct
trap. -
Instances of builtin classes are never exposed to scripts before their internal slots are fully initialized.
-
@@create can be dropped entirely, but we retain the benefit to implementers that all Maps (for example) can have the same in-memory layout, and other objects can't become Maps at runtime.
-
Base class constructors are always called.
-
The ES6 draft language in 5 step 5, and many other similar places, would be backed out. ("If Type(O) is Object and O has a [[NumberData]] internal slot and the value of [[NumberData]] is undefined, then...")
-
The current ES6 draft specifies new builtin constructors (Map, Set, WeakMap) to throw a TypeError if called without
new
. This new convention could be reversed to what ES5 does with Function and Array:Map()
would be the same asnew Map()
. User-defined classes could work this way too. I think developers would appreciate this. -
These productions would be dropped from the ES6 grammar:
MemberExpression : new super Arguments NewExpression : new super
If for whatever reason users want these, they can call
super[Symbol.new]()
. But I think they will no longer be needed.This would reduce the number of
super
forms to these 3:super.IdentifierName // allowed in all methods super[Expression] // allowed in all methods super(arguments) // only allowed in constructors
What happens if I put [Symbol.new] on a non-function object? Is it now constructable? Presumably still not callable though, right?
What about the other direction?
class B { constructor(x) { this.x = x; } static Symbol.create { var o = super(); weakMap.set(o, 12345678); // DOM wrapper foo return o; } }
function C(x) { B.call(this, x); } C.proto = B; C.prototype = {proto: B.prototype};
I think the most concerning part of this proposal is that
constructor(...)
gets replaced by static [Symbol.new](...)
with strange
semantics regarding this
. If we instead had @@new call constructor by
default I think most of these concerns go away but then again we are back
to two initialization functions and the possibility to observe an object
that never went through its constructor.
Another thing to consider is to ensure that @@create initializes all of its slots. For example Date@@create could set its [[DateValue]] to NaN, Map@@create could set its [[MapData]] to an empty list and so on.
This also fits how @@create works for DOM, where the creation of the instance would set up the internal DOM wrapper pointer, never exposing a non initialized DOM object to user code.
From: es-discuss <es-discuss-bounces at mozilla.org> on behalf of Boris Zbarsky <bzbarsky at MIT.EDU>
While true, I'd like to see a concrete proposal about how we actually proceed with making the web platform subclassable in the @@create world. Whereas in the @@new world subclassibility by default is at least somewhat viable...
Hmm, I thought we'd already established how this would work. It's pretty seamless with the @@create approach. See:
gist.github.com/domenic/3ce8f57a33a25473f508
The "domPointer" stuff is kind of handwavey, but it's still pretty clear how it would work.
Following up on Arv's point about the DOM's use of @@create, my understanding is that having the two-phase initialization is quite important for the DOM, e.g. elements created by the parser will be allocated but never initialized (their constructors will never run). You can see this in action today with e.g. HTMLImageElement.
Another thing to consider is to ensure that @@create initializes all of its slots. For example Date@@create could set its [[DateValue]] to NaN, Map@@create could set its [[MapData]] to an empty list and so on.
I think if you go that route you still may need some checks in place to ensure that the initializer (constructor) is only executed once.
For instance, you'd never want the Promise constructor to be called twice on some promise object.
On Wed, Jun 18, 2014 at 8:55 AM, Domenic Denicola <domenic at domenicdenicola.com> wrote:
What happens if I put [Symbol.new] on a non-function object? Is it now constructable? Presumably still not callable though, right?
Yes, it would be constructable but still not callable.
On 6/18/14, 11:39 AM, Erik Arvidsson wrote:
This also fits how @@create works for DOM, where the creation of the instance would set up the internal DOM wrapper pointer, never exposing a non initialized DOM object to user code.
Note that in that setup it's impossible to introduce an HTMLElement constructor like this:
var el = new HTMLElement("iframe");
because the @@create doesn't have the tag name available to create the right sort of object...
This is the flyweight object issue that was mentioned up-thread too: if you have to examine your arguments to decide what sort of object to create, then @@create is not a viable option.
Whether we need to make this sort of constructor possible is a separate issue, of course; for newer specs we're using separate interfaces for separate things, so don't need this sort of constructor, I think.
On 6/18/14, 12:09 PM, Domenic Denicola wrote:
From: es-discuss <es-discuss-bounces at mozilla.org> on behalf of Boris Zbarsky <bzbarsky at MIT.EDU>
While true, I'd like to see a concrete proposal about how we actually proceed with making the web platform subclassable in the @@create world. Whereas in the @@new world subclassibility by default is at least somewhat viable...
Hmm, I thought we'd already established how this would work. It's pretty seamless with the @@create approach. See:
gist.github.com/domenic/3ce8f57a33a25473f508
The "domPointer" stuff is kind of handwavey, but it's still pretty clear how it would work.
So this is basically option 2 from esdiscuss/2014-June/037849 if I understand right?
On Jun 18, 2014, at 8:33 AM, Erik Arvidsson wrote:
I think the most concerning part of this proposal is that
constructor(...)
gets replaced bystatic [Symbol.new](...)
with strange semantics regardingthis
. If we instead had @@new call constructor by default I think most of these concerns go away but then again we are back to two initialization functions and the possibility to observe an object that never went through its constructor.
This is also one of my bigger concerns. I think the rewriting and reinterpretation of what the use wrote as a a constructor may be very problematic. For example,how does this get translated:
class extends C {
constructor() {
super();
super.foo(); //apply the inherited foo method to the new object
}
}
If we had a design that didn't require the rewriting of the user provided constructor I'd be much more comfortable. Thinking...more latter...
On Jun 18, 2014, at 8:39 AM, Erik Arvidsson wrote:
Another thing to consider is to ensure that @@create initializes all of its slots. For example Date@@create could set its [[DateValue]] to NaN, Map@@create could set its [[MapData]] to an empty list and so on.
This also fits how @@create works for DOM, where the creation of the instance would set up the internal DOM wrapper pointer, never exposing a non initialized DOM object to user code.
Right, that's the line I'm exploring. If it can work out @@create are @@new more or less the same thing, except for the constructor rewriting.
From: Boris Zbarsky [mailto:bzbarsky at MIT.EDU]
So this is basically option 2 from
esdiscuss/2014-June/037849 if I understand right?
Yes, although in this case no initialization checks are necessary, just brand checks. Which I guess speaks to your point about how getting this right is tricky.
I am not sure there are too many cases where there's an interesting distinction between allocated and initialized for the DOM, though. Given how few DOM objects have reasonable constructors to begin with, it seems like most of their work is being done (conceptually) in Symbol.create anyway.
On Wed, Jun 18, 2014 at 1:07 PM, Boris Zbarsky <bzbarsky at mit.edu> wrote:
On 6/18/14, 11:39 AM, Erik Arvidsson wrote:
This also fits how @@create works for DOM, where the creation of the instance would set up the internal DOM wrapper pointer, never exposing a non initialized DOM object to user code.
Note that in that setup it's impossible to introduce an HTMLElement constructor like this:
var el = new HTMLElement("iframe");
This particular one can be done because if the constructor returns an object that is used instead.
function HTMLElement(tagName = undefined) { if (tagName === undefined) throw ... var ctor = lookupConstructor(tagName); return new ctor; }
because the @@create doesn't have the tag name available to create the right sort of object...
But the constructor is able to return any object it want, basically making the constructor into a factory method.
This is an ugly pattern so it is discouraged but it does give you some leeway when you need to support strange things.
Allen Wirfs-Brock wrote:
This is also one of my bigger concerns. I think the rewriting and reinterpretation of what the use wrote as a a constructor may be very problematic. For example,how does this get translated:
class extends C { constructor() { super(); super.foo(); //apply the inherited foo method to the new object } }
If we had a design that didn't require the rewriting of the user provided constructor I'd be much more comfortable. Thinking...more latter...
I used to think this way, but dherman pointed out years ago that constructor in ES6 class is a special form, even though it looks like a method. Here's an es-discuss thread from three years ago:
Domenic Denicola wrote:
I am not sure there are too many cases where there's an interesting distinction between allocated and initialized for the DOM, though. Given how few DOM objects have reasonable constructors to begin with, it seems like most of their work is being done (conceptually) in Symbol.create anyway.
This all looks at the past, where the DOM is warty as hell (I can say that, I started it). What about the future?
On Wed, Jun 18, 2014 at 1:40 AM, David Herman <dherman at mozilla.com> wrote:
You're inlining the method body into the @@new body, but I want to make sure I understand what the specification of the Point function itself would be. You've said Point !== Point[@@new] but calling Point(...args) should behave the same as new Point(...args). So then would the specification of the Point function's behavior just be to delegate to Point@@new?
Yes, but that's not central to the proposal.
As it stands in the current draft, Point() would throw a TypeError,
because it tries to set this.x = x
and this
would be undefined. We
could keep the semantics exactly as they are even with @@new: just run
the body of the constructor function... It wouldn't be useful though,
just weird. I dunno, the status quo is weird. I don't feel strongly
about this.
The "super Arguments" call syntax in the ES6 drafts would be constrained to appear only at the top of a constructor, as in Java:
This part is what I'm the most unclear about. What invariant are you trying to maintain? It seems like you're using this to attempt to guarantee that all superclasses have had the chance to initialize their internal fields before user code starts running, [...] But maybe I'm misunderstanding what invariant you're aiming at?
Yeah. I don't care about the new method itself leaking this
. I just
want to make sure it's possible for a class implemented with
reasonable care to initialize its instances fully before exposing
them---and prove it, by making the ES6 builtin classes do so.
I had a syntactic restriction on super
for two reasons, neither one
related to the kind of invariant you're thinking of: (1) if the
super() call isn't there, I wanted to call it implicitly, for
convenience and correctness-by-default; and, (2) until you call the
base class @@new, there is no this
value. But the syntactic
restriction isn't sufficient for purpose #2, and besides none of us
like it, so I will try to find another way. :)
- Base class constructors are always called.
Are you saying you would not only restrict the super call to the top, but require one to always be there? Or would an absence of a super call imply an implicit
super()
?
The latter.
These productions would be dropped from the ES6 grammar:
MemberExpression : new super Arguments NewExpression : new super
Clearly with your design
super[Symbol.new](...args)
is equivalent tonew super
. Is it also emulate-able in the @@create semantics also?
I think yes, if we drop [[Construct]] as Allen separately proposed. As
long as [[Construct]] exists as a separate internal method, new super
is inconvenient to simulate.
If so it seems like the question of whether to keep or drop the
new super
syntax is 100% orthogonal to your proposal.
I think that's right. I assumed the use cases for new super
must be
something to do with how objects are constructed now, but on
reflection I don't know what the use cases are.
On Jun 18, 2014, at 11:09 AM, Brendan Eich wrote:
Allen Wirfs-Brock wrote:
This is also one of my bigger concerns. I think the rewriting and reinterpretation of what the use wrote as a a constructor may be very problematic. For example,how does this get translated:
class extends C { constructor() { super(); super.foo(); //apply the inherited foo method to the new object } }
If we had a design that didn't require the rewriting of the user provided constructor I'd be much more comfortable. Thinking...more latter...
I used to think this way, but dherman pointed out years ago that constructor in ES6 class is a special form, even though it looks like a method. Here's an es-discuss thread from three years ago:
Except in the current design there is no special reinterpretation of the constructor method body semantics. The constructor method definition simply provides body of the constructor function and semantically is exactly the same as the body that is provided in a Function definition. See people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-classdefinitionevaluation and in particular step 10 which calls people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-definemethod which just calls people.mozilla.org/~jorendorff/es6-draft.html#sec-functioncreate . No special processing of the body anywhere along that path. The only special treatment prior to step 10 is about choosing a default constructor body if a constructor method isn't provided in the class declaration.
It is an alternative syntax for providing a the body of a function, but it has no unique semantics. That seems significant to me.
On Wed, Jun 18, 2014 at 2:18 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:
I had a syntactic restriction on
super
for two reasons, neither one related to the kind of invariant you're thinking of: (1) if the super() call isn't there, I wanted to call it implicitly, for convenience and correctness-by-default; and, (2) until you call the base class @@new, there is nothis
value. But the syntactic restriction isn't sufficient for purpose #2, and besides none of us like it, so I will try to find another way. :)
Just for the record, Java maintains a similar syntactic/semantic
invariant on the use of super
, so it can certainly be made to work.
On the other hand, it's also entirely true that nobody likes it very
much in Java, although they live can live with it (and there are
workarounds, like making one of the arguments a call to a static
method to do important work before the superclass constructor is
invoked).
On Jun 18, 2014, at 11:18 AM, Jason Orendorff wrote:
If so it seems like the question of whether to keep or drop the
new super
syntax is 100% orthogonal to your proposal.I think that's right. I assumed the use cases for
new super
must be something to do with how objects are constructed now, but on reflection I don't know what the use cases are.
It's there now mostly just for syntactic consistency. You can say 'new this' so why wouldn't you be able to say 'new super'. It probably has limited use cases but it is one of those things that falls out of the overall expression syntax and I think if somebody does have a use case it would be surprising that it isn't allowed.
Allen Wirfs-Brock wrote:
Except in the current design there is no special reinterpretation of the constructor method body semantics. The constructor method definition simply provides body of the constructor function and semantically is exactly the same as the body that is provided in a Function definition. See people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-classdefinitionevaluation, people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-classdefinitionevaluation and in particular step 10 which calls people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-definemethod, people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-definemethod which just calls people.mozilla.org/~jorendorff/es6-draft.html#sec-functioncreate, people.mozilla.org/~jorendorff/es6-draft.html#sec-functioncreate . No special processing of the body anywhere along that path. The only special treatment prior to step 10 is about choosing a default constructor body if a constructor method isn't provided in the class declaration.
Fair point, things have changed. Still, here are some other bits of magic that make constructor(){} as ClassElement not just a method definition. Here's another:
- 'constructor' is not enumerable but methods are.
It is an alternative syntax for providing a the body of a function, but it has no unique semantics. That seems significant to me.
Not so much to me, and one could argue for static @@new{} sugar similarly.
The most important thing here (I agree with Andreas R.) is -- if possible -- avoiding uninitialized object observability.
Brendan Eich wrote:
Still, here
Er, "there"
are some other bits of magic that make constructor(){} as ClassElement not just a method definition. Here's another:
- 'constructor' is not enumerable but methods are.
Someone should compile the complete list.
From: Brendan Eich <brendan at mozilla.org>
This all looks at the past, where the DOM is warty as hell (I can say that, I started it). What about the future?
Much the same, from what I understand. The parser needs to be able to create elements, including custom elements, without knowing what their constructor signature is. As specced 1, registering the element will set its @@create value to something that creates the element in this way, and presumably the parser will be updated to invoke @@create. (This is the same as how, in ES5, the parser invokes the createdCallback
property passed to registerElement
.)
Dmitri, did I get that right?
On Wed, Jun 18, 2014 at 2:54 PM, Domenic Denicola < domenic at domenicdenicola.com> wrote:
From: Brendan Eich <brendan at mozilla.org>
This all looks at the past, where the DOM is warty as hell (I can say that, I started it). What about the future?
Much the same, from what I understand. The parser needs to be able to create elements, including custom elements, without knowing what their constructor signature is. As specced [1], registering the element will set its @@create value to something that creates the element in this way, and presumably the parser will be updated to invoke @@create. (This is the same as how, in ES5, the parser invokes the
createdCallback
property passed toregisterElement
.)Dmitri, did I get that right?
Channeling Dimitri here since I designed that part...
The important part for Custom Elements is that @@create is non configurable, non writable so that we can use a native implementation that sets up all the internal wrapper pointers and whatnot. This is so that the parser does not have to call into user code at parse time.
However, this still allows user code to provide a constructor that gets
invoked when doing new MyCustomElement(x, y, z)
.
On 6/18/14, 1:46 PM, Domenic Denicola wrote:
Yes, although in this case no initialization checks are necessary, just brand checks.
Well. That depends on the desired behavior.
For example, say you have a DOMPoint as in your example above that is supposed to return Numbers for x and y.
In your pseudocode, what happens if .x or .y is accessed after @@create but before the constructor runs? That depends on what slot initialization, if any, allocateNativeObjectWithSlots performs. If a spec author wanted to not depend on implementation details like that, they would need to define that @@create puts something in the slots separate from whatever work the constructor does.
Note that at this point we're already forcing spec authors to think about the exact @@create behavior to make things subclassable and that in particular, every single existing spec that has a constructor will need to be revisited and modified as needed.
Doing an initialization check instead of a brand check would avoid needing to define @@create behavior in that sort of detail, I think, but adds extra checks that need to be performed, in addition to the brand check, and extra state (initialized or not) to be stored.
Given how few DOM objects have reasonable constructors to begin with
I'm not sure how you're defining "reasonable", but a quick look at Gecko's IDL shows 136 IDL files that have at least constructor in them that takes some arguments and does not simply take a single optional argument (a dictionary, say). For comparison, we have 586 IDL files total...
Some of these are not standards yet, and a few I think have a no-argument constructor in addition to the one with arguments, and a bunch are event constructors that would all presumably be defined in a similar way in the spec, but I think you're significantly underestimating the number of things that have constructors that actually do something with their arguments.
On 6/18/14, 2:54 PM, Domenic Denicola wrote:
Much the same, from what I understand. The parser needs to be able to create elements, including custom elements
Most "DOM" objects being added nowadays are not elements at all. We need a less ambiguous name for "web platform things". I've sometimes called them "IDL objects" or "Web IDL objects", but that's not all that clear either..
On 6/18/14, 1:58 PM, Erik Arvidsson wrote:
This particular one can be done because if the constructor returns an object that is used instead.
Ah, I see.
This is an ugly pattern so it is discouraged
Especially given that it requires special care when subclassing somethin with such a constructor, correct?
On Jun 18, 2014, at 11:41 AM, Brendan Eich wrote:
Allen Wirfs-Brock wrote:
Except in the current design there is no special reinterpretation of the constructor method body semantics. The constructor method definition simply provides body of the constructor function and semantically is exactly the same as the body that is provided in a Function definition. See people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-classdefinitionevaluation, people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-classdefinitionevaluation and in particular step 10 which calls people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-definemethod, people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-definemethod which just calls people.mozilla.org/~jorendorff/es6-draft.html#sec-functioncreate, people.mozilla.org/~jorendorff/es6-draft.html#sec-functioncreate . No special processing of the body anywhere along that path. The only special treatment prior to step 10 is about choosing a default constructor body if a constructor method isn't provided in the class declaration.
Fair point, things have changed. Still, here are some other bits of magic that make constructor(){} as ClassElement not just a method definition. Here's another:
- 'constructor' is not enumerable but methods are.
Yes, but that's just preserving the 'constructor' property attribute conventions established by function definition. Nothing to do with the semantics of the actual constructor body or that a "class" object".
It is an alternative syntax for providing a the body of a function, but it has no unique semantics. That seems significant to me.
Not so much to me, and one could argue for static @@new{} sugar similarly.
I think that fact that these really are semantically equivalent:
function F(x) {this.foo=}; class F {constructor(x) {this.foo=x}};
is pretty important to the evolutionary nature ES6 classes.
The most important thing here (I agree with Andreas R.) is -- if possible -- avoiding uninitialized object observability.
I agree that uninitialized observability is a pain and has been a on-going source of reentrancy bugs in the the more complex built-in constructors. I want to explore whether making the constructor arguments available to @@create provides an alternative way to eliminate that issue.
Also, much of the initialization checking complexity in the legacy built-ins is about maintain backwards compatibility for edgy use cases (for example, installing a built-in constructor that has factory called behavior as an instance method on one of its instances and then invoking it as as a method). Jason's explicit split between "called as a function" and "called as a method" make it easier to specify that but I'm not sure the other forward facing complexities it introduces is worth the spec. simplification we get for a few legacy built-in constructors.
There is a lot of legacy compatibility vs. future functionality trade-offs that have been made in the current design. It's good to look at those trade-off and see if any of them need to be rebalanced. But we need to be careful about the scope of changes we are considering and whether in the end we actually get to a better place.
On Wed, Jun 18, 2014 at 9:01 PM, Boris Zbarsky <bzbarsky at mit.edu> wrote:
Note that at this point we're already forcing spec authors to think about the exact @@create behavior to make things subclassable and that in particular, every single existing spec that has a constructor will need to be revisited and modified as needed.
Revisiting existing classes and making them suitable for subclassing seems like something that would be hard to avoid. Allen had to do it for all the built-in classes to make sure certain methods return this.constructor and such, I doubt we're going to avoid that.
On Wed, Jun 18, 2014 at 3:08 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
I think that fact that these really are semantically equivalent:
function F(x) {this.foo=x}; class F {constructor(x) {this.foo=x}};
is pretty important to the evolutionary nature ES6 classes.
That's the easy case. The class F
version in Jason's proposal
invokes a superclass constructor, but that does nothing so it's all
the same.
The harder question is:
function F(x) { this.x = x; }
function G(y) { this.y = y; }
G.prototype = new F;
vs
function F(x) { this.x = x; }
class G extends F { constructor(y) { this.y = y; } }
Allen Wirfs-Brock wrote:
Not so much to me, and one could argue for static @@new{} sugar similarly.
I think that fact that these really are semantically equivalent:
function F(x) {this.foo=x}; class F {constructor(x) {this.foo=x}};
is pretty important to the evolutionary nature ES6 classes.
Jason covered the combinations: his proposal supports class subclassing function, etc. What concretely do you mean here, if not that? If you mean refactoring from one to the other, what observably differs?
Again it's important to separate independent parts of the counter-proposal (which even Jason is changing, e.g. no diff between C() and new C()). The high order bit remains the uninitialized observability of the draft-ES6 way.
On 6/18/14, 3:14 PM, Anne van Kesteren wrote:
Revisiting existing classes and making them suitable for subclassing seems like something that would be hard to avoid.
I think the difference for me is whether making a class subclassable before careful auditing means potentially introducing suboptimal behavior (that we presumably fix when either we do the audit or someone reports a bug) or whether it means potentially introducing a security bug.
In the former case, we can conceivably allow subclassing immediately (possibly only in nightly builds or whatnot) and then work on resolving the issues people find. In the latter case the auditing needs to be a lot more stringent before subclassing is allowed, and allowing subclassing without auditing is just a non-starter. I'm OK shipping somewhat buggy code for people to experiment with in nightly builds, but I'm not OK shipping security bugs.
On Jun 18, 2014, at 1:43 PM, Brendan Eich wrote:
Allen Wirfs-Brock wrote:
Not so much to me, and one could argue for static @@new{} sugar similarly.
I think that fact that these really are semantically equivalent:
function F(x) {this.foo=x}; class F {constructor(x) {this.foo=x}};
is pretty important to the evolutionary nature ES6 classes.
Jason covered the combinations: his proposal supports class subclassing function, etc. What concretely do you mean here, if not that? If you mean refactoring from one to the other, what observably differs?
My understanding of the proposal was that:
class F {constructor(x) {this.foo=x}};
turns into the equivalent of:
function F(x) { /* ??? the initial message really isn't explicit about what does here */ }; F[Symbol.new] = {Symbol.new { //use an object literal to create a "method" kind of function var obj = super(); obj.foo = x }}[Symbol.new].toMethod(F);
which is quite different from you get for:
function F(x) {this.foo=x};
function F(x) { /* ??? the initial message really isn't explicit about what does here */ }; F[Symbol.new] = {Symbol.new { //use an object literal to create a "method" kind of function var obj = super(); obj.foo = x }}[Symbol.new].toMethod(F);
which is quite different from you get for:
function F(x) {this.foo=x};
Yes - the proposed semantics are quite different. The whole thrust of the proposal is to fuse initialization and allocation, which is completely at odds with how user-defined "classes" work in ES5.
I think Allen's suggestion of providing constructor arguments to @@create is promising. That would allow the implementation to allocate and initialize an object in one go, if desired. That seems to embody the advantages of fusing initialization and allocation, without the headache for the user.
I think Allen's suggestion of providing constructor arguments to @@create
is promising. That would allow the implementation to allocate and initialize an object in one go, if desired. That seems to embody the advantages of fusing initialization and allocation, without the headache for the user.
Never mind, that doesn't work. Sorry for the noise.
For background, here is the original proposal presentation about using @@create to enable subclassing of built-ins. meetings:subclassing_builtins.pdf
In particular see material that starts at slide 19. In this proposal, the initialized/uninitialized state isn't really about ensuring that an object has be correctly initialized. It's more about distinguishing calls to the the constructor as a factory(possibly a name-spaced factory or a factory installed as a method of another object) from calls (or super calls) to the constructor to initialize an instance.
On Wed, Jun 18, 2014 at 5:29 PM, Kevin Smith <zenparsing at gmail.com> wrote:
I think Allen's suggestion of providing constructor arguments to @@create is promising. That would allow the implementation to allocate and initialize an object in one go, if desired. That seems to embody the advantages of fusing initialization and allocation, without the headache for the user.
Never mind, that doesn't work. Sorry for the noise.
I'd like to understand. Why doesn't it work?
I'd like to understand. Why doesn't it work?
Allen wondered the same, so I'll just paste my reply to him:
So I was a little embarrassed about not thinking it through, and wanted to just void the comment.
But specifically, if the subclass wants to have a different set of constructor arguments than its base class, then presumably the subclass would have to override @@create with that signature.
class MyMap extends Map { constructor(a, b, c) { super(); /* other initialization */ } static [Symbol.create](a, b, c) { return super(); } }
Is that right?
That seems like too much of a burden for the user. And besides, it pollutes the nice separation of responsibilities that we currently have.
The current design does posit uninitialized objects, which implementers are obviously uncomfortable with. Could it be that we just haven't yet developed the implementation techniques for dealing with such uninitialized objects? Or is there a real problem with being able to observe them?
On Wed, Jun 18, 2014 at 4:09 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
Jason covered the combinations: his proposal supports class subclassing function, etc. What concretely do you mean here, if not that? If you mean refactoring from one to the other, what observably differs?
My understanding of the proposal was that:
class F {constructor(x) {this.foo=x}};
turns into the equivalent of:
function F(x) { /* ??? the initial message really isn't explicit about what does here */ }; F[Symbol.new] = {Symbol.new { //use an object literal to create a "method" kind of function var obj = super(); obj.foo = x }}[Symbol.new].toMethod(F);
which is quite different from you get for:
function F(x) {this.foo=x};
But again, what user-observable difference are you pointing to? new F(0)
in either case creates a new object inheriting from F.prototype,
sets that object's "foo" property to 0, and returns that object.
Subclassing either one is just class G extends F {...}
.
As a side note, just because you can write something in a complicated
way doesn't necessarily mean it's complicated. I think having F()
and new F()
do the same thing is simpler for users than having them
do different things. Having a single hook for object creation is
simpler than having two (like C++) or three (like Ruby and Smalltalk).
function F(x) {this.foo=x}
is short, not simple.
On Wed, Jun 18, 2014 at 4:09 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
function F(x) { /* ??? the initial message really isn't explicit about what does here */ };
I think focusing on what was in the initial message vs. what was explained later (or remains open for discussion) does us all a disservice. No proposal springs fully formed from Zeus' head. It's a process.
The most important thing here (I agree with Andreas R.) is -- if possible -- avoiding uninitialized object observability.
I agree that uninitialized observability is a pain and has been a on-going source of reentrancy bugs in the the more complex built-in constructors. I want to explore whether making the constructor arguments available to @@create provides an alternative way to eliminate that issue.
That means depending on the class/built-in a subclass needs to override either the @@create method or the constructor function or both?
For example let's take the Map built-in. The @@create method will initialise [[MapData]] to an empty list early on, that way Map instances are never observed uninitialised. Processing the iterable argument to fill the Map may either occur in @@create or in the Map constructor function. Subclasses will need to consult the specification to find out which method needs to be overridden if the iterable argument needs to be preprocessed.
For other (more special) built-ins like String/Number/Function/..., @@create will need to perform the complete initialisation including processing the arguments. Otherwise uninitialised objects are still observable. As a result subclasses are required to override @@create and overriding the constructor is a no-op (w.r.t. arguments handling).
Kevin Smith wrote:
The current design does posit uninitialized objects, which implementers are obviously uncomfortable with. Could it be that we just haven't yet developed the implementation techniques for dealing with such uninitialized objects? Or is there a real problem with being able to observe them?
André replied well, but I just wanted to confirm: this is not some shallow whiny-implementor-knee-jerk-reaction problem. It's a real problem, for JS programmers as well as implementors.
From a security perspective as well, I have always been uncomfortable with
observable uninitialized objects, though I have been too lazy to try demonstrating a concrete attack.
On Thu, Jun 19, 2014 at 8:39 AM, André Bargull <andre.bargull at udo.edu> wrote:
That means depending on the class/built-in a subclass needs to override either the @@create method or the constructor function or both?
For example let's take the Map built-in. The @@create method will initialise [[MapData]] to an empty list early on, that way Map instances are never observed uninitialised. Processing the iterable argument to fill the Map may either occur in @@create or in the Map constructor function.
Right. It should happen in the constructor. Map[@@create] can ignore its arguments and return an empty Map. Subclasses will not need to override @@create.
Subclasses will need to consult the specification to find out which method needs to be overridden if the iterable argument needs to be preprocessed.
Or just try it and see what happens, which is what people seem to do in practice. For Map, everything would just work.
However, TC39 could not then evolve Map[@@create] to take arguments in a later edition. Existing subclasses would rely on Map[@@create] ignoring its arguments.
For other (more special) built-ins like String/Number/Function/..., @@create will need to perform the complete initialisation including processing the arguments. Otherwise uninitialised objects are still observable. As a result subclasses are required to override @@create and overriding the constructor is a no-op (w.r.t. arguments handling).
All correct. :-|
For a time yesterday, I hoped that @@create + constructor would solve everything. Classes that need to fully initialize would use @@create; all other code would use constructor; nobody would have to care which. But Kevin's right: subclasses would need to care, and it's a burden.
Allen asked me to fill out what @@new would mean. Here it is.
How
new X
worksnew X(...args)
====X[@@new](...args)
How it works for ES5 builtin constructors
Number(value)
is specified in one section.Number[@@new](value)
is specified in another section.To support subclassing,
Number[@@new]()
, unlike ES5 1, would set the new object's prototype tothis.prototype
rather thanNumber.prototype
. Otherwise it all works just like ES5.The same goes for Object, Array, Function, Date, and the other builtins.
How it works for regular functions
Function.prototype[@@new](...arguments)
is called. It works like this:Let proto =
this.[[Get]]("prototype", this)
.If proto is not an object, throw a TypeError.
Let obj = ObjectCreate(proto).
If the this value or any object on its prototype chain is an ECMAScript language function (not a class), then
a. Let F be that object.
b. Let result =
F.[[Call]](obj, arguments)
.c. If Type(result) is Object then return result.
Return obj.
For regular functions, this behaves exactly like the
[[Construct]]
internal method in ES5 2. Step 4 is there to support regular functions and ES6 classes that subclass regular functions.How it works for ES6 classes
The special
constructor
method in ClassDeclaration/ClassExpression syntax would desugar to a static @@new method. This class:would amount to this:
As in the current drafts, ES6 would have to do something mildly fancy (see 3 and step 8 of 4) to perform the desugaring.
If a class has no
constructor
method, it inherits the base class's static @@new method.The "super Arguments" call syntax in the ES6 drafts would be constrained to appear only at the top of a constructor, as in Java:
The RocketPack constructor would behave like this:
This has different runtime semantics compared the
super()
syntax in the current ES6 draft, but it serves the same purpose.And that's all.
Benefits
As with Allen's proposal, we would drop [[Construct]] and the
construct
trap.Instances of builtin classes are never exposed to scripts before their internal slots are fully initialized.
@@create can be dropped entirely, but we retain the benefit to implementers that all Maps (for example) can have the same in-memory layout, and other objects can't become Maps at runtime.
Base class constructors are always called.
The ES6 draft language in 5 step 5, and many other similar places, would be backed out. ("If Type(O) is Object and O has a [[NumberData]] internal slot and the value of [[NumberData]] is undefined, then...")
The current ES6 draft specifies new builtin constructors (Map, Set, WeakMap) to throw a TypeError if called without
new
. This new convention could be reversed to what ES5 does with Function and Array:Map()
would be the same asnew Map()
. User-defined classes could work this way too. I think developers would appreciate this.These productions would be dropped from the ES6 grammar:
If for whatever reason users want these, they can call
super[Symbol.new]()
. But I think they will no longer be needed.This would reduce the number of
super
forms to these 3: