new instantiation design alternatives
On 9/11/14, 12:35 PM, Allen Wirfs-Brock wrote:
gist.github.com/allenwb/291035fbf910eab8e9a6 summaries the main syntactic changes since the meeting and provides rationales them. These features are common to both alternates. this is a good place to start, after reading the meeting notes.
Allen, thanks for putting this together!
I have a question: what do the uses of "this^" in this document mean? Are those meant to be "new^"?
oh oops! they are supposed to be "new^". Will fix...
It seems like new^ is a new argument that is implicitly passed to all functions.
What does new^ mean in an arrow function?
How does new^ interact with a proxy whose target is a constructor?
I meant to add: I think it's great that we're still iterating on this. Surely there's a way.
On Sep 11, 2014, at 1:37 PM, Jason Orendorff wrote:
It seems like new^ is a new argument that is implicitly passed to all functions.
What does new^ mean in an arrow function?
How does new^ interact with a proxy whose target is a constructor?
see rwaldron/tc39-notes/blob/master/es6/2014-07/instantiation-reform.pdf
new^
has the value of the receiver argument to the [[Construct[[ internal method. This is a parameter newly added to [[Construct]]. Normally it is set to the the value that new
is applied to. But for new super
calls it is set to the current new^
value. In that regard it is like the "receiver" parameter to [[Get]] and [[Set]]. If a constructor is invoked via [[Call]] new^
has the value undefined
. that's how you can test for "called as a function" vs. "called as a constructor".
new^
is lexically scoped, just this this
and super
. If an arrow function references new^
it is the value of the closest lexically enclosing constructor function (a function that implements [[Construct]]).
Just like [[Get]] and [[Set]], the trap for [[Construct]] is extended to include a "receiver" parameter.
Ob. bikeshed: why not new?
as the magic pair of tokens? Seems
unambiguous given new
as a reserved identifier since dayone.
On Sep 11, 2014, at 3:03 PM, Brendan Eich wrote:
Ob. bikeshed: why not
new?
as the magic pair of tokens? Seems unambiguous givennew
as a reserved identifier since dayone./be
"new?" would be fine,. Actually better. But we shied away from it, so as to not impinge upon future use of "?". Even if there was no lexical ambiguity there might be conceptual confusion with some future usage.
But, if there is a general willingness to to use "?", I'd be all for it.
BTW, "new^" (other whatever) is intended to be a single token, not a pair of tokens.
Le 12 sept. 2014 à 00:03, Brendan Eich <brendan at mozilla.org> a écrit :
Ob. bikeshed: why not
new?
as the magic pair of tokens? Seems unambiguous givennew
as a reserved identifier since dayone.
If one day we introduce the so-called existential operator foo?.bar
, it will be quite confusing to already have new?.constructor
for something else.
Allen Wirfs-Brock wrote:
"new?" would be fine,. Actually better. But we shied away from it, so as to not impinge upon future use of "?". Even if there was no lexical ambiguity there might be conceptual confusion with some future usage.
When last we considered refutable patterns, e.g., match (e) { case {x, y, ?z}: /* x and y, maybe z */ }, the ? came in front and was in the pattern language -- a new and different sub-grammar.
What other possible future ? uses were there?
But, if there is a general willingness to to use "?", I'd be all for it.
BTW, "new^" (other whatever) is intended to be a single token, not a pair of tokens.
+1 to 1 token.
Claude Pache wrote:
Le 12 sept. 2014 à 00:03, Brendan Eich<brendan at mozilla.org> a écrit :
Ob. bikeshed: why not
new?
as the magic pair of tokens? Seems unambiguous givennew
as a reserved identifier since dayone.If one day we introduce the so-called existential operator
foo?.bar
, it will be quite confusing to already havenew?.constructor
for something else.
Hah, I should have read ahead. You're right we were considering foo?.bar here:
I wrote that and championed it. But I probably forgot about it ten minutes ago when replying to Allen, because it seemed to fail for good at past TC39 meetings:
esdiscuss.org/topic/sept-18-tc39-meeting-notes#content-2
(search for "non-starter").
Then you rallied in this thread:
esdiscuss.org/topic/the-existential-operator
but it still didn't end well.
" If a constructor body contains an assignment of the form*this**=*then
automatic allocation is not performed and the constructor is expected to
perform manual allocation."
If I'm understanding this correctly, this means that snippet (A) would
never have access to this
, but snippet (B) would have an implicit
this
binding -- is that correct?
(A)
class Foo extends Bar {
constructor() {
if (false) {
this = super();
}
this; // undefined
}
}
(B)
class Foo extends Bar {
constructor() {
// No call to `this =` present in the constructor
this; // the automatically allocated object -- i.e. this !== undefined
}
}
If this is the case, it occurs to me that it would raise issues for things like automated refactoring and/or dead code elimination (as a minifier might do). Normally a minifier (or even a human) would expect to be able to eliminate the entire conditional altogether if they were confident the condition never evaluated to true. But with this static pre-check acting as the indicator for whether automatic allocation/binding should happen, doing so would cause the constructor to act very unexpectedly differently in the two cases.
I wish I could suggest an alternative, but nothing comes to mind right now.
Thoughts?
Le 12 sept. 2014 à 08:39, Jeff Morrison <lbljeffmo at gmail.com> a écrit :
" If a constructor body contains an assignment of the form this = then automatic allocation is not performed and the constructor is expected to perform manual allocation." If I'm understanding this correctly, this means that snippet (A) would never have access to
this
, but snippet (B) would have an implicitthis
binding -- is that correct?(A)
class Foo extends Bar { constructor() { if (false) { this = super(); } this; // undefined } }
(B)
class Foo extends Bar { constructor() { // No call to `this =` present in the constructor this; // the automatically allocated object -- i.e. this !== undefined } }
If this is the case, it occurs to me that it would raise issues for things like automated refactoring and/or dead code elimination (as a minifier might do). Normally a minifier (or even a human) would expect to be able to eliminate the entire conditional altogether if they were confident the condition never evaluated to true. But with this static pre-check acting as the indicator for whether automatic allocation/binding should happen, doing so would cause the constructor to act very unexpectedly differently in the two cases.
I wish I could suggest an alternative, but nothing comes to mind right now.
Thoughts?
So, let's explore how to cope with the design that consists to determine the automaticity of allocation/binding independently of the actual code of the constructor (as it was the case in my original design.)
The rule must be the following one:
- For constructors in classes, automatic allocation occurs if and only if there is no
extends
clause. - For constructors defined the ES1-5 way (outside classes), automatic allocation occurs.
That is satisfying for most cases while remaining backward compatible with ES1-5, but it is a priori problematic in the following situations, where one may want manual allocation whereas the rule mandates an automatic one.
(1) Base classes (i.e., classes without extends
clause) that want manual allocation. The legacy possibility of returning an alien object from the constructor remains. Alternatively, the following hack is possible, provided that we accept Function.prototype
as a constructor.
class Base extends Function.prototype { /* ... */ }
(IIUC, the prototype chain for both constructor and instances will be the same as without the extends Function.prototype
clause.) If that situation occurs often, we might want to provide sugar around that.
(2) Constructors defined outside classes that want manual allocation of this
. For these, I propose the following syntax:
function Derived(a, b) extends Base { /* ... */ }
as an almost equivalent of:
function Derived(a, b) { /* ... */ }
Derived.__proto__ = Base
Derived.prototype = { __proto__: Base.prototype || Object.prototype, constructor: Derived }
except that the automatic allocation of this
does not occurs in the former case. Like constructors defined in classes, the extends Function.prototype
trick is possible.
From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Brendan Eich
Ob. bikeshed: why not
new?
as the magic pair of tokens? Seems unambiguous givennew
as a reserved identifier since dayone.
This reads really well for the if (new?) { ... }
case, but poorly for the this = Object.create(new?.prototype)
case.
Thanks for this!
A question: why is it important to provide this new functionality ("new^", "this = whatever") to basic constructors? Why not use class syntax as an opt-in to these features?
Le 12 sept. 2014 à 15:19, Kevin Smith <zenparsing at gmail.com> a écrit :
Thanks for this!
A question: why is it important to provide this new functionality ("new^", "this = whatever") to basic constructors? Why not use class syntax as an opt-in to these features?
ES classes are only syntax; the objects defined by that syntax are just regular functions. Basically, these functions could have been defined the traditional way and have their properties related to prototypal inheritance manually adjusted afterwards. It would be artificial to limit a feature based on how the objects were constructed.
Sorry to interject, but was the rationale for needing a new syntax for this (vs. an API-based solution) presented anywhere? I can't seem to find it.
Sorry to interject, but was the rationale for needing a new syntax for this (vs. an API-based solution) presented anywhere? I can't seem to find it.
Feel free to correct me here...
The current setup (with @@create) was designed to provide sugar for
ES5-style OOP. As such, it separates allocation (the new
part) from
initialization (the constructor part). It's very elegant.
However, this appears to be unsatisfactory when subclassing built-in and host-provided classes. For those cases, we don't want to expose "allocated but uninitialized" objects (e.g. DOM objects implemented in C++). So separating allocation from initialization is not desired for those cases.
This led us to the idea of setting the "this" value from within the constructor itself, essentially fusing "constructor" and "@@create" into one function.
"new^" is argument originally provided to @@create. (Is that right?)
"this=" is how you initialize the this object (previously the return value from @@create).
Allen, is that a fair run-down?
I'm not sure I understand your point here... Are you saying that the only way to solve the DCE/refactoring problem is to either always implicitly allocate or never implicitly allocate?
Even when written explicitly, either by an IDE or a human, the
constructor(a, b, c) { this = new super(...arguments); ... }
pattern is usually bad. It is fine in a certain special case I mention below. It would be a disaster to have this pattern be the default smoothed over by a syntactic shortcut. Here's the problem:
----- at time v1 ----- class Point { constructor(x, y) this.x = x; this.y = y; } equals(otherPt) { return this.x === otherPt.x && this.y === otherPt.y; } }
class ColoredPoint extends Point { constructor(x, y, color) { this = new super(x, y); this.color = color; } // whether we override equals here is not the issue }
---- at later time/revision v2 ---- class Point { constructor(x, y, fudgeFactor = 0.000001) { this.x = x; this.y = y; this.fudgeFactor = fudgeFactor; } equals(otherPt) { return abs(this.x - otherPt.x) <= fudgeFactor && abs(this.y - otherPt.y) <= fudgeFactor; } }
Under normal circumstances, as shown above, this is a valid evolution of the Point class, even without examining or revising existing clients like ColoredPoint. Since the Point constructor had only two non-optional parameters, it could normally assume that existing clients had only called it with two arguments. Thus, it would normally be assumed compatible with existing clients to add new optional arguments. Passing all arguments by default breaks this assumption, making the "fragile base class" problem much worse.
The special case where the super(...arguments) is ok, even needed, is when the base class constructor already has a rest parameter.
EIBTI applies forcefully here.
Le 12 sept. 2014 à 17:07, Jeff Morrison <lbljeffmo at gmail.com> a écrit :
I'm not sure I understand your point here... Are you saying that the only way to solve the DCE/refactoring problem is to either always implicitly allocate or never implicitly allocate?
-Jeff
I didn't say it is the only way. It is one obvious way to solve the problem, and I was exploring whether that path was a viable one.
On Sep 11, 2014, at 11:39 PM, Jeff Morrison wrote:
" If a constructor body contains an assignment of the form this = then automatic allocation is not performed and the constructor is expected to perform manual allocation." If I'm understanding this correctly, this means that snippet (A) would never have access to
this
, but snippet (B) would have an implicitthis
binding -- is that correct?(A)
class Foo extends Bar { constructor() { if (false) { this = super(); } this; // undefined } }
(B)
class Foo extends Bar { constructor() { // No call to `this =` present in the constructor this; // the automatically allocated object -- i.e. this !== undefined } }
If this is the case, it occurs to me that it would raise issues for things like automated refactoring and/or dead code elimination (as a minifier might do). Normally a minifier (or even a human) would expect to be able to eliminate the entire conditional altogether if they were confident the condition never evaluated to true. But with this static pre-check acting as the indicator for whether automatic allocation/binding should happen, doing so would cause the constructor to act very unexpectedly differently in the two cases.
I wish I could suggest an alternative, but nothing comes to mind right now.
First a minor but technically important point: this
in (A) above is/isn't uninitialized rather than not having/having the value undefined.
Good overall point , tools that eliminate dead code will need to be aware of the significant of this =
.
Arguably, this is a point in favor of alternative two (no auto allocation).
Another possibility that we've considered is to tag non-auto-allocating constructors in their declaration header For example:
constructor() new {...}
function foo() new {...}
But, overall, we were trying to minimize syntactic embellishments and bikesheding opportunities.
On 12 September 2014 17:18, Mark S. Miller <erights at google.com> wrote:
Even when written explicitly, either by an IDE or a human, the
constructor(a, b, c) { this = new super(...arguments); ... }
pattern is usually bad. It is fine in a certain special case I mention below. It would be a disaster to have this pattern be the default smoothed over by a syntactic shortcut. Here's the problem:
----- at time v1 ----- class Point { constructor(x, y) this.x = x; this.y = y; } equals(otherPt) { return this.x === otherPt.x && this.y === otherPt.y; } }
class ColoredPoint extends Point { constructor(x, y, color) { this = new super(x, y); this.color = color; } // whether we override equals here is not the issue }
---- at later time/revision v2 ---- class Point { constructor(x, y, fudgeFactor = 0.000001) { this.x = x; this.y = y; this.fudgeFactor = fudgeFactor; } equals(otherPt) { return abs(this.x - otherPt.x) <= fudgeFactor && abs(this.y - otherPt.y) <= fudgeFactor; } }
Under normal circumstances, as shown above, this is a valid evolution of the Point class, even without examining or revising existing clients like ColoredPoint. Since the Point constructor had only two non-optional parameters, it could normally assume that existing clients had only called it with two arguments. Thus, it would normally be assumed compatible with existing clients to add new optional arguments. Passing all arguments by default breaks this assumption, making the "fragile base class" problem much worse.
Thanks Mark, this was exactly my concern as well. In general, it is bogus to assume that the parameter lists of a base and a derived constructor bear any relation. And even if they happen to do so today, they might no longer tomorrow, which your example demonstrates quite well. So just silently forwarding an argument list nilly-willy is broken.
I think it's fine to have a default "new super" call, but only with an empty argument list. That would also be more in line with what other languages do.
On Sep 12, 2014, at 8:01 AM, Kevin Smith wrote:
Sorry to interject, but was the rationale for needing a new syntax for this (vs. an API-based solution) presented anywhere? I can't seem to find it.
Feel free to correct me here...
The current setup (with @@create) was designed to provide sugar for ES5-style OOP. As such, it separates allocation (the
new
part) from initialization (the constructor part). It's very elegant.However, this appears to be unsatisfactory when subclassing built-in and host-provided classes. For those cases, we don't want to expose "allocated but uninitialized" objects (e.g. DOM objects implemented in C++). So separating allocation from initialization is not desired for those cases.
This led us to the idea of setting the "this" value from within the constructor itself, essentially fusing "constructor" and "@@create" into one function.
"new^" is argument originally provided to @@create. (Is that right?)
actually the, it's the this
passed to the @@create method.
"this=" is how you initialize the this object (previously the return value from @@create).
Allen, is that a fair run-down?
yes
On Sep 12, 2014, at 8:18 AM, Mark S. Miller wrote:
Even when written explicitly, either by an IDE or a human, the
constructor(a, b, c) { this = new super(...arguments); ... }
pattern is usually bad. It is fine in a certain special case I mention below. It would be a disaster to have this pattern be the default smoothed over by a syntactic shortcut. Here's the problem:
----- at time v1 ----- class Point { constructor(x, y) this.x = x; this.y = y; } equals(otherPt) { return this.x === otherPt.x && this.y === otherPt.y; } }
class ColoredPoint extends Point { constructor(x, y, color) { this = new super(x, y); this.color = color; } // whether we override equals here is not the issue }
---- at later time/revision v2 ---- class Point { constructor(x, y, fudgeFactor = 0.000001) { this.x = x; this.y = y; this.fudgeFactor = fudgeFactor; } equals(otherPt) { return abs(this.x - otherPt.x) <= fudgeFactor && abs(this.y - otherPt.y) <= fudgeFactor; } }
Under normal circumstances, as shown above, this is a valid evolution of the Point class, even without examining or revising existing clients like ColoredPoint. Since the Point constructor had only two non-optional parameters, it could normally assume that existing clients had only called it with two arguments. Thus, it would normally be assumed compatible with existing clients to add new optional arguments. Passing all arguments by default breaks this assumption, making the "fragile base class" problem much worse.
Mark, making such a change in a v2 would normally be considered bad practice in any OOP language I've worked with and probably would violate most local style guidelines. Subclassing for implementation sharing is at best gray box encapsulation. The developer of a subclass who over-rides and/or super invokes methods of a base class has to know some things about the base. Ideally, those requirements are captures in an explicitly documented subclassing contract provided by the base class' developer, but even if there is no such documentation there is always an implicit subclassing contract for every class that is used as a base class.
If you are the developer of a such a base class, there are practical constraints on if and how you can change your subclassing contract.
If you have access to all code that subclasses your base class, you can make any changes you want as long as you are willing to updated the code of every subclass. This is one of the tasks that motivated the invention of OO refactoring tools. You v2 change would be a change that fall into into this category. If you have access to all subclasses you can make the change, but you are also responsible for updating the subclasses.
But if you don't have access to all subclassing clients (for example if you base class is part of a widely used library or framework) you simply can't change your subclassing contract in this manner because, as you point out, you can't update existing client code that depends upon the existing contract. Whether or not auto new super occurs, the problem still exists as a subclass client may well have explicitly coded new super(...arguments)
in its subclass constructor. Arguably, such a subclass would be operating within the rules of the implicit subclassing contract of the v1 Point as its parameter list implies that it will pay attention to only two arguments. If the v1 developer wanted to future proof its constructor it should have stated it as constructor (x,y, ...reservedForTheFuture)
. Note that this general problem applies to all methods that might be super invoked, not just constructors.
To me, this issue is one of ease of use for the most common use case versus a slight refactoring hazard for a rare, and and likely ill-advised design change pattern. The most common use case is a subclass that simply adds an additional constructor parameter corresponding to an additional instance variable. And it that case, most people we shown the alternatives to prefer this:
class ColoredPoint extends Point {
constructor(x, y, color) {
this.color = color;
}
}
over
class ColoredPoint extends Point {
constructor(x, y, color) {
this = new super(x, y);
this.color = color;
}
}
On Sep 12, 2014, at 8:26 AM, Andreas Rossberg wrote:
Thanks Mark, this was exactly my concern as well. In general, it is bogus to assume that the parameter lists of a base and a derived constructor bear any relation. And even if they happen to do so today, they might no longer tomorrow, which your example demonstrates quite well. So just silently forwarding an argument list nilly-willy is broken.
I think it's fine to have a default "new super" call, but only with an empty argument list. That would also be more in line with what other languages do.
Other languages that do that are predominately statically-typed OO languages with static overloading. In that situation, the most generic choice is non-parameters as some overload has to be chosen for the implicit call site.
But for JS, which does have a static overload selection requirement, we have other alternatives and the no-argument choice seems highly error prone.
Consider Mark's example again. If developers are saying they would prefer to write:
class ColoredPoint extends Point {
constructor(x, y, color) {
this.color = color;
}
}
Then some will do it (regardless of the actual semantics) and they will silently get an instance without x or y properties. That is pretty clearly not their intent.
I think this is a much bigger error hazard then the v2 refactoring issue that Mark is concerned about.
On Sep 12, 2014, at 4:22 AM, Claude Pache wrote:
Le 12 sept. 2014 à 08:39, Jeff Morrison <lbljeffmo at gmail.com> a écrit :
That is satisfying for most cases while remaining backward compatible with ES1-5, but it is a priori problematic in the following situations, where one may want manual allocation whereas the rule mandates an automatic one.
(1) Base classes (i.e., classes without
extends
clause) that want manual allocation. The legacy possibility of returning an alien object from the constructor remains. Alternatively, the following hack is possible, provided that we acceptFunction.prototype
as a constructor.class Base extends Function.prototype { /* ... */ }
(IIUC, the prototype chain for both constructor and instances will be the same as without the
extends Function.prototype
clause.) If that situation occurs often, we might want to provide sugar around that.
This is actually how I intend to specify "auto allocation" to work for the class {}
case. Function.prototype is a constructor that is specified to unconditionally allocated and return an ordinary object. So, in reality there is no real difference between class Base {
and class Base extends Function.prototype {
(ignore that Function
is a mutable binding). But in the usage document I found it similar to talk about the no extends
as a special simple case.
(2) Constructors defined outside classes that want manual allocation of
this
. For these, I propose the following syntax:function Derived(a, b) extends Base { /* ... */ }
as an almost equivalent of:
function Derived(a, b) { /* ... */ } Derived.proto = Base Derived.prototype = { proto: Base.prototype || Object.prototype, constructor: Derived }
except that the automatic allocation of
this
does not occurs in the former case. Like constructors defined in classes, theextends Function.prototype
trick is possible.
I also considered this, but decided that we could avoid talking about what was likely to be a controversial syntactic extension until Es7.
Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
Mark, making such a change in a v2 would normally be considered bad practice in any OOP language I've worked with and probably would violate most local style guidelines. Subclassing for implementation sharing is at best gray box encapsulation. The developer of a subclass who over-rides and/or super invokes methods of a base class has to know some things about the base. Ideally, those requirements are captures in an explicitly documented subclassing contract provided by the base class' developer, but even if there is no such documentation there is always an implicit subclassing contract for every class that is used as a base class.
For better or worse, adding additional default arguments to existing functions is common practice. And I don't even agree that it is necessarily bad practice. There is no problem with it, except where people pass redundant arguments to functions -- that is bad practice. The language shouldn't default to that practice itself.
On Sep 12, 2014, at 8:26 AM, Andreas Rossberg wrote:
I think it's fine to have a default "new super" call, but only with an empty argument list. That would also be more in line with what other languages do.
Other languages that do that are predominately statically-typed OO languages with static overloading. In that situation, the most generic choice is non-parameters as some overload has to be chosen for the implicit call site.
But for JS, which does have a static overload selection requirement, we have other alternatives and the no-argument choice seems highly error prone.
No, this doesn't have anything to do with types or overloading -- since it is just syntactic sugar for a (uniquely, syntactically determined) explicit call, it is completely independent of such concerns, and would work just as "well" in any of these languages. If anything, such a feature would in fact be less problematic in such languages.
Consider Mark's example again. If developers are saying they would prefer to write:
class ColoredPoint extends Point { constructor(x, y, color) { this.color = color; } } ``
I don't find this a convincing in favour of implicit argument passing, rather the opposite. From my perspective, it unnecessarily obfuscates what's going on.
But either way, I have a hard time believing that many programmers with a background in the C syntax OO language family would expect the above to invoke Point(x,y, color), as that's what no other language in that family would do. So in addition to the refactoring hazard, I expect that such code would rather be confusing for the majority of programmers.
Also, in practice it is rather rare that the parameter list of a derived constructor happens to be an extension of the base one. Even all other considerations aside, I don't see a good reason to optimise for this minority case.
And it that case, most people we shown the alternatives to prefer this:
class ColoredPoint extends Point { constructor(x, y, color) { this.color = color; } }
over
class ColoredPoint extends Point { constructor(x, y, color) { this = new super(x, y); this.color = color; } }
I gotta side with Mark on this one.
Regarding preferring the first over the second, part of that may be because the syntax for the second is kinda...gnarly. If we want to merge allocation with initialization, then I'm going to want to invoke C# (yet again):
class ColoredPoint extends Point {
constructor(x, y, color) : super(x, y) {
this.color = color;
}
}
Here's a sketch of what I'm thinking:
ClassConstructor:
constructor ( StrictFormalParameters ) ClassCreateExpression[opt] {
FunctionBody }
ClassCreateExpression:
: LeftHandSideExpression
Within the function body of ClassConstructor, "isnew" is a keyword. It evaluates to true if the function was called via [Construct] and false otherwise.
The ClassCreateExpression is only called when the constructor is called via [Construct]. It is equivalent to returning a value from @@create in the previous design. It is required for classes with an extends clause. If the result of evaluating the expression is not an object, then an error is thrown.
Within the ClassCreateExpression:
- "this" refers to the function upon which "new" was evaluated.
- "super" is only allowed as a function call, and is equivalent to "new BaseClass()", where BaseClass is the result of the extends clause.
Simulating two-phase initialization, and making call and construct equivalent:
class C {
constructor(x) : this["@@create"]() {
if (!isnew)
return new C();
this[0] = x;
}
static ["@@create"]() {
var a = [];
Object.setPrototypeOf(a, this.prototype);
return a;
}
}
An abstract base class:
class AB {
constructor() : undefined {}
}
I think all of the ideas and examples map over pretty well, while avoiding
a TDZ for this
, "this=", and any funky new tokens. The cost would be
that you have to use class syntax to get the extended features. I think
that's a pretty good tradeoff.
Le 12 sept. 2014 à 17:26, Allen Wirfs-Brock <allen at wirfs-brock.com> a écrit :
Another possibility that we've considered is to tag non-auto-allocating constructors in their declaration header For example:
constructor() new {...}
function foo() new {...}
But, overall, we were trying to minimize syntactic embellishments and bikesheding opportunities.
The visual clue may be almost already there: According to the second variant, an extends
clause on the class indicates that there is no auto-allocation (unless the constructor is completely left out, of course). And an absence of extends
indicates that there is auto-allocation in most cases; it just remains to force to write Base extends Function.prototype
in the rarer case one want allocate this
manually. — But well, it is mostly personal taste, just like the * on generator functions.
On Thu, Sep 11, 2014 at 4:15 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
new^
is lexically scoped, just thisthis
andsuper
. If an arrow function referencesnew^
it is the value of the closest lexically enclosing constructor function (a function that implements [[Construct]]).Just like [[Get]] and [[Set]], the trap for [[Construct]] is extended to include a "receiver" parameter.
Thanks. The analogy to [[Get]] and [[Set]] helps a lot.
(Superficially, possible alternatives to new^
could include
class()
and class this
.)
One more minor question. Suppose I mistakenly type:
class ColorPoint extends Point {
constructor(x, y, color) {
this = super(x, y); // oops: should be `new super(x, y)`
this.color = color;
}
}
Then new ColorPoint(0, 0, "red")
throws a ReferenceError, because
super(x, y)
implicitly uses this
. Is that right? Asking just
because it seems like it'd be an easy mistake to make.
On Sep 12, 2014 6:39 PM, "Jason Orendorff" <jason.orendorff at gmail.com>
wrote:
On Thu, Sep 11, 2014 at 4:15 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
new^
is lexically scoped, just thisthis
andsuper
. If an arrow function referencesnew^
it is the value of the closest lexically enclosing constructor function (a function that implements [[Construct]]).Just like [[Get]] and [[Set]], the trap for [[Construct]] is extended to include a "receiver" parameter.
Thanks. The analogy to [[Get]] and [[Set]] helps a lot.
(Superficially, possible alternatives to
new^
could includeclass()
andclass this
.)One more minor question. Suppose I mistakenly type:
class ColorPoint extends Point { constructor(x, y, color) { this = super(x, y); // oops: should be `new super(x, y)` this.color = color; } }
Then
new ColorPoint(0, 0, "red")
throws a ReferenceError, becausesuper(x, y)
implicitly usesthis
. Is that right? Asking just because it seems like it'd be an easy mistake to make.
It will [[Call]] Point instead of [[Construct]] which may or may not work depending on how Point is defined.
On Sep 12, 2014, at 8:55 PM, Erik Arvidsson wrote:
On Sep 12, 2014 6:39 PM, "Jason Orendorff" <jason.orendorff at gmail.com> wrote:
On Thu, Sep 11, 2014 at 4:15 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
new^
is lexically scoped, just thisthis
andsuper
. If an arrow function referencesnew^
it is the value of the closest lexically enclosing constructor function (a function that implements [[Construct]]).Just like [[Get]] and [[Set]], the trap for [[Construct]] is extended to include a "receiver" parameter.
Thanks. The analogy to [[Get]] and [[Set]] helps a lot.
(Superficially, possible alternatives to
new^
could includeclass()
andclass this
.)One more minor question. Suppose I mistakenly type:
class ColorPoint extends Point { constructor(x, y, color) { this = super(x, y); // oops: should be `new super(x, y)` this.color = color; } }
Then
new ColorPoint(0, 0, "red")
throws a ReferenceError, becausesuper(x, y)
implicitly usesthis
. Is that right? Asking just because it seems like it'd be an easy mistake to make.It will [[Call]] Point instead of [[Construct]] which may or may not work depending on how Point is defined.
No, Jason was correct. It would get a Reference Error because super(x,y)
implicitly tries to pass this
which is still in its TDZ.
Kevin Smith wrote:
If we want to merge allocation with initialization, then I'm going to want to invoke C# (yet again):
class ColoredPoint extends Point { constructor(x, y, color) : super(x, y) { this.color = color; } }
Gotta credt C#'s parents, C++, which would require Point not super.
I think you are really onto something here. We do not want anything
looking like an expression-statement in the constructor body, which
besides being verbose has unwanted degrees of freedom (requiring TDZ for
this
, running into the dead code problem Jeff cited, etc.).
It sure seems to me that we rather want a special form in the constructor head. This came up in ES4.
Separately I agree this = new super(x, y);
is just crazy long and very
likely to be left out in whole or in part (the new
).
On Sun, Sep 14, 2014 at 7:23 PM, Brendan Eich <brendan at mozilla.org> wrote:
Kevin Smith wrote:
If we want to merge allocation with initialization, then I'm going to want to invoke C# (yet again):
class ColoredPoint extends Point { constructor(x, y, color) : super(x, y) { this.color = color; } }
Gotta credt C#'s parents, C++, which would require Point not super.
I think you are really onto something here. We do not want anything looking like an expression-statement in the constructor body, which besides being verbose has unwanted degrees of freedom (requiring TDZ for
this
, running into the dead code problem Jeff cited, etc.).It sure seems to me that we rather want a special form in the constructor head. This came up in ES4.
Separately I agree
this = new super(x, y);
is just crazy long and very likely to be left out in whole or in part (thenew
)./be
As a follow up to these comments re: this = new super(x, y);
, I couldn't
find anything in this thread or the gists that discussed how, if at all,
super
syntax or semantics will change in method bodies.
On Sep 14, 2014, at 5:27 PM, Rick Waldron wrote:
As a follow up to these comments re:
this = new super(x, y);
, I couldn't find anything in this thread or the gists that discussed how, if at all,super
syntax or semantics will change in method bodies.
See the third file in each of the two alternative gists (Specifically, gist.github.com/allenwb/5160d109e33db8253b62#file-3super-rules-md and gist.github.com/allenwb/53927e46b31564168a1d#summary-of-revised-semantics-for-super-based-references which I believe are identical)
The last file in each gist (gist.github.com/allenwb/5160d109e33db8253b62#file-5nakedsuperrationale-md ) is the rationale for this part of the design.
On Sun, Sep 14, 2014 at 8:51 PM, Allen Wirfs-Brock <allen at wirfs-brock.com>
wrote:
On Sep 14, 2014, at 5:27 PM, Rick Waldron wrote:
As a follow up to these comments re:
this = new super(x, y);
, I couldn't find anything in this thread or the gists that discussed how, if at all,super
syntax or semantics will change in method bodies.See the third file in each of the two alternative gists (Specifically, gist.github.com/allenwb/5160d109e33db8253b62#file-3super-rules-md and gist.github.com/allenwb/53927e46b31564168a1d#summary-of-revised-semantics-for-super-based-references which I believe are identical)
The last file in each gist ( gist.github.com/allenwb/5160d109e33db8253b62#file-5nakedsuperrationale-md, gist.github.com/allenwb/5160d109e33db8253b62#file-5nakedsuperrationale-md , again should be the same in each gist) is the rationale for this part of the design.
Thanks—there was a lot to read through and this is exactly what I was looking for.
On Sep 14, 2014, at 6:01 PM, Rick Waldron wrote:
On Sun, Sep 14, 2014 at 8:51 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
On Sep 14, 2014, at 5:27 PM, Rick Waldron wrote:
As a follow up to these comments re:
this = new super(x, y);
, I couldn't find anything in this thread or the gists that discussed how, if at all,super
syntax or semantics will change in method bodies.See the third file in each of the two alternative gists (Specifically, gist.github.com/allenwb/5160d109e33db8253b62#file-3super-rules-md and gist.github.com/allenwb/53927e46b31564168a1d#summary-of-revised-semantics-for-super-based-references which I believe are identical)
The last file in each gist (gist.github.com/allenwb/5160d109e33db8253b62#file-5nakedsuperrationale-md ) is the rationale for this part of the design.
Thanks—there was a lot to read through and this is exactly what I was looking for.
Rick
Right, I encourage lots of reading and thinking on this topic ;-)
On Sep 14, 2014, at 4:23 PM, Brendan Eich wrote:
Kevin Smith wrote:
If we want to merge allocation with initialization, then I'm going to want to invoke C# (yet again):
class ColoredPoint extends Point { constructor(x, y, color) : super(x, y) { this.color = color; } }
Gotta credt C#'s parents, C++, which would require Point not super.
I think you are really onto something here. We do not want anything looking like an expression-statement in the constructor body, which besides being verbose has unwanted degrees of freedom (requiring TDZ for
this
, running into the dead code problem Jeff cited, etc.).
the dead code problem (and we can debate whether it is actually a significant problem) goes away with alternative 2 (no automatic mew super in derived class constructors.
The TDZ constrained this
is an elegant solution which we had a good consensus on at the last TC39 meeting. Since then we have developed has it further unifies various allocation patterns that come up in JS object constructions. Everybody really need to really work through all the use cases given in the Gists and similarly work through them all with what every other alternatives they may think would be better.
It sure seems to me that we rather want a special form in the constructor head. This came up in ES4.
the C/C# constructor header approach butts heads with other ES features and isn't expressive for the sort of dynamic classes that ES allows.
In terms of headbutting, consider
constructor({a: x, b: y), [a1,a2,a3,...arest], c ) : super(??, ??) {}// what do I put here to super call the constructor with only the first two arguments)
perhaps:
constructor{a: x, b: y), [a1,a2,a3,...arest], c ) : super(arguments[0], arguments[1]) {}
but that means that the special header form must allow arbitrary expressions. Are those expression in the parameter scope of the body scope.
Most importantly, a single constrained expression isn't expressive enough. The problem, is that a subclass constructor may need to perform arbitrary compelx computations before super newing the its base constructor. For example, consider a TypeArray subclass constructor that filters various collections passed to to the derived constructor to produce a simple list iterator that it passed to the base constructor.
The constrained super expression is a slippery slope that leads you to a cliff. Better to go with the full generality and expressiveness of the function body.
Separately I agree
this = new super(x, y);
is just crazy long and very likely to be left out in whole or in part (thenew
).
If left-out, programs will fail quickly and noisily (reference errors) especially with the no auto super design alternative.
And this hardly seems like a significant length issue as we are talking about something that at typically occurs only once per subclass with an extends clause. And not even for all subclasses.
and ignoring white space we are taking about either +5 characters ("this=") or 8 characters ("this=new") if you think the alternative is is just "super" without a "new". But in gist.github.com/allenwb/291035fbf910eab8e9a6 we addressed why it is really a bad idea to make "super(a,b)" be a [[Construct]] operation rather than a [[Call]] operation. Basically, JS is currently very consistent in that new foo()
and foo()
do not have the same language level meanings. It would be a big WTFJS if we changed things such that sometimes the syntax for a [[Call]] really means a [[Construct]]. Especially, if that meaning is dependent upon how the enclosing function was invoked rather than being statically determined. Example, in the rationale document.
As I mentioned a couple of times in conversations, those of use who worked on developing this design found that the syntax really grows upon you. It's big advantage is that is is unambiguously explicit. If you want to invoke a constructor "as a constructor" you always say new
. new foo()
or new super(), it doesn't make a difference, new
always means [[Constructor]]. If you want to invoke a constructor "as a function" you do a normal function invocation. Again, either foo()
or super()
. It's the same. The [[Call]] syntax is never repurpose to mean [[Construct]] so there are no special cases to explain or remember. Also, if you want to designate the this
value used within a constructor you always explicit say this=
, whether it is this = super()' or
this=mayFactoy()or
this = new Proxy(new super(), {...});. Again, it is always explicit, no magic
supercalls that magically sets
this` as a side-effect.
It's this explicitness that grows on you. The code actually says what it means.
I want to give a +1 to Allen. The syntax really is what-you-see-is-what-you-get, which is a great virtue. The C++-derived initializer is cute and tempting, but not nearly as powerful or easy to grok.
That said, I feel weakly that requiring the correct this = new super(...)
and/or this = Object.create(new^.prototype)
invocation is a high price to pay for every derived class ever. In particular, "basic constructor functions" (ES5 "classes") that don't wish to use the superclass constructor don't need this = Object.create(new^.prototype)
, so making ES6 classes require that is strange. It feels like protypal inheritance has "broken" once you start using ES6 classes, and it requires you to fix it by manually doing something that was previously implicit.
I think I'd most be in favor of a third option that implicitly adds this = Object.create(new^.prototype)
if no this
-assignment is present. That way, the superclass constructor is never implicitly called, which is kind of what you would expect. But if you do no this =
assignment, things don't totally break: you still get your prototype and a valid this
, but you don't inherit the allocation or initialization logic from the superclass.
From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Allen Wirfs-Brock Sent: Monday, September 15, 2014 03:54 To: brendan at mozilla.org Cc: Jeff Morrison; Mark S. Miller; es-discuss Subject: Re: new instantiation design alternatives
On Sep 14, 2014, at 4:23 PM, Brendan Eich wrote:
Kevin Smith wrote:
If we want to merge allocation with initialization, then I'm going to want to invoke C# (yet again):
class ColoredPoint extends Point { constructor(x, y, color) : super(x, y) { this.color = color; } }
Gotta credt C#'s parents, C++, which would require Point not super.
I think you are really onto something here. We do not want anything looking like an expression-statement in the constructor body, which besides being verbose has unwanted degrees of freedom (requiring TDZ for this
, running into the dead code problem Jeff cited, etc.).
the dead code problem (and we can debate whether it is actually a significant problem) goes away with alternative 2 (no automatic mew super in derived class constructors.
The TDZ constrained this
is an elegant solution which we had a good consensus on at the last TC39 meeting. Since then we have developed has it further unifies various allocation patterns that come up in JS object constructions. Everybody really need to really work through all the use cases given in the Gists and similarly work through them all with what every other alternatives they may think would be better.
It sure seems to me that we rather want a special form in the constructor head. This came up in ES4.
the C/C# constructor header approach butts heads with other ES features and isn't expressive for the sort of dynamic classes that ES allows.
In terms of headbutting, consider
constructor({a: x, b: y), [a1,a2,a3,...arest], c ) : super(??, ??) {}// what do I put here to super call the constructor with only the first two arguments)
perhaps:
constructor{a: x, b: y), [a1,a2,a3,...arest], c ) : super(arguments[0], arguments[1]) {}
but that means that the special header form must allow arbitrary expressions. Are those expression in the parameter scope of the body scope.
Most importantly, a single constrained expression isn't expressive enough. The problem, is that a subclass constructor may need to perform arbitrary compelx computations before super newing the its base constructor. For example, consider a TypeArray subclass constructor that filters various collections passed to to the derived constructor to produce a simple list iterator that it passed to the base constructor.
The constrained super expression is a slippery slope that leads you to a cliff. Better to go with the full generality and expressiveness of the function body.
Separately I agree this = new super(x, y);
is just crazy long and very likely to be left out in whole or in part (the new
).
If left-out, programs will fail quickly and noisily (reference errors) especially with the no auto super design alternative.
And this hardly seems like a significant length issue as we are talking about something that at typically occurs only once per subclass with an extends clause. And not even for all subclasses.
and ignoring white space we are taking about either +5 characters ("this=") or 8 characters ("this=new") if you think the alternative is is just "super" without a "new". But in gist.github.com/allenwb/291035fbf910eab8e9a6 we addressed why it is really a bad idea to make "super(a,b)" be a [[Construct]] operation rather than a [[Call]] operation. Basically, JS is currently very consistent in that new foo()
and foo()
do not have the same language level meanings. It would be a big WTFJS if we changed things such that sometimes the syntax for a [[Call]] really means a [[Construct]]. Especially, if that meaning is dependent upon how the enclosing function was invoked rather than being statically determined. Example, in the rationale document.
As I mentioned a couple of times in conversations, those of use who worked on developing this design found that the syntax really grows upon you. It's big advantage is that is is unambiguously explicit. If you want to invoke a constructor "as a constructor" you always say new
. new foo()
or new super(), it doesn't make a difference, new
always means [[Constructor]]. If you want to invoke a constructor "as a function" you do a normal function invocation. Again, either foo()
or super()
. It's the same. The [[Call]] syntax is never repurpose to mean [[Construct]] so there are no special cases to explain or remember. Also, if you want to designate the this
value used within a constructor you always explicit say this=
, whether it is this = super()' or
this=mayFactoy()or
this = new Proxy(new super(), {...});. Again, it is always explicit, no magic
supercalls that magically sets
this` as a side-effect.
It's this explicitness that grows on you. The code actually says what it means.
perhaps:
constructor{a: x, b: y), [a1,a2,a3,...arest], c ) : super(arguments[0], arguments[1]) {}
but that means that the special header form must allow arbitrary expressions. Are those expression in the parameter scope of the body scope.
Yes and yes.
Most importantly, a single constrained expression isn't expressive enough. The problem, is that a subclass constructor may need to perform arbitrary compelx computations before super newing the its base constructor. For example, consider a TypeArray subclass constructor that filters various collections passed to to the derived constructor to produce a simple list iterator that it passed to the base constructor.
Would this work?
class C extends B {
constructor(a, b, c) : super(...gimmeSomeComplexSuperArgsYo(a, b,
c)) { // ... } }
Or using static helper methods, if you prefer:
class C extends B {
constructor(a, b, c) : super(...this.gimmeSomeSuperArgsYo(a, b, c))
{ // ... } static gimmeSomeSuperArgsYo(a, b, c) { // ... } }
If being (painfully) explicit is desired, then you could require the "new" in there:
class C extends B {
constructor() : new super() { }
}
It seems to me that with the proposed design we're going to have to branch on "new^" continually: if we don't, then the function will always fail if [Call]'ed and it contains a this-setter.
With the class create expression approach, that branch is taken care of automatically.
Domenic Denicola wrote:
I want to give a+1 to Allen. The syntax really is what-you-see-is-what-you-get, which is a great virtue. The C++-derived initializer is cute and tempting, but not nearly as powerful or easy to grok.
"Cute" doesn't enter into it. The reason for a special form is to remove the bogus degrees of freedom you did not defend above. WYSIWYG is an orange to the sour apple at stake here, which you then immediately acknowledge:
That said, I feel weakly that requiring the correct
this = new super(...)
and/orthis = Object.create(new^.prototype)
invocation is a high price to pay for every derived class ever.
Verbosity is bad, but that's a superficial measure. Consider bug habitat, which compounds in some polynomial or exponential way with all the extra mandatory tokens.
WYSIWYG can be abused to defend lots of mandatory boilerplate, even
lambda-coding everything. But we're adding class
precisely to make the
prototypal pattern both more convenient and less bug-prone when
open-coded via functions. If this requires concise special forms, so be it.
Brendan Eich wrote:
But we're adding
class
precisely to make the prototypal pattern both more convenient and less bug-prone
"than"
Allen Wirfs-Brock wrote:
the C/C# constructor header approach butts heads with other ES features and isn't expressive for the sort of dynamic classes that ES allows.
In terms of headbutting, consider
constructor({a: x, b: y), [a1,a2,a3,...arest], c ) : super(??, ??) {}// what do I put here to super call the constructor with only the first two arguments)
perhaps:
constructor{a: x, b: y), [a1,a2,a3,...arest], c ) : super(arguments[0], arguments[1]) {}
but that means that the special header form must allow arbitrary expressions. Are those expression in the parameter scope of the body scope.
You meant "or", I'm sure. Kevin replied "yes" and "yes", which is funny, but either way, parameters are in scope. That's a no-brainer. Why is this a dilemma?
Most importantly, a single constrained expression isn't expressive enough. The problem, is that a subclass constructor may need to perform arbitrary compelx computations before super newing the its base constructor. For example, consider a TypeArray subclass constructor that filters various collections passed to to the derived constructor to produce a simple list iterator that it passed to the base constructor.
FWIW, I wouldn't want such a thing!
In the C++ family (Andreas is right, there's a history and lineage to consider), the special head form may require you to keep it simple and put any such conditioning in the base class, or an intermediary. This doesn't bite back much.
Are you sure you've sorted use-cases by use-frequency?
The constrained super expression is a slippery slope that leads you to a cliff. Better to go with the full generality and expressiveness of the function body.
You need to demonstrate this, instead of assuming it.
Separately I agree
this = new super(x, y);
is just crazy long and very likely to be left out in whole or in part (thenew
).If left-out, programs will fail quickly and noisily (reference errors) especially with the no auto super design alternative.
And then people will take TC39's name in vain. Every time.
We're trying to get this right, so arguing about design means minimizing spank-the-user-to-write-boilerplate footguns. Saying "there's an error" does not refute. There had better be an error! But can't we do better?
It's this explicitness that grows on you. The code actually says what it means.
Classes as sugar, not as salt. The way this is headed, people will go back to functions. I'm not kidding.
This is probably not a constructive feedback, but thanks for changing super semantics in constructors, as it's fixing the new/super difference of previous design.
On 15 September 2014 06:24, Brendan Eich <brendan at mozilla.org> wrote:
In the C++ family (Andreas is right, there's a history and lineage to consider), the special head form may require you to keep it simple and put any such conditioning in the base class, or an intermediary. This doesn't bite back much.
Well, regarding this particular aspect, the wider curly braces family doesn't have a consistent opinion. Our current design is following Java more closely than C++, which I think is fine (modulo dubious defaults). C++ needed the special syntax primarily because of its rather crazy initialization vs assignment story.
If we were to go with a C++ like solution, we should refrain from abusing colon, though. It should be "constructor(x) extends super(x+1) {}". Want to safe the colon for type annotations. :)
Herby Vojčík wrote:
This is probably not a constructive feedback, but thanks for changing super semantics in constructors, as it's fixing the new/super difference of previous design.
I remember your past posts:
esdiscuss.org/topic/super-in-constructor-should-be-distinct-re-weak-set-map-subclassing
and others. Helpful, thanks -- and please keep reviewing draft ES6!
On Sun, Sep 14, 2014 at 10:47 PM, Domenic Denicola < domenic at domenicdenicola.com> wrote:
I want to give a +1 to Allen. The syntax really is what-you-see-is-what-you-get, which is a great virtue. The C++-derived initializer is cute and tempting, but not nearly as powerful or easy to grok.
Based on the gists and everything presented in this thread, I'm also +1
here for the same reason Domenic points out. The intention of code that
reads: this = new super();
should be very easily understood, based on my
own experience—I was confident in the semantics before I began reading the
explanations and rationale. That's not to say that I expect everyone to
have the same experience.
That said, I feel weakly that requiring the correct
this = new super(...)
and/orthis = Object.create(new^.prototype)
invocation is a high price to pay for every derived class ever. In particular, "basic constructor functions" (ES5 "classes") that don't wish to use the superclass constructor don't needthis = Object.create(new^.prototype)
, so making ES6 classes require that is strange. It feels like protypal inheritance has "broken" once you start using ES6 classes, and it requires you to fix it by manually doing something that was previously implicit.I think I'd most be in favor of a third option that implicitly adds
this = Object.create(new^.prototype)
if nothis
-assignment is present.
In ClassDefinitionEvaluation step 2 and 3 (
people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-classdefinitionevaluation)
this is already covered by the semantics for the extends Superclass
syntax.
That way, the superclass constructor is never implicitly called, which is kind of what you would expect.
Which is how it works in the existing design:
class Abstract { constructor() { this.a = "own instance property"; } m() { return "method defined on Abstract.prototype"; } }
class Derived extends Abstract { constructor() {} }
var d = new Derived(); console.log(typeof d.a === "undefined"); // superclass constructor is never implicitly called console.
var d = new Derived();
console.log(typeof d.a === "undefined"); // superclass constructor is never implicitly called console.log(typeof d.m === "function"); // protoParent link is present as expected.
This should be extended to subclass definitions with no constructor method:
class Derived extends Abstract {}
var d = new Derived(); console.log(typeof d.a === "undefined"); // superclass constructor is never implicitly called
Andreas Rossberg wrote:
Well, regarding this particular aspect, the wider curly braces family doesn't have a consistent opinion. Our current design is following Java more closely than C++, which I think is fine (modulo dubious defaults). C++ needed the special syntax primarily because of its rather crazy initialization vs assignment story.
You're right, it's the grand-parent I was thinking of (C++) but Kevin mentioned C# as well (tough younger brother of Java :-P).
If we were to go with a C++ like solution, we should refrain from abusing colon, though. It should be "constructor(x) extends super(x+1) {}".
Nice!
Want to safe the colon for type annotations.:)
Someone building a compile-to-JS language (not LLJS) pointed out to me
what LLJS already did: C-style type declarator
annotations, with [no
LineTerminator here] in between.
Seems doable at a glance in ECMA-262's grammar, provided declarator doesn't start sprouting C-like leading asterisk or left parenthesis. The trade-off is greater conciseness vs.colon annotation syntax, and no constraints from the market (even if only in developers' brains) due to TypeScript.
(Yeah, Dart-like -- why not?)
On Sep 14, 2014, at 9:24 PM, Brendan Eich wrote:
Allen Wirfs-Brock wrote:
the C/C# constructor header approach butts heads with other ES features and isn't expressive for the sort of dynamic classes that ES allows.
In terms of headbutting, consider
constructor({a: x, b: y), [a1,a2,a3,...arest], c ) : super(??, ??) {}// what do I put here to super call the constructor with only the first two arguments)
perhaps:
constructor{a: x, b: y), [a1,a2,a3,...arest], c ) : super(arguments[0], arguments[1]) {}
but that means that the special header form must allow arbitrary expressions. Are those expression in the parameter scope of the body scope.
You meant "or", I'm sure. Kevin replied "yes" and "yes", which is funny, but either way, parameters are in scope. That's a no-brainer. Why is this a dilemma?
Because, if it is in the parameter scope, rather than the body scope then Kevin's "gimmeSomeComplexSuperArgsYo" function can't be an inner function of the constructor. It has to be an unencapsualted function outside of the class definition. If it is in the body scope it is code that physically outside of the { }'s but evaluated as if it was inside the {}'s.
Most importantly, a single constrained expression isn't expressive enough. The problem, is that a subclass constructor may need to perform arbitrary compelx computations before super newing the its base constructor. For example, consider a TypeArray subclass constructor that filters various collections passed to to the derived constructor to produce a simple list iterator that it passed to the base constructor.
FWIW, I wouldn't want such a thing!
that was just a quick off-the cuff for instance. I probably wouldn't recommend that particular design either. But the point is that while "invoke the super class constructor first" is probably the most common use case (that's why I favor the auto-super design alternative) for other use cases you may need to perform arbitrary complex computations before invoking the super class constructor. And JS is a statement-based language or arbitrary computation means potentially multiple statements.
In the C++ family (Andreas is right, there's a history and lineage to consider), the special head form may require you to keep it simple and put any such conditioning in the base class, or an intermediary. This doesn't bite back much.
C++/C# aren't the only C syntax object-based language. Other very widely used C syntax languages (eg, Java) don't use that special head form at all. So it is hard to argue that the C++/C# header form is universally familiar. (BTW, what percentage of JS programmers now come from a C++/C#/Java background. Seems kind of backwards looking).
Also, most of the cited precedent language are subclass===subtype languages, unlike JS, Ruby, etc. So we need to think extra hard about whether precedent is relevant.
Are you sure you've sorted use-cases by use-frequency?
I didn't claim they were. In the gist, the use cases are roughly ordered by increasing complexity and secondarily by likely use-case frequency.
The constrained super expression is a slippery slope that leads you to a cliff. Better to go with the full generality and expressiveness of the function body.
You need to demonstrate this, instead of assuming it.
It's certainly something I've encountered many times. And it's just a special of the general pattern of a method doing a super-call to the method it over-rides. Sometimes you want to do a super call first ( "after" in AOP or Lisp parlance), sometimes you want to do the super call at the end (a "before" wrapper) and sometimes you want to do it in the middle ("around"). Lots of examples of all of these exist in real world code.
Separately I agree
this = new super(x, y);
is just crazy long and very likely to be left out in whole or in part (thenew
).If left-out, programs will fail quickly and noisily (reference errors) especially with the no auto super design alternative.
And then people will take TC39's name in vain. Every time.
We're trying to get this right, so arguing about design means minimizing spank-the-user-to-write-boilerplate footguns. Saying "there's an error" does not refute. There had better be an error! But can't we do better?
It's this explicitness that grows on you. The code actually says what it means.
Classes as sugar, not as salt. The way this is headed, people will go back to functions. I'm not kidding.
I have to respectfully disagree with this unprovable conjecture.
But feel free to propose and champion a complete alternative proposal that address all aspects of object instantiation design. That's probably a better approach than trying to incrementally change details of a coherent design until it is something completely different.
On Sep 14, 2014, at 8:48 PM, Kevin Smith wrote:
Would this work?
class C extends B { constructor(a, b, c) : super(...gimmeSomeComplexSuperArgsYo(a, b, c)) { // ... } }
Or using static helper methods, if you prefer:
class C extends B { constructor(a, b, c) : super(...this.gimmeSomeSuperArgsYo(a, b, c)) {
this would have to be:
constructor(a, b, c) : super(...new^.gimmeSomeSuperArgsYo(a, b, c)) {
note^, new^
rather than this
On a new
call we don't have a this
at this point. It's why we are calling the super
constructor. But thanks, this points out that even with this design we have to deal with this
TDZ issues.
// ... } static gimmeSomeSuperArgsYo(a, b, c) { // ... } }
If being (painfully) explicit is desired, then you could require the "new" in there:
class C extends B { constructor() : new super() { } }
It seems to me that with the proposed design we're going to have to branch on "new^" continually: if we don't, then the function will always fail if [Call]'ed and it contains a this-setter.
With the class create expression approach, that branch is taken care of automatically.
If you want a [[Call]] to the constructor to do an implicit new
, then you need to branch on new^
:
class C extends B { constructor (a,b,c) { if (!new^) return new C(a,b,c); // continue with instance initialization code } }
But [[Call]] same as [[Construct]] is not normal default JS behavior, so it shouldn't be unexpected that you need to explicitly say something if that is what you want. The whole point of new^ is that a constructor function that wants differing call vs construct behavior needs to explicitly implement the alternatives. Just like today, if you only want your constructors to support one or the other of [[Call]]/[[Construct]] you code for what you care about and errors will occurs if it is invoke the other way.
On Sep 14, 2014, at 7:47 PM, Domenic Denicola wrote:
I want to give a +1 to Allen. The syntax really is what-you-see-is-what-you-get, which is a great virtue. The C++-derived initializer is cute and tempting, but not nearly as powerful or easy to grok.
That said, I feel weakly that requiring the correct
this = new super(...)
and/orthis = Object.create(new^.prototype)
invocation is a high price to pay for every derived class ever. In particular, "basic constructor functions" (ES5 "classes") that don't wish to use the superclass constructor don't needthis = Object.create(new^.prototype)
, so making ES6 classes require that is strange. It feels like protypal inheritance has "broken" once you start using ES6 classes, and it requires you to fix it by manually doing something that was previously implicit.I think I'd most be in favor of a third option that implicitly adds
this = Object.create(new^.prototype)
if nothis
-assignment is present. That way, the superclass constructor is never implicitly called, which is kind of what you would expect. But if you do nothis =
assignment, things don't totally break: you still get your prototype and a validthis
, but you don't inherit the allocation or initialization logic from the superclass.
Thanks, that's a plausible third alternative to the two variations we already have. I'll take a look at creating a gist for that alternative.
Since there has been a lot of talk about verboseness of this=
I want to compare several alternatives for a derived class that wants to do it's own allocation: (this is derived from the last example in gist.github.com/allenwb/291035fbf910eab8e9a6 ):
If this=
assignment is not allowed then any locally allocating method has to look something like this:
constructor(a) {
let newO = new^.specialAllocationMethod(); //or Object.create(new^), etc.
newO.a = a;
return newO;
}
with `this`` assignment it looks like:
constructor(a) {
this = new^.specialAllocationMethod(); //or Object.create(new^), etc.
this.a = a;
}
Which is more verbose?
On Mon, Sep 15, 2014 at 2:42 PM, Allen Wirfs-Brock <allen at wirfs-brock.com>
wrote:
On Sep 14, 2014, at 7:47 PM, Domenic Denicola wrote:
I want to give a +1 to Allen. The syntax really is what-you-see-is-what-you-get, which is a great virtue. The C++-derived initializer is cute and tempting, but not nearly as powerful or easy to grok.
That said, I feel weakly that requiring the correct
this = new super(...)
and/orthis = Object.create(new^.prototype)
invocation is a high price to pay for every derived class ever. In particular, "basic constructor functions" (ES5 "classes") that don't wish to use the superclass constructor don't needthis = Object.create(new^.prototype)
, so making ES6 classes require that is strange. It feels like protypal inheritance has "broken" once you start using ES6 classes, and it requires you to fix it by manually doing something that was previously implicit.I think I'd most be in favor of a third option that implicitly adds
this = Object.create(new^.prototype)
if nothis
-assignment is present. That way, the superclass constructor is never implicitly called, which is kind of what you would expect. But if you do nothis =
assignment, things don't totally break: you still get your prototype and a validthis
, but you don't inherit the allocation or initialization logic from the superclass.Thanks, that's a plausible third alternative to the two variations we already have. I'll take a look at creating a gist for that alternative.
Since there has been a lot of talk about verboseness of
this=
I want to compare several alternatives for a derived class that wants to do it's own allocation: (this is derived from the last example in gist.github.com/allenwb/291035fbf910eab8e9a6 ):If
this=
assignment is not allowed then any locally allocating method has to look something like this:
constructor(a) { let newO = new^.specialAllocationMethod(); //or Object.create(new^), etc. newO.a = a; return newO; }
with `this`` assignment it looks like:
constructor(a) { this = new^.specialAllocationMethod(); //or Object.create(new^), etc. this.a = a; }
Which is more verbose?
The first is also objectionable because it breaks existing implicit return semantics.
Rick Waldron wrote:
The first is also objectionable because it breaks existing implicit return semantics.
Say what? Constructors can return a different object from this
, that's
just JS.
(Anyway the relevant comparison is verbosity of the proposal vs. something implicit, not a more verbose strawman.)
this would have to be:
constructor(a, b, c) : super(...new^.gimmeSomeSuperArgsYo(a, b, c)) { note^,
new^
rather thanthis
Right - in my sketch I explained that "this" inside of the create expression is the same thing as your "new^". I understand that might be confusing, though. Regardless of the token, the idea is the same.
If you want a [[Call]] to the constructor to do an implicit new
, then you
need to branch on
new^
:
Well, what I'm trying to point out is that it's always an error to set "this" if "new^" is undefined. So it's kind of strange to force the user to explicitly write that conditional:
constructor(x, y) {
if (new^)
this = new super(x);
this.y = y;
}
With the header approach, it's taken care of:
constructor(x, y) : super(x) {
this.y = y;
}
(using colon just to avoid bikeshedding)
Because, if it is in the parameter scope, rather than the body scope then Kevin's "gimmeSomeComplexSuperArgsYo" function can't be an inner function of the constructor. It has to be an unencapsualted function outside of the class definition.
No different than default expressions, right? We got rid of hoisting body declarations above them, if I remember correctly.
But feel free to propose and champion a complete alternative proposal that
address all aspects of object instantiation design. That's probably a better approach than trying to incrementally change details of a coherent design until it is something completely different.
Let my share my perspective here.
The @@create design was very simple and elegant, and as far as I know there were no serious objections, except...
Built-ins don't want separation of allocation from initialization. In Jason's thread, I asked why we couldn't just add the constructor params to the "@@create" function, but realized that wouldn't work because it would require most subclasses to implement both a constructor and an "@@create" (in order to remap the constructor args).
After seeing this thread, I realized that you could, in fact, fix "@@create" by simply moving the underlying concept to the constructor head. That allows the user to remap constructor args without having to reimplement two separate methods.
In my opinion, this approach preserves everything that we like about the previous approach while fixing the known issues.
On Mon, Sep 15, 2014 at 2:57 PM, Brendan Eich <brendan at mozilla.org> wrote:
Rick Waldron wrote:
The first is also objectionable because it breaks existing implicit return semantics.
Say what? Constructors can return a different object from
this
, that's just JS.
Yikes, I should've been more specific. Generally, it's considered an
anti-pattern to write constructors that explicitly return an object to
override this
(for all the reasons you'd expect)—that's not to say that it
isn't done or doesn't exist (and certainly I didn't mean to imply that it
wasn't possible). Design that might rely on that pattern would conflict
with widely accepted best practices.
Kevin Smith wrote:
Well, what I'm trying to point out is that it's always an error to set "this" if "new^" is undefined. So it's kind of strange to force the user to explicitly write that conditional:
constructor(x, y) { if (new^) this = new super(x); this.y = y; }
With the header approach, it's taken care of:
constructor(x, y) : super(x) { this.y = y; }
(using colon just to avoid bikeshedding)
Isn't the latter (since it specifies ": super(x)") actually identical to
constructor(x, y) {
this = new super();
this.y = y;
}
IOW, isn't it "I am constructor only and will throw if {[Call]]ed)?
Isn't the latter (since it specifies ": super(x)") actually identical to
constructor(x, y) { this = new super(); this.y = y; }
IOW, isn't it "I am constructor only and will throw if {[Call]]ed)?
No. The idea is that the "class create expression" is only called when the constructor is "new"d. It's purpose is to set up the "this" value when [[Construct]] is called, exactly as @@create used to.
Rick Waldron wrote:
On Mon, Sep 15, 2014 at 2:57 PM, Brendan Eich <brendan at mozilla.org <mailto:brendan at mozilla.org>> wrote:
Rick Waldron wrote: The first is also objectionable because it breaks existing implicit return semantics. Say what? Constructors can return a different object from `this`, that's just JS.
Yikes, I should've been more specific. Generally, it's considered an anti-pattern to write constructors that explicitly return an object to override
this
(for all the reasons you'd expect)—that's not to say that it isn't done or doesn't exist (and certainly I didn't mean to imply that it wasn't possible).
Oh, your "is also objectionable" in the context of a design discussion sounded like a "shouldn't be expressible", not "some frown on that" :-|.
(dherman among others is a fan of return-override; anyway, it's here to stay, including in class constructors.)
Design that might rely on that pattern would conflict with widely accepted best practices.
Allen's newO
example was showing a longer form alternative to this = new super(...)
, as a kind of worst to worse comparison where we all
seek better. It was not talking about what's good or bad style. The
newO
example's use of return-override ain't great if you frown on that
practice, but it is the least of our worries.
It also composes in any event. How a constructor returns the new object doesn't break subclassing or anything else, I hope! Someone holler if I'm wrong.
Allen Wirfs-Brock wrote:
On Sep 14, 2014, at 9:24 PM, Brendan Eich wrote:
perhaps: >> >>
constructor{a: x, b: y), [a1,a2,a3,...arest], c ) : super(arguments[0], arguments[1]) {}
>> >> but that means that the special header form must allow arbitrary expressions. Are those expression in the parameter scope of the body scope.You meant "or", I'm sure. Kevin replied "yes" and "yes", which is funny, but either way, parameters are in scope. That's a no-brainer. Why is this a dilemma?
Because, if it is in the parameter scope, rather than the body scope then Kevin's "gimmeSomeComplexSuperArgsYo" function can't be an inner function of the constructor. It has to be an unencapsualted function outside of the class definition. If it is in the body scope it is code that physically outside of the { }'s but evaluated as if it was inside the {}'s.
Yeah, as Kevin already noted in his reply, parameter scope is good enough. That resolves the hoisting to left of {, as we did for default parameters. Right?
Point is, how does this case differ in principal? Just because special forms follow the closing ) of the formal parameter list does not mean they see the {-delimited-on-the-left scope.
FWIW, I wouldn't want such a thing!
that was just a quick off-the cuff for instance. I probably wouldn't recommend that particular design either. But the point is that while "invoke the super class constructor first" is probably the most common use case (that's why I favor the auto-super design alternative) for other use cases you may need to perform arbitrary complex computations before invoking the super class constructor. And JS is a statement-based language or arbitrary computation means potentially multiple statements.
This is reductive. JS as stmt/expr-based can rationalize any amount of lower-level encoding of what might be better done by higher-level special forms. If the proposal has too many degrees of freedom, deal with it. Don't just appeal to JS-as-it-is. That is not stable ground from which to argue.
Are you sure you've sorted use-cases by use-frequency?
I didn't claim they were. In the gist, the use cases are roughly ordered by increasing complexity and secondarily by likely use-case frequency.
Sort the other way. It'll be illuminating if not decisive.
The constrained super expression is a slippery slope that leads you to a cliff. Better to go with the full generality and expressiveness of the function body.
You need to demonstrate this, instead of assuming it.
It's certainly something I've encountered many times. And it's just a special of the general pattern of a method doing a super-call to the method it over-rides. Sometimes you want to do a super call first ( "after" in AOP or Lisp parlance), sometimes you want to do the super call at the end (a "before" wrapper) and sometimes you want to do it in the middle ("around"). Lots of examples of all of these exist in real world code.
The design biases by default (implicit) and brevity toward one or another. It does not equate all of those. The issue I'm flagging is mainly what should be the default, but also whether we want special (form) syntax instead of verbose imperative boilerplate. Two separate considerations!
It's this explicitness that grows on you. The code actually says what it means.
Classes as sugar, not as salt. The way this is headed, people will go back to functions. I'm not kidding.
I have to respectfully disagree with this unprovable conjecture.
It's not conjecture, it is a hypothesis. We'll find out, at least if we stick to the championed course. However:
But feel free to propose and champion a complete alternative propos that address all aspects of object instantiation design. That's probably a better approach than trying to incrementally change details of a coherent design until it is something completely different.
Nope, you haven't got TC39 consensus, or near that, yet. So playing a "champion a different proposal" power move is risky, and makes it look like you can't adapt from championed (but not use-case-frequency-sorted) proposal to something even better.
As Kevin wrote in his reply, we are (via dialectics, discussion) trying to improve on the championed straw man. So jumping to "my way or the high way" is unjustified, risky, and kind of crappy. Please regroup! I'll gladly endorse the championed proposal if you actually deal with substantive feedback.
An HTML attachment was scrubbed... URL: esdiscuss/attachments/20140916/10cacdab/attachment
It’s just an example, but we recently changed Angular.js in such a way that we could pre-populate a controller instance (instantiated via user-specified constructor functions) with data before calling the constructor itself. This is a convenience, so that the data is readily available for the controller to use as soon as it begins its life.
Needless to say, returning an arbitrary value from the constructor doesn’t do what is expected, because the populated data isn’t re-configured for the newly returned value, and the return value is essentially ignored. It doesn’t have to work this way, but it does, and it would have been nice if returning values from constructors was never in the cards to begin with — similar to how it’s not possible in other languages. We essentially are using a version of placement new in JS, and it’s rather nice for providing these conveniences.
It’s fair to say we’re doing it wrong, but it’s hard to ignore the advantages/flexibility of it. What is the advantage of returning an arbitrary object from a constructor, other than being similar to JS as it is today?
An HTML attachment was scrubbed... URL: esdiscuss/attachments/20140916/4d057e99/attachment-0001
On Sep 15, 2014, at 9:58 PM, Alex Kocharin <alex at kocharin.ru> wrote:
What "controller instance" is, and why can't you add your own stuff after creating an object?
It’s an instance of an object, there’s nothing special here. We don’t add stuff after it’s created because it’s convenient for it to be there to begin with, that’s where it’s useful (and while it would be possible to re-add it if they returned a new value, we don’t because there’s no reason to ever support such crazy code). But as I said before, this is just an example of something which is basically impossible to accomplish with return values from a constructor.
Spending too much time focusing on that doesn’t really add much to the discussion, though.
The advantage of returning an arbitrary object is that constuctor controls exactly what it creates. If it needs to return an array instead of an object, inserting custom methods on it for example, it's possible. Without that ability it's not.
You could return a primitive or function, but then you may as well be using a factory rather than a constructor, and that would make a lot more sense for that behaviour. So for use cases which make sense, a constructor is always going to return an object — and at that point, returning a new object is basically just disallowing placement new.
Also, it's about encapsulation. Whatever constructor is doing is it's own stuff, and external libraries shouldn't mess with it. Same thing as with private variables in C++ for example, they limit flexibility considerably, but people don't argue that they should be deprecated.
Well in the case of the example, I would argue that we’re not really violating encapsulation. You certainly could break encapsulation, but in this case we’re doing it because a developer specifically asked to have properties bound to an instance of their object, and the specific properties to populate, as well.
Anyways, as I said earlier, this is really just an example of how you can get something out of using this strategy, and why the flexibility of being able to return a custom value is not always a good thing (and why such behaviour really fits a factory pattern better).
But again, I’m not sure this adds a whole lot to the topic, it’s really just there as an example of how the behaviour you’re talking about can be harmful. It takes guarantees out of the system, for arguably no real gain, which can be complicated to accommodate (read: more complicated than it’s worth). Lets just agree to disagree for now, it’s not worth continuing this sub-topic =)
Le 15 sept. 2014 à 04:47, Domenic Denicola <domenic at domenicdenicola.com> a écrit :
I think I'd most be in favor of a third option that implicitly adds
this = Object.create(new^.prototype)
if nothis
-assignment is present. That way, the superclass constructor is never implicitly called, which is kind of what you would expect.
FWIW, it is quite near of what is done in PHP, which many web developers are familiar with:
- if you want to call the super (a.k.a. parent) constructor, you call it. If you don't call it, it won't be called. (It is the evident path to me, apparently it is not for some people used to other languages?);
- you are not constrained to call the super constructor at the beginning, (or at the end, or whatever);
- in any case, you get inheritance right. (It is a obligatory in PHP, and it should be the default in JS if you don't do something special.)
The main semantic difference is that the instance (this
) is available in PHP before you call the super constructor.
Le 15 sept. 2014 à 01:23, Brendan Eich <brendan at mozilla.org> a écrit :
Separately I agree
this = new super(x, y);
is just crazy long and very likely to be left out in whole or in part (thenew
).
It is just a question of culture. It is still shorter than parent::__construct(x, y)
you should type PHP, and which I never have imagined to complain about (and there are many things to complain about in PHP!), because it describes exactly what you mean (nothing more, nothing less) in few words.
On Mon, Sep 15, 2014 at 8:37 PM, Alex Kocharin <alex at kocharin.ru> wrote:
15.09.2014, 23:23, "Rick Waldron" <waldron.rick at gmail.com>:
On Mon, Sep 15, 2014 at 2:57 PM, Brendan Eich <brendan at mozilla.org> wrote:
Rick Waldron wrote:
The first is also objectionable because it breaks existing implicit return semantics.
Say what? Constructors can return a different object from
this
, that's just JS.Yikes, I should've been more specific. Generally, it's considered an anti-pattern to write constructors that explicitly return an object to override
this
(for all the reasons you'd expect)—that's not to say that it isn't done or doesn't exist (and certainly I didn't mean to imply that it wasn't possible). Design that might rely on that pattern would conflict with widely accepted best practices.Writing constructors that override
this
is not an anti-pattern. It's an implementation detail that should not ever matter to any outside code.If your code breaks because somebody returns another object from constructor, you're doing it wrong.
Overriding this
with an explicit return object will break the link to the
constructor's prototype object properties:
function C() { return {}; }
C.prototype.m = function() { return "Previously on Lost"; };
var c = new C();
console.log(c.m()); // nope.
An HTML attachment was scrubbed... URL: esdiscuss/attachments/20140916/a0b90711/attachment
On Tue, Sep 16, 2014 at 11:10 AM, Alex Kocharin <alex at kocharin.ru> wrote:
16.09.2014, 18:56, "Rick Waldron" <waldron.rick at gmail.com>:
On Mon, Sep 15, 2014 at 8:37 PM, Alex Kocharin <alex at kocharin.ru> wrote:
15.09.2014, 23:23, "Rick Waldron" <waldron.rick at gmail.com>:
On Mon, Sep 15, 2014 at 2:57 PM, Brendan Eich <brendan at mozilla.org> wrote:
Rick Waldron wrote:
The first is also objectionable because it breaks existing implicit return semantics.
Say what? Constructors can return a different object from
this
, that's just JS.Yikes, I should've been more specific. Generally, it's considered an anti-pattern to write constructors that explicitly return an object to override
this
(for all the reasons you'd expect)—that's not to say that it isn't done or doesn't exist (and certainly I didn't mean to imply that it wasn't possible). Design that might rely on that pattern would conflict with widely accepted best practices.Writing constructors that override
this
is not an anti-pattern. It's an implementation detail that should not ever matter to any outside code.If your code breaks because somebody returns another object from constructor, you're doing it wrong.
Overriding
this
with an explicit return object will break the link to the constructor's prototype object properties:function C() { return {}; }
C.prototype.m = function() { return "Previously on Lost"; };
var c = new C();
console.log(c.m()); // nope.
This only proves that your example has a bug in it, and correct code should look like this:
This contradicts your previous argument:
It's an implementation detail that should not ever matter to any outside code.
An HTML attachment was scrubbed... URL: esdiscuss/attachments/20140916/9e035c49/attachment
On Sep 15, 2014, at 5:10 PM, Brendan Eich wrote:
Allen Wirfs-Brock wrote:
But feel free to propose and champion a complete alternative propos that address all aspects of object instantiation design. That's probably a better approach than trying to incrementally change details of a coherent design until it is something completely different.
Nope, you haven't got TC39 consensus, or near that, yet. So playing a "champion a different proposal" power move is risky, and makes it look like you can't adapt from championed (but not use-case-frequency-sorted) proposal to something even better.
We have long standing consensus on the current ES6 class design and that includes a super()
constructor call that can be arbitrarily placed within the constructor body. We had the discussion about an other forms forms of super constructor invocation, including special header forms, before reaching that consensus.
The motivating issue for the current discussion is that @@create can expose uninitialized instances, not whether we should move super constructor invocation into special declaration header form. Unless it directly relates to the solution of the @@create discussion I don't think we should be dropping our consensus on other aspects of the design.
On Sep 16, 2014, at 6:52 AM, Claude Pache wrote:
Le 15 sept. 2014 à 04:47, Domenic Denicola <domenic at domenicdenicola.com> a écrit :
I think I'd most be in favor of a third option that implicitly adds
this = Object.create(new^.prototype)
if nothis
-assignment is present. That way, the superclass constructor is never implicitly called, which is kind of what you would expect.FWIW, it is quite near of what is done in PHP, which many web developers are familiar with:
- if you want to call the super (a.k.a. parent) constructor, you call it. If you don't call it, it won't be called. (It is the evident path to me, apparently it is not for some people used to other languages?);
- you are not constrained to call the super constructor at the beginning, (or at the end, or whatever);
- in any case, you get inheritance right. (It is a obligatory in PHP, and it should be the default in JS if you don't do something special.)
The main semantic difference is that the instance (
this
) is available in PHP before you call the super constructor.—Claude
Note that this also how the current plan of record for ES6 works and nobody has (at least recently) complained about it. The motivating issue for the current discussion is that @@create can expose uninitialized instances.
Even with the current ES6 design, we (almost) have a way around that. There is no reason that a constructor has to used the this
value produced via @@create. For example, the Date constructor could simply ignore its initial this
value and directly allocated a new Date instance. For example:
Specification algoritm of a hypothetical Date constructor: 1. Let newDate be ObjectCreate(%DatePrototype%, ([[DateValue]]). 2. initialize newDate based upon the argument values 3. Return newDate
or if self hosted in ES, class Date { constructor(...args) { //ignore this value passed in by new operator let newDate = privateAllocatedDateObject(); //... initialize newDate using args return newDate } }
But there are still a couple problems with this approach in conjunction withf the current ES6 deisgn.
.#1 The legacy behavior of Date is completely different when it is invoked as a function from what it is when it is invoked as a constructor. So, the constructor algorithm has to be able to determine how it was invoked. For spec algorithms this is easy, we just write "if this function was invoked as a constructor, then ...". But currently in ES6 self hosted code here is no way to make that determination. Also, even in the current ES6 spec. code we have extra state and constructor logic to sort out situations where a constructor was "called as a function" but for the purpose of doing constructor initialization (eg, super()
calls in a constructor) rather than for its actual "called as a function" behavior.
.#2 The above algorithm is hostile to subclassing Date. The problem is that it sets the [[Prototype]] of the object it creates to Date.prototype but it really needs to be set to the subclasses prototype object. Currently ES6 has no way to communicate (even at the pseudo code) the actual invoked subclass constructor (or its prototype) to a superclass constructor that does internal allocation of this sort. (In the current ES6 design, this is taken care of in @@create, but in this example we are avoiding @@create)
Claudes idea of extending [[Construct]] to include a "receiver" argument solves that problem. It allows us to write the Data algorithm as:
1. If this function was invoked via [[Call]], then
a. do whatever
b. return something
2. Assert: this function was invoked via [[Construct]].
3. Let derivedConstructor be the receiver argument to this [[Construct]] call.
4. Let proto be Get(derivedConstructor,"prototype").
5. Let newDate be ObjectCreate(proto, ([[DateValue]]).
6. Initialize newDate based upon the argument values
7. Return newDate
For ES6 self hosted code, we hare proposing the new^
that can be used to both discriminate [[Call]] from [[Construct]] calls and to access the original constructor object. So a ES6 self-hosted version of Date can be:
class Date { constructor(...args) { if (new^) { //ignore this value passed in by new operator let newDate = privateAllocatedDateObject(); Object.setPrototypeOf(newDate, new^.prototype); //... initialize newDate using args return newDate } /do whatever is required when Date is called as a function return something; } }
And a subclass constructor can be written as
class SubDate extends Date { constructor(...args) { let newObj = new super(...args); // initialize any subclass specific instance state return newObj; } }
Note that the explicit new
is necessary because we need to invoke the super class constructor using [[Construct]]. The explicit return
is necessary to make the subclass constructor return the object allocated and explicitly returned by the superclass constructor. However, note that this subclass constructor, as written above, doesn't inherit the any of the "called as a function" behavior. If it wants that it would need to be written like:
class SubDate extends Date {
constructor(...args) {
if (new^) {
let newObj = new super(...args);
// initialize any subclass specific instance state
return newObj;
}
return super(...args); //call superclass constructor "as a function"
//perhaps do somethings before or after the above call.
}
}
So, what the essentially of the new ES6 class allocation proposal (in all of its alternative forms) is to add this^
and the receiver argument to [[Constructor]]. This is enough to solve the @@create exposing uninitialized instances issue. Whether we also get rid of @@create and whether we have a TDZ on this
or allow assignment to this
in constructors are non-essential secondary issues. We can talk about them but we should let them become consensus breakers.
Allen Wirfs-Brock wrote:
We have long standing consensus on the current ES6 class design and that includes a
super()
constructor call that can be arbitrarily placed within the constructor body.
I'm ok with consensus if it's real and strong. We aren't there yet, and AFAIK we never had cow-path-based use-cases for super calls tucked into the middle of constructors. We definitely had concerns about uninitialized objects, and people wanted to deal with constructor called as function. But conditional super()? I don't remember that.
On Sep 16, 2014, at 12:57 PM, Brendan Eich wrote:
Allen Wirfs-Brock wrote:
We have long standing consensus on the current ES6 class design and that includes a
super()
constructor call that can be arbitrarily placed within the constructor body.I'm ok with consensus if it's real and strong. We aren't there yet, and AFAIK we never had cow-path-based use-cases for super calls tucked into the middle of constructors. We definitely had concerns about uninitialized objects, and people wanted to deal with constructor called as function. But conditional super()? I don't remember that.
The ES6 max-min class and @@create instantiation proposals have always allowed arbitrarily placed super
calls in methods (and up until the recent discussions) we treated class constructors as just the constructor
method of the class prototype. The cow path for arbitrarily placed method super` is well paved and supports the fully generality of before, after, and around specialization in over-riding subclass methods without requiring additional syntactic affordance. It's a cow path that is about as old as OO programming and is just as applicable to constructor methods as to any other kind of method.
We've also always had conditional super calls, eg: if (cond) super();
has been valid in any method, including constructors.
But as you say , the real issue we are trying to address now is eliminating exposure of uninitialized non-ordinary objects and support for constructors called as functions. I think we're on a good path to solve those. Unfortunately, I think the super in function header discussion is mostly a distraction and not really helping focus the discussion on the core issues.
[Snipped text that talks mostly about what but not why.]
Allen Wirfs-Brock wrote:
But as you say, the real issue we are trying to address now is eliminating exposure of uninitialized non-ordinary objects and support for constructors called as functions. I think we're on a good path to solve those.
Could be!
Unfortunately, I think the super in function header discussion is mostly a distraction and not really helping focus the discussion on the core issues.
It is an attempt to address the "uninitialized" problem, but I hear your
concern that it is too restrictive (re: before/around super
advice).
It would help, I think, if you replied to Kevin's mail dated 12:05pm yesterday, which ended:
""" Well, what I'm trying to point out is that it's always an error to set "this" if "new^" is undefined. So it's kind of strange to force the user to explicitly write that conditional:
constructor(x, y) {
if (new^)
this = new super(x);
this.y = y;
}
With the header approach, it's taken care of:
constructor(x, y) : super(x) {
this.y = y;
}
(using colon just to avoid bikeshedding) """
Le 15 sept. 2014 à 23:19, Kevin Smith <zenparsing at gmail.com> a écrit :
Isn't the latter (since it specifies ": super(x)") actually identical to
constructor(x, y) { this = new super(); this.y = y; }
IOW, isn't it "I am constructor only and will throw if {[Call]]ed)?
No. The idea is that the "class create expression" is only called when the constructor is "new"d. It's purpose is to set up the "this" value when [[Construct]] is called, exactly as @@create used to.
When you write:
constructor(x, y) {
if (new^)
this = new super(x);
this.y = y;
}
is it intentional that you call the super-constructor only when you have been called with new
, and not in case of a direct call?
For instance, if I wanted to support to be called through the legacy SuperConstructor.call(this, ...args)
trick in addition to be new’d, I'd rather try the following:
constructor(x, y) {
if (new^)
this = new super(x);
else
super.constructor(x);
this.y = y;
}
The point here is that the purpose of the constructor method is not only allocation, but also (and primarily) initialisation.
I find the new suggestions hard to make sense of and the current approach quite elegant:
Foo[[Construct]](arg) ::=
1. let obj = Foo[@@create]()
2. Foo.call(obj, arg)
To me, it seems that this is how things should work internally, because that’s what’s actually going on. Is there no other way to fix the following two problems? For example: to fix #2, couldn’t we somehow prevent direct invocations? I also don’t fully understand what #1 means.
- If instances aren't sufficiently initialized by @@create, then instance objects could leak (e.g. via a nefarious decoupling between the @@create allocation process and the constructor-function initialization process)
- @@create could be called directly
Observation about the new approach:: could the if (new^) ...
check be turned into a method?
Axel
I agree, the thread was about defaults when no super call is present.
Allen Wirfs-Brock <allen at wirfs-brock.com>napísal/a:
Unfortunately, I think the super in function header discussion is mostly a distraction and not really helping focus the discussion on the core issues.
Claude Pache <claude.pache at gmail.com>napísal/a:
For instance, if I wanted to support to be called through the legacy SuperConstructor.call(this, ...args)
trick in addition to be new’d, I'd rather try the following:
constructor(x, y) {
if (new^)
this = new super(x);
else
super.constructor(x);
this.y = y;
}
Oh, this is really bulky.
Makes me think of fixing it by some slight magic. Like, adding modifier to method deginition to say it is a constructor (new keuword before opening left brace) and keeping stack of new^s in thread-local storage which wull be used in new-tagged methods, even if [[Call]]ed.
But that is not a nice solution at all. :-/
The point here is that the purpose of the constructor method is not only allocation, but also (and primarily) initialisation.
constructor(x, y) { if (new^) this = new super(x); else super.constructor(x); this.y = y; }
The point here is that the purpose of the constructor method is not only allocation, but also (and primarily) initialisation.
Yes - you are right. And I think we can safely assume that users will refuse to write such boilerplate. I'll have to consider things for a bit before replying to the larger point.
On Sep 17, 2014, at 6:40, "Kevin Smith" <zenparsing at gmail.com<mailto:zenparsing at gmail.com>> wrote:
constructor(x, y) {
if (new^)
this = new super(x);
else
super.constructor(x);
this.y = y;
}
The point here is that the purpose of the constructor method is not only allocation, but also (and primarily) initialisation.
Yes - you are right. And I think we can safely assume that users will refuse to write such boilerplate.
That seems fine. Enabling different behaviour for called vs. constructed should only be used to explain the builtins; user code should not do so themselves. So it makes sense to me that those trying to do that would get "punished" with having to type more.
The more pressing ergonomic question is whether this = new super(...)
is too much. I think it's fine, especially in comparison to Base.call(this, ...)
. (For those counting characters, the score comes down to how long your superclass name is, and I'd wager most are longer than the five characters required for the new syntax to start winning.)
I'll have to consider things for a bit before replying to the larger point.
That seems fine. Enabling different behaviour for called vs. constructed should only be used to explain the builtins; user code should not do so themselves. So it makes sense to me that those trying to do that would get "punished" with having to type more.
Yes, but if we guide users toward "this = new super", we actually change the current paradigm where the constructors can be called or new'd.
As an example, this:
function C() { B.call(this) }
functions perfectly well as an object initializer, with or without "new".
But this:
class C extends B { constructor() { this = new super() } }
does not. It throws if you attempt to use it as an initializer without "new".
(This is also somewhat of an issue for the extended header approach.)
Do we want to shift the paradigm? Part of the design goal for classes was that we didn't want to shift any paradigms. That's what I need more time to think about.
On Sep 17, 2014, at 7:10, "Kevin Smith" <zenparsing at gmail.com<mailto:zenparsing at gmail.com>> wrote:
That seems fine. Enabling different behaviour for called vs. constructed should only be used to explain the builtins; user code should not do so themselves. So it makes sense to me that those trying to do that would get "punished" with having to type more.
Yes, but if we guide users toward "this = new super", we actually change the current paradigm where the constructors can be called or new'd.
As an example, this:
function C() { B.call(this) }
functions perfectly well as an object initializer, with or without "new".
That's not true, is it? Assuming someone (e.g. B) actually assigns a property of this
, an error will occur (or worse, a global will be created, in sloppy mode).
Constructors can be called or newed today only if you insert the error-prone incantation if (!(this instanceof C)) { return new C(...arguments); }
, but now we're back to paying a cost.
In short, I see no shift from today's semantics.
But this:
class C extends B { constructor() { this = new super() } }
does not. It throws if you attempt to use it as an initializer without "new".
(This is also somewhat of an issue for the extended header approach.)
Do we want to shift the paradigm? Part of the design goal for classes was that we didn't want to shift any paradigms. That's what I need more time to think about.
That's not true, is it? Assuming someone (e.g. B) actually assigns a property of
this
, an error will occur (or worse, a global will be created, in sloppy mode).
Sorry, when I say "called", I mean called with a proper "this" variable.
Under the current system, constructors work as "this" initializers, with or without "new". Hopefully that explains my meaning a little better.
An HTML attachment was scrubbed... URL: esdiscuss/attachments/20140917/30917a8f/attachment-0001
Domenic Denicola wrote:
On Sep 17, 2014, at 7:10, "Kevin Smith" <zenparsing at gmail.com <mailto:zenparsing at gmail.com>> wrote:
That seems fine. Enabling different behaviour for called vs. constructed should only be used to explain the builtins; user code should not do so themselves. So it makes sense to me that those trying to do that would get "punished" with having to type more.
Yes, but if we guide users toward "this = new super", we actually change the current paradigm where the constructors can be called or new'd.
As an example, this:
function C() { B.call(this) }
functions perfectly well as an object initializer, with or without "new".
That's not true, is it? Assuming someone (e.g. B) actually assigns a property of
this
, an error will occur (or worse, a global will be created, in sloppy mode).
Maybe Kevin meant "can be new'd to create and initialize, and can be call'd just to initialize the existing one".
Le 11 sept. 2014 à 18:35, Allen Wirfs-Brock <allen at wirfs-brock.com> a écrit :
These two Gist have parallel construction for easy comparison. I suggest approaching this is by first readying through one of the Gists and then doing a side by side read through of the alternative to see the differences in the designs and usage.
gist.github.com/allenwb/5160d109e33db8253b62 with implicit super construct if no local allocation gist.github.com/allenwb/53927e46b31564168a1d explicit super construct required if no local allocation
I appreciate it if major constructive feedback on any of these documents were made via Gist comments.
The following is probably apparent from the present thread, but I think it should be stated clearly.
A big problem with those proposals, is that they add a semantic change, namely implicit super-constructor calls aka the automatic allocation feature, which is neither part of the currently specced design (the one with @@create), nor contributes to solve the particular problem that lead to the redesign (observability of allocated but uninitialised built-ins/DOM objects), nor (unless I missed something) is properly justified in the Gists.
Those implicit super-constructor calls might make sense, but they also introduce traps (e.g., spurious super calls in case of incorrect use, as found in Section "Some AntiPatterms" [sic] of the Gists), and need therefore be carefully considered before being introduced.
My advice is to modify the proposal, removing entirely that novel feature (as Domenic said: if this = ...
is absent, just do an implicit this = Object.create(new^.prototype)
), and focusing to tackle only the issue that was intended to be solved. It would remove much distraction in the discussion.
One may propose an additional implicit-super-call feature, if it solves a particular issue. Or propose a new design where such a feature plays a central role in resolving the original issue. But it should be justified and discussed separately to avoid confusion.
On Wed, Sep 17, 2014 at 9:59 AM, Domenic Denicola < domenic at domenicdenicola.com> wrote:
On Sep 17, 2014, at 6:40, "Kevin Smith" <zenparsing at gmail.com> wrote:
constructor(x, y) { if (new^) this = new super(x); else super.constructor(x); this.y = y; }
The point here is that the purpose of the constructor method is not only allocation, but also (and primarily) initialisation.
Yes - you are right. And I think we can safely assume that users will refuse to write such boilerplate.
That seems fine. Enabling different behaviour for called vs. constructed should only be used to explain the builtins; user code should not do so themselves. So it makes sense to me that those trying to do that would get "punished" with having to type more.
The more pressing ergonomic question is whether
this = new super(...)
is too much. I think it's fine, especially in comparison toBase.call(this, ...)
. (For those counting characters, the score comes down to how long your superclass name is, and I'd wager most are longer than the five characters required for the new syntax to start winning.)
Not to simply dogpile here, but I agree with Domenic, regarding this = new super()
. I'll add that this form has high "teachability" value: the syntax
expresses the semantics in a nearly "literary" way, ie. "Allocate this
subclass instance with an new instance of its super class. Then proceed
with initialization".
I agree with Domenic that any derived-class constructor that needs to allocate with specific arguments and that must distinguish new'ing from calling should have to write an if-else. That's a hard case, EIBTI, etc.
(Allen: well-done on that aspect of the design, BTW. new^
is growing
on me, too.)
Rick Waldron wrote:
The more pressing ergonomic question is whether `this = new super(...)` is too much. I think it's fine, especially in comparison to `Base.call(this, ...)`. (For those counting characters, the score comes down to how long your superclass name is, and I'd wager most are longer than the five characters required for the new syntax to start winning.)
Not to simply dogpile here, but I agree with Domenic, regarding
this = new super()
. I'll add that this form has high "teachability" value: the syntax expresses the semantics in a nearly "literary" way, ie. "Allocate this subclass instance with an new instance of its super class. Then proceed with initialization".
Check me here, should if you disagree: the hazard that remains is when
Java-trained programmers call super without new, thinking that's how to
call the base constructor in all cases. They'll get spanked with a
reference error when testing the constructor invoked with new
:
gist.github.com/allenwb/53927e46b31564168a1d#calling-super-instead-of-invoking-super-with-new
I'm on board with teaching around this hazard. (You know me, always willing to spank Java-heads a little :-P.)
What about the big choice between implicit vs. explicit-only base constructor calling? This seems like a bug to me:
gist.github.com/allenwb/53927e46b31564168a1d#not-explicitly-assigning-this-in-a-derived-constructor
Whereas this:
seems winning, given classes as sugar, DWIM and DDWIDM. This is option 1, not option 2.
Sorry for not tracking better: Is the capability leak a fatal objection to option 1?
On 17 September 2014 19:04, Brendan Eich <brendan at mozilla.org> wrote:
I agree with Domenic that any derived-class constructor that needs to allocate with specific arguments and that must distinguish new'ing from calling should have to write an if-else. That's a hard case, EIBTI, etc.
I agree. In fact, I don't see a reason to make it even that simple. My
understanding was that there is wide agreement that invoking
constructors without 'new' should be discouraged, and that functions
behaving differently for Call and Construct are considered a legacy
anti-pattern. In the light of that, I'm stilling missing the
compelling reason to introduce new^ at all. In particular, since TDZ
for this
allows you to make the distinction fairly easily even
without a new construct, if you are so inclined.
On 9/17/14, 1:15 PM, Andreas Rossberg wrote:
In the light of that, I'm stilling missing the compelling reason to introduce new^ at all.
Say you have:
class A extends B { constructor() { this = new super(); } };
class B { constructor() { // what here? } };
(apologies for any obvious syntax mistakes).
And then you do:
var a = new A();
This will delegate the object construction to B. How does B know to create the object with A.prototype as the prototype?
In the new^ world, this is fairly straightforward. B's constructor does:
this = Object.create(new^.prototype);
Without new^, how does this work?
Regarding:
"... My understanding was that there is wide agreement that invoking constructors without 'new' should be discouraged, and that functions behaving differently for Call and Construct are considered a legacy anti-pattern..."
I AGREE
I don't know all the story and forces to keep constructor support the "new" and "no-new" invocation.
But I had the idea of "class MyClass... " in the new specification, was an opportunity to:
- Give "class MyClass.. ." a clear behavior oriented to classes, without relic behavior
- Leave "new MyFunction... " as usual, with all the power of "classic" JavaScript
Something like:
class MyClass extends .. { constructor(arg1, arg2) : base(arg1) { this.arg2 = arg2; } }
where the "this" is already created, the base invocation is allowed only at beginning, and no "MyClass()" (no new) invocation allowed (maybe it can only detected at runtime, and raise an exception)
But probably I cannot see all the landscape and the "compelling reasons" to support "with new/without new" constructors, yet
Angel "Java" Lopez @ajlopez
On Sep 16, 2014, at 4:55 PM, Brendan Eich wrote:
... It would help, I think, if you replied to Kevin's mail dated 12:05pm yesterday, which ended:
""" Well, what I'm trying to point out is that it's always an error to set "this" if "new^" is undefined. So it's kind of strange to force the user to explicitly write that conditional:
constructor(x, y) { if (new^) this = new super(x); this.y = y; }
With the header approach, it's taken care of:
constructor(x, y) : super(x) { this.y = y; }
(using colon just to avoid bikeshedding) """
actually I did respond:
On Sep 15, 2014, at 11:24 AM, Allen Wirfs-Brock wrote:
On Sep 14, 2014, at 8:48 PM, Kevin Smith wrote:
It seems to me that with the proposed design we're going to have to branch on "new^" continually: if we don't, then the function will always fail if [Call]'ed and it contains a this-setter.
With the class create expression approach, that branch is taken care of automatically.
If you want a [[Call]] to the constructor to do an implicit
new
, then you need to branch onnew^
:constructor (a,b,c) { if (!new^) return new C(a,b,c); // continue with instance initialization code } }
But [[Call]] same as [[Construct]] is not normal default JS behavior, so it shouldn't be unexpected that you need to explicitly say something if that is what you want. The whole point of new^ is that a constructor function that wants differing call vs construct behavior needs to explicitly implement the alternatives. Just like today, if you only want your constructors to support one or the other of [[Call]]/[[Construct]] you code for what you care about and errors will occurs if it is invoke the other way.
Allen
or using your above example.
Most people today don't explicitly deal with constructors called as functions. If you con't care you probably just write
constructor(x, y) { this = new super(x); this.y = y; }
and a runtime error will occur if it is called "as a function".
If you want to also support "called as a function", say to do a new
like above you would probably code:
class C extends B { constructor(x, y) { if (!new^) return new C(x,y); this = new super(x); this.y = y; } }
So you only need to branch on new^
if you want to explicitly support both constructor and function behavior.
Allen Wirfs-Brock wrote:
actually I did respond:
Sorry, missed that message somehow. Sold!
On Sep 17, 2014, at 6:17 AM, Herby Vojčík wrote:
Claude Pache <claude.pache at gmail.com>napísal/a:
For instance, if I wanted to support to be called through the legacy
SuperConstructor.call(this, ...args)
trick in addition to be new’d, I'd rather try the following:constructor(x, y) { if (new^) this = new super(x); else super.constructor(x); this.y = y; }
Oh, this is really bulky.
this isn't something that anyone will be coding every five minutes. It's in only in a constructor of a derived classes and only if you want the constructor to have different behavior for [[Call]] and [[Construct]]. How often do you code such a constructor.
What we had been recommending under the current @@create-based design is that ES programmer code their constructors to only work as initialization methods (in other words to use the same logic whether [[Call]] or [[Construct]] invoked. Because super()
initialization was a [[Call]].
I would still make the same recommendation. Code you constructors assuming they are [[Construct]] invoked (possibly by a subclass) and don't worry about [[Call]] behavior. If you really want to do crazy things like some of the legacy built-in constructors you do need to make that discrimination, but in that case extra explicit logic is desirable.
Makes me think of fixing it by some slight magic. Like, adding modifier to method deginition to say it is a constructor (new keuword before opening left brace) and keeping stack of new^s in thread-local storage which wull be used in new-tagged methods, even if [[Call]]ed.
But that is not a nice solution at all. :-/
The point here is that the purpose of the constructor method is not only allocation, but also (and primarily) initialisation.
And there really aren't complications for the allocation/initialization use case. It is only when you call the [[Call]] use case that there are complications. But rightfully so as you have to use one function body to do two different things. But not a big deal, as this really should be rare.
I still feel like Kevin's point has not yet been resolved. How can we make this work with today's patterns?
import {C} from './C.js';
function D() { C.call(this); } D.prototype = { proto: C.prototype, constructor: D, ... }
Now assume that C.js initially used the ES5 pattern above and it was later
changed to use class syntax using this =
.
class C extends B { constructor() { this = new super(); } }
Since we are doing a C[[Call]] we will get a runtime error here.
With @@create we could mix and match the two different forms.
On Sep 17, 2014, at 6:39 AM, Kevin Smith wrote:
constructor(x, y) { if (new^) this = new super(x); else super.constructor(x); this.y = y; }
The point here is that the purpose of the constructor method is not only allocation, but also (and primarily) initialisation.
Yes - you are right. And I think we can safely assume that users will refuse to write such boilerplate. I'll have to consider things for a bit before replying to the larger point.
Most users most of the time don't need that boiler plate. In the unusual cases where they need it, this design forces them to be explicit about their intent. I think that's a good thing.
Also note there are other initialization patterns that a user or framework might choose to use. We talk about one of these (bottom up initialization) quite a bit at the last TC39 meeting. The "Patterns for Alternative Construction Frameworks sections of my Gists explored some of these patterns. (gist.github.com/allenwb/5160d109e33db8253b62#patterns-for-alternative-construction-framework and gist.github.com/allenwb/53927e46b31564168a1d#constructor-called-as-function-patterns ). Note that in some cases, the cleanest thing is to factor the actual initialization code into a separate method.
On Sep 17, 2014, at 7:24 AM, Kevin Smith wrote:
That's not true, is it? Assuming someone (e.g. B) actually assigns a property of
this
, an error will occur (or worse, a global will be created, in sloppy mode).Sorry, when I say "called", I mean called with a proper "this" variable.
Under the current system, constructors work as "this" initializers, with or without "new". Hopefully that explains my meaning a little better.
Justin case there is any confusion about this, let me remind everyone, that under both of the currently proposed alternatives nothing changes from the current behavior for "basic constructors" (ie, constructors defined using function
definitions. They always allocated and bind this
to a new ordinary object when invoked via [[Construct]]. See gist.github.com/allenwb/53927e46b31564168a1d#file-4nonclass-constructors-md
Just like in ES1-5.
It's only constructors defined within class definitions that have new behavior of (possibly entering the body with this
in the TDz). I another variation of the design (I call it alternative #3 or Domenic's variation since we suggested it) is to treat class constructors just like basic constructor and always allocate and this-bind an ordinary object on a [[Construct]]. It's a plausible alternative but also has a few pros and cons.
On Sep 17, 2014, at 7:29 AM, Claude Pache wrote:
Le 11 sept. 2014 à 18:35, Allen Wirfs-Brock <allen at wirfs-brock.com> a écrit :
These two Gist have parallel construction for easy comparison. I suggest approaching this is by first readying through one of the Gists and then doing a side by side read through of the alternative to see the differences in the designs and usage.
gist.github.com/allenwb/5160d109e33db8253b62 with implicit super construct if no local allocation gist.github.com/allenwb/53927e46b31564168a1d explicit super construct required if no local allocation
I appreciate it if major constructive feedback on any of these documents were made via Gist comments.
The following is probably apparent from the present thread, but I think it should be stated clearly.
A big problem with those proposals, is that they add a semantic change, namely implicit super-constructor calls aka the automatic allocation feature,
only the first alternative. They second does not do implicit allocations or super-constructor calls. This is the primary difference between the two alternatives that were present.
which is neither part of the currently specced design (the one with @@create), nor contributes to solve the particular problem that lead to the redesign (observability of allocated but uninitialised built-ins/DOM objects), nor (unless I missed something) is properly justified in the Gists.
the @@create design in does implicit allocation. It does so by the ordinary [[Construct]] calling @@create which is responsible for doing allocation ([[Construct]] is even specified with a fall back allocation if @@crete doesn't exist). ES new
has always done "implicit allocation" within [[Construct]] and the primary purpose of constructor bodies has been to initialize the implicitly allocated object.
Auto-super constructor calls a a separable issue and don't appear in alternative #2.
The motivation was simplifcation of what we believed to be the most common use case:
class D extends B { constructor (b,c) { this.c = c; //no need for an explicit super call } }
alternative #2 requires an explicit super call for this use case:
class D extends B { constructor (b,c) { this = new super(b); this.c = c; //no need for an explicit super call } }
That's the difference.
Those implicit super-constructor calls might make sense, but they also introduce traps (e.g., spurious super calls in case of incorrect use, as found in Section "Some AntiPatterms" [sic] of the Gists), and need therefore be carefully considered before being introduced.
Hence the emergence of alternative #2 (and also the Domenic variation)
My advice is to modify the proposal, removing entirely that novel feature (as Domenic said: if
this = ...
is absent, just do an implicitthis = Object.create(new^.prototype)
), and focusing to tackle only the issue that was intended to be solved. It would remove much distraction in the discussion.
There are already good arguments for alternative #2, among them is that it eliminates the significant dead-code issue that was discovered here. The Domenic variation is plausible although it still has the dead code issue is tied to this=
presence and (I think) has a few more error hazards if not.
On Sep 17, 2014, at 10:15 AM, Andreas Rossberg wrote:
On 17 September 2014 19:04, Brendan Eich <brendan at mozilla.org> wrote:
I agree with Domenic that any derived-class constructor that needs to allocate with specific arguments and that must distinguish new'ing from calling should have to write an if-else. That's a hard case, EIBTI, etc.
I agree. In fact, I don't see a reason to make it even that simple. My understanding was that there is wide agreement that invoking constructors without 'new' should be discouraged, and that functions behaving differently for Call and Construct are considered a legacy anti-pattern. In the light of that, I'm stilling missing the compelling reason to introduce new^ at all. In particular, since TDZ for
this
allows you to make the distinction fairly easily even without a new construct, if you are so inclined.
Boris covered the most important use case for this^.
The other one is self-hosting built-ins. A lot of the cruft in the present ES6 spec. is about making sure that things like this ES5 code:
Date.prototype.foo = Date;
console.log(typeof ((new Date()).foo()); //"string" according to ES5 spec.
console.log(typeof( new Date())); //"object" according to ES5 spec.
can continue to work the same in a self-hosted implementation of Data. Basically, just looking at the this value isn't sufficient to distinguish there two case in the self hosted world. It helps a lot when you have to do this sort of crap to have an reliable way for ES code l to distinguish if they were invoked via [[Call]] or [[Construct]]. Since we need to provide access to the original constructor (Boris point) it's a nice bonus that we can use the same mechanism to make that distinction in the (hopefully very rare) cases where it is actually needed.
On Sep 17, 2014, at 10:50 AM, Erik Arvidsson wrote:
I still feel like Kevin's point has not yet been resolved. How can we make this work with today's patterns?
import {C} from './C.js';
function D() { C.call(this); } D.prototype = { proto: C.prototype, constructor: D, ... }
Now assume that C.js initially used the ES5 pattern above and it was later changed to use class syntax using
this =
.class C extends B { constructor() { this = new super(); } }
Since we are doing a C[[Call]] we will get a runtime error here.
Of course, if you don't need to change anything if you want to keep using your pre-ES6 class model.
But, since C is existing ES5 code, the appropriate way to update using ES6 features and maintain compatability with ES5 level subclasses would be::
class C extends B { constructor() { if (this^) { // invoked as: new C() or new super() from ES6 level code this = objToInitialize = new super(); } else { //legacy compat with ES5-level subclasses that do C.call invocation assert(typeof this == "object"); //if this is undefined then C must have been invoke: as:C() rather than as: C.call(this); we may or may not need to support that case. objToInitialize = this; } // initialization logic applied to objToInitialize //... //note, explicit return is not required } }
You have to worry about this sort of stuff when evolving a library or framework.. New application level code shouldn't have to deal with it.
Correction below: s/this^/new^/
On Tue, Sep 16, 2014 at 6:48 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
On Sep 16, 2014, at 12:57 PM, Brendan Eich wrote:
Allen Wirfs-Brock wrote:
We have long standing consensus on the current ES6 class design and that includes a
super()
constructor call that can be arbitrarily placed within the constructor body.I'm ok with consensus if it's real and strong. We aren't there yet, and AFAIK we never had cow-path-based use-cases for super calls tucked into the middle of constructors. We definitely had concerns about uninitialized objects, and people wanted to deal with constructor called as function. But conditional super()? I don't remember that.
The ES6 max-min class and @@create instantiation proposals have always allowed arbitrarily placed
super
calls in methods (and up until the recent discussions) we treated class constructors as just theconstructor
method of the class prototype. The cow path for arbitrarily placed method super` is well paved and supports the fully generality of before, after, and around specialization in over-riding subclass methods without requiring additional syntactic affordance. It's a cow path that is about as old as OO programming and is just as applicable to constructor methods as to any other kind of method.
I don't think this is really correct. As far as I can tell, the established pattern is
- languages that reveal uninitialized objects (Python, Ruby) allow base-class constructor calls anywhere
- languages that ensure the base-class constructor is called (C#, C++, Java) require it to be called up front
The proposed changes make ES fall in neither category.
Just for the sake of capturing everything, here's an updated version of the extended header idea:
gist.github.com/zenparsing/5efa4459459b9f04d775
Cheers!
powerful also thanks to its flexibility, I think JS.next should fall in the first category: base-class constructor calls anywhere - this makes the second one possible, and also other patterns available too.
my .02
On 17 September 2014 19:24, Boris Zbarsky <bzbarsky at mit.edu> wrote:
On 9/17/14, 1:15 PM, Andreas Rossberg wrote:
In the light of that, I'm stilling missing the compelling reason to introduce new^ at all.
Say you have:
class A extends B { constructor() { this = new super(); } };
class B { constructor() { // what here? } };
(apologies for any obvious syntax mistakes).
Well, nothing has to go there. Since B does not have an extends
clause, it has -- according to Allen's gist -- its this
initialized
implicitly (with an object having the derived constructor's prototype,
like you want).
What new^ adds to the table is the ability to bypass this mechanism and implement non-standard inheritance or creation patterns within the class syntax. But honestly, I don't understand why the class syntax has to directly support such special use cases, let alone encourage them with seductive syntax.
I feel like I'm missing something.
On 9/18/14, 12:51 PM, Andreas Rossberg wrote:
Well, nothing has to go there. Since B does not have an extends clause, it has -- according to Allen's gist -- its
this
initialized implicitly (with an object having the derived constructor's prototype, like you want).
Wait. How does the |new super()| invocation know what prototype to use, exactly? Is it basically passing in the new^ value as some sort of hidden state under the hood without exposing it with a name?
What new^ adds to the table is the ability to bypass this mechanism and implement non-standard inheritance or creation patterns within the class syntax.
Well, it adds the ability to explain what happens when you do:
class A extends HTMLElement {
...
}
right?
On 15 September 2014 19:20, Brendan Eich <brendan at mozilla.org> wrote:
Andreas Rossberg wrote:
Want to safe the colon for type annotations.:)
Someone building a compile-to-JS language (not LLJS) pointed out to me what LLJS already did: C-style
type declarator
annotations, with [no LineTerminator here] in between.
This obviously is off-topic for this thread but I can't resist...
C-style (actually, Algol-style) declaration syntax is an evolutionary dead end. It does not scale. It quickly becomes unreadable (to both humans and parsers) when types have more structure than just being names. It buries the most important piece of information (what is being declared) behind/inside a random pile of type syntax. It has the wrong scoping order for e.g. functions (which is why C++11 needed to introduce an alternative syntax for function types). And so on and so forth.
I'm really glad that TypeScript and others broke with this unfortunate C tradition and adopted a better, also well-established notation (while other competition showed less taste). I rather not regress behind that. ;)
On Thu, Sep 18, 2014 at 6:57 PM, Boris Zbarsky <bzbarsky at mit.edu> wrote:
On 9/18/14, 12:51 PM, Andreas Rossberg wrote:
Well, nothing has to go there. Since B does not have an extends clause, it has -- according to Allen's gist -- its
this
initialized implicitly (with an object having the derived constructor's prototype, like you want).Wait. How does the |new super()| invocation know what prototype to use, exactly? Is it basically passing in the new^ value as some sort of hidden state under the hood without exposing it with a name?
Yes, exactly.
What new^ adds to the table is the ability to bypass this mechanism
and implement non-standard inheritance or creation patterns within the class syntax.
Well, it adds the ability to explain what happens when you do:
class A extends HTMLElement { ... }
right?
You haven't specified the constructor body, I assume you have intentionally omitted it, and in that case the above is equivalent to class A extends HTMLElement { constructor() { this = new super(); } }
There is no explicit passing of new^ here.
new^ is really just for 'constructors that are callable as functions as well'. Andreas is right that it is esoteric, but supporting that seems to be a requirement for classes design.
From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Dmitry Lomov
new^ is really just for 'constructors that are callable as functions as well'.
This is not really correct. Looking through Allen's gists, it's important in several scenarios. E.g. allocating an exotic object, but still getting the prototype linkage correct:
class Base2 {
constructor(x) {
this = [ ]; //create a new exotic array instance
Object.setPrototypeOf(this, new^.prototype);
this[0] = x;
}
isBase2 () {return true}
}
Or ignoring the super-class constructor, but still getting the prototype linkage correct:
class Derived4 extends Base2 {
constructor (a) {
this = Object.create(new^.prototype);
this.a = a;
}
isDerived4() {return true};
}
Or deriving from an abstract base class:
class D7 extends AbstractBase {
constructor () {
this = Object.create(new^.prototype); //instances are ordinary objects
}
}
Note that if we adopted "the Domenic alternative", i.e. implicit this = Object.create(new^.prototype)
, the latter two would no longer need the corresponding line in their constructor, leaving usage of new^
for (a) different [[Call]] behavior and (b) exotic allocation.
On Thu, Sep 18, 2014 at 8:41 PM, Domenic Denicola < domenic at domenicdenicola.com> wrote:
From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Dmitry Lomov
new^ is really just for 'constructors that are callable as functions as well'.
This is not really correct. Looking through Allen's gists, it's important in several scenarios. E.g. allocating an exotic object, but still getting the prototype linkage correct:
class Base2 { constructor(x) { this = [ ]; //create a new exotic array instance Object.setPrototypeOf(this, new^.prototype); this[0] = x; } isBase2 () {return true} }
Or ignoring the super-class constructor, but still getting the prototype linkage correct:
class Derived4 extends Base2 { constructor (a) { this = Object.create(new^.prototype); this.a = a; } isDerived4() {return true}; }
Ok, you are right, I stand corrected. However, the above cases are quite exotic, to the point that I question the need to support them with class syntax. Still I agree that new^ is a fine device for expressing them.
Or deriving from an abstract base class:
class D7 extends AbstractBase { constructor () { this = Object.create(new^.prototype); //instances are ordinary objects } }
I am not sure what do you mean by "abstract" class here.
Note that if we adopted "the Domenic alternative", i.e. implicit
this = Object.create(new^.prototype)
, the latter two would no longer need the corresponding line in their constructor, leaving usage ofnew^
for (a) different [[Call]] behavior and (b) exotic allocation.
I think "the Dominic alternative" is problematic in that it makes it easy to forget to call base constructor. In majority of the cases you really want to do it, and implicit "this = Objec.create(new^.prototype)" will do the wrong thing without warning you.
Dmitry
On Sep 18, 2014, at 11:29 AM, Dmitry Lomov wrote:
You haven't specified the constructor body, I assume you have intentionally omitted it, and in that case the above is equivalent to class A extends HTMLElement { constructor() { this = new super(); } }
There is no explicit passing of new^ here.
new^ is really just for 'constructors that are callable as functions as well'. Andreas is right that it is esoteric, but supporting that seems to be a requirement for classes design.
But just so there is no confusion, there is implicit passing of new^ going on in this example. That's necessary in order for HTMLElement to correctly the the [[Prototype]] of the object allocates.
From: dslomov at google.com [mailto:dslomov at google.com] On Behalf Of Dmitry Lomov
I am not sure what do you mean by "abstract" class here.
I'm just quoting Allen's gist. It's a base class whose constructor always throws (in the gist, by doing this = undefined
). Almost all DOM classes are currently like this, for example.
I think "the Dominic alternative" is problematic in that it makes it easy to forget to call base constructor. In majority of the cases you really want to do it, and implicit "this = Objec.create(new^.prototype)" will do the wrong thing without warning you.
My argument for it is that it works exactly like normal function-based inheritance works today. That is:
function Base() { }
Base.prototype.foo = function () { };
function Derived() {
// note: no Base.call(this)
}
Derived.prototype = Object.create(Base.prototype);
var d = new Derived();
assert(typeof d.foo === "function");
Despite omitting the Base.call(this)
, which is analogous to omitting this = new super()
, I still have a correctly set-up prototype linkage. I would prefer if the following ES6 code:
class Base { foo() { } }
class Derived extends Base {
constructor() { /* note: no this = new super() */ }
}
var d = new Derived();
assert(typeof d.foo === "function");
also had the assertion pass. With alternative 1 (no implicit super; no implicit this = Object.create(new^.prototype)
), the assertion will fail, as the prototype linkage will be incorrect.
From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Domenic Denicola
I'm just quoting Allen's gist. It's a base class whose constructor always throws (in the gist, by doing
this = undefined
). Almost all DOM classes are currently like this, for example.
Actually, this is a really important point. Since HTMLElement is abstract (throws on construct in all cases), the only way of extending it is by doing this = Object.create(new^.prototype)
. You cannot extend it with this = new super()
, either implicitly or explicitly, since new super()
throws.
So if you want to extend HTMLElement, you will need this = Object.create(new^.prototype)
, either implicitly or explicitly. (And I argue for implicitly.)
On Thu, Sep 18, 2014 at 10:06 PM, Domenic Denicola < domenic at domenicdenicola.com> wrote:
From: dslomov at google.com [mailto:dslomov at google.com] On Behalf Of Dmitry Lomov
I am not sure what do you mean by "abstract" class here.
I'm just quoting Allen's gist. It's a base class whose constructor always throws (in the gist, by doing
this = undefined
). Almost all DOM classes are currently like this, for example.
Got it.
I think "the Dominic alternative" is problematic in that it makes it easy to forget to call base constructor. In majority of the cases you really want to do it, and implicit "this = Objec.create(new^.prototype)" will do the wrong thing without warning you.
My argument for it is that it works exactly like normal function-based inheritance works today. That is:
function Base() { } Base.prototype.foo = function () { }; function Derived() { // note: no Base.call(this) } Derived.prototype = Object.create(Base.prototype); var d = new Derived(); assert(typeof d.foo === "function");
Despite omitting the
Base.call(this)
, which is analogous to omittingthis = new super()
, I still have a correctly set-up prototype linkage. I would prefer if the following ES6 code:
class Base { foo() { } } class Derived extends Base { constructor() { /* note: no this = new super() */ } } var d = new Derived(); assert(typeof d.foo === "function");
also had the assertion pass. With alternative 1 (no implicit super; no implicit
this = Object.create(new^.prototype)
), the assertion will fail, as the prototype linkage will be incorrect.
Note that in "no explicit constructor invocation" case you will fail earlier (on return from Derived constructor), and this is an early error.
On Thu, Sep 18, 2014 at 10:11 PM, Domenic Denicola < domenic at domenicdenicola.com> wrote:
From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Domenic Denicola
I'm just quoting Allen's gist. It's a base class whose constructor always throws (in the gist, by doing
this = undefined
). Almost all DOM classes are currently like this, for example.Actually, this is a really important point. Since HTMLElement is abstract (throws on construct in all cases), the only way of extending it is by doing
this = Object.create(new^.prototype)
. You cannot extend it withthis = new super()
, either implicitly or explicitly, sincenew super()
throws.So if you want to extend HTMLElement, you will need
this = Object.create(new^.prototype)
, either implicitly or explicitly. (And I argue for implicitly.)
I see; thanks for explaining HTMLElement to me. I argue for explicitly, on the following grounds. Imagine:
class A { constructor(x) { this.x = x; } }
class B extends A { constructor(a, b) { this = new super(a+b); // do more } }
Need to extent HTMLElement notwithstanding, I believe that since all Bs are instances of As they must be properly initialized, which includes that 'x' should be set on them. I assume that this is a majority case, more major than HTMLElement.
If you implicitly do 'this = Object.create(new^.prototype)' you default into wrong, un-initialized state for those base classes that actually provide a constructor. It is very easy to make this mistake - just forget the super call.
Same applies for extending many of exotic builtiins, such as RegExp and TypedArrays. You just cannot create a typed array using Object.create(Uint8Array.prototype). Same applied for constructible DOM objects (Events etc).
It is just the truth that there is no one-size-fits-all, or even one-size-fits-most way to call a super constructor, or more generally, initialize an instance of a subclass. We should just embrace that there is no good default here.
On Sep 18, 2014, at 1:11 PM, Domenic Denicola wrote:
From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Domenic Denicola
I'm just quoting Allen's gist. It's a base class whose constructor always throws (in the gist, by doing
this = undefined
). Almost all DOM classes are currently like this, for example.Actually, this is a really important point. Since HTMLElement is abstract (throws on construct in all cases), the only way of extending it is by doing
this = Object.create(new^.prototype)
. You cannot extend it withthis = new super()
, either implicitly or explicitly, sincenew super()
throws.So if you want to extend HTMLElement, you will need
this = Object.create(new^.prototype)
, either implicitly or explicitly. (And I argue for implicitly.)
But that won't give you a real HTMLElement exotic object, if there is such a thing, and won't initialize it properly.
But you can get around that problem if the HTMLElement constructor is defined appropriately using this^:
class HTMLElement { constructor(...args) { if (this^ === HTMLElement) then throw new DOMError("can't create using new"); if (this !== undefined) { //assert: must have been called via new super() this = privateAllocatedDOMElement(); //initialize newly allocated DOMElement } } }
On Thu, Sep 18, 2014 at 10:26 PM, Allen Wirfs-Brock <allen at wirfs-brock.com>
wrote:
On Sep 18, 2014, at 1:11 PM, Domenic Denicola wrote:
From: es-discuss [mailto:es-discuss-bounces at mozilla.org] On Behalf Of Domenic Denicola
I'm just quoting Allen's gist. It's a base class whose constructor always throws (in the gist, by doing
this = undefined
). Almost all DOM classes are currently like this, for example.Actually, this is a really important point. Since HTMLElement is abstract (throws on construct in all cases), the only way of extending it is by doing
this = Object.create(new^.prototype)
. You cannot extend it withthis = new super()
, either implicitly or explicitly, sincenew super()
throws.So if you want to extend HTMLElement, you will need
this = Object.create(new^.prototype)
, either implicitly or explicitly. (And I argue for implicitly.)But that won't give you a real HTMLElement exotic object, if there is such a thing, and won't initialize it properly.
But you can get around that problem if the HTMLElement constructor is defined appropriately using this^:
Of course s/this^/new^/, I presume.
class HTMLElement { constructor(...args) { if (this^ === HTMLElement) then throw new DOMError("can't create using new"); if (this !== undefined) {
new^, not 'this'
From: Allen Wirfs-Brock [mailto:allen at wirfs-brock.com]
But that won't give you a real HTMLElement exotic object, if there is such a thing, and won't initialize it properly.
But you can get around that problem if the HTMLElement constructor is defined appropriately using this^:
That's a good point, thanks for explaining. (I assume you mean new^
? You keep saying this^
here and in other messages so I wonder if you've had a shift in thinking.)
At this point my only real argument for implicit this = Object.create(new^.prototype)
is that it's what happens with function-based inheritance. I agree with Dmitry that there probably is no good default so I guess I just find taking cues from what functions do to be the best guidance. But requiring explicit initialization seems fine too.
of course! I don't know why my brain wiring keeps producing "this^".
On Sep 18, 2014, at 1:30 PM, Domenic Denicola wrote:
From: Allen Wirfs-Brock [mailto:allen at wirfs-brock.com]
But that won't give you a real HTMLElement exotic object, if there is such a thing, and won't initialize it properly.
But you can get around that problem if the HTMLElement constructor is defined appropriately using this^:
That's a good point, thanks for explaining. (I assume you mean
new^
? You keep sayingthis^
here and in other messages so I wonder if you've had a shift in thinking.)At this point my only real argument for implicit
this = Object.create(new^.prototype)
is that it's what happens with function-based inheritance. I agree with Dmitry that there probably is no good default so I guess I just find taking cues from what functions do to be the best guidance. But requiring explicit initialization seems fine too.
yes, but basic function constructors are the equivalent of class definitions without an extends clause. It's been suggested that an optional extends clause that function
could someday be added to function definitions:
if we did that, we would probably apply the same rules as for subclasses with an extends. But it probably actually isn't needed. When you look closely, the semantics of
class Foo extends Bar { constructor () {} }
are exactly what we would defined for
function Foo() extends Bar { }
so it adds no know expressiveness.
Domenic Denicola wrote:
But requiring explicit initialization seems fine too.
So are you withdrawing your variation (option 3 or whatever it is)? Not advocating, just asking (trying to keep up!).
Unless the head syntax camp rallies somehow, I'm convinced: option 2 (gist.github.com/allenwb/5160d109e33db8253b62) is the winner.
From: Brendan Eich [mailto:brendan at mozilla.org]
So are you withdrawing your variation (option 3 or whatever it is)? Not advocating, just asking (trying to keep up!).
Since nobody else seems to be fighting for it, and I am highly interested in consens-ing upon something, I guess I am.
On Sep 18, 2014, at 2:04 PM, Domenic Denicola wrote:
From: Brendan Eich [mailto:brendan at mozilla.org]
So are you withdrawing your variation (option 3 or whatever it is)? Not advocating, just asking (trying to keep up!).
Since nobody else seems to be fighting for it, and I am highly interested in consens-ing upon something, I guess I am.
I'm find with it too, so I'll present it next week as the recommended design.
Kevin Smith wrote:
Just for the sake of capturing everything, here's an updated version of the extended header idea:
gist.github.com/zenparsing/5efa4459459b9f04d775
Cheers!
In the interest of the full dialectic, I encourage everyone tempted by new^ and all-degrees-of-freedom-including-hanging-yourself to look at this. It doesn't address some use-cases (constructor called differing from new'ed, notably), but perhaps that could be orthogonal via some MOP (if truly the hard/exceptional/native-legacy case).
gist.github.com/zenparsing/5efa4459459b9f04d775
Cheers!
In the interest of the full dialectic, I encourage everyone tempted by new^ and all-degrees-of-freedom-including-hanging-yourself to look at this. It doesn't address some use-cases (constructor called differing from new'ed, notably), but perhaps that could be orthogonal via some MOP (if truly the hard/exceptional/native-legacy case).
That version almost, but not quite, holds together. Another revision is forthcoming...
Le 19 sept. 2014 à 06:11, Kevin Smith <zenparsing at gmail.com> a écrit :
gist.github.com/zenparsing/5efa4459459b9f04d775
Cheers!
In the interest of the full dialectic, I encourage everyone tempted by new^ and all-degrees-of-freedom-including-hanging-yourself to look at this. It doesn't address some use-cases (constructor called differing from new'ed, notably), but perhaps that could be orthogonal via some MOP (if truly the hard/exceptional/native-legacy case).
That version almost, but not quite, holds together. Another revision is forthcoming...
I'm not sure that having two different ways to do subclassing is good. For instance, suppose that I have the following:
class B extends Array {
constructor(...args) extends new Array(...args) {
// ....
}
}
I guess that, if someone try the following, it will not work as intended:
class C extends B {
constructor(...args) {
/* A */
super(...otherArgs)
/* B */
}
}
What is confusing, is that it could work when extending other classes than B
, with no obvious reason.
IIUC, they could make working by writing something resembling to:
class C extends B {
static creator(...args) {
/* A */
return otherArgs
}
constructor(...args) extends new super(... C. creator(...args)) {
/* B */
}
}
Here, the creator
static methods joined to the extends clause of constructor play the role of @@create, and are called in bottom-up order, whereas the constructor methods are called in top-down order. I've thought yesterday of such a design where the constructor is split in two optional parts like that. Well finally, it would be simply equivalent to:
class C extends B {
constructor(...args) {
/* A, using new^ */
this = new super(...otherArgs)
/* B, using this */
}
}
I'm not sure that having two different ways to do subclassing is good.
This is noted in the "downsides" section of the gist. I'll expand on this point in a follow up...
class C extends B {
static creator(...args) { /* A */ return otherArgs } constructor(...args) extends new super(... C. creator(...args)) { /* B */ } }
Note that in the latest version "super" is not actually allowed within the initializer expression. This is to avoid confusion with the meaning of "super" inside of method bodies (which does not change from the current ES draft).
I'm not sure that having two different ways to do subclassing is good.
Follow me down the rabbit hole...
I think we all want a unified model of class inheritance. In ES5, the model is unified, but only because the built-ins are not truly subclassable.
The ES5 model looks like this:
- Every object starts off with the same undifferentiated shape. I'm imagining a circle.
- The constructor is passed the newborn object and is tasked with differentiating it in whatever manner it sees fit.
Let's call this Model A.
This model works really well. It's simple, flexible, dynamic, and composable. Subclassing works via prototype chains and the composability of constructors. Ignoring @@create, the current ES6 draft does a beautiful job of capturing and optimizing this model.
However, it falls short when we try to subclass built-ins and exotics:
- Built-ins and exotics start life with a different and fully differentiated state. I'm imagining squares, stars, triangles, and octagons.
- The constructors of built-ins and exotics are not composable. Instead of differentiating objects, they birth objects.
Let's call this Model B.
The design introduced by this thread ("this=new super") effectively changes the inheritance model to match Model B, while supporting Model A as a legacy feature. In my opinion, this is a mistake. It sacrifices the common case of classes rooted on regular objects to the edge case of classes rooted on built-ins and exotics. It makes the ES inheritance model more fussy and less functional.
In my opinion, constructors conforming to Model B should be considered legacy cruft. Model A should be the unified inheritance model going forward, and new built-ins and exotics should be written to conform to Model A, or should be future-friendly with Model A.
The class initializer syntax shouldn't be viewed as purely a means of
parent class initialization. It a general-purpose facility for providing a
"this" value to a constructor when invoked via new
. It can be used for
many novel features, one of which is supporting legacy Model B parent
classes.
So, is this "Model A" centered vision plausible?
I wish it was ... and it looks like migration it's possible with some (a lot!) overhead ... here a stupid ES5ish example on how we could subclass an Array (missing all methods that should return new MyArray instead of Array such slice, splice, concat, etc...)
function MyArray() {
'use strict'; // new MyArray or die
// as ES5 compatible alias for `this`
// since `this = ...` would throw otherwise
var self = this;
// whenever it's needed, if needed
self = duper.apply(self, arguments);
// overwrite the returned instance in all cases
// or in all cases duper.apply was not needed
return self === this ? duper.call(this) : self;
}
MyArray.prototype = Object.create(
Array.prototype,
{constructor:{value: MyArray }}
// other methods and stuff ...
);
var ma = new MyArray(1, 2, 3);
[
ma instanceof MyArray,
ma instanceof Array,
ma,
test.apply(null, ma),
{}.toString.call(ma)
].join('\n');
// just to test if it works OK
function test() {
return [].slice.call(arguments);
}
/*
true
true
1,2,3
1,2,3
[object Array]
*/
The used duper
function is a temporary abomination such:
function duper() {
var
proto = Object.getPrototypeOf(this),
descriptors = Object.getOwnPropertyDescriptors(this),
// might fail, just as example for Array subclassing
self = Object.getPrototypeOf(proto)
.constructor.apply(this, arguments)
;
return Object.defineProperties(
Object.setPrototypeOf(self, proto),
descriptors
);
}
That should be aware of both exotic and built-ins and behave accordingly
(e.g. HTMLDivElement would result in document.createElement('div')
instead) while getOwnPropertyDescriptors has been previously
discussed already.
This does not look great, good, or probably even reasonable, but it's somehow "shimmable" which is honestly my only major concern about subclassing built-ins and exotic objects.
My .02
Best
On Fri, Sep 19, 2014 at 1:25 PM, Kevin Smith <zenparsing at gmail.com> wrote:
I'm not sure that having two different ways to do subclassing is good.
Follow me down the rabbit hole...
I think we all want a unified model of class inheritance. In ES5, the model is unified, but only because the built-ins are not truly subclassable.
The ES5 model looks like this:
- Every object starts off with the same undifferentiated shape. I'm imagining a circle.
- The constructor is passed the newborn object and is tasked with differentiating it in whatever manner it sees fit.
Let's call this Model A.
This model works really well. It's simple, flexible, dynamic, and composable. Subclassing works via prototype chains and the composability of constructors. Ignoring @@create, the current ES6 draft does a beautiful job of capturing and optimizing this model.
However, it falls short when we try to subclass built-ins and exotics:
- Built-ins and exotics start life with a different and fully differentiated state. I'm imagining squares, stars, triangles, and octagons.
- The constructors of built-ins and exotics are not composable. Instead of differentiating objects, they birth objects.
Let's call this Model B.
I am curious why you claim that "constructors of bulit-ins and exotics are not composable". I believe the current proposal goes a long way towards composition (via subclassing) of all kinds of constructors (be it bulitins, exotics, proxies or normal objects).
The design introduced by this thread ("this=new super") effectively changes the inheritance model to match Model B, while supporting Model A as a legacy feature. In my opinion, this is a mistake. It sacrifices the common case of classes rooted on regular objects to the edge case of classes rooted on built-ins and exotics. It makes the ES inheritance model more fussy and less functional.
What do you mean by "less functional" here? If in a sense of "functional language", then I'd argue that starting with a circle and cutting it into octagon corner by corner is less functional than starting with octagon, using your colorful metaphors :) (i.e. you imperatively modify the shape of the primordial object until it becomes Uint8Array - why is this useful?)
In my opinion, constructors conforming to Model B should be considered legacy cruft. Model A should be the unified inheritance model going forward, and new built-ins and exotics should be written to conform to Model A, or should be future-friendly with Model A.
The class initializer syntax shouldn't be viewed as purely a means of parent class initialization. It a general-purpose facility for providing a "this" value to a constructor when invoked via
new
. It can be used for many novel features, one of which is supporting legacy Model B parent classes.So, is this "Model A" centered vision plausible?
In my view, your "should be"s above should be better motivated. You state that "Model A" is the right model, even though it does not cover the exotics (that are with us forever, since we are in the browser) and you state that future exotics should be compatible with Model A, without covering the reasons for this. Sure if you believe that Model A is the model, then you are right, but you haven't motivated that belief.
Thanks Dmitry - great response!
I am curious why you claim that "constructors of bulit-ins and exotics are not composable". I believe the current proposal goes a long way towards composition (via subclassing) of all kinds of constructors (be it bulitins, exotics, proxies or normal objects).
For example, it is not possible with Model B to take two unrelated classes, use one as a base, and mixin the other one (including the initialization logic). This is possible with Model A (and ES5 inheritance today).
I know that it's possible to do that using the "this=new super" design, by proactively branching on "new^" in any class with an extends clause. But I believe that this technique is quite painful to write and to that extent the design discourages it.
Also, (and I understand that this is an unsupported claim) I believe users have very little interest in changing the inheritance model. It's been successful for Javascript, as far as I can tell.
What do you mean by "less functional" here? If in a sense of "functional language", then I'd argue that starting with a circle and cutting it into octagon corner by corner is less functional than starting with octagon, using your colorful metaphors :) (i.e. you imperatively modify the shape of the primordial object until it becomes Uint8Array - why is this useful?)
What if I wanted a Uint8Array that incorporated behavior from some other class (which required initialization logic)?
Think of it like adding to the circle rather than cutting from it. To create a Map (for instance) we take a circle and attach the components necessary for it to function as a Map. The fact that it can function as a Map should not prohibit the user from attaching the raw components necessary for it to function as any other arbitrary class. To implement that in JS, we might use private fields (which we'll hopefully address post-ES6).
This is all very beautiful and abstract in my mind : ) I'm interested to know the extent to which it's practical.
Thanks for your thoughtful and argumented response!
On Fri, Sep 19, 2014 at 5:27 PM, Kevin Smith <zenparsing at gmail.com> wrote:
Thanks Dmitry - great response!
I am curious why you claim that "constructors of bulit-ins and exotics are not composable". I believe the current proposal goes a long way towards composition (via subclassing) of all kinds of constructors (be it bulitins, exotics, proxies or normal objects).
For example, it is not possible with Model B to take two unrelated classes, use one as a base, and mixin the other one (including the initialization logic). This is possible with Model A (and ES5 inheritance today).
I know that it's possible to do that using the "this=new super" design, by proactively branching on "new^" in any class with an extends clause. But I believe that this technique is quite painful to write and to that extent the design discourages it.
Ah mixins, right! Well, here is what I think: classes just have to be specially coded for mixability anyway. For example, all state that they have on instances should be private state (which we don't yet have). Since some classes are mixable and some are not (and I argue that most (all?) ES5 classes are not, because their state is not private), it is a fair requirement that mixable class constructors are written in mixin-compatible way. It is quite achievable with the current proposal, although, as you mention, not quite prettily. I can imagine a syntax extension that will make that easier (e.g. 'this = mixin super(...)').
[Of course it is only your second class that has to be designed to be mixable. It is pretty straightforward to design it so that if F is not mixable and G is, you can still get F mixin G to work].
Incidentally, a well-thought-out proposal for mixins in ES7 will be lovely.
Also, (and I understand that this is an unsupported claim) I believe users have very little interest in changing the inheritance model. It's been successful for Javascript, as far as I can tell.
What do you mean by "less functional" here? If in a sense of "functional language", then I'd argue that starting with a circle and cutting it into octagon corner by corner is less functional than starting with octagon, using your colorful metaphors :) (i.e. you imperatively modify the shape of the primordial object until it becomes Uint8Array - why is this useful?)
What if I wanted a Uint8Array that incorporated behavior from some other class (which required initialization logic)?
Just from implementation perspective, it is a nightmare to imagine Uint8Array mixable with AudioBuffer :) Uint8Array + pure JavaScript mixable class is quite plausible though (again mixability has to be designed).
Hope this helps, Dmitry
Heh, we've gone around in circles.
Remember that the original motivating reason for @@create was to separate allocation and instantiation, in reaction to implementers saying that it would be madness for you to be able to do
var o = {};
Map.call(o);
Set.call(o);
WeakMap.call(o);
Uint8Array.call(o);
// ...
and get a hybrid map/set/weakmap/byte array/etc. (If you look through older drafts, before @@create, you'll find that this was at the time possible.)
The argument is that each of these requires an exotic type of object with a different backing implementation, and no single object should have more than one backing type.
So,
What if I wanted a Uint8Array that incorporated behavior from some other class (which required initialization logic)?
is essentially an explicit anti-goal of the ES6 object-construction design journey.
Heh, we've gone around in circles.
Yeah. My brain is fried from thinking about this too much. Thanks for the context!
At the last TC39 meeting ( rwaldron/tc39-notes/blob/master/es6/2014-07/jul-30.md#44-instantiation-reform-review-create-design-rationale-and-possible-alternatives and rwaldron/tc39-notes/blob/master/es6/2014-07/jul-31.md#44-follow-up-instantiation-reform-create ) we agreed to a general direction to try for a new object instantiation design to replace @@create.
Since then I have gotten feedback and had design discussions with a number of individuals. This has lead to a number of refinements of the core design and one remaining point where there are strong contrary positions. The point of contention is about whether or not a subclass construction ever implicitly calls its superclass constructor.
gist.github.com/allenwb/291035fbf910eab8e9a6 summaries the main syntactic changes since the meeting and provides rationales them. These features are common to both alternates. this is a good place to start, after reading the meeting notes.
I have prepared two longer Gists that outline the two alternatives designs, presents design rationales, and provides usage examples for a number of likely use cases. Note that there is more commonalities then differences among the two alternatives. the syntactic choices and semantics of [[Construct]] are the same for both.
These two Gist have parallel construction for easy comparison. I suggest approaching this is by first readying through one of the Gists and then doing a side by side read through of the alternative to see the differences in the designs and usage.
gist.github.com/allenwb/5160d109e33db8253b62 with implicit super construct if no local allocation gist.github.com/allenwb/53927e46b31564168a1d explicit super construct required if no local allocation
I appreciate it if major constructive feedback on any of these documents were made via Gist comments.