-
Notifications
You must be signed in to change notification settings - Fork 62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Should Record wrappers have a null prototype? #71
Comments
Not speaking to the reasons, but: every non-nullish primitive must have a boxed object form and thus a prototype; this is an axiom of the language so far. iow, |
I'm not sure I understand the multiple must statements as axioms as these value types are not necessarily primitives, which already is a loose term as the definition of primitive isn't set beyond a list of types.
This is a good point, but I think the weight of having mutable immutable values is enough of a counter to warrant more discussion than an abrupt statement about requiring a single design. These types do need protocols to be cased out in the spec even if they delegate so there effectively is special casing in some sense. It seems the value of these types are high enough that we should weigh if they are special, and if they are not special why they shouldn't be inheriting from It seems discussion can come towards documenting or exploring:
|
@bmeck they're either primitives or objects; each category brings with it a number of criteria for consistency, such as:
etc. |
I don't see how they must be one or the other, but do see value in keeping consistency. I would note there are some exceptions with Overall these all seem up for discussion and not actual requirements to my knowledge and seem to already have a variety of edge cases in the spec. |
Indeed, that is true - strings are an exotic legacy exception, one I would not wish to see repeated. In your case, that's because |
I don't see how the string issues like having keys would be prevented, Tuples and Records have their "own" properties. |
Something I don't understand is how this case differs from, say, |
@littledan we saw presentations about issues with integer indexed types at TC39 and issues of how they are a great source of issues. I'm not necessarily convinced in light of that presentation that duplicating some pattern that has historically caused issues is a good path to take. |
@bmeck Huh, that doesn't match my takeaway from the presentation. I guess my takeaways were things like, 1) Don't forward integer property access up the prototype chain (we could do this whether or not we have a null prototype) 2) Don't go back and make pre-existing things subclassable/add another Anyway, it'd be great to get a review from @natashenka at some point on this proposal! |
To bring the discussion back around to the original question in the issue, here are some thoughts I have regarding prototypes for records and tuples: All non-nullish primitives so far have an exotic "boxing object" wrapper equivalent that has a prototype (it must, because it is an object). In terms of the exotic object wrapper's prototype for On mutable prototypes, or Further, on "custom prototypes", the current proposal does not specify any ability to define a "custom" prototype for a |
(It would still make sense to me to have |
@ljharb I think that would make sense, preventing |
This doesn't seem to have grounding to me. The usefulness of Tuple.prototype is being stated as being useful to allow extending Tuple.prototype from what I am reading. I don't believe this is in purely the realm of SES as prototypes and boxings do have affects when shared across Realms (whatever your VM/host is calling them / frames / whatever). If you mutate a prototype in 1, you have to mutate it in other Realms to keep the code consistent. A lot of my concerns are around if the previous path is a good path. Using a prototype seems to me as though we are just using a new namespace very similar to Object.prototype and stating that it won't be a problem this time around but I'm not seeing evidence about why it won't be a problem. I do see some claims that we already do things this way, so we should continue them, but not reasoning about why it differs from the problems of the heavily inherited builtins like Object since this proposal is very much in the same level of utility. |
@bmeck Do you have another suggestion for the semantics? Personally, I'm open to |
@littledan I think we should think about the reasons we never add things to
Given the potential of banning holes on Tuples, and Tuples thus having a constrained set of property names being usable I think it is probably fine to use a prototype for Tuples as the collisions for own properties are purely on numeric indices and length which are likely not problematic for developers as the usage of Tuples is entirely coupled to items in numeric indices and the length. I do think there are problems with allowing overriding string named properties like However, for Records I do not think it wise to give them a prototype. I cannot think of a reasonable way in which we could safely ever put anything on it and would think it to be the same for developers of libraries and applications. A developer must check for any sort of behavior alteration in order to understand how a Record may be used since they are effectively arbitrary key/values unlike Tuples. Given things like A potential compromise is to give Tuples a prototype and a null prototype for Records or give Records a empty prototype that is not extensible for now and see if we can loosen it later if we want to continue arguing (strict mode is stuck throwing anyway); I believe that this is potentially fine even if they are not completely mimicking a duality Object/Array since Object.prototype has proven problematic and it would be extremely difficult to prove to me that Record is not in a similar situation without also proving that extending Object.prototype is reasonable/something people should rely on given the plethora of things like lint rules and dangers in the wild. |
Record.prototype imo has to be an object, and a mutable one, to be able to polyfill future additions, including Symbol-based protocols that wouldn’t have any conflict with Record keys. |
I do see issues in the wild with using a prototype, but those are based upon conflicts with own keys and things like spreading naively. If the prototype cannot have conflicts with keys, I have no concrete objections to a prototype. I do have strong objections to the conflicts as we have seen real world issues with |
@littledan No, to be clear, I think |
Right, my question was, if you could elaborate on the protocols that we might want to add later for Objects and Object-like things like Records. Thinking through the use cases could help us understand whether we need the Tuple-like treatment of allowing further methods. (Even if this need is clear to you, it might not be clear to everyone on the thread.) |
There was discussion during the stage 4 advancement of object spread about a possible symbol protocol that could let an object override what Object.assign/spread saw; that would apply conceptually to records as well, I’d think. |
Maybe someone could develop that idea as a more detailed proposal/investigation, so we can understand the implications better. That could help us work through this issue. Anyone interested? Are there any other ideas protocols that would potentially make sense? (Pattern matching???) Beyond this one, I'm pretty skeptical that it'd make sense to add more protocols to ordinary objects. Even for spread, I could imagine that a symbol-based protocol could be used to override behavior, but then the default behavior (if no method is found) could be the current one. This would give a good combination of reliability for simple cases like Records with extensibility for other types. |
I'd be interested in forming a document on any results we form about these design discussions and conclusions so that they can apply towards things in the future. |
Sorry in advance if this is a silly question, but are the Tuple and Registry prototypes sealed? Is it possible to apply something like Object.setPrototypeOf (# {}, {...})? |
@pabloalmunia you can't change the [[Prototype]] of a primitive; the prototype object is as mutable as any other prototype in the language. |
Right, the operation to set the prototype would throw when applied to Record or Tuple wrapper objects. @ljharb If we do add a protocol for rest/spread, it will need to treat the absence of a Symbol-based method (i.e., Get returns undefined) to lead to the current behavior--this would be necessary for objects with a null prototype to work as they do currently. In this case, I think it would be fine for Records to also take that null-prototype path. (And maybe we shouldn't even bother adding the symbol to Object.prototype, I'm not sure.) That wouldn't exclude us from creating a protocol that gets used when the symbol is added, though. Do you have any ideas for other protocols that we'd need for Object.prototype, or reasons why this story wouldn't work? |
I don't; if we added such a protocol, the presence of it would have to override the default behavior (which, you're correct, would be the same in the absence of the symbol). In other words, if i added |
In my humble opinion, you can write some as: Record.prototype[Symbol.whatever] = () => ...;
#{}[Symbol.wathever]() When you get the property, the abstract operation GetV (https://tc39.es/ecma262/#sec-getv) is called and this operation ToObject(V) is called too. As a result, you obtain the object wrapper before to get the property. |
Yes, as long as Record primitives inherit from Record.prototype (see #71 (comment)) |
@ljharb @bmeck @rricard @rickbutton @devsnek and I had a call where we discussed this issue. To summarize some key points:
After the call, I looked at the callers of ToPrimitive, and I think they all should throw when applied to Records and Tuples. Maybe we could think of ToPrimitive as "convert to atomic primitives" which doesn't include Records and Tuples--that seems to be what the current callers are after. We discussed two possible paths @ljharb concluded that, if we take the integrity goals at high priority, that he'd prefer A to B, as B seemed a bit too exotic. I have trouble understanding the concern about wrappers being exotic objects, since lots of things (e.g., String and Tuple wrappers, and TypedArrays) behave exotically with integer indices, and Record wrappers will likely be specified as exotic objects anyway, but I'm fine with settling on A. I'd like to tentatively conclude that, for now, we'd have a null prototype and follow option A, but be open to considering B until Stage 3, when we should draw a final conclusion. I think this exploration of the design space, where we've found multiple plausible options including one good enough for experimentation in the playground, is sufficient for demonstrating viability for Stage 2. |
Now we don't allow symbols in records. but what if we allow them in the future? |
@Jack-Works To select option B, we'd have to be OK with ruling out Symbols as property keys for Records in the future (or being OK with them not having these same integrity properties). I'm currently leaning towards option A. |
This comment has been minimized.
This comment has been minimized.
@ljharb The two were never in contrast... |
This was included before, and turns out to be needed in the context of #71
If a developer wants a prototype, while still maintaining immutability, would using Ex: const myRecord = #{
__proto__: Object.prototype
}; Allowing opt-in prototypes? |
I'd hope not; my understanding is that it'd just be a normal string |
Hmm... it would definitely break immediate script author's expectations, but considering that it's from annex B I would personally be fine with it. So, that code above would just throw a type error due to the value of the simple string property I really think I'm just too used to storing functions in arrays/objects, if/when this proposal passes and is implemented, I'll get used to it. |
The current spec defines a null prototype for Records. This is a decision that has been working well so far. The rationale has been explained earlier by @littledan here: #71 (comment) Finally, to clarify: |
a later proposal stage)
the semantics. (we can add things in follow-on proposals)
in-the-weeds.
I have concerns about value types using prototypes.
Object.prototype
is soo widely used that mutating and adding properties to it is virtually impossible withoutSymbol
s I feel like the same would become true forRecord
quickly. Likewise, adding methods toArray
is very fraught with compatibility problems and have similar concerns forTuple
. Do we have reasons that we want property delegation to occur on the value itself vs something like using static methods instead? Is the usage of mutable prototypes intended to be for consistency or some other reason?Could prototypes be added later if desired? It seems mutable prototypes goes against some of the intentions of immutable structures as properties on the prototype could be added or removed at runtime thereby affecting things like
record.isSafe
fromconst record = {| foo: 1 |}
by the prototype delegation.The text was updated successfully, but these errors were encountered: