-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Why no initializers? #26
Comments
Initializers add non-essential complexity. The natural scoping of such initializers is different from the scoping currently used in class bodies for computed property name expressions. Supporting that means in a sequence like: class C {
...
[this.name]() {...}
var v = this.name();
...
} The expression All of that complexity is just not necessary. JS programmers already know how to initialize instance state. They do it by performing assignments within the body of a constructor. This well entrenched pattern works just as well for instance variables as it does for instance own properties. Similarly, JS developers well understand the role of constructors. We don't think that the complexity that would have to be introduced to make constructors unnecessary in a limited set of use cases (note, there is no parameterization of initializers) is justified. |
I think that a huge fraction of people who would think of themselves as JS programmers believe that the way you initialize instance state is |
In which standard edition of ECMAScript can you do that? Note that we have explicitly stayed away from that syntax so that dialects such as TS and other transpilers based extensions can continue to support such syntax. |
Notice my careful phrasing of "people who would think of themselves as JS programmers". |
Initialisers often help code be more self-documenting. Additionally banning initialisers from the declaration of instance variables would mean that in order to initialise everything all such variables would be listed twice. In some large/complex classes with a lot of instance variables this could make the code unnecessarily long/less clear/more effort to update. |
On this issue - I bet 95% of devs using plain initializers ( As for TypeScript concerns - sounds like we have the next MooTools 👊 😿 |
If you have initializers then you have to decide something that I think has no correct answer. If you evaluate the initialization expression once during class setup and copy that value to each instance, that may be "too early", increases static startup cost, and creates enormous traps when the initializer is mutable (e.g. If you evaluate the initialization expression once per class instance, that may be "too often", and needlessly incurs cost when the initializer is a complex evaluation of an unchanging value. ~50% of devs will tell you this is the wrong behavior. It's also a trap because initializers observing virtual behavior [1] will presumably be running before any derived class constructors finish; people get this wrong all the time. See https://stackoverflow.com/questions/43595943/why-are-derived-class-property-values-not-seen-in-the-base-class-constructor / microsoft/TypeScript#1617 If initializers exist then the syntax needs to clearly imply which of these happen; I don't think it does (and thus think initializers should not exist) |
I would be very surprised to learn it's anything like that large a fraction.
Not having initializers does not save you. The following, as spec'd in this proposal, throws: class Base {
constructor() {
this.init();
}
init(){}
}
class Derived extends Base {
var m;
init(){
this->m = 0;
}
}
new Derived; because at the time that Nor can this be trivially resolved by adding fields to |
As it should. Both the superclass coder is at fault for using an over-ridable method for instance initialization and the subclass coder is at fault for putting post construction dependencies into a over-ridden method that (hopefully) has been documented as being called during superclass construction. If you write subclasses you need to know the subclass extension contract of your superclass. But easily done correctly: class Base {
constructor() {
this->init();
}
hidden init(){}
}
class Derived extends Base {
var m;
constructor() {
super();
this->init();
}
hidden init(){
this->m = 0;
}
}
new Derived; |
This has been Stage 3 for ages, and has been the de facto way to initialize class state in at least one major JS framework (React). Not only is it shocking to see TC39 members argue for abandoning these features which are widely used, but removing the primary feature of class fields: initializers. I must be missing something obvious, but what is the difference between
and
(besides the added boilerplate)? The entire point of these class field, for a pretty sizable number of developers, was to avoid constructor boilerplate. As others have said, this proposal seems to take a big step backward in usefulness, while creating significant churn in the JS community in the process. I struggle to see the point in any of it. |
@arackaf Also, the second one might invoke setters, and you have to be careful to correctly forward your arguments to super. |
@Kovensky - ah! Thank you - yes, I now remember a change to class fields whereby the
would, iirc, now shadow an inherited field, rather than invoke the setter. Thank you! |
Complexity. Which you guys are demonstrating my trying to figure out the interaction between the initializer and inherited fields. Understanding linear execution in a constructor method is easy. |
I see the declarative field as simpler from a mental model standpoint, as a declared field is just a field in your instance, and it avoids for you all the things you'd have to be careful with if you had to write it in the constructor instead. Not only do you get to give it an initial value, you also get to avoid potential superclass prototype/instance setters, and you don't have to write the rest/spread arguments boilerplate to forward the correct constructor arguments to |
I already understood that, I had just forgotten since it comes up literally never. And for those rare cases of deep inheritance chains with inherited properties, the class field initializer shadows the inherited property as most developers would expect (which is why, I assume, that change was made). The initializers on class fields solved a significant problem many developers face. I really hope TC39 thinks better than to remove it under the (frankly patronizing) assumption that the status quo is too "complex." |
@Kovensky
What is a "field"? It isn't a concept I remember reading about in any JavaScript book. Since you didn't say public field or private field I assume you are generalize over both. What are the semantics that both have in common that are relevant here? |
Again, a sign of lurking complexity. Also not this isn't just about what JS programmer can understand and remember. Behind of this there is implementation complexity and language design complexity.
I assume you are referring to the fact that initializers for public fields (ie, own properties) set the property value using [[DefineOwnProperty]] rather than [[Set]]. But what happens it the initializer expression itself refers to an inherited get accessor. Is it visible or not? Do you know? This is all a form of complexity. When you make common cases look simple by masking underlying mechanism you don't provide any foundation for reasoning about what is going on when you encounter a uncommon case. |
I think what’s missing here is that there are use cases for which almost any complexity might be warranted. Features for the language are not evaluated solely on complexity; that’s just one of many considerations that come into play. This proposal heavily prioritizes avoiding complexity while heavily deprioritizing desired use cases, years of committee consensus and work, years of existing babel codebases proving out the feasibility and desirability of the current proposals (and demonstrating that a very large number of JS programmers seem to intuitively understand how initializers work without explanation), and while overlooking very large consistency and feature holes in the ES6 design that the current proposals fill but this proposal fails to fully satisfy. |
Did implementation concerns not get ironed out prior to this getting to Stage 3?
All language features have hidden "complexity" (what I would call trivia") lurking behind the scenes. I was just reading Axel's Exploring ES2018 and ES2019. I wonder how many JS devs don't know the difference between object spread, and Object.assign, as it pertains to property setters vs defining new properties, and yet manage to use this feature productively nonetheless. |
@arackaf ftr, stage 3 is when implementation concerns are intended to get ironed out, not stage 2. |
@ljharb thank you for clarifying. I have heard of the Chrome team objecting to proposals prior to reaching Stage 3, but I'll take you word on that in the general case. |
Being a browser doesn’t exclude objecting at earlier stages :-) certainly any known implementation issues would be brought up as early as possible; it’s just expected that stage 3 is when engines actually implement it, and thus are able to bring concrete feedback to the champion/committee. |
I understand that this proposal seems very disruptive from the point of view of people who are used to thinking of public fields (with initializers) as "the way it's done", and I sympathize. Of course, it must be noted that lots of React developers think of other non-standard JS extensions as the "way it's done" as well. A clarifying question: are you objecting to the lack of initializers on instance variable declarations (as the thread title indicates) or more generally to the lack of public fields? |
We're well aware that JSX is non-standard, and that we'll always need tooling to support it. The concern is that these class features have been dragging along for literally years, and now, just as instance fields are starting to get implemented, and decorators were about to hit stage 3, you all want to roll this back to the drawing board in order to fix problems which don't in reality exist, while making these features less useful in the process. And yes, I understood this thread to be about initializers on instance fields, ie
The use of initializers is, I think it's accurate to say, the most common reason devs reach for class instance fields. |
@zenparsing I’m specifically objecting here to the lack of initializers; but related, to the inability to omit the constructor, which is a very critical feature of the current proposals. I don’t care if they’re called “fields” or not, i care that i can omit the constructor and define a per-instance initializer expression. If this proposal can offer that, then I’d feel it’s at least be a viable alternative - as of now, it is not one imo. |
While I understand @allenwb 's concern of complexity, I also understand initialization is a very wanted feature, especially for those (like, React users) who do not use deep inheritance at all. Is it possible we can advance this proposal, but keep the door open for future support initialization? I think a controversial but important feature like initialization deserves a separate proposal. |
The door is always open. Also, there is nothing preventing a trasnspiler based support for adding initializers to instance variable definitions including generating the necessary constructor if one is not present. Having the features in those proposal built-in to engines provides a more solid and consistant base for such transpiler-based features then what we have today. |
That's what the TypeScript compiler, and Babel transforms do today. The value in having these features supported natively is that we don't (wouldn't) need those tools anymore. A language proposal which needs tooling support in order to satisfy essential use cases seems inherently flawed. |
@arackaf You always need TS for static type system, and you always need Babel for JSX support.
We should agree JSX/static type system is very very important for React/TS users, which definitely have "essential use cases"! So if JS do not support JSX/static type system, it seems JS will always "inherently flawed"!? On the other side, you ignore the possible complexity of initialization because you may never use deep inheritance and will unlikely meet the edge cases. I don't think it's a fair argument which treat initialization usage as "essential" because of React heavy usage, and on the other side treat confusion risk as "not essential" because React discourage of inheritance. With the same logic, other users who rely on deep inheritance will treat confusion risk as "essential" and initialization convenience as "non-essential". NOTE, I'm not against the idea of initialization. Some teams in my company also use React heavily and myself is a TypeScript lover and I always use initialization AMAP. I just think we don't need to land all features in one time. As my understanding, we still can add initializer in the future. So this should not be the block of this proposal. The main advantage of this proposal IMO is it solves the big issue of |
It seems no one discuss @allenwb example, so I will try: class C {
...
[this.name]() {...}
var v = this.name();
...
} I have to admit, this example shocked me! I always believe I already know 99% of dirty part of JS, but this example just humiliate me. A good excuse is we never write code like that. But I just realize On the other hand, I believe @justsml 's comment "95% of devs using plain initializers" is reasonable. So I doubt if it possible to limit the power of the initializers: Disallow class C {
...
[this.name]() {...}
var v = this.name() // syntax error
var x = 1 // ok
var y = x // ok if we support shortcut for var
var z = f() // syntax error, we can't support shortcut for hidden method
hidden f() { return this.prop }
...
} [update] I just realize that my proposal here is exactly the C# way for field initialization! And in C# way, fields initializer will be executed before constructor (in the order: 1. subclass fields init, 2. superclass fields init, 3. superclass constructor, 4. subclass constructor), this can make @bakkot example valid. What do you think? |
@allenwb One point about initializers which I don't think has been brought up here, is that they greatly help type systems like TypeScript and Flow. They, as of now, seem to have difficulty typing things like class C {
constructor(){
this.x = 0;
}
} requiring you to add boilerplate like this class C {
x: number;
constructor(){
this.x = 0;
}
} while initializers give you the best of all worlds: class C {
x = 0;
} Again, respectfully, I think removing this feature at this point would cause immense harm to the community. |
Can we back up for a minute? If initializers are not essential since you can just use constructor initialization, why isn't private state similarly inessential since you can just use a WeakMap? |
To be honest, I'm fine with current feature set of class. And I even don't need WeakMap because soft private by Symbol is ok for most cases. And TS compile-time only private is also a solution. But the requirements of a real private has a very long history start from ES4 era. It seems it's already be postponed many years and times. Whatever programmers like or not, class field proposal which use controversial |
Yeah I'm fine with backing up and re-trying private state. It's the ditching of widely-used instance field initializers (and requiring yet more changes to decorators) that's disconcerting. |
I don't understand; do you think the negative reaction here will be worse than that of other JavaScript features, such as classes themselves? I don't really see why the community would break (any more than it already is) due to the |
I agree. The JS community is SO unbelievably high-maintenance. The whining and complaining about every little thing is far beyond anything I experienced when I was a C# developer. But yeah, at the end of the day, everyone winds up shutting up, and shipping. I don't think it would be any different with |
The differences I see between the two cases have to do with expressivity and "model rationality". I don’t know if this matches up with any of the motives here quite and it may be inaccurate, but here’s how I would maybe answer that (partly devil’s advocate-y): Private instance state achieved via WeakMap is inexpressive and entails a lot of boilerplate, especially if you want to provide sensible error messages when functions are called with incompatible receivers. Further, relying on WeakMap rather than syntax means that privacy depends on an untampered-with WeakMap intrinsic. Only syntax can get around that. The "public fields" syntax in contrast may increase clarity and readability for many common cases, but it doesn’t increase expressivity or achieve anything currently unaccounted for. It might even be argued that it reduces expressivity, since the shorthand obscures the fact that these new class members amount to a series of to-be-evaluated-later imperative statements. The existing syntax for "to-be-evaluated-later imperative statement list" is "function body", which seems reasonable here and is provided for explicitly by Relatedly, it’s surprising if a feature whose purpose is "just ergonomics" adds complexity to the model. These substantially order-dependent imperative members describing initialization logic are interspersed with declarative members that describe the prototype and constructor, and together they compose a sort of "second constructor" which must be run not before or after the first constructor but rather after the super call or would-be super call of the "first constructor". This seems like a disproportionately complex need for supporting syntax sugar and I think maybe provides a hint that it’s kinda "off". Why are these property assignments different from other initialization logic, and what is the rationale behind their needing to occur prior to other initialization logic? Could most devs using this syntax say "when" those expressions will get evaluated? To disclose: I’m not personally opposed to new syntax for initializing public properties in some form — I’ve had reservations (esp wrt eating up the logical syntactic space for "non-method value property of prototype"), but I’ve come around to it, and I use it myself now. But I still wonder about the things I tried to describe above, and anecdotally have found that many people don’t seem to really "get" what property initializers are doing, and tend to use them in places where prototype methods would be better because it ends up becoming a guessing game. That has made me worry about it a bit again — this confusion was less common before using the new syntax. |
I'm not sure that "they'll get used to it" is a very good approach for JavaScript. A specific example of the worry that @bathos is describing: the currently popular practice of using fields initialized to arrow functions to "fake" methods, resulting in inheritance problems. |
One of the essential features these class instance fields allow is use with decorators. Previously we've used things like
That's one of many things people will need to painfully remove or adjust since, apparently, this model has been too complex all along. |
citation needed. This is not a problem in practice, in my experience. |
There's this. It may not be a problem in the context of React, since you rarely extend a subclass of Component. But I think the fact that these are generally called "arrow function methods" is a bit concerning. It would probably be better to have some kind of language support for efficient method extraction, rather than relying on instance properties that look like methods. |
That's a horribly, horribly contrived article. Have you ever, honestly, in your history of coding in JS, written a unit test verifying that a method exists on And yeah the perf is worse - it's per instance. Which is the whole point. |
I feel like you set me up for that. 😄 Anyway, the point about inheritance is valid. I'm not trying to say that React users are "doing it wrong". I'm trying to say that there might be a better way. |
It's not just React users. See my comment third up from this one. That's a popular idiom in MobX, which certainly is NOT tied to React (though it's popular there, of course). And I believe Glimmer works similarly. Do whatever you want with private, but please do not kill this popular feature! :) |
@arackaf is it incorrect to say that the absence of instance property initializers in this proposal would not preclude them being added by another proposal? Although their absence here feels a bit like a "stance," my impression was that nothing here prevents them. |
@bathos well sure, obviously if this proposal were to kill them off, another proposal could come along and add them back. The point is, there should be a pretty ironclad, outrageously good reason to kill off such a widely used feature this late in the game. |
Whilst the absence of initialisers in this proposal does indeed not ban a future proposal from adding them - it is very clearly part of the intent of this proposal's authors to block such from being added. They consider them to be too complex. (Obviously they're also dropping public class fields as without initialisers there's no point in them) |
Yes, the negative reaction of Same time last year, I already convinced myself we can live with In my experience, when programmers see a new feature, it's common that someone like it, someone dislike it, and someone is uncertain. As a good speaker with a solid comprehension of the whole language, most time I can convince 90% of those who is uncertain or dislike it that the feature is useful and reasonable. For example, class. I know a lot of js programmers think class is unwanted. But you can talk to them to see how they really write code. Suppose, they told you they prefer OLOO instead of class. Then you could just ask them if you always write Note, I'm not here to prove OLOO is bad, just use it as a example. I believe the guys invent and advocate OLOO has their good reasons, but most person never really think deeply of all such so called patterns/good practices, they just learned it from some place and swallowed it. So you can just show them the other side, other choices which they never really considered before. But I found Even I finally convinced them this is what the best we can get (before I see this proposal), and your dislike is just aesthetic prejudice which make no sense in real coding, they still feel unhappy, and some think they will never adopt it. (they think symbol-based solution or TypeScript compile-time private is sufficient.) So I realized what's the different of Because, it's just a personal taste. It's nothing wrong that one is just unlike So I failed on
It will break, the reason I have mentioned before -- some will never adopt it. It's ok that someone keep using OLOO. The key point of class is, if you accept class-style, you now have standard, syntax/semantic consistent way to do it.
Note, as a heavy TypeScript user, I understand TS will keep support their privates for a long time, it's important for TS community. But that doesn't mean keep using two is a good idea in long-term. You of coz can use both so-called design-time private and js native runtime-private. But to be honest, the difference of two is too small and not very important in daily programming. So it will be a meaningless burden for team and most js programmers to think about which one (or both) is right for each fields/methods of each class. Finally we have to move to JS native private. To be clear, the masses of js privates is not the fault of Thank you for listening. |
On the contrary, this is really BAD. It's only ok if just React users use such pattern, because in the context of React, you have other practices and conventions which avoid the dangerous part of it. When it spread to other place, you will lose the context, lose the protect, and finally it will bite you or others somewhere. This is why we call such thing is anti-pattern.
Again, it's no way to kill a feature or pattern when you can always use it via Babel/TypeScript. We just don't convinced public field is a must have in native JS. More about public field. Note, in OO theory, public field is a questionable feature. Languages support them just because it seems very strange/inconsistent to disallow public field when you have all public methods, private fields and private methods. But the linters / compilers / your reviewers may complain about public fields if you really use it. They will tell you you'd better use public accessors wrap private field. On the other hand, In JavaScript world, we never have native private, so even you use accessors, you still need a "public" property to store the value. This is why we are rarely use accessors pattern like other languages. This is another reason why I think Babel/TS experiences are not sufficient enough to prove the usefulness of pubic field, because without real private support, you have no other choice. |
Even in the context of React, the only place where you'd use a public field is when you have something that needs to be accessible by React itself (e.g. |
GitHub comments declaring class instance fields "bad" notwithstanding, they are widely used in the JS community, even outside of React. MobX uses them heavily, as does Glimmer. I would encourage any would-be thought leader to actually look at how this idiom is used in practice before declaring it "bad" on the basis of abstract OO theory. |
off-topic followup question to @arackafcan you link me to those comments, off-thread if possible? I've only stated that *arrow functions* in class instance fields are "bad", but I think the fields themselves are awesome and hugely useful. |
I am not up to speed with the state of front-end web development frameworks in 2018, so forgive me if this comes off as ignorant, but MobX's repo is tagged with "react", says React and itself are a "powerful combination", and
Sorry, I have never even heard of Glimmer. Having never used this, which should be considered the norm, it is not apparent how initializers are essential. I would appreciate seeing more use cases rather than hearing a lot of people use it. This largely sounds like a "nice to have" if the only major use is not writing 2-3 lines. |
npm lists no dependencies of MobX; I'm not sure where you're getting that. https://www.npmjs.com/package/mobx Glimmer is the new state manager for Ember. This is also used in TypeScript and Flow. Familiarizing yourself with the state of front end web development will help you see where and how this feature is used. |
Misreading the OK, but if this is deemed essential it should not be hard to provide a strong example broader than this context as well. |
It's not abstract OO theory. The programmers of other languages keep vigilance against public field for good, both theoretical and practical reasons. Read:
You could find more if you want. Of coz, some problems (eg. binary compatibility of public field and property) do not exist in JavaScript. But we have another trouble problem. It's not real field like Java/C#, but a instance property. It may be not important to you, or all React users. But you should not assume no one use inheritance.
Widely usage does not necessarily mean it's the best option. As I explained several times, we use public field in TS/Babel because we do not have other choice. But if hidden var landed, accessor pattern will be really available to JavaScript programmers, and we can see what happen. For example I expect Can we focus on initializer issue in the thread? I hope someone will inspect the possibility of my proposal in previous comment: #26 (comment) |
One of the very valuable aspects of the current class fields proposal is that it makes it very ergonomic and easy to avoid having to write a constructor at all.
Being forced to write the constructor (the current state of things) allows the common footguns of forgetting to call
super
, of forgetting to call it with the proper arguments, etc - the default constructor's behavior ofconstructor(...args) { super(...args); }
, in practice, is not intuitive enough for most people to properly replicate when they have to add a constructor (for the purpose of adding instance fields).The text was updated successfully, but these errors were encountered: