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

Static class field inheritance issues #5

Closed
gibson042 opened this issue Dec 22, 2017 · 3 comments
Closed

Static class field inheritance issues #5

gibson042 opened this issue Dec 22, 2017 · 3 comments

Comments

@gibson042
Copy link

gibson042 commented Dec 22, 2017

I thought about posting this as a comment on #2, but upon completion it read more like a restatement of both that issue and #1.

Question 1: Should static fields from the parent be initialized on subclasses?

Normal non-static properties of parent classes are already part of the language and are not copied, so this behavior would set static field declarations apart from both non-static properties and from static methods.
Even so, I think the answer should be "yes" (for reasons elaborated upon below).
But it prompts a followup.

Question 1a1: Should static fields on subclasses be initialized with the current value from the parent, or the initial value?

I think the best answer is "current value", despite the resulting dependence upon when a subclass is defined:

class RE {
  static syntax = "POSIX SRE";
}
class SRE extends RE {}
RE.syntax = "POSIX BRE";
class BRE extends RE {}
console.assert(BRE.syntax !== SRE.syntax, "subclass initialized from current parent values");

On the other hand, not initializing static fields from the parent has (in my opinion) even more surprising behavior...

Question 1b1: Should assignment to a static field in an inherited method from the parent's lexical scope create a new binding on the subclass?

If yes:

class Clock {
  static time = 0;
  static tick() {
    return ++this.time;
  }
}
console.assert(Clock.tick() === 1);
class Watch extends Clock {}
console.assert(Clock.tick() === 2);
console.assert(Watch.tick() === 3, "subclass affected by assignment in parent");
console.assert(Watch.tick() === 4, "subclass keeps assigned value");
console.assert(Clock.tick() === 3, "parent not affected by assignment in subclass");
console.assert(Watch.tick() === 5, "subclass no longer affected by assignment in parent");

If no:

class Clock {
  static time = 0;
  static tick() {
    // `this.time` is `Clock.time`, even when `this` is `Sub`.
    return ++this.time;
  }
}
console.assert(Clock.tick() === 1);
class Watch extends Clock {}
console.assert(Clock.tick() === 2);
console.assert(Watch.tick() === 3, "subclass affected by assignment in parent");
console.assert(Watch.tick() === 4, "subclass keeps assigned value");
console.assert(Clock.tick() === 5, "parent affected by assignment in subclass");
console.assert(Watch.tick() === 6, "subclass still affected by assignment in parent");

Static private fields

Regarding static private fields, their very existence must be kept secret from subclasses.
But I think that requirement can inform deliberation on the above options.

Question 2: Should static private fields be accessible from inherited methods called on subclasses?

If the answer is "no", then static private fields essentially break class inheritance (because even a simple method like static getPrivateField() { return this.#privateField; } would throw an exception when the receiver is a descendant class). So let's provisionally make it "yes".

Question 2a1: Should static private field access be locked to the original lexical scope?

A "no" here would violate the secrecy requirement (e.g., (class Sub extends Base { static #privateField = "spied on Base" }).getPrivateField()), so let's again say "yes":

class Clock {
  static #time = 0;
  static tick() {
    return ++this.#time;
  }
}
class Countdown extends Clock {
  static #time = 10;
  static tock() {
    return --this.#time;
  }
}
console.assert(Clock.tick() === 1);
// The following implies a "yes" to Question 1.
console.assert(Countdown.tick() === 1, "inherited methods access sp-fields on subclass but with their lexical scope");
console.assert(Countdown.tock() === 9, "sp-fields on a subclass are independent of parent data");
console.assert(Clock.tick() === 2, "sp-fields on a parent are independent of subclass data");

This behavior is analogous to every class having a WeakMap in which every key is a class (equal to or descended from the context class) and the corresponding value holds the current value for each static private field in the lexical scope of the context class. This perspective also informs what should happen during evaluation when an attempt is made to access a private field from a value that is not a class equal to or descended from the context class (e.g., (class Base { static #x = "trusted"; static x(){ return this.#x } }).x.call(class Decoy { static #x = "untrusted" }))—a runtime error.

Conclusion

The above makes sense to me, but please poke holes in it if you can. Also note that the proposed resolution to Questions 2 and 2a1 is independent of Question 1a1, and a case could be made for resolving the latter by holding onto initial values (although, as noted, that is not my preference and doesn't feel like a good fit for ECMAScript).

@Jessidhia
Copy link

Normal static properties of parent classes are already part of the language; there's just not a nice syntax to declare them.

class A {}
Object.defineProperty(A, 'field', {
  configurable: true,
  writable: true,
  value: 'static field'
})
class B extends A {}
B.field // 'static field'
A.field = 'changed'
B.field // 'changed'
B.hasOwnProperty('field') // false
Object.getPrototypeOf(B).hasOwnProperty('field') // true

@gibson042
Copy link
Author

I refer to those as "normal non-static properties of parent classes" right at the top of this issue, and distinguish them from static fields (i.e., those declared as static within the class body) because I feel like the existing behavior you demonstrate is counterproductive for class inheritance (cf. Question 1b1) and a compatibility risk for static private fields (cf. Question 2a1).

@littledan
Copy link
Member

JavaScript already has its prototype chain inheritance model. Even if we can conceive of another object model that might have certain nice properties, I think it'd add a lot of complexity to support both. Let's settle on static public fields as they are currently proposed--own properties of the constructor where they are defined, and nothing more complicated.

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

3 participants