-
Notifications
You must be signed in to change notification settings - Fork 27
Normative: Mimic a prototype chain for static private fields #7
Conversation
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. This is how Rails' class_attribute :field
works (if we ignore it's instance accessors for the static field). Maybe @wycats can give us his thoughts on how well it's been received?
@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, 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. |
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. |
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 |
@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. |
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. |
The main use-case for the 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 |
@gibson042 I don't understand your example. Where would static private fields come up? |
@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). |
@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. |
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. |
@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. |
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. |
@gibson042 said:
Can you expand on this more? |
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 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. |
@gibson042 I don't quite understand your point. The prototype chain of constructors in subclassing was a deliberate construction to support inherited methods like |
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. |
@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? |
Correct, a position that I am defending in two related ways: its failure to respect the difference between class generalization ( 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. |
Abandoning this alternative in favor of #10 |
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.