- Maybe some people see a one-or-the-other choice, but I see a spectrum of possibilities. And, I think each choice this proposal contains was well-thought-through and justified. We're not talking about "private symbols" as a first-class thing, but rather some subtle semantic changes for reading, writing and defining private fields and methods. The idea to use WeakMap-analogous semantics was partly motivated by a sort of impossible-to-refute logic about not introducing additional things when WeakMap exists, but let's step back from that a little bit and think about what would be best for JavaScript programmers with this as its own feature. In this presentation, I summarize the semantic choices for private fields and methods, which might be different if we adopted private symbol semantics, and the motivations in the committee (not necessarily all goals I share personally) for the semantics being the way they are. If we're revising the semantics, let's agree that we have consensus on these modifications to design goals.
- Search up the prototype chain on read of a private field
- This is what might provide memory savings for private methods in V8's hidden class representation, so that the existing prototype chain can be used, rather than needing a separate representation of the list of private brands in the hidden classes (since these can get out of sync). And other JS engines have a broadly analogous hidden class representation to V8's. (More notes at #implementation)
- Observable impact: [[GetPrototypeOf]] is called--observable if a Proxy is hit in the prototype chain
- Design goals this meets:
- Observability: If you refactor code between, e.g., private methods and lexically scoped functions, there is no observable impact ever
- Defensibility: There should be no way to make a change to the instance which messes with the operation of internal private method calls, whether that's causing an error in case which previously didn't have an error, or removing an error from a previous error path. The ability to dynamically mutate the prototype (either of the instance directly, or higher up in the prototype chain) enables these changes in both directions.
- (+ analogy to WeakMaps)
- Note, the above goals are about observability and defensibility with respect to a particular instance; to enable techniques to make the class hierarchy more defensible/less observable for instantiation (where the current state is a consequence of how ES6 classes work, but doesn't necessarily indicate that future things should be not-defensible-by-default), see #invariants below
- Proxy transparency: private fields/methods on the proxy vs on the target
- Note: Third option (throw an error when adding private on Proxy) not available, as this would give a way to test if an object is a Proxy, contradicting a Proxy design goal
- Design goals this meets:
- It is possible to basically fully encapsulate the target using a Proxy; you can make a Proxy that leaks nothing unintended (maybe not so important if "everybody" uses shadow targets, but then what's the point of the target anyway?)
- Private field semantics are analogous to internal slot semantics, which don't forward to Proxy targets
- (+ analogy to WeakMaps)
- Checking [[IsExtensible]] before adding private fields or methods (not sure if you're proposing this)
- Note, adding private fields to already-"born" objects diverges from internal slots, but this seems to be the only way to let subclasses declare private fields and work coherently with ES6's dynamic prototype chain semantics without doing two passes up and down the prototype chain on instantiation
- Design goals this meets:
- Defensibility: A consumer of an instance can call Object.preventExtensions on it, preventing the "normal" operation of the class (which might add more private fields or methods later, even if that's an anti-pattern)
- (Observability not really an issue, as the constructor would have to choose to return a Proxy that does the observation)
- (+ analogy to WeakMaps)
- Errors vs undefined for reading missing private field (not sure if you're proposing this)
- Design goal this meets:
- Make it easier to catch programming errors
- Provide a brand checking mechanism analogous to how built-in internal slots are used
- (No analogy to WeakMaps)
- Design goal this meets:
- Errors on adding private fields outside of declarations (not sure if you're proposing this; also I don't know whether you'd propose that these writes go up the prototype chain)
- Design goal this meets:
- Encourage a relatively fixed shape, which is more optimizable (even if there are map changes during instantiation, it should be helpful to have those mostly settled down by the time instantiation is done).
- Define private fields up-front, in a way that's analogous to internal slots (analogy only holds for base classes, strictly speaking)
- Be analogous for brand checks for reads to private fields
- (No analogy to WeakMaps)
- Design goal this meets:
- (Note, as far as reification goes: the PrivateName draft meets "integrity" goals that Symbol does not, since it was deemed especially important that PrivateName have this integrity. But I'm also happy to leave this aside, for the decorators proposal.)
- Search up the prototype chain on read of a private field
- Note, it's possible that we have an issue of misunderstanding, if even TC39 delegates are getting confused about what invariants there are. Here are the invariants I'm aware of: (#invariants)
- If an instance has the private methods, that means that it started to run that particular (sub)class constructor, no more, no less
- This property can't be spoofed through Proxy wrapping prototype chain manipulation.
- You can build higher-level guarantees on top of this, e.g., if you freeze the constructor, and the constructor calls super(), you can show that the super constructor was also executed. But, this is a very conditional guarantee because of decisions we made in ES6 classes.
- To address the documentation issue, Neil Kakkar is working on MDN documentation for class fields and private methods (and the educators group is working on documentation for many proposals); we'll try to clarify (but are currently working through the basics). Probably we should also update the explainer (do before meeting!)
- Documentation and education is the only comprehensive way to prevent this sort of "confused deputy problem"--it's always possible that someone could develop incorrect beliefs about how the language works, no matter what we decide. We can just do our best to choose something which, subjectively, is relatively intuitive and useful.
- To the concrete issue of the original prototype chain not being taken, see the class.initialize proposal and Object.setImmutablePrototype proposal (both forthcoming)
- Implementation thoughts (#implementation) -- not clear how much time to spend on this point in committee
- Design goals:
- Private fields and methods should be (at least) as efficient to read/write/call as their public equivalents
- There should not be per-method, per-instance space overhead for private methods (e.g., as a naive private field representation for private methods would have)
- Implementations should not be extremely, prohibitively complex; ideally, machinery for public properties can be reused
- These goals should hold in various cases, to avoid time/space cliffs:
- In the interpreter/baseline compiler as well as in the optimizing compiler
- In warmed ICs as well as the first time code runs at all
- In objects with a shared, stable hidden class, as well as objects which degrade into hashtable-mode
- If brand checks prove expensive to implement for private methods, and we're comfortable with it, let's consider just removing the checks, as originally proposed (link to spec change, and rebase it). This makes more sense to me than adding all these things that can be observed and manipulated from outside of the class.
- However, we're working on the implementation in V8 and JSC, and working through any implementation difficulties. We're also in touch with the SpiderMonkey team, which has a pretty analogous, optimistic plan about how to handle this implementation.
- Design goals:
-
-
Save littledan/f4f6b698813f813ee64a6fb271173fc8 to your computer and use it in GitHub Desktop.
Private Symbol presentation response
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It's possible to refuse it, because
WeakMap
doesn't provide full set of features thatSymbol.private
has. Don't you agree with it?I'm not sure that I understand the goal of this list. Could you please clarify it somehow?
Should it be considered as advantage of
Symbol.private
approach comparing toWeakMap
-like?It seems like it already works like this. Are there are any proposals that violate this stuff?
Seems like
Symbol.private
approach serves this purpose better thanWeakMap
-like. Consider following code:rewritten to
It seems that both
Symbol.private
andWeakMap
-like approaches satisfy this statement/requirement.Could you please clarify meaning of this quote?
Both
Symbol.private
andWeakMap
-like approaches gives us an opportunity to build properMembrane
.Why it should be like so? What problem is solved by such restriction? Do, you understand, that such restriction creates other problems?
Could you please clarify meaning of this quote?
Do you mean, that
[[IsExtensible]]
shouldn't be called before addingprivate
property, so it could appear onfrozen
object later?Could you please clarify meaning of this quote?
Throwing an
Error
instead of returningundefined
IMPLICITLY addsbrand-checks
to ALL private properties accesses and prevents only one tiny set of typos (I can't even call them bugs), like:Do you understand that existing tooling (e.g. TypeScript, Flow and linters) prevents such typos much better (also adding a whole bench of other usefulness) than ES will ever be able to (unless it introduces type checking system)?
Why do you want to add IMPLICIT
brand-check
which already causes issues for proxies, when there is very convenient way of EXPLICITbrand-check
withSymbol.private
approach, that will be used only in cases where it's really necessary? And obviouslySymbol.private
could be used forbuilt-in internal slots
withbrand-check
added if it's needed.What analogies to
WeakMaps
you want to add everywhere in the list?Why do you prefer very small optimization over the huge set of really useful patterns that can replace a lot of
WeakMap
usages with something that much more optimizated in terms of memory and GC, has much more ergonomic syntax and proxy-safe?What does
integrity
mean here?So implicit
brand-check
again.True for both
Symbol.private
andWeakMap
-like approaches.How does it reate to class-fields at all?
Ok, it seems to be irrelevant to my topic of interest (
Symbol.private
vsWeakMap
-like semantic), or did I misunderstood something?As far as I understand both approaches satisfy this goals, because nobody raised such issues for existing proposal and
Symbol.private
will just easily reuse majority (if not all) of optimization that already there for public properties.Does it mean that
brand-check
actually isn't such an important requirement, if you are to move it? In this case, why not to tunnelprivates
troughProxy
? In the end why notSymbol.private
ifbrand-check
the only big difference between them?That is great, but don't we get into situation where implementation leads design and not the opposite (which is more correct)?