tc39 / proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
https://arai-a.github.io/ecma262-compare/?pr=1668
1.72k stars 113 forks source link

Inherited Static properties #43

Closed jridgewell closed 6 years ago

jridgewell commented 7 years ago

What is the output of:

class Base {
  static #field = 'hello';

  static get() {
    return this.#field;
  }
}

class Sub extends Base {}

// This one isn't controversial
Base.get() // => 'hello'

// But what does this do?
Sub.get()

I want it to equal 'hello', but I can't find where the behavior is defined. It's likely just because I'm not familiar enough with the spec text.

Maybe it's an error, though? Since I don't think Sub has #field in its [[PrivateFieldValues]] list, unless there's some super class iteration I'm not seeing.

rbuckton commented 7 years ago

@ljharb my comment doesn't leverage the prototype chain, it merely accesses the [[PrivateFieldValues]] of superclass (which is the result of evaluating ClassHeritage) during ClassDefinitionEvaluation. This only happens once when we are initially handling the class declaration, not during each access. The subclass cannot directly observe changes to private values on the superclass, only indirectly through public methods or accessors defined on the superclass.

ljharb commented 7 years ago

@rbuckton can the subclass observe the existence of private fields on the superclass, either by intentionally or accidentally creating a collision or the absence of one?

bakkot commented 7 years ago

@rbuckton your Option B is pretty much what I've proposed above, I think.

@ljharb I don't think so, no. There's no risk of collisions, since the names themselves are unique (think Symbol), and the subclass cannot refer to the field in any way.

rbuckton commented 7 years ago

@bakkot the problem with Option B in both the private and public static case, is this:

class Base {
  static sharedObject = new PotentiallyExpensiveConstructor();
}

class Sub extends Base {} // new allocation of `PotentiallyExpensiveConstructor`

In a language like C#, the field only exists on Base and setting it on Sub actually sets the value on Base. Of course, C# doesn't allow this in static members, so it doesn't have the issue ES would have with receivers and privates. While I don't have a good way to emulate that for public static fields, the best way to emulate that for private static fields is an Option C where we just copy over the private field records from Base to Sub. As such, calling Sub.incr() would increment Base.#x, e.g.:

