How to fix the `class` keyword
Please go suck a new Egg.
:P
On Wed, Mar 4, 2015 at 1:23 PM, Eric Elliott <eric at paralleldrive.com> wrote:
I know I've raised all these issues on es-discuss before and basically been told to go suck an egg, but we all want the same thing -- a better JavaScript for everybody.
I recommend watching e.g. www.youtube.com/watch?v=hneN6aW-d9w on how to contribute more effectively. Proposing grand design changes when ES6 is done when you don't really have established credibility is unlikely to do you any favors. If you prefer reading, annevankesteren.nl/2014/03/contributing-to-standards has some tips as well.
Eric, you can design your own language and have that compile to JS. Then you can get exactly what you want.
I’m saying the following as someone who does not think that classes are perfect (I dislike that they look so different from what they are internally translated to), but they will “just work” for most people and they are backwards-compatible with much current code.
Make class inheritance compositional similar to the way stamps are composed. chimera.labs.oreilly.com/books/1234000000262/ch03.html#prototypal_inheritance_with_stamps In other words, change the behavior of
extend
, or deprecateextend
and replace it with something like acompose
keyword that can compose any number of classes.
Traits (or mixins) are coming! A class will be the location where you assemble (any number of) traits. IMO, it’s better to have a different construct for this than to force classes to serve this use case. In my view, this is your most important point and traits are the answer.
Deprecate
new
.new
violates both the substitution principle and the open / closed principle. Thenew
keyword is destructive because it adds zero value to the language, and it couples all callers to the details of object instantiation. If you start with a class that requiresnew
(all classes in ES6) and you later decide you need to use a factory instead of a class, you can’t make the change without refactoring all callers. This is especially problematic for shared libraries and public interfaces, because you may not have access to all of the code using the class. You may think it doesn’t do any harm to just call a factory function with thenew
keyword, but thenew
keyword triggers behaviors that change what’s going on when the function is invoked. If you can’t count on the function to behave the same way for all callers, you can’t predict what the software will do. That’s bad. Make sure thatclass
obeys the substitution principle when you switch from a class to a factory and vise verse. This is an important point, because if callers are counting on any behavior or property of a class, and you decide to change the implementation to a factory, you’ve just broken the calling code. Additionally, if callers are counting on the behavior of a factory, and you switch the implementation to a class, that’s similarly problematic, though as it stands, there’s no good reason to switch from a factory to a class.
You can easily return an instance of a subclass or any other object from a class constructor. Engines can an will optimize this so that there won’t be any difference between a factory function and a class in this regard. In other words: you can make the changes you are describing.
The behavior of
this
. It always refers to the new instance in a class constructor. In a factory function,this
is dynamic, and follows a completely different set of rules. Possible solution: deprecatethis
and instead refer to the class or function by name. A major drawback of this solution is that it would break.call()
,.apply()
and.bind()
, unless we also change their behavior to override the function name reference.
This assumes that you want to go back and forth between classes and factory functions. If you don’t then this
is not an issue.
I know I've raised all these issues on es-discuss before and basically been told to go suck an egg, but we all want the same thing -- a better JavaScript for everybody.
I agree. Classes do have drawbacks, but they also have advantages: static analyzability, backwards compatibility, the ability to subclass built-ins. I can clearly feel your passion, but you are not being completely fair w.r.t. their pros and cons.
After traits (in ES7?), I don’t see anything major missing from your wish list. Things won’t be exactly like you want them, but you’ll be able to do everything you want to.
Did you seriously just plug your blog post in es-discuss?
Are you really explaining the open-closed principle and composition vs inheritance to a mailing list of people interested in language design?
About why class is added: esdiscuss.org/topic/is-class-syntax-really-necessary
ECMAScript would have looked different had it been designed from scratch today - but given your criticism everything apart from mixins for classes is impossible without breaking compatibility and mixins are being explored separately anyway.
From: Eric Elliott <eric at paralleldrive.com>
I've already posted this on my Medium blog here:
On Wed, Mar 4, 2015 at 8:03 AM Benjamin (Inglor) Gruenbaum <inglor at gmail.com>
wrote:
Did you seriously just plug your blog post in es-discuss?
Yes, and this is not the first time: esdiscuss/2013-June/031589
As someone who has fifty or sixty thousand lines of code using the new class syntax, I find this hard to understand. When I run into cases that aren't suited for the new class syntax, the solution is easy: I don't use it. If one tool isn't fit for the job, pick up another! After all, no one is proposing we remove the old prototypal stuff.
The class syntax solves one set of problems. The more flexible prototypal stuff solves another, and from my own experience they work pretty well together.
Joe
While we're on the topic, is multiple inheritance on radar for ES7? The grammar of the ES6 draft spec I used for reference back when I first implemented ES6 classes seemed to indicate support for multiple inheritance. I don't know if I misread it, or if at one point that was a planned feature.
It's not terribly important to me since I seem to average one use case of multiple inheritance every fifty thousand lines of code or so, and I can usually fudge it. I'm just curious if multiple inheritance is on the radar or not. Thanks.
Best, Joe
On Wed, Mar 4, 2015 at 8:05 AM, joe <joeedh at gmail.com> wrote:
While we're on the topic, is multiple inheritance on radar for ES7?
On the radar? Yes. See old proposals
strawman:syntax_for_efficient_traits, strawman:classes_with_trait_composition, strawman:trait_composition_for_classes, strawman:classes_as_inheritance_sugar
that use traitsjs.org -like trait composition as a cleaner substitute for multiple inheritance.
But I for one am not in a hurry to revive this direction. The "const classes" I just mentioned are much higher priority. So let's say that traits/MI are on the radar for some future ES eventually possibly.
Not sure why this is a reply to me - but I completely agree. All class
does is take an extremely common idiom and makes it simpler and easier to
get right with better defaults - it is the same prototypical stuff. It's
an addition based on what people do and it's still entirely possible to do
it any other ES5 way.
There is a trap one could fall into, after reading one "classic" or even "authoritive" book. After that it might seem one understood the essence of everything.
For fairness, there were couple of valid points noticed: composition/aggregation (will be achieved, and already now can be, by mixins), and, probably, that you'll have to refactor call sites if you decide to switch to the raw factory (that's why eg Python chose to instantiate without 'new' keyword).
Other than that, yes, factory is just a desugarred class pattern, and yes, sugar matters if you want to design a practical language. In case of class-based code reuse, prototypes were also desugarred version of it (that's why JS at some point was "unexplained", and as a result made it harder to be practical quicker). Eg in my class I give prototypes as an advanced topic now, since it's just an implementation detail (the same again as in Python, which is also delegation-based).
The most valuable advice would be from Eric Arvidsson - just to design your language (likely, to be a "compiler" writer is easier nowadays), and exactly there you'll see how sugar matters.
Dmitry
I have to be honest, there is ONE sticky point to the way classes work
right now. Why have them throw when called without new
? The way you
traditionally guard against needing to rewrite call sites later would be to
check if this instanceof Constructor in the constructor. I believe this is
now impossible, correct?
- Matthew Robb
I have to be honest, there is ONE sticky point to the way classes work right now. Why have them throw when called without
new
? The way you traditionally guard against needing to rewrite call sites later would be to check if this instanceof Constructor in the constructor. I believe this is now impossible, correct?
Allowing the user to specify the "call" behavior for class constructors is something to consider for post-ES6.
On Wed, Mar 4, 2015 at 1:47 PM, Kevin Smith <zenparsing at gmail.com> wrote:
Allowing the user to specify the "call" behavior for class constructors is something to consider for post-ES6.
Unfortunately, based on this kind of logic (not the first I have seen it on here) I would ask that we stop calling ES6 classes "simple sugar on existing conventions". This is a major breaking difference in the most surface-y aspect of classes and how they are used in people's code.
- Matthew Robb
Making the following two things no longer interchangeable completely breaks my mental model for JS:
var x = new X;
and
var x = {}; X.call(x);
Yes the above is useless for built in types but it's very widely employed in user code, libraries, and frameworks. Making the above no longer true is a huge mistake imo and it changes the language in a fundamental way. We used to have objects and objects that you could call (functions). Now we have objects, functions, and something else... For what?
I thought the idea of max/min classes was to stay as true to the OOP patterns used today as possible? So shouldn't the question be reversed and classes should work like sugar for functions intended for use as constructors instead of breaking that model and requiring justification in a later draft to restore expected behavior?
- Matthew Robb
I thought the idea of max/min classes was to stay as true to the OOP patterns used today as possible? So shouldn't the question be reversed and classes should work like sugar for functions intended for use as constructors instead of breaking that model and requiring justification in a later draft to restore expected behavior?
This was where we started out. But it was found that the "just sugar" design was incompatible with the design goal of allowing subclassing of built-ins.
It's an inescapable dilemma: JavaScript has two simultaneous models for "classes": the builtin model, in which objects are "birthed" with a certain internal shape, and the function-constructor model, in which objects are birthed as undifferentiated plain objects and it is up to constructors to give them shape.
In order to allow subclassing as built-ins, we had to opt for the builtin model.
This is actually a good thing, because it paves the way for user-defined classes with per-instance (private) internal shape.
On 04/03/2015, at 20:13, Matthew Robb wrote:
Making the following two things no longer interchangeable completely breaks my mental model for JS:
var x = new X;
and
var x = {}; X.call(x);
This has never been the same and it isn't interchangeable. In one case the prototype is X.prototype and in the other it's Object.prototype.
Have you never seen this line in constructors?
if ( !( this instanceof X ) ) return new X(a.copy.of.the.arguments.goes.here);
I just want public and protected properties, as well as data properties.
After those, class and I are buddies.
On 04/03/2015, at 20:17, Matthew Robb wrote:
Sorry this should have read:
var x = Object.create(X.prototype); X.call(x); ```
Ooops, too late, sorry... :
On Wed, Mar 4, 2015 at 2:29 PM, Kevin Smith <zenparsing at gmail.com> wrote:
In order to allow subclassing as built-ins, we had to opt for the builtin model.
Help me make sure I am understanding correctly. This decision does not
help in making DOM subclassable. It doesn't simply or easily integrate into
all the existing and somewhat common OOP patterns in user land. What
built-ins are we talking about then? The decision to fork things here seems
MOSTLY motivated by a desire to support class extends Array{}
... Am I
crazy? If not.... There had to be a better way.
On the issue of calling class constructors, I would AT LEAST have preferred implicit new on all calls to class constructors. Sure you might get extra allocation weight but the way it stands now seems like it could only lead to errors in people's assumptions... Assumptions people have built up in their experience using the language and that are not only safe TODAY but considered best practice.
- Matthew Robb
This decision does not help in making DOM subclassable. It doesn't simply or easily integrate into all the existing and somewhat common OOP patterns in user land.
I don't understand these sentences - can you explain? As far as I'm aware, the current design does enable subclassing host objects.
What built-ins are we talking about then? The decision to fork things here
seems MOSTLY motivated by a desire to support
class extends Array{}
Well, any subclassing design would have to support JS builtins, obviously.
There had to be a better way.
Not really. If there were, we wouldn't have made such big a last-minute change. Other than not having the ability to "call" a class constructor, what specifically are your concerns with the design?
On the issue of calling class constructors, I would AT LEAST have preferred implicit new on all calls to class constructors.
Some people want that, and it's a pattern that can in principle be supported by a future version of the language. However, I don't believe that it's the right default. By having a default "call" behavior (other than throwing), the addition of any custom "call" behavior to a class definition will result in a breaking change for clients of that class.
On Mar 4, 2015, at 1:50 PM, Matthew Robb wrote:
On Wed, Mar 4, 2015 at 2:29 PM, Kevin Smith <zenparsing at gmail.com> wrote: In order to allow subclassing as built-ins, we had to opt for the builtin model.
Help me make sure I am understanding correctly. This decision does not help in making DOM subclassable. It doesn't simply or easily integrate into all the existing and somewhat common OOP patterns in user land. What built-ins are we talking about then? The decision to fork things here seems MOSTLY motivated by a desire to support
class extends Array{}
... Am I crazy? If not.... There had to be a better way.
Actually this decision really had nothing to do with subclassing built-ins or anything else. It just was made as part of the set of changes related to subclassing. In fact, the updated subclassing design provides an new reliable way for a function body to distinguish whether it was invoked using [[Call]] or [[Construct]].
The main reason we disabled [[Call]] invocation of class constructors was because some TC39 member are interested in possibly providing an enhanced semantics for [[Call]] invocation of class constructors. Making such calls illegal in ES6 leaves room for considering such enhancements that would otherwise have been precluded. One of those possible enhancement that has been talked about is to implicitly treat a [[Call]] of a class constructor as an implicit 'new', just like you are suggesting.
On the issue of calling class constructors, I would AT LEAST have preferred implicit new on all calls to class constructors. Sure you might get extra allocation weight but the way it stands now seems like it could only lead to errors in people's assumptions... Assumptions people have built up in their experience using the language and that are not only safe TODAY but considered best practice.
Not sure what assumptions you are referring to. [[Call]] invocation of ECMAScript functions have never done an automatic allocation so doing so could not be conforming to anybodies existing assumptions.
On 3/4/15 4:50 PM, Matthew Robb wrote:
This decision does not help in making DOM subclassable.
Wait, why not?
What built-ins are we talking about then?
Array, Map, Set, Promise come to mind offhand.
Matthew Robb wrote:
On the issue of calling class constructors, I would AT LEAST have preferred implicit new on all calls to class constructors. Sure you might get extra allocation weight but the way it stands now seems like it could only lead to errors in people's assumptions...
An error caught by a throw is better than one that escapes as a silent-but-deadly (never mind allocation weight) difference in runtime semantics.
In case it helps, the idea mooted for ES7 is that you'd add a "call
handler" to the class for when it is invoked without new
:
class Point2D { constructor(x, y) { this.x = x, this.y = y; } [Symbol.call](x, y) { return new this.constructor(x, y); } ... }
I used this.constructor
, but of course using Point2D
directly is
possible. In that case, subclasses would have to override the
[Symbol.call]
method, which seems undesirable and easily avoided as
shown above.
Bottom line, we don't want an implicit call handler in ES6. We need to get this right in ES7, and failing hard for now is the only way to be future-proof.
What built-ins are we talking about then?
Array, Map, Set, Promise come to mind offhand.
Error is another one where subclassability will be handy.
On Thu, Mar 5, 2015 at 5:46 AM, Brian Blakely <anewpage.media at gmail.com>
wrote:
I just want public and protected properties, as well as data properties.
After those, class and I are buddies.
I observed on Twitter that data properties can be hammered in surprisingly tersely, even without mixins:
let C = Object.assign(class {
method1(){}
method2(){}
get accessor1(){}
get accessor2(){}
}.prototype, {
dataProperty: 1,
dataProperty: 2,
}).constructor;
That is, if you don't mind them being enumerable, or that Symbol-keyed properties aren't included. :P
I do, however, have a small personal grievance about the class syntax: why doesn't it use commas to separate method and accessor properties? Consider the disconnect. Object literal: commas. Array literal: commas. Class literal: no commas. I appreciate that classes are not literals in quite the same sense, in that they aren't really a list of properties that are assigned to a single object, but it still seems quite un-Javascryptic to have property after property with no commas between.
Leon Arnott wrote:
I do, however, have a small personal grievance about the class syntax: why doesn't it use commas to separate method and accessor properties? Consider the disconnect. Object literal: commas. Array literal: commas. Class literal: no commas. I appreciate that classes are not literals in quite the same sense, in that they aren't really a list of properties that are assigned to a single object, but it still seems quite un-Javascryptic to have property after property with no commas between.
Class bodies are not literals, they're a distinct special form. Do not be thrown by method shorthands in object literals -- convergent evolution ;-).
One proof that class body is not an object literal is how magic
constructor is. Another is how super
works.
Finally, commas are unwanted overhead in the current design. In a design that adds data properties, e.g.,
gist.github.com/jeffmo/054df782c05639da2adb
the proposed punctuator is semicolon, which would be required only after data properites, not after methods or constructors (which end in braced bodies). This is C-like in the sense that C++, Java, C#, etc. are C-like.
So actually, tradition on top of avoiding unwanted overhead in between methods today.
Well, they're a distinct special form that can be used in expression
position, and evaluates to a function, so they're definitely, to bring back
an old coin, quasi-literal. (I personally think your reasoning entails that
arrows aren't function literals because of their lexical this
bindings.)
So, stuff inside class bodies should be considered more like function statements, not property definitions - even though they have a lot of very property-ish things, like computed names, or shorthand methods, or accessors, or shorthand generators... :|
And, according to that gist, data properties should be considered to be
like assignment statements. Would that make a hypothetical class { [foo, bar] = [1,2]; }
a destructuring or a computed property name? Either answer
seems dissatisfying (the former because it precludes the latter; the latter
because it mis-resembles the former).
Leon Arnott wrote:
Well, they're a distinct special form that can be used in expression position, and evaluates to a function, so they're definitely, to bring back an old coin, quasi-literal. (I personally think your reasoning entails that arrows aren't function literals because of their lexical
this
bindings.)
(That doesn't follow, but let's not quibble -- in any case, "literal" is the wrong word.)
The bigger problem is that we do not want class syntax for declaring properties on the prototype, which is what
class C { d: 42, constructor(){} }
would denote if you really mean object initialiser expressing the prototype object as class body.
So, stuff inside class bodies should be considered more like function statements, not property definitions - even though they have a lot of very property-ish things, like computed names, or shorthand methods, or accessors, or shorthand generators... :|
It's neither fish nor fowl.
Special forms are special, they mean what we say. Of course we should use good taste and clear precedent where we can. I'm saying object initialisers are not good precedent, and false taste (like fake sugar
I've already posted this on my Medium blog here: medium.com/@_ericelliott/how-to-fix-the-es6-class-keyword-2d42bb3f4caf
It seems inevitable that the
*class*
keyword in JavaScript is going to catch on, but that’s a problem because it’s fundamentally broken in many ways.Now that it’s out there and people are using it, it seems like the only logical way forward is to try to make it better for everybody.
In JavaScript, any function can instantiate and return objects. When you do so without a constructor, it’s called a factory function. The new
*class*
syntax can’t compete with the power and flexibility of factories — specifically stamps, and object pools are not the only factory use-case.There is a whole section on object construction in the GoF “Design Patterns” book www.amazon.com/gp/product/0201633612?ie=UTF8&camp=213733&creative=393185&creativeASIN=0201633612&linkCode=shr&tag=eejs-20&linkId=XXUP5DXMFH5VS2UI
which exist only to get around the limitations of constructors and classes.
See also: Three Different Kinds of Prototypal OO. ericleads.com/2013/02/fluent-javascript-three-different-kinds-of-prototypal-oo
The bottom line: Class doesn’t give you any power that isn’t already supplied by factory functions and the prototypal OO built into the language. All you’re doing when you create a class is opting into a less powerfull, less flexible mechanism and a whole lot of pitfalls and pain. medium.com/javascript-scene/the-two-pillars-of-javascript-ee6f3281e7f3
Is there any hope that the
*class*
keyword will ever be useful? Maybe. Why should we bother?Why don’t we just create a lint rule and move on?
The
class
keyword is creating a rift in the JavaScript community. I for one have plans to create an ESLint config that prohibits*class*
and share it as far and as wide as I can — and since I’m building a community of JavaScript developers that currently includes ~40,000 people, that’s far enough to have an impact.Classes could be useful. What if we want to build abstractions on top of it? What if we want to do more things in the language itself that could benefit with
*class*
integration (such as built-in traits)?We could make these changes opt-in by adding config to the class itself. That would prevent breaking changes and hopefully make the whole community happy. As it stands, we're just making people with classical inheritance backgrounds happy -- at least until they fall into one of the many pitfalls ahead of them.
Shouldn’t the entire JavaScript community benefit from
class
? How to Fixclass
In other words, change the behavior of
*extend*
, or deprecateextend
and replace it with something like acompose
keyword that can compose any number of classes. 2. *Deprecatenew
. **new*
violates both the substitution principle and the open / closed principle. The*new*
keyword is destructive because it adds zero value to the language, and it couples all callers to the details of object instantiation. If you start with a class that requires*new*
(all classes in ES6) and you later decide you need to use a factory instead of a class, you can’t make the change without refactoring all callers. This is especially problematic for shared libraries and public interfaces, because you may not have access to all of the code using the class. You may think it doesn’t do any harm to just call a factory function with the*new*
keyword, but the*new*
keyword triggers behaviors that change what’s going on when the function is invoked. If you can’t count on the function to behave the same way for all callers, you can’t predict what the software will do. That’s bad. 3. Make sure thatclass
obeys the substitution principle when you switch from a class to a factory and vise verse. This is an important point, because if callers are counting on any behavior or property of a class, and you decide to change the implementation to a factory, you’ve just broken the calling code. Additionally, if callers are counting on the behavior of a factory, and you switch the implementation to a class, that’s similarly problematic, though as it stands, there’s no good reason to switch from a factory to a class.The third point may be the most difficult, but if we can catalog every possible breaking change here, there may be some hope for
*class*
in the future, assuming we can get consistency baked into the language spec.If we can’t fix these problems with
*class*
, we should push to deprecate the keyword entirely, because as it exists today,class
is broken and absolutely should not be used medium.com/javascript-scene/the-two-pillars-of-javascript-ee6f3281e7f3 . Catalog of substitution breaksthis
. It always refers to the new instance in a class constructor. In a factory function,*this*
is dynamic, and follows a completely different set of rules. Possible solution: deprecatethis
and instead refer to the class or function by name. A major drawback of this solution is that it would break*.call()*
,*.apply()*
and*.bind()*
, unless we also change their behavior to override the function name reference.instanceof
- IMO, this is broken anyway because it doesn’t do what the name describes, and from a user’s perspective, it flat out lies when you try to use it across execution contexts, or when the constructor prototype property changes. Possible solution: deprecateinstanceof
.I know I've raised all these issues on es-discuss before and basically been told to go suck an egg, but we all want the same thing -- a better JavaScript for everybody. Being inclusive is practically baked into JS DNA. That's how
class
found its way into JS in the first place, even though we already had something much better: A very good system for prototypal OO (object literals, prototype delegation, and concatenative inheritance via dynamic object extension).~ee