tc39/proposal-private-fields-in-in

Explicit first class private name alternative

littledan opened this issue · 18 comments

In the July 2020 TC39 meeting, @hax mentioned a goal of reification for private names. Previously, @erights and @waldemarhorwat discussed the option of these first class values for private name brand checks.

I wrote a gist to explore some concrete details about how object-based explicit references to private names (as @erights was encouraging) could work to permit exception-free brand checks to the presence of private names in classes.

With this idea, the syntax #x in obj would be replaced by (private.name #x).has(obj)--a little bit longer, yes, but it accomplishes the same function. private.name creates a fresh object with methods to access the private name.

This approach would be compatible with the membrane-transparency goals that @erights has advocated for. In particular, you could pass this private name as well as the object over a membrane, and the membrane system can unwrap them both to do the check appropriately. This would be impossible if the private name were passed around as a primitive directly--if ergonomic brand checks are reliable and do not use method calls in its semantics, then the membrane system has no chance to unwrap the object.

Overall, I don't know how to weigh the ergonomics goal that @ljharb has articulated with the reification goal that @hax has articulated. To me, neither one seems to be an absolute requirement, but rather tradeoffs about value judgements that we as a committee can make. I hope this gist helps us explore more of the design space.

(I wonder if, for the ergonomics goal, we could be looking more to @erights and @waldemarhorwat 's guards proposal, which used the :: syntax to do a check about whether a JS value matches a guard. This could be a longer-term solution compatible with a near-term capability of first-class private names for "has" checks. Guards are also based on first-class values.)

In that case, I would expect (private.name #x) in obj to work, instead of the very strange .has approach (that private fields have WeakMap semantics doesn't mean normal users will ever really think about that).

I'm not really sure how to make that work with membranes... We could rename the has method to in, but I don't really think it's feasible to make the in operator with this approach. Why is the in operator a requirement for this feature?

It’s not a requirement, it’s what i (and i think users) will expect. Private fields are property-like, and in is how property presence is tested.

I can understand that some developers might prefer this particular proposal, but we're trying to iterate based on feedback from committee. Do you think it won't be possible/sufficient to learn/use the method-based syntax I'm proposing?

I think it will be unintuitive and hard to discover. The use of the private operator, when it's not used for private fields otherwise, is confusing; and its precedence forcing use of inline parens is also awkward.

I agree that your suggestion achieves the goal of "detect if an object has a field, without try/catch", but I don't think it does so in a satisfactory way, and I don't think it actually addresses the mental model concerns from @hax's objection.

Thanks for explaining. I guess I just don't see the syntax I am suggesting as that bad. (I think the parens would actually be optional, as private.name PrivateName would be a PrimaryExpression.)

I would be interested in hearing more from @hax on his thoughts on both the syntax and the mental model concern. Maybe I misunderstood his past comments, so clarification would be helpful. I thought @hax was specifically suggesting something like this.

hax commented

This would be impossible if the private name were passed around as a primitive directly--if ergonomic brand checks are reliable and do not use method calls in its semantics, then the membrane system has no chance to unwrap the object.

@littledan I'm not sure I fully understand this part, do u mean current ergonomic brand check syntax could break membrane transparency?? And in previous discussion of private fields , I remember someone have proved that even private symbol could still keep membrane transparency.

we could be looking more to @erights and @waldemarhorwat 's guards proposal

This is a very interesting idea. Do u have a more concrete example that how it should look like?

hax commented

that private fields have WeakMap semantics doesn't mean normal users will ever really think about that

@ljharb I think this is one key disagreement we have. My point is users will not think about it because currently there is no refication. Reification would build a concept model, in only make sense if reification is symbol.

hax commented

Private fields are property-like, and in is how property presence is tested.

I also discussed it in last meeting. Private fields are property-like only in syntax aspect, not semantic. This is not very obvious if users only write o.#x (very close to get own property syntax/semantic), but when use #x in o users will be lead to think about the reflection-related syntax/semantic of property which private fields never have.

hax commented

and I don't think it actually addresses the mental model concerns from @hax's objection.

To be clear, my objection have three points. 1. syntax of this proposal should depend on the semantic of reification; 2. KEY in o syntax implies o[KEY] syntax; 3. too fast to advance a proposal from stage 0 to 3 in 4 months.

So (private.name #x).has(o) at least addressed my second point. And it also match the first point --- private.name #x actually belongs to reification proposal.

Note, whether reification should use map-like or symbol semantic is a separate issue, and whether the syntax of (private.name #x).has(o) is ergonomic enough is also a separate issue.

hax commented

I thought @hax was specifically suggesting something like this.

To be clear my objection is not based on any specific alternative solution, what I mean is we can only make choice until we figure out the path of reification first --- this proposal should depend on reification proposal.

I'm not convinced about point 2: if KEY is an identifier, then yes I'd expect o[KEY], but the fact that #x is not an identifier (and that o[#x] is a syntax error) disconnects it from that model. On the other hand, trying to reify private names introduces significantly more complex mental models that I honestly have a much more difficult time reasoning about ("does the private name have the object, or the object have the private name?", etc). If reification has value in itself then it should be its own separate proposal, but let's not block this proposal on that.

@littledan I'm not sure I fully understand this part, do u mean current ergonomic brand check syntax could break membrane transparency?? And in previous discussion of private fields , I remember someone have proved that even private symbol could still keep membrane transparency.

@hax To clarify, private fields, like WeakMaps and internal slots, are not Proxy-transparent. They can work through membrane systems that unwrap things at the appropriate point. I presented on this at TC39 with this slide deck. Various kinds of private symbols were proposed at TC39, and they were all found to not meet certain constraints that committee participants raised.

The membrane issue is specifically that, if private names were primitives, then when passing them through a membrane, you couldn't add the unwrapping logic to it, the way you can for, say, a WeakMap. I think this proposal, by only allowing lexically present private names to be used, doesn't run into issues with membranes, since the class body is usually run after everything is unwrapped, whereas if you pass the name outside, you may want to use it on something membrane-wrapped.

hax commented

Various kinds of private symbols were proposed at TC39, and they were all found to not meet certain constraints that committee participants raised.

I remember private symbol could satisfy membrane transparency with some Proxy API small additions (but not very sure, @jridgewell may know that).

Anyway, if we finally will have reification in any form , i hope the syntax could match the semantic, for example, if it's weakmap, it shouldn't use in.

Private symbols can work with membranes, but it requires non-trivial changes to Proxy and the membrane implementations. It's much simpler to use a map-based API for them.

@hax @jridgewell OK, these sound like arguments for the get/set/has API which is proposed in the gist that I linked.

There's a conceptual disconnect between a weakmap-inspired API (get/set/has) and the idea of a private property, which I assume would confuse more developers than the disconnect between #x in obj/x in obj vs obj.#x/obj[x].

If we want get/set/has, then I'd suggest finding a way to name the API to make it clear that this is a map representation of the property.

This proposal reached stage 3 at today's TC39.