proposal-class-public-fields icon indicating copy to clipboard operation
proposal-class-public-fields copied to clipboard

Configurable vs nonconfigurable properties

Open littledan opened this issue 9 years ago • 30 comments

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

  • Nonconfigurability gives us "more guarantees" about the instances that are created (modulo how much flexibility the rest of ECMAScript gives you).
  • Deleting properties creates more hidden classes, so it can be slow (just like creating new properties later); making it an error makes it easier to stay within the efficient code mode.

Con

  • This is somewhat of a divergence from other class-related syntactic constructions, which all define configurable properties.
  • It's not clear (to me) how an optimizing JS implementation could take advantage of nonconfigurability for optimizations, given all the other things going on. Hidden class transitions from deleting a property on an instance are a well-established technique.

Thoughts? @erights @domenic @dherman @allenwb @zenparsing

littledan avatar Apr 12 '16 06:04 littledan

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

littledan avatar May 05 '16 21:05 littledan

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.

erights avatar May 05 '16 22:05 erights

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.

domenic avatar May 05 '16 22:05 domenic

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

erights avatar May 05 '16 22:05 erights

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.

domenic avatar May 05 '16 22:05 domenic

Sure. This is a valid question.

erights avatar May 05 '16 22:05 erights

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.

zenparsing avatar May 05 '16 22:05 zenparsing

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.

erights avatar May 05 '16 23:05 erights

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?

zenparsing avatar May 05 '16 23:05 zenparsing

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

erights avatar May 06 '16 02:05 erights

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

  • configurable should stay true here
  • the implementation of decorators is wrong
  • aurelia framework has bad luck and needs to find another way

thoughts?

doktordirk avatar Aug 18 '16 09:08 doktordirk

:+1:

VMBindraban avatar Aug 19 '16 08:08 VMBindraban

@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 avatar Aug 22 '16 14:08 littledan

@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 avatar Aug 22 '16 14:08 doktordirk

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

littledan avatar Aug 22 '16 18:08 littledan

TypeScript uses [[Set]]/assignment:

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

jeffmo avatar Aug 22 '16 18:08 jeffmo

What's the verdict on this?

RWOverdijk avatar May 28 '17 11:05 RWOverdijk

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

littledan avatar May 29 '17 10:05 littledan

Configurable. It follows from orthogonality.

On May 28, 2017 4:39 AM, "Wesley Overdijk" [email protected] 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 .

erights avatar May 29 '17 16:05 erights

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

michaelficarra avatar May 31 '17 15:05 michaelficarra

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.
  2. 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
  3. 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.
  4. 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.
  5. 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.

allenwb avatar May 31 '17 17:05 allenwb

@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 avatar May 31 '17 17:05 erights

@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

allenwb avatar May 31 '17 22:05 allenwb

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
    }
}

Jamesernator avatar Jun 22 '17 23:06 Jamesernator

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:

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

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

littledan avatar Jun 23 '17 10:06 littledan

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.

erights avatar Jun 23 '17 14:06 erights

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

zenparsing avatar Jun 23 '17 14:06 zenparsing

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 expect that the direction is not towards Orthogonal Classes, although it still might be.
  • If we expect the direction not to be towards orthogonality, then I think that declared public properties should default to non-configurable.
  • If they do default to configurable, but not part of any orthogonal growth direction, then I don't see their point. I'd prefer not adding them to the language if they're not adding any value.

erights avatar Jun 23 '17 15:06 erights

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.

ljharb avatar Jun 23 '17 16:06 ljharb

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

littledan avatar Jun 26 '17 14:06 littledan