tc39 / proposal-class-public-fields

Stage 2 proposal for public class fields in ECMAScript
https://tc39.github.io/proposal-class-public-fields/
487 stars 25 forks source link

Configurable vs nonconfigurable properties #36

Open littledan opened 8 years ago

littledan commented 8 years ago

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

littledan commented 8 years ago

@erights I'd be interested to hear your thoughts on this.

erights commented 8 years ago

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.

domenic commented 8 years ago

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.

erights commented 8 years ago

Sure. If we had a choice about what this means I would take on that argument. We don't.

domenic commented 8 years ago

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.

erights commented 8 years ago

Sure. This is a valid question.

zenparsing commented 8 years ago

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.

erights commented 8 years ago

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.

zenparsing commented 8 years ago

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?

erights commented 8 years ago

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".

doktordirk commented 8 years ago

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?

VMBindraban commented 8 years ago

:+1:

littledan commented 8 years ago

@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.

doktordirk commented 8 years ago

@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.

littledan commented 8 years ago

@doktordirk Is that because TypeScript defines configurable properties (with Set)?

jeffmo commented 8 years ago

TypeScript uses [[Set]]/assignment:

http://www.typescriptlang.org/play/index.html#src=class%20C%20%7B%0A%09f%20%3D%2042%3B%0A%7D

RWOverdijk commented 7 years ago

What's the verdict on this?

littledan commented 7 years ago

At the recent TC39 meeting, we didn't revisit enumerability; the current proposal still uses enumerable/configurable: true.

erights commented 7 years ago

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 .

michaelficarra commented 7 years ago

@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.

allenwb commented 7 years ago

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:

  1. By default, JavaScript has traditionally allowed for extensive dynamic manipulation of runtime program structures. New features should respect that tradition and permit such manipulation unless there is an strong over-riding reason to do otherwise.
    1. The built-in constructors of ES1-5 are essentially class definitions and prior to ES6 offered the best model for the details of the "native" ES class model. Configurability (and other characteristics such as methods are not constructors) of ES6 class members follow the conventions established by the legacy built-ins
    2. Hence the normal default configurability of class members is configurable: true, writable: true. The prototype property of class constructors is configurable: false, writable: false because that was the precedent established by the built-ins.
    3. In a few places writable properties are a user hazard because a naive assignment could violate some important structural invariant. In those cases, we use the attribute combination writable: false, configurable: true. This still allows intentional modification but requires the user to better understand what they are doing.
    4. Programmer can explicitly use 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.

erights commented 7 years ago

@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?

allenwb commented 7 years ago

@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

Jamesernator commented 7 years ago

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
    }
}
littledan commented 7 years ago

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:

Anyway, I think decorators on fields will be a good way to expose nonconfigurability, nonenumerability, etc.

erights commented 7 years ago

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 on this. You can also throw an exception containing this, 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 from super(), 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.

zenparsing commented 7 years ago

@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.

erights commented 7 years ago

Are you saying that given the current direction of class syntax, you are not in favor of "public" field declarations?

No, I am saying:

ljharb commented 7 years ago

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.

littledan commented 7 years ago

@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.