class Base {
  static #x = 0;
  static incr() { return ++this.#x; }
}
class Sub extends Base {}
Base.incr(); // 1
Sub.incr(); // 2
Sub.incr(); // 3
Base.incr(); // 4

This would be the same behavior if you instead wrote this example as:

class Base {
  static #x = 0;
  static incr() { return ++Base.#x; }
}
...
bakkot commented 7 years ago

@rbuckton at the meeting @erights proposed that static pubic fields could desugar to an accessor pair which read from and updated a synthetic closed-over variable, which would correspond to your option C here (but for public rather than private fields). However, he was then convinced that this was a bad idea on the basis that such "fields" would not be frozen by Object.freeze, despite syntax implying they would.

While that constraint doesn't apply to private fields, personally I would be kind of opposed to private fields having this behavior while public fields did not.

zenparsing commented 7 years ago

I have long held the opinion that all public fields should desugar to getter/setter pairs over private fields. It's the cleanest solution.

erights commented 7 years ago

Just for the record:

Was there an issue thread where this aspect of public instance properties was debated?

ljharb commented 7 years ago

despite syntax implying they would.

However, that desugaring would both have user confusion issues, and also not be what people have been using for years without complaint via babel (and typescript, maybe?).

littledan commented 6 years ago

To resolve some of the issues here, I'm wondering what you'd all think of just leaving out static private long-term. The rationale would be:

I'm leaning towards sticking to leaving public static fields as is currently specified, because:

@jridgewell You mentioned at the meeting that you had some strong evidence that we need to make public static fields be re-initialized on subclasses. Could you share the relevant links?

@zenparsing I'm a little skeptical about desugaring to getter/setter pairs over private fields because:

I'm not sure what to do about the Object.freeze mismatch. This proposal has long had the property that public fields could be frozen and private fields cannot be. The difference here corresponds to ordinary own properties on instances, and internal slots of instances. Private fields are designed to be largely analogous to internal slots, and public fields simply are data properties, which explains the difference.

I thought we discussed Object.freeze not affecting private fields in TC39 and being OK with that because of the WeakMap analogy, but I don't remember discussing the alternative recently that public fields would also not be freeze-able and be based on accessors.

I can understand why you'd want freeze to affect private fields, but it's harder for me to understand why you'd want freeze to not affect public fields--is the idea that programmers could get tempted to implement immutable data structures using public fields and frozen objects, but then shoot themselves in the foot when they switch to private fields and then hit some rare codepath which modifies the field values? One reason why it may not be as important for private fields to be frozen is that all the code manipulating them would be in the class body, not from a random library user, so it's easier to audit and ensure that your library is not mutating the field values.

If a developer wants to define an "unfreezable" public field, it's easy to do with the follow-on decorators proposal to expand into the same public getter/setter and private underlying storage. We in TC39 could take our task for now as defining some sensible defaults, and then leave lots of other possibilities to decorators (circling around and adding a set of useful decorators to the standard library).

ljharb commented 6 years ago

Offline in the recent meeting, the possibility that, with accessors, Object.freeze would not cause public fields to be frozen, convinced @erights that that direction was a non-starter.

I think it would be completely fine to leave out static private.

littledan commented 6 years ago

@ljharb It seemed like @erights was OK with it in this more recent thread, though. Mark, is the issue here about being consistent between static and instance fields with respect to Object.freeze semantics, or were you just not thinking about the freeze constraint when you wrote https://github.com/tc39/proposal-class-fields/issues/43#issuecomment-348646013 ?

@bakkot wrote,

While that constraint doesn't apply to private fields, personally I would be kind of opposed to private fields having this behavior while public fields did not.

Which behavior are you talking about there--the copy-to-subclass/accessor-like behavior, or something about object freezing?

bakkot commented 6 years ago

@littledan The accessor-like behavior.

erights commented 6 years ago

The issue is consistency. If we're ok with public instance properties turning into inherited accessors over encapsulated per-instance state as @zenparsing suggests, then yes, I'd be happy for public static properties to be class accessors over some kind of encapsulated static state (see below). What I care about most regarding this question is that they both make the same choice between data and accessor.

What I just noticed in writing this reply is how consistency itself leaves us two options for "some kind of encapsulated static state":

I still prefer the first. I mention the second because I realize that the consistency argument does not argue against it.

erights commented 6 years ago

Regarding the "freeze" behavior issue, if we go for "declared public fields are accessors, whether instance or static", then public fields remain mutable under freeze in ways that are consistent with both:

Defensive classes could then freeze classes, prototypes, and instances without changing the meaning of their declared properties. A client of an instance could no longer change its behavior by adversarially freezing it, since it would already be frozen.

Note that I changed the phrasing from @zenparsing 's to say "encapsulated per-instance state" rather than private field. They are observationally equivalent, so the terminology should not suggest more specificity than needed. It is this rephrasing that enables us to see better how both public static fields choices are compatible with this way of doing public instance fields.

littledan commented 6 years ago

@erights Thanks for this thoughtful explanation of alternatives and their implications. I take it that, in addition to these possibilities, you are also OK with the current proposal's treatment of public fields; is that right?

erights commented 6 years ago

Reluctantly ok, yes. My first choice is still to simply omit them. Always err towards the side of the smaller language.

zenparsing commented 6 years ago

@littledan

(Regarding public-fields-as-accessors)

This differs significantly from current user practice, so it should increase transition costs

I don't find the transition cost argument particularly convincing when talking about language features under development. If we are in a position where transpilers and closely-related (but far less important) languages are significantly impinging upon feature development, then the process is broken.

such pervasive use of accessors could be slower on startup

My hope has always been that the implementation of private state/internal slots/etc. would encourage simpler fixed-shape object implementations in the long run.

it's another point of divergence with the semantics users currently have for public fields, which may be working OK as is for programmers

As with any cooperative and long-lasting endeavor we need to carefully balance path-dependence with our desired long-term future state.

@erights

My first choice is still to simply omit them

That was my first choice as well; public fields are pretty hard to justify over just putting the assignments directly into the constructor. By using syntactic field definitions, the programmer is trying to express a notion about the fixed shape of instances, and for classes, fixed shape should (IMO) be represented by one feature: private state.

An implementation of public fields as private state with accessors also leaves open the possibility of expressing, quite easily, something that we've talked about several times in the past: const classes. The idea of a const class is that it has a tamper-proof API.

The primary downside of using the accessor approach (as far as I can tell) is that it might be confusing for programmers that expect public fields to be enumerable own properties. If a programmer wants to use class syntax as a factory for creating object-as-dictionary style objects, then they would still need to use assignments within the constructor.

On the other hand, I've always been quite happy imagining a future where classes are primarily const, and class syntax is primarily used to express fixed-shape objects. Implementing a field syntax with own enumerable data properties has never really fit in with that vision.

Cheers!

littledan commented 6 years ago

My hope has always been that the implementation of private state/internal slots/etc. would encourage simpler fixed-shape object implementations in the long run.

I don't actually see how these differ in how fixed the "shape" is. For private fields, committee members expected private fields to throw exceptions when they're not yet written to, and then be readable from later initializers. Somehow, we need to record that state transition, either in the hidden class of the object or in some other place off to the side. Either way, there are observable state transitions during initialization, so implementation-wise you don't get the biggest benefit of a static shape. Instead, the feature encourages fixed shape by making it so that, when things go well, you always get all the fields by the time the constructor returns. I don't really see how we can do any better, or how accessors would change anything.

ljharb commented 6 years ago

By using syntactic field definitions, the programmer is trying to express a notion about the fixed shape of instances

This is decidedly not what I am ever trying to express with public fields, fwiw. I don't care about the fixed shape of instances as a developer; that's something that implementors care about. If I cared, I'd be using a type system.

littledan commented 6 years ago

We've been continuing the discussion in https://github.com/tc39/proposal-static-class-features/ , while this proposal omits static fields.