Giter Site home page Giter Site logo

Comments (70)

allenwb avatar allenwb commented on July 30, 2024 3

@littledan Yes, my lexical proposal doesn't look like a private field because it isn't a private field. And that's the whole point. The various proposals for static private fields get into trouble because all other uses of fields, slots, and properties within class definitions have inheritance semantics and so it is a natural expectation that static private fields also have such semantics. But as the various alternatives show, trying to give them such a semantics creates significant issues. Class scoped lexical declarations are a better solution because they support the primary use cases without creating the hard to satisfy expectation (perhaps impossible to satisfy elegantly) that they have inheritance implications.

I think your speculation (it's not really evidence) that such class scoped lexical declarations would be confusing is weak. Just because there has been past proposals to assign other meanings to such declarations when they occur in a class body doesn't mean that most JS programmers would be confused by giving such declarations their normal meaning. In fact, one of the reasons for rejecting let x=y; as the definitional form for public instance fields is that it would be inconsistent with the normal lexical scoped meaning of that syntactic form.

Pedagogically, consider a likely progression of leaning JS class syntax:

  1. Lean about statements and (block scoped) declarative forms
  2. Learn class declaration syntax and (prototype) method definitions
  3. Learn about public instance fields
  4. Learn that static means methods and fields are properties of constructor object
  5. Learn about private instance slots and #id lexical scoping semantics, and slot inheritance.

At this point a student might ask, "how do I share a private variable among all the parts of a class definition?" Teacher says: "Put a let declaration inside the class body". Student says, "Oh, of course. I should have thought of that."

As language designers, we should recognize that when we are having to come up with smelly hacks like solutions 3-7 that we have gone down a poor design path and it's time to go back and reexamine the fundamental assumptions that took us down that path.

I think class scoped lexical declarations are a much better solution. But lacking adoption that or some other clean design, I would favor no support for these use cases (solution 1) over any of the other current alternatives.

from proposal-class-fields.

loganfsmyth avatar loganfsmyth commented on July 30, 2024 2

Is this something that should be considered for a feature? It seems like a footgun that people could easily not think about, and it's a potentially unexpected difference between instance privates and static privates.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024 2

OK, from this thread, I think the options 1, 2, 5 and 6 are the most reasonable. My goal for the November meeting (where this question is on the agenda) will be to decide between options 1 and 6 for what we should do right now, as a modification to the existing Stage 3 proposal, and get a feeling for whether to pursue 2 or 5 (or both) as a follow-up.

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024 1

It would be nice if there was a keyword that could be used to s;ways reference the current class, like static - then you could ergonomically do static#x for the common use case, and this#x for when you wanted a method that would throw when inherited.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024 1

OK, that's another reference which is recommending the exact same thing--not repeating the class name when defining another static method.

It sounds like there's a reasonable class for adding this static. feature, but I don't think this proposal needs to block on it. The case in question is pretty narrow--it's calling a static method on a subclass, which might be based on using this or this.constructor as the receiver, and then referring to a private field within the method. I haven't seen any evidence that this is widespread currently--the only examples I can think of for calling static methods on subclasses don't refer to any properties. I don't think it's worth complicating this proposal for this edge case.

from proposal-class-fields.

allenwb avatar allenwb commented on July 30, 2024 1

Don't solution 3-7 all smell like special cases hacks that are trying to fix a design wart that shouldn't be there in the first place? None of them are elegant. This is why I eventually abandoned static privates as a concept and championed solution 2, instead.

@littledan argues that solution 2 isn't orthogonal. I disagree and instead will argue that none of the other solution are orthogonal: solution 3 - a special kind of prototype lookup that only occurs for static slot access that is different from instance slot access; solution 4 - special casing this based accesses to static slots making it different from instance slot access; solution 5 adding special (non-orthogonal ) syntax specifically for accessing static slots; solution 6 - giving the class name binding (if there is one) special semantics that only relate to static slot access; solution 7 - ?? I'm not sure what the "previous" rule is, but the whole concept of requiring use of a linter in this way is certainly a smell.

It's worth stepping back a bit and examine what use cases static slots (and methods) are trying to support. I believe that use case is: provide a singleton binding that is visible throughout the body of a class declaration such that the bound value can be used as a private communication channel among the units of executable code (prototype, instance, and static methods, initializers, etc.) that are defined within the class body. In other words, it is simply a lexical binding that is scoped to the class body.

Solution 2 argues that the orthogonal support for this use case is to simply use one of the existing lexical declaration forms to define the binding. It is not, non-orthogonal with private slots because because it isn't using a private slot to support the use case. The "class local" binding would not use the # names or property access syntax. They would have ordinary identifier based names just like all other lexically scoped bindings.

In summary, I continue to believe that solutions 1 or 2 are the only really alternatives are trying to fix a broken design by introducing special case WTF behaviors to the language.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024 1

@allenwb I agree that your solution is appropriate in terms of functionality. My biggest concern is that it won't "look private" to ordinary JavaScript programmers. I'm not sure if the lexical scoping intuition is enough; instead, this proposal gives consistent use of # as the private sigil for classes. As a piece of evidence, there have been several threads on this repository that proposed using let x = y; as the syntax for public instance field declarations. Jumping from that to private static field declarations makes sense to us as language designers, but maybe not to normal users.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024 1

@bakkot The semantics you're suggesting are really unintuitive to me still. Ultimately, it's a little hard to reason about since it would seem like generally strange design to store mutable state in a property of a class. I imagine it could come up in a situation like this:

class Counted {
  static #nextId = 0;
  #id;
  constructor() {
    this.#id = this.constructor.#nextId++;
  }
}

class FooCounted extends CountedInstances { }

class BarCounted extends CountedInstances { }

With your semantics, FooCounted and BarCounted each get their own counters. Is this the sort of use case you're thinking of? It just seems unusual to use classes in this way, to get a sort of singleton mutable state container. Does any other language work like this? It also seems less useful in JS than it might be in other languages with JS's single inheritance.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024 1

Previously I would have told anyone setting foo.prototype.field = 0 that this was a bad idea which the language allowed because it didn't really distinguish between fields and methods, but that's no longer the case with the introduction of this feature.

We're still not exposing fields on prototypes, or own methods. I think there's a qualitative difference here between prototype fields and static fields, that (I'd hope that) subclassing is less common than base classes, and referring to statics using this rather than the constructor name is less common, and mutating static fields is less common than mutating instance fields. I don't think we actually want to encourage any of these things. The unlikeliness of the combination makes it less likely to be that we need complicated special case behavior like what you're proposing to give really beautiful behavior for the intersection of everything. Adding complex cases here also adds burden to the mental model. Ultimately, when doing sufficiently advanced things, users will have to understand the object model and what everything does, and we don't have a sufficiently articulated pretty subset where you don't have to understand anything. Anyway, I'll put this topic as part of the presentation for the committee in November so we can get more views than just mine here.

from proposal-class-fields.

bakkot avatar bakkot commented on July 30, 2024 1

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

from proposal-class-fields.

bakkot avatar bakkot commented on July 30, 2024 1

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

from proposal-class-fields.

bakkot avatar bakkot commented on July 30, 2024 1

@littledan The accessor-like behavior.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024 1

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

from proposal-class-fields.

bakkot avatar bakkot commented on July 30, 2024

It is indeed an error, for exactly the reason you give.

from proposal-class-fields.

bakkot avatar bakkot commented on July 30, 2024

Mmmaybe? @littledan

I don't think of it as a difference, though. Private fields are not inherited through prototypes. That's true for static fields just as for instance fields.

from proposal-class-fields.

loganfsmyth avatar loganfsmyth commented on July 30, 2024

Yeah I'm on the fence. I could kind of see it as expected since the constructor extends the parent, the same way the instance does, it's just that class instances have an established chained initialization process, so it's easy to support in there, where class constructors don't have that. On the other hand, I bet it'd be a pain to make this work and or support in Babel.

I don't feel super strongly either way, but I figured it was worth asking.

from proposal-class-fields.

jridgewell avatar jridgewell commented on July 30, 2024

I had a prototype crawler originally, and it worked fine. I do see it as a difference between instance and static privates that non-spec-writers will definitely trip over.

class Base {
  #instance;
  static #static;

  get() {
    return this.#instance;
  }

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

class Sub extends Base {};

// This works
new Sub().get();

// This throws
Sub.get();

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

Well, I think this is the sort of question that led static private fields to be left out of the earlier private fields proposal by @zenparsing. @erights has brought up the question about whether subclasses should have a separate private field for subclass static fields. If the answer is "no" to that question (as we settled on), I don't see why users would use this.#static rather than Base.#static (which will work just fine).

Do you think we should have separate static private fields for each subclass? Or, do you think this.#static is much more intuitive than Base.#static to use within static methods? I would've thought that it would be more common to use the class name within static methods, generally.

I agree with @bakkot that, in a sort of spec-formal way, the current semantics are consistent. But if this would be significantly confusing/underpowered for users, maybe it should be reconsidered.

from proposal-class-fields.

jridgewell avatar jridgewell commented on July 30, 2024

I was trying to think of some example that would use @@species with a static field, but I can't seem to come up with a concrete example where I would use this.#static over Base.#static.

Maybe just early error on this.#static for static private fields?

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

Maybe just early error on this.#static for static private fields?

It's hard for me to see how we can generalize that, in a few ways:

  • Should we early error for (this).#static? For anything besides Base.#static?
  • What should we do with decorators, when it may be not known until the decorators run whether #static is static or on instances?

@ljharb Why use that keyword when you can use the name of the class? Such a name exists for class expressions which want it, too.

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

@littledan to reduce the number of places I have to make a change when I want to rename the class.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

@ljharb That doesn't seem like a primary use case. I imagine it will be more work changing the scattered usage sites than what's inside the class definition. Also, you can use class expressions for an internally visible name (though this will leak through the class's .name...).

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

Yes, I understand the use case isn't highly compelling; but it's incredibly common in the airbnb codebase to call static methods from instance methods, or statics from other statics, and it's very messy having to repeat the class name identifier.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

I imagine that you do end up repeating the class name identifier when calling static methods from instance methods...

I've heard the opposite argument, that this is harmful because it is confusing, and should be avoided whenever possible.

Maybe we'd benefit from searching through code to see how many static methods use this. My guess would be, not very many of them.

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

I think the combination of "static methods using this" and "static or instance methods using the hardcoded class name" and "instance methods using this.constructor (which is unreliable)" would all indicate where this kind of a keyword would be potentially useful.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

Let's break this down:

  • static methods using this
    If that's common, well, this is really the target case that is affected, if used together with calling static methods on subclasses.
  • static or instance methods using the hardcoded class name
    This is the case that's already working without issue.
  • instance methods using this.constructor
    If you're using this.constructor to call a static method... well, that seems roundabout enough that it seems like you really want the genericity, that your subclass will do something different. If it's that sort of case, maybe you're really looking for different, individual mutable fields for each subclass (which this proposal doesn't provide either).

Overall, I see this thread as a reason to walk back on static private fields, but I don't see a clear way forward. I don't think going up the prototype chain makes sense with the rest of the private field design. Maybe we really should be adding "class instance" private static fields.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

How often do people even call static methods on subclasses? Outside of ES6 classes, do people even bother setting up the inheritance chain?

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

Probably not; that's one of the benefits of ES6 classes - that you don't have to remember all the steps.

The case that's already working is not without issue - having to hardcore the class name is the issue.

I very much doubt people using this.constructor are doing it because they want the roundabout method - in my experience, people do it because they think it's the only way to reference the class name, and because repeating the class name is a very bad practice in other languages, so they assume it's one in JS.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

This "repeating the class name is bad practice" idea is new to me. Could you point me to a style guide where it's mentioned (in any language)?

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

Here's one for Ruby: https://github.com/bbatsov/ruby-style-guide#def-self-class-methods

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

@ljharb In the linked case, a technique is described for not repeating the class name when defining static methods. In JS syntax, the class name is never repeated in that case. Do you have any other reference that talks about not repeating the class name for another case?

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

@littledan the overarching principle is DRY - repeating yourself is a bad practice. http://www.korenlc.com/ruby-classes/ mentions this (in the same context; of defining static methods).

In PHP 5.5+, this principle is satisfied by static::staticMethod() - the existence of this feature serves identical use cases as what I'm suggesting static.staticMethod() might do in JS.

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

Oh I totally agree it's not worth complicating this proposal; I brought it up because the future possibility of a syntactic static.foo lookup could be used to address static private fields in the future, even if we made them early errors for this proposal.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

static.foo is already an early error. Do you think we should ban private static fields because of the runtime errors that they currently cause?

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

Sorry I'm not being clear :-)

I mean, if we decide to disallow static private fields for the time being, then we could later allow them via static#foo versus this#foo inside a static method, and then there'd be a cleaner way to use them.

from proposal-class-fields.

gibson042 avatar gibson042 commented on July 30, 2024

Similar question for the inverse case (static method on the base class referencing a private name that exists on the subclass but potentially only there, i.e., crossing lexical scopes):

class Base {
	static getNode() {
		return this.#node;
	}
}
class Sub extends Base {
	static #node = "sub";
}

Base.getNode() // => throws
Sub.getNode() // => ???

from proposal-class-fields.

loganfsmyth avatar loganfsmyth commented on July 30, 2024

@gibson042 This proposal only coverts truly private properties, not "protected"-like behavior, so your example would be an error. When you do .#node, you're asking for the #node declared in the current scope, which has none in the case of Base. You are right that the this object in your getNode does have an internal slot for the #node declared in Sub, but it is impossible to access it from another class declaration. If Base has a static #node = "base"; assignment of its own, that'd also be an entirely separate internal slot that does not relate to the one declared in Sub.

from proposal-class-fields.

bakkot avatar bakkot commented on July 30, 2024

@gibson042, to add a bit to @loganfsmyth's comment:

The code you've written, under this proposal, would be an early error, because #node is not declared anywhere that the body of getNode can see it.

from proposal-class-fields.

gibson042 avatar gibson042 commented on July 30, 2024

Thank you for the clarification. But it leaves unaddressed the central issue of surprising interactions between static methods and private members.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

We discussed this issue at the September 2017 TC39 meeting. Thanks to @jridgewell for bringing it up.

Let's go through a few possibilities here:

  1. We could ban static private fields and methods. This would be a little unfortunate, as there's no other way to share state or behavior among things inside a class; in particular, static private methods seem useful for internally sharing behavior that deals with private fields.
  2. We could revisit static private fields and methods being based instead on lexically scoped functions and variables. (This is what @allenwb had been proposing earlier.) However, users might interpret these as instance fields or methods (judging from bugs in these repositories suggesting exactly this syntax for those purposes), and it breaks "orthogonality" from a user perspective--when you want something which is static and private, suddenly you jump into entirely different syntax.
  3. We could do a sort of prototype chain walk on static private field/method access. But this would be very weird in the context of the rest of private class features, where we try hard to not do any sort of observable object operations at all during private element access. With this out of the way, the idea is that there actually isn't any genericity/dynamic dispatching in static private field/method access--it's just always getting at the same thing. So the idea from here is to make it so that the right receiver is used.
  4. We could ban static private fields and methods when the receiver is this.. (Thanks to @tschneidereit for this suggestion.) That would eliminate the most common hazard case, but would leave in others (which might be more obscure), such as this.constructor..
  5. We could ban static private fields and methods when the receiver is anything but static.. (Thanks to @ljharb and @rpalmer57 for this suggestion.) This would eliminate all hazard cases, but it would add a dependency on a not-yet-proposed feature.
  6. We could ban static private fields and methods when the receiver is anything but the constructor name and, as a follow-on, allow static. as well. Classes tend to have an immutable binding to the constructor available inside them (unless they are anonymous), so this is typically available. I think we could make this usually be an early error, rather than a runtime error as currently, if we keep track in static semantics of which class name corresponds to which syntactic private name. There will be some corner-cases that would be missed and a runtime error is triggered (e.g., if a method contains a local variable of the same name as the enclosing class), but it should be possible to get everything you'd expect to come in normal code. I don't think the restriction is extremely onerous, but I'd be interested in opinions.
  7. We could leave things unchanged, and encourage linter authors to enforce the previous rule. This sort of idea was brought up in committee, but it seemed that the committee was interested in making sure JS is intuitive to use without a linter as well.

To me, at this point, I'm leaning towards option 6. What do you think?

from proposal-class-fields.

bakkot avatar bakkot commented on July 30, 2024

Another option: we could install static fields (and static private methods) on subclasses when they're defined.

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

I like options 5 or 6. If @bakkot's option would remove the footgun (such that Sub.get() === 'hello' in the OP), then that seems like it might be better (although I still like static. as a followon in that case)

from proposal-class-fields.

jridgewell avatar jridgewell commented on July 30, 2024

We could revisit static private fields and methods being based instead on lexically scoped functions and variables

What does this mean? (Code example would be even better than prose, 😉 )

although I still like static. as a followon in that case

I think having a static that behaves something like super is worth exploring regardless of our choice here. It just becomes even more valuable if we decide to limit static private usage.

Another option: we could install static fields (and static private methods) on subclasses when they're defined.

I'm hesitant on this approach, since it breaks with inherited public fields. I can later change inheritance chain to something else entirely, and I don't think privates should be preserved in that case (since the publics aren't).

from proposal-class-fields.

bakkot avatar bakkot commented on July 30, 2024

I'm suggesting static public fields get install on subclasses too, so it wouldn't break with that. In general inheriting fields has footguns, and I think it is an advantage of this approach that it avoids inheriting.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

@bakkot I'm sorry for the omission above. I'm pretty sceptical of the idea though. When would you actually want this behavior in your program (as opposed for just intuiting it out of a sense of consistency)?

from proposal-class-fields.

bakkot avatar bakkot commented on July 30, 2024

@ljharb It would remove the footgun, yes. Sub would have its own #field (with the same Private Name), which get would happily read from.


@littledan I think you'd want it for some of the same reasons we install instance fields from superclasses on instances of subclasses, rather than inheriting them.

In particular, the difference between

class Base {
  static prop = 0;
}

class Derived extends Base {}

Derived.prop = 1;

Base.prop === 0; // true

and

class Base {
  static opts = { prop: 0 };
}

class Derived extends Base {}

Derived.opts.prop = 1;

Base.opts.prop === 0; // false

is I think something which would be surprising to a lot of people - especially given that there is not this dichotomy for instance fields. More generally, prototypical inheritance of fields is weird; the reason I like this solution is that it avoids it.

(Also, it solves the problem in this thread without introducing more weird edge cases.)


@allenwb My proposed solution actually doesn't feel like a special case hack to me. It feels like aligning static fields with instance fields in a pretty natural way.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

@bakkot It's still hard for me to think of when you'd have options that you mutate like that. Do you think you could be a little more concrete in the example? TypeScript has Static Properties whose semantics I thought were like the current proposal, not your proposal; is there any evidence (e.g., stack overflow threads, people complaining) that people are running into this issue?

from proposal-class-fields.

bakkot avatar bakkot commented on July 30, 2024

@littledan TypeScript's static properties actually may or may not be like the current proposal depending on whether the environment running the compiled code has the ability to manipulate prototypes directly: if it does static properties are inherited with prototypical inheritance, but if it doesn't static properties are copied to the subclass.

So evidently very few people are using static properties in a way where the difference would be observable. Maybe @RyanCavanaugh would know more?

As to what people would expect - I don't actually know. The difference I describe feels surprising to me; even if it's something you'd rarely run into, I don't like how inheritance differs between static and instance fields. Besides, it solves the problem with static private properties - they'd Just Work. In light of that, do you think my proposal is more surprising than any of your 1-7?

I'm also interested in why you feel skeptical of it more generally.

from proposal-class-fields.

bakkot avatar bakkot commented on July 30, 2024

@littledan

With your semantics, FooCounted and BarCounted each get their own counters.

This happens with the current semantics too, is my point, except that they don't get them until the subclasses are first initialized. That is, the counters in FooCounted and BarCounted will hold get the value from Counted until the first time each is initialized, at which point e.g. FooCounted's count will branch off and then hold "number of times Counted was initialized before the first time FooCounted was initialized + number of times FooCounted has been initialized". That seems way less intuitive.

And while I also agree that putting mutable state in a static field seems like a bad idea, if the language allows it, people are going to do it. There is value in consistency even for features people should not use.

I'm not aware of any language which works like my proposal, but because of the above behavior I'm not aware of any language which works like the current proposal either. If we've got to do something new, at least we should be consistent about it, and ideally do something which will work as expected in as many cases as possible (e.g. for static private methods).Though I am more concerned about people referencing static properties with this in a static method, or through the class name, than with this.constructor in a non-static method.

I agree that it seems like "putting mutable state in a static property" is probably an unusual use case. But aside from that case my proposal works very much like the current semantics, so if we're holding that aside as too exceptional to be concerned about, why do we care? And if we're not, it really seems to me that the current proposal's semantics are less intuitive. Either way, mine has the advantage of making static private fields and methods work.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

OK, it seems like the argument is not as much based on any particular use case, but instead, "the JS object model works with Set creating a shadowing own property if it's a data property on the prototype; ES6 classes have a prototype chain set up for constructors; the intersection of these two things is ugly, therefore we should create a bunch of extra own properties to prevent it from showing up." Hope that's not an unfair caricature, but if that's what it boils down to, I don't see how we're really solving the problem. You'll still have plenty of other objects which are possible to construct in JS which don't create extra own properties to cover over the weirdness of Set.

from proposal-class-fields.

bakkot avatar bakkot commented on July 30, 2024

@littledan The main argument is "it would be nice if static private fields and methods worked for subclasses", but yes, your description is an accurate summary of my argument for it being a desirable behavior in general rather than just a kludge to get static private to work.

I'm not sold on the idea that we shouldn't worry about that weird behavior because it's present anyway. Yes, it's possible to construct objects which have fields on their prototypes which are absent on instances, but this is the first time we are proposing syntax specifically for adding non-function prototype-placed fields. Previously I would have told anyone setting foo.prototype.field = 0 that this was a bad idea which the language allowed because it didn't really distinguish between fields and methods, but that's no longer the case with the introduction of this feature.

In other words, there are a lot of historical footguns we can't do anything about, but that's no reason to copy them to new features.

from proposal-class-fields.

eddyw avatar eddyw commented on July 30, 2024

You could solve it as:

class Base {
  static prop = 1
  static #prop = 1
  static get() {
    return this.prop + this.#prop
  }
}
class Sub extends Base {
  static get() {
    return super.get.call(Base)
  }
}
console.assert(Base.get() === Sub.get())

However, if overwriting the static prop in Sub, it won't take it into account. For instance:

class Base {
  static prop = 1
  static #prop = 1
  static get() {
    return this.prop + this.#prop
  }
}
class Sub extends Base {
  static prop = 100 // ignored!
  static get() {
    return super.get.call(Base)
  }
}

No perfect solution, but I guess it's kind of the point of private fields

from proposal-class-fields.

rbuckton avatar rbuckton commented on July 30, 2024

Per the discussion today, I've been thinking about two possible solutions to the original problem:

Option A

Option A is a mechanism that causes the runtime behavior of static private fields to behave like the currently proposed behavior for static public fields:

Given:

class Base {
  static #x = 0;
  static inc() { return ++this.#x; }
}
class Sub extends Base {}

Lets say we change the spec in the following ways:

  • Modify ClassDefinitionEvaluation to perform the following additional steps before Step 33:

    33. If superclass is not undefined, then
       a. Let superclassPrivates be superclass.[[PrivateFieldValues]].
       b. For each entry from superclassPrivates,
         i. Perform ? PrivateFieldAdd(entry.[[PrivateName]], F, entry.[[PrivateField]]).

  • Modify PrivateFieldAdd, Step 5 to read:

    5. Append { [[PrivateName]]: P, [[PrivateField]]: { [[PrivateValue]]: value } } to O.[[PrivateFieldValues]]

  • Modify PrivateFieldGet, Step 5 to read:

    5. Let value be entry.[[PrivateField]].[[PrivateValue]].
    6. While value has a [[PrivateValue]] internal slot,
       a. Set value to be value.[[PrivateValue]].
    7. Return value.

  • Modify PrivateFieldSet, Step 5 to read:

    5. Set entry.[[PrivateField]].[[PrivateValue]] to value.

With these changes, each subclass gets an indirect reference to the private value on the superclass. Now when we call Sub.inc() and access this.#x with Sub as the receiver, we'll read the value 0 from Base, but when we write the value we replace the indirect reference with a direct value. Further subclasses of Sub would now have an indirect reference to the value on Sub.

This approach results in the same observable runtime behavior as public static fields as currently proposed and is only really viable if we decide to move forward with the current behavior rather than re-initializing statics:

Base.inc(); // 1

Sub.inc(); // 2
Sub.inc(); // 3

Base.inc(); // 2
Base.inc(); // 3

However, it has been discussed that the behavior of public static fields may not be desirable, which brings us to Option B.

Option B

Alternatively, If instead we decide to re-initialize static public fields we could use a different approach for private static fields and methods.

Option B would be to store a [[PrivateFieldDefinitions]] internal slot on a class constructor that contains Records with [[PrivateName]] and [[PrivateFieldInitializer]] internal slots. Then we modify ClassDefinitionEvaluation to copy the [[PrivateFieldDefinitions]] of superclass to F and then run all of the [[PrivateFieldDefinitions]] of F to initialize the private fields.

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

To restate what I stated today in committee: it must be possible (and the common, default behavior) that any code whatsoever outside the class declaration, including superclasses or subclasses, can not observe anything about private fields; including their names, their values, their absence, or their mere existence.

I'm not 100% sure I understand #43 (comment), but anything that involves the prototype chain with privates is imo a nonstarter.

from proposal-class-fields.

rbuckton avatar rbuckton commented on July 30, 2024

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

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

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

from proposal-class-fields.

rbuckton avatar rbuckton commented on July 30, 2024

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

from proposal-class-fields.

zenparsing avatar zenparsing commented on July 30, 2024

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

from proposal-class-fields.

erights avatar erights commented on July 30, 2024

Just for the record:

  • @bakkot 's summary of my position is correct.
  • I would not object to @zenparsing 's suggestion. If public instance properties were accessors, then clearly it would be surprising for public static properties not to be accessors.

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

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

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?).

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

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:

  • Use cases for private static are relatively weak--there are adequate replacements available for any case I can think of easily:
    • For shared constants (private static fields), a non-exported lexically scoped variable defined outside the class could work.
    • For shared behavior which accesses private fields or methods, a private instance method could work (you might have to .call it, but it's hard to imagine why you'd ...
  • The prototype chain, inherited static private issue is pretty inherent; it's been explained above that walking any sort of prototype chain has issues, and alternatives which are based on not doing private static field access on subclasses all have their faults. We could address that with multiple initializations of public static fields, but...

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

  • Public static fields are already an established pattern in JavaScript with the current semantics, found in lots of JS code which adds data properties to constructors after the function or class declaration after it runs (which could be cleaned up by putting it in the class, giving the programmer more freedom over ordering and less uncertainty due to Define instead of Set). The analogous semantics provide a smooth migration path.
  • I haven't heard a concrete use case where you actually want the multiple initialization. The only cases I can think amount to treating subclassing as a mechanism to get a new instance of a stateful factory--which strikes me as a bit far afield from a more declarative idea of what subclassing means that I thought would be more important for JS.
  • I'm still not convinced that writing to a static field on a subclass from where the field was defined will be any more common of a hazard from other ways that programmers can set up prototype chains with data properties on prototypes and get confused. The cases that I've seen where you'd do such a write seem pretty contrived.

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

  • This differs significantly from current user practice, so it should increase transition costs (but then again, so does our choice of Define over Set)
  • IIRC feedback from the V8 team (I can't remember whether this came from @bmeurer, @verwaest or someone else) was that such pervasive use of accessors could be slower on startup. Maybe implementers in other engines could give feedback about whether that's likely to be the case for them too, cc @bterlson @kmiller68 @jswalden
  • As @ljharb mentions, it's another point of divergence with the semantics users currently have for public fields, which may be working OK as is for programmers.

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

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

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.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

@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 #43 (comment) ?

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

from proposal-class-fields.

erights avatar erights commented on July 30, 2024

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":

  • One encapsulated state location per the class on which the public static property is declared.
  • One encapsulated state location per class which is or extends this class.

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

from proposal-class-fields.

erights avatar erights commented on July 30, 2024

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:
* private fields remaining mutable under freeze (which is essential of course)
* the interpretation of Object.freeze as a means to tamper proof an API surface.

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.

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

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

from proposal-class-fields.

erights avatar erights commented on July 30, 2024

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

from proposal-class-fields.

zenparsing avatar zenparsing commented on July 30, 2024

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

from proposal-class-fields.

littledan avatar littledan commented on July 30, 2024

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.

from proposal-class-fields.

ljharb avatar ljharb commented on July 30, 2024

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.

from proposal-class-fields.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.