Closed jridgewell closed 6 years ago
That example seems terrible to me; the current semantics are what I think everybody expects for static publics, and I would strongly object to static public advancing in any form besides the current one.
Its plus side is that it's consistent throughout the inheritance chain (always refer to the defining class' field). Downside is definitely that it's significantly different than the usual object-prototype property inheritance (writes shadow the inherited field).
Maybe @allenwb and @waldemarhorwat would like to comment?
I don't find that a downside; static publics are just a property on the constructor.
I think it is essential that class Foo {} ; Foo.bar = true;
be equivalent to class Foo { static bar = true }
. These are the semantics everyone I've talked to assumes, they're the semantics babel has used for static public fields for years, and they're the semantics that all of the existing documentation for them on the internet assumes.
This is another area where the syntax is bringing out conflicts between our intuitions about how classes should behave.
As you probably know, I've always thought that public static and instance "fields" (insofar as we have them at all) should be getters over private state. But even though it is a good design, it holds together well, and has many advantages, as @ljharb rightly points out, it conflicts with user's expectations that fields are simple sugar over assignment.
I'm increasingly concerned that the direction we're taking for class features is a mistake.
Some @getify quotes from a private twitter DM discussions:
FWIW, us going further down this road of moving
class
to not just be syntactic sugar for prototypes, but now its own complicated thing altogether... with all these little nooks and crannies.........this is part of why my teaching on the topic of
class
is essentially... "use it only if you're willing to stick to the narrow, static subset" and i strongly warn people to stay away from them if they plan to try to leverage complex dynamicism (as I like to do)
class
was "sold" to the community as, "hey, it's just nicer syntactic sugar"... but many of us knew it was inevitable that it would become its own pit of feature creep. ;-)
I'm becoming increasing convinced that a complex "full featured" class definition syntax is not a good fit with the JavaScript basic object programming model. It just takes too many special case behaviors to try to twist the simple JS model into approximating either a Java-like static class model or a Smalltalk/Ruby/Python-like dynamic class model.
But there remains a few fundamental use cases that aren't addressed by the JS object model. In particular, any thing that requires efficient object encapsulation of hidden state.
I think that there is a pretty strong argument to be made that all that is essential to "completing" the ES class model is private instance state and "static" (ie, runs at class definition time) class initializers. Given those two capabilities pretty much all of the other features that have been proposed can be synthesized in user land code.
I've written about a variation of this proposal and why I have not been pushing for it at https://github.com/tc39/proposal-static-class-features/blob/master/ALTERNATIVES.md#switching-all-fields-to-being-based-on-accessors . Even if these semantics only apply to static fields, and instance fields remain own properties, I think similar arguments apply.
@jridgewell Do you have a response to the downsides listed here ? Otherwise, I am thinking of closing this issue.
Yup, inlining the objections with my comments. Note that I'm omitting anything to do with instance fields, since they're not affected by this discussion.
In the case of static fields, presumably, the read and write would happen on the superclass where they are defined, ignoring the receiver (otherwise, the TypeError issue from private static fields is then ported to public static fields as well, as the subclass constructor would not have its own value!). Private static fields would also follow this accessor pattern.
Yes and no. For public static with accessor semantics, the base (defining) class also wouldn't have a data property, it'd be an accessor property. So the write is happening on the base class, but it's just calling the base class' setter to write to the closed over value.
Private static would follow this, be letting subclasses copy over private methods/accessors. Like https://github.com/tc39/proposal-static-class-features/pull/7.
In all cases, the getter would throw a TypeError when the receiver is an object which does not "have" the private field (e.g., its initializer has not run yet), an effectively new TDZ, which could reduce programmer errors.
I don't understand this argument. Are you saying the base class won't have the accessor property defined yet? Or that a static initializer in the subclass might try to access the base-defined private static before it's been initialized?
If it's the first, I just don't understand. If it's the second, maybe we're thinking of different subclass initializations. Just like private instance methods/accessors are defined before public/private instance properties, private static accessors/methods would be installed before any static initializers have run. With that, I don't see how that would create a new TDZ.
Public fields would no longer be own properties. This may be rather confusing for programmers, who may expect features like object spread to include public instance fields.
Agreed this is a downside. I'm not certain how big of an issue it is though (I have no code bases using public static fields, afaik). I imagine the primary use case is known keys, making them a bit like records (and private fields), but I'm sure it'll trip someone up.
Does not help implementations and might hurt startup time (maybe)... At least on the implementation side, however, there would either have to be checks on each access to see if the field was initialized, or the initialization state would have to show up in its "hidden class". Either way, there's no efficiency gain if the fields are "already there, just in TDZ". In some implementations, startup time could be even worse than with own properties, until the system learns to optimize out the accessors.
I think this is tied to (and answered by) the earlier TDZ comment? If not, let me know.
Loses the object model--we'd have to start again. Data properties have an object model permitting non-writable, non-enumerable and non-configurable properties, including its use by Object.freeze. If we want to provide these sorts of capabilities to public fields, they would have to be built again separately.
We already have accessors, though. I imagine any freeze/enumerable/descriptor mutations would be better handled by a class decorator than freezing/monkey-patching the class after creation.
Yup, inlining the objections with my comments. Note that I'm omitting anything to do with instance fields, since they're not affected by this discussion.
The current proposal has a nice property that instance and static fields have basically the same semantics, except for the things which have to be different by construction: which objects they attach to and when the initializers are executed. In your proposal, the parallelism would no longer hold in a pretty significant way. Do you think this is really justified?
Loses the object model--we'd have to start again. Data properties have an object model permitting non-writable, non-enumerable and non-configurable properties, including its use by Object.freeze. If we want to provide these sorts of capabilities to public fields, they would have to be built again separately.
We already have accessors, though. I imagine any freeze/enumerable/descriptor mutations would be better handled by a class decorator than freezing/monkey-patching the class after creation.
From @bakkot's account to me of a discussion with @erights, it sounded like the combination of these two issues (Object.freeze
and consistency with instance fields) was critical and that we'd be unlikely to get consensus on the alternative semantics for that reason.
In all cases, the getter would throw a TypeError when the receiver is an object which does not "have" the private field (e.g., its initializer has not run yet), an effectively new TDZ, which could reduce programmer errors.
I don't understand this argument. Are you saying the base class won't have the accessor property defined yet? Or that a static initializer in the subclass might try to access the base-defined private static before it's been initialized?
If it's the first, I just don't understand. If it's the second, maybe we're thinking of different subclass initializations. Just like private instance methods/accessors are defined before public/private instance properties, private static accessors/methods would be installed before any static initializers have run. With that, I don't see how that would create a new TDZ.
I'm not really making an argument here, but rather describing in a bit more detail the semantics that I thought some other people were proposing, and a (possibly) positive outcome of it. I'll give a desugared example of what these semantics are here and what the TDZ would be; I'd be interested in understanding how this compares to the semantics you're imagining.
If we go with the "accessor" version of static fields, then
class C {
static x = y;
}
would desugar into something like
let _x = ~empty~;
class C {
get x() {
if (_x === ~empty~) throw new TypeError();
return _x;
}
set x(v) { _x = v; }
}
C.x = y;
In particular, if there are multiple static field declarations, they can see each other (both through this
and because C
is no longer in TDZ at that point). So, the TypeError
might actually be hit. Private instance fields have a similar TypeError case. When I was referring to whether an object "has" a private field, I meant whether its _x
is ~empty~
or filled with something.
Overall, the conclusion of the public fields proposal's champion group has been that it's good enough if you only get those sorts of TypeErrors for private fields, and if public fields (both static and instance) continue to return undefined
in those sorts of cases.
The current proposal has a nice property that instance and static fields have basically the same semantics, except for the things which have to be different by construction... In your proposal, the parallelism would no longer hold in a pretty significant way. Do you think this is really justified?
Except for static private, which we can't agree on because static is a fundamentally different inheritance model than instance fields (prototype-chains vs own-property). I think this locks down the a parallelism between public/private, with instance fields having semantics that are feasible and statics having its own semantics that are feasible.
From @bakkot's account to me of a discussion with @erights, it sounded like the combination of these two issues (Object.freeze and consistency with instance fields) was critical and that we'd be unlikely to get consensus on the alternative semantics for that reason.
I'm going to omit consistency with instance fields, since I talked about it above.
For Object.freeze
, I think it's a weak argument. Specifically, this assumes that static public must be equivalent to constructor.prop = 1
(@ljharb's position), and that Object.freeze
will therefor apply. I think this is just a nicety on top of the core use case of defining a property on a constructor. Accessor semantics ticks the core use case.
Maybe a better usecase of freezing a constructor would persuade me? The only time I've called Object.freeze
is when I'm planning to reuse a record (known-keys, not a dictionary) object to avoid GC. I could imagine using it on an instance of a class if maybe you want to prevent shadowing of prototype properties (still not very handy in my book), but I can't imagine a real use for freezing the constructor.
I'd be interested in understanding how this compares to the semantics you're imagining... When I was referring to whether an object "has" a private field, I meant whether its _x is ~empty~ or filled with something. Overall, the conclusion of the public fields proposal's champion group has been that it's good enough if you only get those sorts of TypeErrors for private fields, and if public fields (both static and instance) continue to return undefined in those sorts of cases.
Ah, thank you for explaining! I actually had this in OP, defining both a let
after (TDZ semantics) or before (undefined
semantics) the class body evaluation. I'd learn towards undefined
myself, but either is doable.
My use case is that i would prefer to freeze every constructor - nay, almost every object - i ever produce. I don’t, currently, for performance reasons - but in other words: “nobody should modify objects they don’t own, and i want to have certainty about my own objects, which requires preventing unauthorized mutations”.
My use case is that i would prefer to freeze every constructor - nay, almost every object - i ever produce.
Is this extensions to the object, or any value mutations? We already have static accessors and objects accessors, so I imagine if you want to prevent any value mutations you already account for them? Given that, I don't see Object.freeze
as a strong argument (it's already not full-proof), and something better left to a decorator or whatever function you're already using to freeze accessors and data properties.
function freeze(object) {
const descriptors = Object.getOwnPropertyDescriptors(object, Object.getOwnPropertyNames(object));
for (const prop in descriptors) {
const desc = descriptors[prop];
if (desc.set) {
Object.defineProperty(object, prop, {
// set enumerability, configurability, etc
...desc,
set: undefined,
});
}
}
Object.freeze(object);
}
@freeze
class Test {
static field = 1;
// Freezes already have to account for this.
static get prop() {
}
static set prop(v) {
}
}
Except for static private, which we can't agree on because static is a fundamentally different inheritance model than instance fields (prototype-chains vs own-property). I think this locks down the a parallelism between public/private, with instance fields having semantics that are feasible and statics having its own semantics that are feasible.
I see that it's hard to make private static happen with support for inheritance without inserting this difference between static and instance. But, I don't see why this means that inconsistency isn't a problem. We can consider this a cost/benefit tradeoff, but I'd expect some committee members to see the weights the other way.
The only time I've called Object.freeze is when I'm planning to reuse a record (known-keys, not a dictionary) object to avoid GC.
What makes you think Object.freeze will avoid GC?
My use case is that i would prefer to freeze every constructor - nay, almost every object - i ever produce.
Is this extensions to the object, or any value mutations? We already have static accessors and objects accessors, so I imagine if you want to prevent any value mutations you already account for them? Given that, I don't see Object.freeze as a strong argument (it's already not full-proof), and something better left to a decorator or whatever function you're already using to freeze accessors and data properties.
For context, many TC39 members have been taking this possibility of freezing the world, based on Object.freeze applied to many objects, as a big design goal for the programming language. Although internal slots (and, as such, private fields) are not frozen, I believe @erights has argued that public fields, both static and instance, look so much like ordinary properties (even if they are actually based on accessors) that programmers would expect them to get frozen by the same process.
But, I don't see why this means that inconsistency isn't a problem. We can consider this a cost/benefit tradeoff, but I'd expect some committee members to see the weights the other way.
It's definitely an downside of the proposal, I agree. But I see it as being a more consistent path than public static and either private static with a footgun and class lexical functions. And it's the only one I can see that can answer issues with prototype-inheritance when we expand to outer lexical scoped private names.
What makes you think Object.freeze will avoid GC?
They're cached on the instance until a mutation happens. All the freeze does is ensure the caller does't mutate the record directly, mutations have to be done through APIs to ensure cache-busting.
For context, many TC39 members have been taking this possibility of freezing the world, based on Object.freeze applied to many objects, as a big design goal for the programming language.
Who is doing the freezing in this case?
It's definitely an downside of the proposal, I agree. But I see it as being a more consistent path than public static and either private static with a footgun and class lexical functions.
OK, given that we've reached a tradeoff between different goals, maybe we should try to close on this issue by getting broader feedback from other committee members about the weighting of these goals. What do you think of that?
And it's the only one I can see that can answer issues with prototype-inheritance when we expand to outer lexical scoped private names.
If you're thinking that the private field access should forward up to the prototype object by getting converted into an accessor under the hood, it doesn't seem like those semantics would create consistency between public and private: If you added setX
and setY
methods, you'd see the difference. And this is the core of the problem--we can't change how ordinary properties work with inheritance.
They're cached on the instance until a mutation happens. All the freeze does is ensure the caller does't mutate the record directly, mutations have to be done through APIs to ensure cache-busting.
I'm not sure which cache you're talking about. I believe GCs will continue to happen as long as allocations are happening, even if you do Object.freeze
.
Who is doing the freezing in this case?
There are different possibilities--for now, it might be a framework for isolating and freezing platform objects such as Caja, and in the future, there may be a "frozen realm" API to have these already be built-in. cc @erights
Maybe we should try to close on this issue by getting broader feedback from other committee members about the weighting of these goals. What do you think of that?
👍
It doesn't seem like those semantics would create consistency between public and private: If you added setX and setY methods, you'd see the difference. And this is the core of the problem--we can't change how ordinary properties work with inheritance.
It definitely will be a difference, but the alternative private would be to throw on this.#x
access because sub-objects don't have the branding. It's close to the the Java system @allenwb described in https://github.com/tc39/proposal-static-class-features/issues/4#issuecomment-356141242.
Classic Java is interesting in that it appears to be a pure OO language (all procedures are defined as methods within class definitions) but it unobtrusively also has fairly extensive support for lexically-resolved calls. But that support is disguised so successfully as apparent object-resolved calls that many Java programmers don't even realize it exists as such or that they are using them. Where are they hiding? Behind the private keyword with an assist from Java's static type system. Every Java private method is a procedure that is always invoked via a lexically-resolved call.
It definitely will be a difference, but the alternative private would be to throw on this.#x access because sub-objects don't have the branding.
Yes, I was imagining throwing on this.#x
. You can actually already get at this exact same case with the Stage 3 class fields feature:
class C {
#x;
getX() { return this.#x; }
}
let a = Object.create(new C());
a.getX(); // TypeError
I'd be surprised if the object literal case differed here.
It's close to the the Java system @allenwb described in #4 (comment).
I see how Java has other semantics that are internally consistent, but Java doesn't have JS's entire prototype inheritance system. I don't see how to square those.
They're being cached on a class instance.
I'm still not sure what you're getting at. I don't see how that particular mutation would cause an allocation.
If you mean reusing objects as opposed to creating new objects creates less load on GC, then I agree. However, I don't think reusing objects is the only use case to Object.freeze
.
You can actually already get at this exact same case with the Stage 3 class fields feature:
Hm, that's a good point. Perhaps that means private instance fields would have to be "accessors" too (though they're not really accessor descriptors). This would then be copied down by Object.create
, like my prototype inheritance example above.
However, I don't think reusing objects is the only use case to Object.freeze
This has just been my only use of it so far. Again, other use cases would be welcome.
Hm, that's a good point. Perhaps that means private instance fields would have to be "accessors" too (though they're not really accessor descriptors). This would then be copied down by Object.create, like my prototype inheritance example above.
Maybe we could do this for private fields, but we fundamentally can't do this for public fields which are part of object literals. For example:
let x = {
i: 0,
inc() { this.i++; }
};
let y = Object.create(x);
x.inc();
y.inc();
x.i; // 1
y.i; // 2
This has just been my only use of it so far. Again, other use cases would be welcome.
If I understand things correctly, frozen objects are half of the core primitive that SES is based on (if the other half is closures). In this model, a frozen class can be shared between two mutually untrusting parties. Object.freeze
on a class currently eliminates all communication channels. Even with private fields not freezing, there is still no communication channel unless the class specifically decides to expose one with public methods.
However, if static public fields go in in an un-freezable way, you'll be able to communicate between mutually untrusting contexts that share the frozen class--rendering Object.freeze
less useful than it currently is in any situation where someone uses a static public field. The "own property" semantics are, under this framework, defensible by default, whereas the accessor semantics are indefensible by default. Of course, the other semantics would be available through decorators, we're just discussing what the default should be.
Specifically, explicit getters and setters are a choice by the author to have a non-trivially-freezeable public interface - all getters and setters are power user features, used explicitly; simply having a public static field isn’t likely to indicate the programmer’s explicit knowledge that they’ve opened up a communication channel.
Not that it carries much weight, but my support for copy-on-write inheritance of static public fields (i.e., because they as just plain properties on constructors) was explicitly contingent upon removal of static private fields and replacement of functionality via class body lexical declarations.
Maybe we could do this for private fields, but we fundamentally can't do this for public fields
Agreed.
In this model, a frozen class can be shared between two mutually untrusting parties. Object.freeze on a class currently eliminates all communication channels.
👍
rendering Object.freeze less useful than it currently is in any situation where someone uses a static public field explicit getters and setters are a choice by the author to have a non-trivially-freezeable public interface... simply having a public static field isn’t likely to indicate the programmer’s explicit knowledge that they’ve opened up a communication channel.
So my take away from these points is that we need trivially-freezable fields, and that users will likely expect fields to be freezable (because they're used to own-fields).
Would adding a second method to freeze accessors (both explicit and fields) be too much? If the goal is to eliminate any communication channels, explicit accessors aren't trivially freezable (unless they're supposed to allow communication?). Something like Object.freezeSetters
, which would silently noop setters. Or would this be getting to far into "redoing the object model", one of the original objections?
Couple that with general dissemination that static fields are actually accessors, and require a freezeSetters
call to properly freeze.
I think a separate proposal to add a way to change all configurable setters to ThrowTypeError as part of freezing would be interesting; but i don’t think it’s existence would convince me that using getters and setters here is intuitive.
Would adding a second method to freeze accessors (both explicit and fields) be too much? If the goal is to eliminate any communication channels, explicit accessors aren't trivially freezable (unless they're supposed to allow communication?). Something like Object.freezeSetters, which would silently noop setters. Or would this be getting to far into "redoing the object model", one of the original objections?
Couple that with general dissemination that static fields are actually accessors, and require a freezeSetters call to properly freeze.
I'm not sure how you would identify relevant setters and freeze them. All I can think of is an extension to the object model, e.g. possibly a proxy trap. At that point, we would probably want to consider how to freeze other things, e.g. Maps.
Even if that were done, we would still have the lack of parallelism with ordinary properties in objects and prototype chains. Would this all be worth it?
I'm not sure how you would identify relevant setters and freeze them. All I can think of is an extension to the object model, e.g. possibly a proxy trap.
Wouldn't it just be adding another branch to SetIntegrityLevel?
Else level is "frozenAccessors",
For each element k of keys, do
Let currentDesc be ? O.[[GetOwnProperty]](k).
If currentDesc is not undefined, then
If currentDesc.[[Set]] is not absent, then
Let desc be the PropertyDescriptor{[[Set]]: throwerThing}.
Perform ? DefinePropertyOrThrow(O, k, desc).
Even if that were done, we would still have the lack of parallelism with ordinary properties in objects and prototype chains. Would this all be worth it?
I'm still just trying to find out all the details that need to be considered. At this point, I think it's just if anyone else has input.
Wouldn't it just be adding another branch to SetIntegrityLevel?
Oh, you'd overwrite all setters, not just the ones that are for static public fields? Huh. This feels a little ad-hoc to me. Not all mutations to an object are by setters, and setters don't always mutate the underlying object.
Oh, you'd overwrite all setters, not just the ones that are for static public fields?
At the moment. If the goal is no communication channels, then yes. If explicit setters are meant to be a channel, then this won't work.
This feels a little ad-hoc to me... and setters don't always mutate the underlying object.
Isn't that what we've defined a static field? 😉
From the OP (and yours), they'd be let variables, not actually mutations on the constructor.
Not all mutations to an object are by setters
That'd still be covered by Object.freeze
. It'd be a double-tap to ensure totally-frozen-can't-modify-anything-at-all:
const o = {};
Object.freeze(o);
Object.freezeSetters(o);
Edit: doh, hit enter too early.
Isn't that what we've defined a static field? 😉 From the OP (and yours), they'd be let variables, not actually mutations on the constructor.
Yes, in this alternative, static fields would be setters. "let variables" is really more of a desugaring device for this thread.
My point here was that other setters might be used for things that have nothing to do with static public fields, and then Object.freezeSetters
would strangely nullify them.
That'd still be covered by Object.freeze. It'd be a double-tap to ensure totally-frozen-can't-modify-anything-at-all:
There are other sorts of other kinds of mutations which are not covered by either, such as the one in Map.prototype.set. This could be addressed by a new metaobject protocol operation.
In the end, we decided to promote the original, non-accessor-like semantics to Stage 3.
A few objections were made that the shadowing semantics of the current static inheritance at the Jan meeting. Instead, semantics more like Java's or C#'s might be better?
These would be inherited normally through prototypes, eliminating shadowing:
And could be extended to private statics, now that we don't have any ambiguity where the reads/writes go (go to defining class), through coping of the private methods at class extension time (I can't seem to find the PR @littledan wrote with that):