tc39 / proposal-static-class-features

The static parts of new class features, in a separate proposal
https://arai-a.github.io/ecma262-compare/?pr=1668
128 stars 27 forks source link

Normative: Mimic a prototype chain for static private fields #7

Closed littledan closed 6 years ago

littledan commented 6 years ago

With this patch, reads and writes to a static private field which is inherited from a superclass will behave similarly to reads and writes to public properties: Reads will forward to the superclass constructor, and writes will create a new value which is not forwarded. Thanks to @rbuckton for the idea; this patch is an iteration of spec drafts by him.

Note that static private fields here are still private, not protected or public. This patch only affects semantics when their use within a superclass occurs with a subclass receiver, for example an access to a private static field from a public static method, which is then called from a subclass.

wycats commented 6 years ago

@jridgewell I'm not sure if I'm the most unbiased source for class_attribute, since I co-authored it in Rails. That said, I believe that at least for Ruby, class_attribute semantics are the best general-purpose inheritance behavior for what JS calls "static fields".

For what it's worth, I also believe that these semantics are better than Ruby's built-in class variables, in which writes on a subclass mutate a shared field with the superclass.

jridgewell commented 6 years ago

in which writes on a subclass mutate a shared field with the superclass

Isn't that because declaration in a subclass is actually a superclass assignment?

class Base
  @@test = "Base"
end

class Sub < Base
  @@test = "Sub"
end

Base.class_variable_get("@@test") # => "Sub"

