Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

Configurable vs nonconfigurable properties #36

Open
littledan opened this issue Apr 12, 2016 · 30 comments
Open

Configurable vs nonconfigurable properties #36

littledan opened this issue Apr 12, 2016 · 30 comments

Comments

@littledan
Copy link
Member

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
Copy link
Member Author

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

@erights
Copy link

erights commented May 5, 2016

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
Copy link
Member

domenic commented May 5, 2016

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
Copy link

erights commented May 5, 2016

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

@domenic
Copy link
Member

domenic commented May 5, 2016

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
Copy link

erights commented May 5, 2016

Sure. This is a valid question.

@zenparsing
Copy link
Member

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
Copy link

erights commented May 5, 2016

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
Copy link
Member

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
Copy link

erights commented May 6, 2016

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
Copy link

while i hardly get what this is about, it is clearly still the root of my problem.

with reference to this proposal loganfsmyth/babel-plugin-transform-decorators-legacy#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?

@VMBindraban
Copy link

👍

@littledan
Copy link
Member Author

@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
Copy link

@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
Copy link
Member Author

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

@jeffmo
Copy link
Member

jeffmo commented Aug 22, 2016

@RWOverdijk
Copy link

What's the verdict on this?

@littledan
Copy link
Member Author

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

@erights
Copy link

erights commented May 29, 2017 via email

@michaelficarra
Copy link
Member

@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
Copy link
Member

allenwb commented May 31, 2017

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.

@erights
Copy link

erights commented May 31, 2017

@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
Copy link
Member

allenwb commented May 31, 2017

@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
Copy link

Jamesernator commented Jun 22, 2017

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
Copy link
Member Author

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.

@erights
Copy link

erights commented Jun 23, 2017

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
Copy link
Member

@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
Copy link

erights commented Jun 23, 2017

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.

@ljharb
Copy link
Member

ljharb commented Jun 23, 2017

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
Copy link
Member Author

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests