Feedback on the Relationship strawman

# David Bruant (11 years ago)

Feedback based on strawman:relationships

Curiosity question: What do "geti" and "seti" refer to? (the i specifically)

Is it necessary for r to be able to be a string or a symbol? It looks superflous, but maybe there is a good reason.

In the [[GetRelationship]] algorithm, if r.[[Get]](@geti) is undefined, then return x.[[Get]](r) but what is this supposed to do if r is an object? x[r] would coerce r to a String before passing it to [[Get]], but here the object is passed directly without coercion. I feel this question is answered step 5.I of interaction with proxies which talks about stringification.

I wonder if implicit stringification is necessary. I'm afraid most of the time, it'll mean "[Object object]" will be passed as key and result in an unexpected behavior (collision of keys, etc.) I would be in favor to either do nothing or throw when r.[[Get]](@geti)/r.[[Get]](@seti) is undefined. In any case, devtools can show a warning saying that r didn't have a @geti or @seti property if necessary.

That the private "fields" make a relationship only to a value and don't follow the property descriptor path is a good feature. The idea of using Object.getOwnPropertyDescriptor on private symbols has a bad taste to it.

It's not written in the strawman explicitly, but if this proposal is in, all the whitelist/unknownPrivateSymbol trap mess is going away. Probably for the best :-)

It's taking me some time to understand the Map/WeakMap behavior. Sharing my understanding step by step:

var o = {};
var wm = new WeakMap();
wm.set(o, 12);
wm.get(o);

WeakMap.prototype.set === WeakMap.prototype[@seti]

So wm.set(o, 12); is equivalent to o at wm = 12 and o at wm is equivalent to wm.get(o, 12), unless o isn't a key in which case o.[[Prototype]] is attempted. I don't understand the need to climb the prototype chain of the key. Hmm... It's true that in o.azerty, 'azerty' is looked up on o, then its prototype, then its prototype, etc.

Nits: no need to declare this as argument of [[(Weak)MapGetInherited]]. Also, maybe you want to use the ES6 included [[GetPrototype]] instead of [[Prototype]] (I'm not sure about this one).

Are there default @geti/@seti for Object.prototype?

# Tom Van Cutsem (11 years ago)

2013/4/11 David Bruant <bruant.d at gmail.com>

Hi,

Feedback based on doku.php?id=strawman: relationshipsstrawman:relationships

Context: Mark and I have been putting this together after Mark's earlier presentation of this idea at the previous TC39 meeting. We have been privately discussing this with Sam and Allen, but haven't reached consensus yet. We welcome input from the broader community.

Curiosity question: What do "geti" and "seti" refer to? (the 'i' specifically)

"i" for "index" (the field is used as an index into the receiver object)

Is it necessary for r to be able to be a string or a symbol? It looks superflous, but maybe there is a good reason.

First, I don't think Mark meant to propose a new concrete type "Relationship". The word "relationship" (and the variable "r") is only used as a collective to describe all the values that can be used as indices of objects. In this light, an index r can be a string, a (unique) symbol, or a (private) field.

The renaming from "private symbol" to "private field" is intentional: unique symbols and private symbols ended up being very different, so better to give them distinct names.

In the [[GetRelationship]] algorithm, if r.[Get] is undefined, then return x.[Get] but what is this supposed to do if r is an object? x[r] would coerce r to a String before passing it to [[Get]], but here the object is passed directly without coercion. I feel this question is answered step 5.I of interaction with proxies which talks about stringification. I wonder if implicit stringification is necessary. I'm afraid most of the time, it'll mean "[Object object]" will be passed as key and result in an unexpected behavior (collision of keys, etc.)

Yes, but this is also the behavior we have today, right? Nothing new under the sun. I even wonder whether we can change this, wouldn't it be backwards-incompatible?

I would be in favor to either do nothing or throw when r.[Get]/r.[Get] is undefined. In any case, devtools can show a warning saying that r didn't have a @geti or @seti property if necessary.

That the private "fields" make a relationship only to a value and don't follow the property descriptor path is a good feature. The idea of using Object.**getOwnPropertyDescriptor on private symbols has a bad taste to it.

Indeed. Another reason why we renamed "private symbols" to "private fields".

It's not written in the strawman explicitly, but if this proposal is in, all the whitelist/unknownPrivateSymbol trap mess is going away. Probably for the best :-)

Absolutely.

It's taking me some time to understand the Map/WeakMap behavior. Sharing my understanding step by step: var o = {}; var wm = new WeakMap(); wm.set(o, 12); wm.get(o);

WeakMap.prototype.set === WeakMap.prototype[@seti] So wm.set(o, 12); is equivalent to "o at wm = 12" And "o at wm" is equivalent to "wm.get(o, 12)", unless o isn't a key in which case o.[[Prototype]] is attempted. I don't understand the need to climb the prototype chain of the key. Hmm... It's true that in "o.azerty", 'azerty' is looked up on o, then its prototype, then its prototype, etc.

Exactly. The proto-chain walk was introduced to make private field access resemble normal JS property lookup. When this is not desirable, one can continue to use the plain WeakMap get/set methods to associate state with an object.

Nits: no need to declare 'this' as argument of [[(Weak)MapGetInherited]]. Also, maybe you want to use the ES6 included [[GetPrototype]] instead of [[Prototype]] (I'm not sure about this one).

Yes, [[GetPrototype]] is probably the right internal method. The spec language on the strawman page wasn't yet written to conform to the proper ES6 style guidelines.

Are there default @geti/@seti for Object.prototype?

Not sure if this is necessary: what would these default implementations do? Probably just stringify their |this| value and use that as a string index. That's no different from the current fallback semantics if @geti/@seti are undefined.

# David Bruant (11 years ago)

Le 11/04/2013 10:39, Tom Van Cutsem a écrit :

2013/4/11 David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>>

In the [[GetRelationship]] algorithm, if r.[[Get]](@geti) is
undefined, then return x.[[Get]](r) but what is this supposed to
do if r is an object?
x[r] would coerce r to a String before passing it to [[Get]], but
here the object is passed directly without coercion. I feel this
question is answered step 5.I of interaction with proxies which
talks about stringification.
I wonder if implicit stringification is necessary. I'm afraid most
of the time, it'll mean "[Object object]" will be passed as key
and result in an unexpected behavior (collision of keys, etc.)

Yes, but this is also the behavior we have today, right? Nothing new under the sun. I even wonder whether we can change this, wouldn't it be backwards-incompatible?

I'm only interested in the semantics of the new syntax, so backward-compat isn't a concern. o1 at o2 coercing o2 to a string when it's an object would be consistent with what we have today, but I don't believe today's o1[o2] behavior is desirable. Maybe I'm wrong. Does anyone have examples of code making good use of the implicit o2.toString() for o1[o2]?

I feel that either .toString returns a constant string in which case a string would be more appropriate or the string used as key is generated dynamically in which case, this is more of a Map use case than an object use case.

Also, the consistency wouldn't be perfect. Currently o1[o2] coerces o2 into a string unconditionally while o1 at o2 would if o2 is run-time-conditionnally lacking @geti (same for @seti). In terms of use, we'll see what people answer to the above question, but in o[x], x is rarely expected to be an object, while in o at x, x is expected to be an object (hopefully with @geti/@seti).

Are there default @geti/@seti for Object.prototype?

Not sure if this is necessary: what would these default implementations do?

The same thing that private symbols would do.

Probably just stringify their |this| value and use that as a string index. That's no different from the current fallback semantics if @geti/@seti are undefined.

I think I better understand the mismatch between what I understood and your intention. I expected relationship and the @ syntax to substitute to private symbols while it seems you "only" wanted to provide the basic mechanism that people can build on top of. Take the following example: var o = {}, r = {}, r2 = {};

 o at r = 12;
 console.log(o at r); // 12
 console.log(o at r2); // ?

With the strawman as it is and string coercion, the second console.log would log 12 giving the false impression that o and r2 "have a relationship" or "are in a relationship" (is my wording confusing? :-s). I don't believe this is a good default. A sensible Object.prototype default @geti/@seti would pretty much turn any object inheriting from Object.prototype into a private field and o at r2 would be undefined (since no value has been previously added). Of course, that's just a default. People could re-configure or shadow, etc. I feel it is a more useful default. It provides something very close to what people expect from private symbols which this strawman is aiming at replacing.

Regardless of the default Object.prototype implementation, I think the current default with null-[[Prototype]]'d objects isn't good either. The string coercion will lead to the exact same confusion I noted above with o at r/o at r2.

# Tom Van Cutsem (11 years ago)

2013/4/11 David Bruant <bruant.d at gmail.com>

Le 11/04/2013 10:39, Tom Van Cutsem a écrit :

Yes, but this is also the behavior we have today, right? Nothing new under the sun. I even wonder whether we can change this, wouldn't it be backwards-incompatible?

I'm only interested in the semantics of the new syntax, so backward-compat isn't a concern. o1 at o2 coercing o2 to a string when it's an object would be consistent with what we have today, but I don't believe today's o1[o2] behavior is desirable. Maybe I'm wrong. Does anyone have examples of code making good use of the implicit o2.toString() for o1[o2]?

Ok, you're right. The new syntax does give us some leeway to change this behavior. The benefit of sticking to current semantics is that refactoring o[r] to o at r is less prone to unintended changes. But there's something to be said for o at r never stringifying r.

Are there default @geti/@seti for Object.prototype?

Not sure if this is necessary: what would these default implementations do?

The same thing that private symbols would do.

Under this proposal, a private field (aka private symbol) is simply a {Weak}Map. The @geti/@seti of a {Weak}Map will retrieve or store the binding like its get/set methods would.

Normal Objects cannot store Object->Object bindings, only String->Object

bindings, so I don't see how the default @geti/@seti implementation on Object.prototype could do the same thing as those on {Weak}Map.prototype.

Probably just stringify their |this| value and use that as a string index. That's no different from the current fallback semantics if @geti/@seti are undefined.

I think I better understand the mismatch between what I understood and your intention. I expected relationship and the @ syntax to substitute to private symbols while it seems you "only" wanted to provide the basic mechanism that people can build on top of. Take the following example: var o = {}, r = {}, r2 = {};

o at r = 12;
console.log(o at r); // 12
console.log(o at r2); // ?

With the strawman as it is and string coercion, the second console.log would log 12 giving the false impression that o and r2 "have a relationship" or "are in a relationship" (is my wording confusing? :-s). I don't believe this is a good default.

I can see how this is confusing. Perhaps if, in o at r, r is neither a string nor a unique symbol nor a private field, we should throw rather than coerce to string. Deferring to Mark.

# David Bruant (11 years ago)

Le 11/04/2013 14:23, Tom Van Cutsem a écrit :

2013/4/11 David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>>

Le 11/04/2013 10:39, Tom Van Cutsem a écrit :
Yes, but this is also the behavior we have today, right? Nothing
new under the sun. I even wonder whether we can change this,
wouldn't it be backwards-incompatible?
I'm only interested in the semantics of the new syntax, so
backward-compat isn't a concern.
o1 at o2 coercing o2 to a string when it's an object would be
consistent with what we have today, but I don't believe today's
o1[o2] behavior is desirable. Maybe I'm wrong. Does anyone have
examples of code making good use of the implicit o2.toString() for
o1[o2]?

Ok, you're right. The new syntax does give us some leeway to change this behavior. The benefit of sticking to current semantics is that refactoring o[r] to o at r is less prone to unintended changes.

It could be considered by authors to use syntax to differentiate when they do something public ([] and .) and something private (@). This visual distinction would only follow the trend of using _properties in JavaScript to denote something expected to be private; convention also used in Dart to denote privacy.

I'm doubtful of the use of strings and symbols in the right-hand side.

    Are there default @geti/@seti for Object.prototype?


Not sure if this is necessary: what would these default
implementations do?
The same thing that private symbols would do.

Under this proposal, a private field (aka private symbol) is simply a {Weak}Map. The @geti/@seti of a {Weak}Map will retrieve or store the binding like its get/set methods would.

Normal Objects cannot store Object->Object bindings, only String->Object bindings, so I don't see how the default @geti/@seti implementation on Object.prototype could do the same thing as those on {Weak}Map.prototype.

By using a WeakMap under the hood. Trying to give a try of what I would expect; this is a functional definition using code, not a formal specification (especially when it comes to how storage is performed):

 (function(){
     // nested weakmaps
     var mappings = new WeakMap();

     Object.prototype[@geti] = function(o){
         var thisMappings = mappings.get(this);
         if(!thisMappings)
             return undefined;
         return thisMappings.get(o);
     }

     Object.prototype[@seti] = function(o, v){
         var thisMappings = mappings.get(this);
         if(!thisMappings){
             thisMappings = new WeakMap();
             mappings.set(this, thisMappings);
         }

         thisMappings.set(o, v);
     }
 })();

// following this definition, we have:

 var o = {}, r = {}, r2 = {};

 o at r = 12;
 console.log(o at r); // 12
 console.log(o at r2); // undefined
 console.log(r at o); // undefined

As I wrote "Object.prototype[] = ", I realized that this is global authority that opens an undesired communication channels. If that's the case, another idea would be to be able to generate such @geti/@seti pairs (so that only parties with access to a given pair have access to the related storage). Per-class pairs could be generated to have per-class private properties, etc.

# Tom Van Cutsem (11 years ago)

2013/4/11 David Bruant <bruant.d at gmail.com>

It could be considered by authors to use syntax to differentiate when they do something public ([] and .) and something private (@). This visual distinction would only follow the trend of using _properties in JavaScript to denote something expected to be private; convention also used in Dart to denote privacy.

I'm doubtful of the use of strings and symbols in the right-hand side.

There's something to be said for using the o at r syntax only for private fields.

Under this proposal, a private field (aka private symbol) is simply a {Weak}Map. The @geti/@seti of a {Weak}Map will retrieve or store the binding like its get/set methods would.

Normal Objects cannot store Object->Object bindings, only String->Object bindings, so I don't see how the default @geti/@seti implementation on Object.prototype could do the same thing as those on {Weak}Map.prototype.

By using a WeakMap under the hood. Trying to give a try of what I would expect; this is a functional definition using code, not a formal specification (especially when it comes to how storage is performed):

I think this is unnecessarily complicating the proposal. By defining a standard Object.prototype[@geti] method that uses a WeakMap under the hood, potentially every object now needs this hidden WeakMap storage.

The code you wrote seems a fine abstraction that can be provided by a library.

As I wrote "Object.prototype[] = ", I realized that this is global

authority that opens an undesired communication channels. If that's the case, another idea would be to be able to generate such @geti/@seti pairs (so that only parties with access to a given pair have access to the related storage). Per-class pairs could be generated to have per-class private properties, etc.

Let's first try to get consensus on what is already there before adding more features.

Thanks for the feedback, Tom