Other than that surprising behavior, I'm not certain which (between this PR's semantics or just using write on superclass) is the more expected. Definitely with the current static inheritance precedence, the first, but other languages (Ruby -- with caveats --, Java, C#) seem to have decided on the second.

littledan commented 6 years ago

Note that a huge disadvantage of these semantics is that it creates a parallel prototype chain for private fields. Inherited private static fields would always point to the original prototype, and not change due to setPrototypeOf. This is to avoid Proxy observability issues. (In the specification, rather than being a prototype, it's functions that close over a property descriptor, but this amounts to the same thing.) In a real implementation, somehow this second prototype will have to be stored and used when appropriate. I'm worried about the mental model overhead here.

littledan commented 6 years ago

@jridgewell @wycats Interesting parallel! Is there any more documentation somewhere about why these semantics are preferred?

EDIT: From the documentation, it looks like those fields are public. So, it seems like these semantics are more the parallel with static public fields. It'd be helpful to understand why this is useful for private fields as well.

gibson042 commented 6 years ago

If I'm reading this correctly, it specifies copy-on-write behavior similar to the existing prototype model. But unfortunately, that seems far more useful for instances than for classes. If RegExp.$n properties (the closest existing analog AFAICT) were specified like this, then behavior would be even weirder than it already is:

class IrregExp extends RegExp { … }

// Seems harmless…
(new RegExp("(.)(.*)")).exec("foo");
assert(`${RegExp.$1}-${RegExp.$2}` === "f-oo");
assert(`${IrregExp.$1}-${IrregExp.$2}` === "f-oo", "initial child read-through");

// …until you dig deeper.
(new IrregExp("(.*)")).exec("bar");
assert(`${RegExp.$1}-${RegExp.$2}` === "f-oo", "parent ignores child update");
assert(`${IrregExp.$1}-${IrregExp.$2}` === "bar-oo", "child update adds sparse bindings");

I'm not even sure that's better than the Ruby class variable semantics (a single binding for the entire class hierarchy).

I also wonder how bad allowing a prototype walk would actually be, because the mere invocation of [[GetPrototypeOf]] can't even confirm that what's being sought is a private property, let alone its name.

wycats commented 6 years ago

The main use-case for the class_attribute semantics in Rails was static fields used for stateful configuration:

class ActiveRecord {
  static #connection = DatabaseConnection.fromConfig();

  static set connection(conn) {
    this.#connection = conn;
  }
}

class ApplicationModel extends ActiveRecord {

}

// override the default ApplicationModel behavior
ApplicationModel.connection = new PostgresConnection();

class Article extends ApplicationModel {
  // subclasses of ApplicationModel share an instance of PostgresConnection
}

class User extends ActiveRecord {
  // direct subclasses of ActiveRecord share an instance of the default connection
}

This kind of example is also why re-initialization is undesirable. In this case, each subclass would get a new instance of the database connection, which is deeply wrong.

I tried to port the semantics and motivations that I had for class_attribute (when I designed it back in 2010) to the current proposal, but I may have made some silly mistakes. Please let me know if I made any mistakes in the porting.

littledan commented 6 years ago

@gibson042 I don't understand your example. Where would static private fields come up?

gibson042 commented 6 years ago

@littledan The non-standard, ancient RegExp.$n properties are read-only but updated by RegExpExec. In other words, they act like static public getters around static private fields.

@wycats Subclass bindings (or other functionality that mimics them) do not imply field re-initialization, merely that updates on the parent don't affect already-initialized subclasses (a state which this PR allows only after a write on the subclass field).

littledan commented 6 years ago

@gibson042 Whatever semantics we adopt for private static fields, it won't affect existing things that use internal slots. The semantics of these features is defined by this draft specification which doesn't reference private static fields.

gibson042 commented 6 years ago

I'm not saying existing behavior will change, I'm just using it as a model by which to evaluate proposed functionality. Private fields are very much like internal slots, and copy-on-write would break that similarity in a surprising way.

littledan commented 6 years ago

@gibson042 I see, interesting parallel. I'd like to argue that the use of internal slots on the constructor RegExp is more like a one-off aberration--I don't think we'll define more library features this way; we certainly wouldn't've added a similar feature today. I don't think language features need to be consistent with it.

gibson042 commented 6 years ago

I used that example because it demonstrates a preexisting within-the-ecosystem application of mutable private state at the class/constructor level, not because I think the language itself needs to embrace such patterns. Maybe it was a poor choice; there are other examples at #5.

Regardless, I oppose copy-on-write for static fields because a class hierarchy is different from an instance prototype chain—it is surprising and counterintuitive for changes on a parent class to sometimes affect descendant classes and sometimes not.

wycats commented 6 years ago

@gibson042 said:

because a class hierarchy is different from an instance prototype chain

Can you expand on this more?

gibson042 commented 6 years ago

In UML-like abstract terms, the relationship between a subclass and a parent class is generalization while the relationship between an instance and its prototype is better characterized as something like refinement.

In concrete terms, both relationships are most literally defined by the [[Prototype]] slot, but beyond that they are differentiated by other aspects. Subclasses are guaranteed to invoke their parents as part of construction with new, while explicit construction itself is strictly optional for the instance–prototype relationship (and avoided by e.g. Object.create or Object.setPrototypeOf). On the other hand, the prototype of an instance—to distinguish that concept from an arbitrary object—is the "prototype" property of a constructor function (which is itself referenced by the "constructor" property of the prototype, unless that initial state is explicitly overridden), while subclassing is a unidirectional link involving only the two entities (although the class can be invoked to create an instance with such a prototype, it does not itself posses one).

In practical terms, the presence of blank-slate prototypes for instances establishes both an expectation that properties will seen on reads unless occluded by instance mutation, and a clear benefit from prototype extension. Parent classes, on the other hand, are not prototypes in the same sense... they're independently useful functions with their own purposes, their collection of static fields (especially private ones) can't shrink or grow after class instantiation AFAIK, and their subclasses don't really have anything to gain from further mutation once the inheritance has been established.

littledan commented 6 years ago

@gibson042 I don't quite understand your point. The prototype chain of constructors in subclassing was a deliberate construction to support inherited methods like Array.from, as @allenwb has described. What's the relevant difference with subclassing? Is the major difference about there being no constructor pipeline for constructors? It seems to me like @wycats 's options subclassing kind of use case that class_attributes fulfills makes some sense, and I don't really understand what object oriented priniciple it might be violating. However, I'm not convinced that we need these semantics built-in as opposed to provided by a decorator.

gibson042 commented 6 years ago

I'm saying that the prototype chain of constructors serves a different purpose than the prototype chain of instances, even though both are implemented via the [[Prototype]] slot, and that failing to respect that reduces the value of static private fields.

wycats commented 6 years ago

@gibson042 if I understand correctly, you're describing a semantic for subclasses that you think would be preferable, while the current semantics are modelled after the programming model that @allenwb described here?

gibson042 commented 6 years ago

Correct, a position that I am defending in two related ways: its failure to respect the difference between class generalization (extends) and instance prototype refinement (new), and its inability to effectively reproduce the existing behavior of RegExp properties that are close analogs to static fields.

However, the approach in this PR does at least answer my questions:

If there really is consensus around them, then I wouldn't try to interfere other than to cement the above in test262.

littledan commented 6 years ago

Abandoning this alternative in favor of https://github.com/tc39/proposal-static-class-features/pull/10