Open littledan opened 8 years ago
@erights I'd be interested to hear your thoughts on this.
This syntax is declarative, so it should result in something with declarative semantics, in the sense of obeying an eternal invariant rather than a momentary invariant. If the properties are non-configurable, then they will continue to exist with the same meaning for the lifetime of that object. If they are configurable, then their continued existence is an imperative matter, not a declarative one. We already have a perfectly fine syntax for imperatively creating properties that may disappear.
This all reminds me of a similar argument we had a long time ago: Do we need both let
and const
? From a performance perspective the answer is clearly no. If a let
declared variable is not elsewhere assigned, then all the same optimizations easily apply. From a careful programmer reading of code, the answer is clearly again no, since the programmer can also search for all in-scope occurrences and see the absence of assignment. However, every line of code we read deeply is in the context of lots of relevant code that we read less carefully. The const
is a quick syntactic promise of stability -- it gives the causal reader a guarantee about what changes they need not worry further about.
Now that we've lived with const
and let
for a while, I hope this advantage is now clear. Stability of property existence is just as important. Unlike let
vs const
, in the absence of an enforcing construct, the reader cannot discern the same guarantee only by a moderately deeper syntactic examination.
Doesn't your argument also apply to the const o = { foo: bar, baz: qux }
syntax as well? That's some pretty declarative declaration of the foo
and baz
properties.
Sure. If we had a choice about what this means I would take on that argument. We don't.
Which raises the question of whether the principle you give in that post is more important than consistency with the rest of the language's declarative property/method syntax.
Sure. This is a valid question.
After having lived with let and const for a while now, I'm convinced that the only practical result is a burden of useless choice.
The other consistency we need to pay attention to is between declarative class properties and declarative private fields. Obviously there are some necessary differences between them. But we should avoid gratuitous differences when we can. Private fields exist stably during the lifetime of the object.
Apologies, the previous comment is nether here nor there with regard to public class fields. I've been doing quite a lot of programming lately where the let vs. const thing has been a factor.
Are static public fields supposed to be non-configurable as well?
Although I can see a few reasons why the case for non-configurable static properties is weaker, I still think that they should be non-configurable.
Btw, on terminology, I try to be careful to say "public property" and "private field". To avoid confusion, I think we should avoid terms like "public field".
while i hardly get what this is about, it is clearly still the root of my problem.
with reference to this proposal https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy/pull/44 @loganfsmyth argued that a decorator should set configurable to false. this causes problems in the aurelia framework since the properties cannot be observed anymore by adding getters/setter, but must fall back to dirty checking. so, either
thoughts?
:+1:
@doktordirk I don't think configurability affects Aurelia's use case here. Presumably, the Aurelia decorator would transform the field declaration into a getter/setter pair before it's actually defined on an instance, so there would be no Set of a nonconfigurable property.
@littledan aurelia isn't (by default) using decorators for this, but only when bootstrapping the view-model's view. one can add an observable decorator though. That some nuisance but does fix the problem. it still highlights though that using configurable:false has side effects.
ps: i might mention that this (currently?) is not an issue with the typescript implementation of decorators.
@doktordirk Is that because TypeScript defines configurable properties (with Set)?
TypeScript uses [[Set]]
/assignment:
http://www.typescriptlang.org/play/index.html#src=class%20C%20%7B%0A%09f%20%3D%2042%3B%0A%7D
What's the verdict on this?
At the recent TC39 meeting, we didn't revisit enumerability; the current proposal still uses enumerable/configurable: true.
Configurable. It follows from orthogonality.
On May 28, 2017 4:39 AM, "Wesley Overdijk" notifications@github.com wrote:
What's the verdict on this?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-class-public-fields/issues/36#issuecomment-304508459, or mute the thread https://github.com/notifications/unsubscribe-auth/AAQtzFuFGsKw2Rkq8wcb0cYS9s4PXLC7ks5r-VzcgaJpZM4IFFFN .
@littledan No, the current spec text uses configurable: false.
@erights The orthogonal classes proposal is still (unless something has changed in the last meeting that I missed) very controversial, and I don't think it should be used as justification for a change in this proposal. I personally do not agree that each aspect is orthogonal. I believe the most common case should be the one with the simplest syntax, and the number of truly nonsensical combinations undermines the basic orthogonality principle.
I suspect that @erights wasn't referring to the Orthogonal Class proposal proposal, but rather to the overall orthogonality of the ES specification.
Issues of property configurability defaults (for classes and elsewhere) were deeply debated during the development of ES6. We reach certain conclusions based upon analyzing the existing precedents of the language and who to most consistently extend those precedents to encompass new features.
Roughly here is what we concluded:
prototype
property of class constructors is configurable: false, writable: false because that was the precedent established by the built-ins.Object.defineProperty
or Object.seal
within their code to over-write the defaults.These rules should be followed for new class features. Class fields should default to configurable: true, writable: true. Not doing so will just create consistency WTFs that make the language harder to learn and use.
It's fine to in the future to consider adding new kinds of class declarations (via new keywords, decorators, etc.) that facilitate define classes with deferent defaults.
@allenwb wrote:
The prototype property of class constructors is configurable: false, writable: false because that was the precedent established by the built-ins.
You meant configurable: false, writable: true, right?
@erights Nope, for class constructors it's configurable: false, writable: false.
See Step 16 of https://tc39.github.io/ecma262/#sec-runtime-semantics-classdefinitionevaluation and Step 5a of https://tc39.github.io/ecma262/#sec-makeconstructor
I'll be honest I don't really see the point of public fields if it just turns this:
class Counter {
constructor() {
this.x = 0
}
increment() {
this.x += 1
}
}
Into this:
class Counter {
x = 0;
increment() {
this.x += 1
}
}
Having two different syntaxes for pretty much the same thing when half the time you'll be adding additional properties in the constructor anyway like this:
class Task {
ready = false;
constructor(initializer) {
this.initializer = initializer
}
...
}
seems a bit pointless, but if the fields are non-configurable then at least you can guarantee that there's actually a semantic difference between these three definitions, which personally I'd expect given that public fields feel like they should be a guarantee of some field:
class Task {
ready = false; // Guaranteed to exist
initializer; // Guaranteed to exist as well
constructor(initializer) {
this.initializer = initializer
}
}
class Task {
ready = false; // Guaranteed to exist
constructor(initializer) {
this.initializer = initializer // But without the field declaration this can't be certain to exist
// so 'initializer' in someTask can't be relied upon to detect capabilities
}
}
class Task {
constructor(initializer) {
this.ready = false
this.initializer = initializer
}
}
A nice bonus with non-configurable fields is that if there's ever a proposal to add some form of typing then there won't be any issue with class fields having non-existent properties. e.g.
class Point {
// Syntax similar to TypeScript just for sake of exposition
x: Number = 0; // Guaranteed to both exist and be a number
y: Number = 0;
constructor(x, y) {
this.x = x
this.y = y
}
}
A nice bonus with non-configurable fields is that if there's ever a proposal to add some form of typing then there won't be any issue with class fields having non-existent properties. e.g.
There are so many things that can go wrong here and expose fields not existing, or being in different states, besides deleting them; I don't have much confidence that if we tweaked one thing we'd be able to have guaranteed stable object shapes. This includes:
in
on this. You can also throw an exception containing this
, leaking the unfinished instancesuper()
points to. And further, superclass constructors may return whatever they want from super()
, e.g., as is taken advantage of in custom element upgradeundefined
, which could fail a bit faster). So the field will be initialized as undefined, and then rewritten to the appropriate Number. This eliminates any sort of type stability.Anyway, I think decorators on fields will be a good way to expose nonconfigurability, nonenumerability, etc.
To me, the issue hinges on orthogonality. In the Orthogonal Classes framework, placement (class, prototype, instance) is orthogonal from visibility and type. If we wish to preserve this as a framework for growth, then it implies configurability. Non-configurability would be a non-orthogonal surprise.
The compromise that we arrived at in the state of this CL, aside from configurability, can still be rationalized as being within the Orthogonal Classes framework. However, frankly, it also makes further steps towards Orthogonal Classes less likely because there's still no keyword for overriding default placement in order to place something on an instance (e.g., "own") or a prototype (e.g., "shared"). The current CL, again aside from configurability, can also be rationalized as being simply a starting point for going forward without any Orthogonal Classes framework and accomplishing these overrides with annotations.
If we now expect the second is more likely, then I agree with @Jamesernator . He exactly states my position prior to Orthogonal Classes:
Having two different syntaxes for pretty much the same thing ... seems a bit pointless, but if the fields are non-configurable then at least you can guarantee that there's actually a semantic difference between these three definitions, which personally I'd expect given that public fields feel like they should be a guarantee of some field
As for @littledan 's points:
- As you're evaluating the initializer, fields are added one-by-one. You can see this using
in
onthis
. You can also throw an exception containingthis
, leaking the unfinished instance
Yes, this breaks a hard guarantee. But it is mostly-statically apparent in the code of the constructor whether this
might leak. For constructors that don't clearly look like they're leaking a partially constructed this
few do. Programmers already depend on the absence of this leakage in other ways: Invariant maintenance is the essence of good oo style. A partially constructed instance does not yet establish these invariants.
- To allow engines to optimize the property access, you'd need a stable offset in the object. However, the superclass constructor may not create all of its own properties through fields.
Although efficiency of the implementation might be a benefit, the main benefit is software engineering -- making the program's behavior more predicable, and more likely to stay within the assumptions the programmer made when they wrote this code.
Also, JS classes let you dynamically mutate the parent class, which changes what
super()
points to. And further, superclass constructors may return whatever they want fromsuper()
, e.g., as is taken advantage of in custom element upgrade
These unfortunate edge cases do further weaken the reliability of the programmer's reasoning. But by itself does not justify taking further unnecessary steps to make that reasoning even less reliable.
- For typing, you often don't know a reasonable initial value of the field until you get to the constructor, unless you use a dummy value as in your example above (a dummy value might not be the best idea, as it may accidentally leak into program logic and be harder to detect than undefined, which could fail a bit faster). So the field will be initialized as undefined, and then rewritten to the appropriate Number. This eliminates any sort of type stability.
Yes, this is unfortunate. This is why, for a long time, I advocated that the initialization should be written in the body of the constructor, so that constructor arguments would be visible from the initialization expression. Alas, I think I made a mistake conceding on this point. But I think this mistake is unrecoverable. Having conceded, I am sure we could never get consensus for moving these declarations back into the constructor.
So, again, we lack a hard guarantee. But partially initialized instances that escape are monsters anyway. If the programmer can get reliable behaviors from fully initialized instances under stable prototype chains, a program's behaviors are less likely to violate its author's expectations.
Anyway, I think decorators on fields will be a good way to expose nonconfigurability, nonenumerability, etc.
I agree it is an adequate way, and therefore either decision here is acceptable. With decorators, we can survive getting the defaults wrong. But we should still try to design good defaults.
@erights Are you saying that given the current direction of class syntax, you are not in favor of "public" field declarations?
Having recently converted some TypeScript code to JS (with public fields), I agree with @Jamesernator
Having two different syntaxes for pretty much the same thing when half the time you'll be adding additional properties ... seems a bit pointless
Without a type system to worry about, it doesn't carry the weight.
Are you saying that given the current direction of class syntax, you are not in favor of "public" field declarations?
No, I am saying:
I think that presumes that even if they are nonconfigurable and not heading towards orthogonality, that they lack value - a statement I disagree with. While I like the philosophy of orthogonality, I prefer configurability in any scenario, and I also think fields (private and public) are hugely important as-is on their own merits.
@erights I don't think orthogonality is all-or-nothing. My hope is that soon, we will have public and private instance and static fields, and public and private shared methods, static methods, and shared/static accessors. I've written a draft specification of this idea, though I still have to break it out into a proposal, etc, which I hope to do over the next week or two.
The current document defines properties that are nonconfigurable, based on a change by @michaelficarra . I wanted to open this bug to discuss whether they should be configurable or nonconfigurable, and lay out the points in both directions.
Pro
Con
Thoughts? @erights @domenic @dherman @allenwb @zenparsing