Skip to content
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

add initial text #1

Merged
merged 14 commits into from
Dec 15, 2023
Merged

add initial text #1

merged 14 commits into from
Dec 15, 2023

Conversation

acutmore
Copy link
Owner

brain dump ( duplicate PR to the bloomberg one)

Copy link
Collaborator

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only read the readme so far, but it looks good overall!

README.md Outdated
key1 === key2; // separate objects
CompositeKey.equal(key1, key2); // true
CompositeKey.equal(key1, key3); // false
Reflect.ownKeys(key1); // [] - opaque empty object from the outside
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it just a {}, or is it an instance of CompositeKey? Is it frozen or mutable?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I've updated the README now. They are instances of CompositeKey and not frozen. Also I think it was unclear before, that each call returns a fresh instance. So they are more 'normal' in that sense, like other types in the language. But they are special that CompositeKey.equal checks the internal-slot of the argument, so only real instances work.

README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated
r1.x; // 0
r1 === r2; // false

let s = new Set([r1], { keyBy: Symbol.keyBy });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So keyBy is allowed to not be a function? Is this a shorthand for (x) => x[Symbol.keyBy]()?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and yes, though it could have additional semantic meaning such as throwing a TypeError if the value does not contain the string/symbol. The main motivation for this is not changing the default keying semantics of a plain new Set() while also making it convenient to switch to a Map / Set that does look up the protocol.

However this could also be achieved by the keyBy option only accepting a function keeping the core constructor API cleaner and adding a static helper method maybe something like: new Set.keyed(), new KeyedSet(), or new CompositeKey.Set()

README.md Outdated

### Map and Set config (phase 1)

Allow `Map` and `Set` instances to be customized with a lookup function that will produce the value that represents the keys values for that collection. In this mode the representative values are considered equal if they are `SameValueZero` equal, or if they are both `CompositeKey`s and equal according to `CompositeKey.equal`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be a way to opt-out of the CompositeKey special equality semantics in these collections? Without it, how can a program associate data to specific instances of a CompositeKey (besides the return override + private fields stamping hack).

One solution may be to use the CompositeKey equality semantics only when keyBy option is used?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah exactly. The CompositeKey is only treated special when it is returned by the keyBy function.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I don't disagree with this choice, I'll point out that maybe the answer should be "don't do that"? If CompositeKey just had 100% value semantics when used as a map key then that's one less point of complexity, and I'm struggling to come up with a reasonable use case where someone should want to distinguish. Also, SameValueZero already has differences from any other equality predicate (=== and Object.is, specifically) so the fact that maps are automatically collapsing two things that aren't strictly equal into a single key isn't inconsistent with existing behavior for ±0 or NaN. That said, I think this would actually make it quite a bit harder to polyfill as well. But I wanted to get both sides of the argument on paper.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think this could go either way, but really I'm not convinced application developers should be using so many CompositeKeys directly.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: #2

README.md Outdated
- The downside to this is that when comparing if two values are equal by comparing their `CompositeKeys`, both values need to produce a full `CompositeKey` rather than doing this gradually and exiting early as soon as one part does not match. A separate API for this use case could avoid this issue.
- What about `WeakMap` and `WeakSet`?
- More investigation required.
- a `keyBy` function for these makes less sense because if the function returns a new value then the only reference to that value will be held weakly and therefore eligible for collection.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would disagree. For example a keyBy implementation that returns a CompositeKey would have its special equality semantics apply.

README.md Outdated
Comment on lines 185 to 186
- A `new CompositeKey(0, 0)` does not hold any lifetime information, a matching `CompositeKey` can always be created if the _inputs_ are available.
- Allowing only `CompositeKeys` that were created from at least one value that itself is allowed as a `WeakMap` key could be an option.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, only composite keys composed of at least one object eligible as WeakMap key would itself be allowed as WeakMap key.

README.md Show resolved Hide resolved
- CompositeKey construct no longer varargs
- `keyBy` Map/Set config only accepts function
- Add Map.usingKeys helper instead
- Make `Symbol.keyBy` a method rather than a getter
README.md Outdated Show resolved Hide resolved
README.md Outdated
Comment on lines 97 to 99
let key1 = new CompositeKey([0, 0]);
let key2 = new CompositeKey([0, 0]);
let key3 = new CompositeKey([0, 1]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the switch to requiring an array wrapping the values? I actually find this more confusing, making people think arrays are somehow special.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was commented that varargs makes it hard/impossible to expand in the future. e.g can't add an options bag.
The first arg could be any iterable similar to Map/Set.

I have also added a CompositeKey.of(v1, v2, ...vs) to keep the varargs style. The thinking here is that this is like Temporal where the constructors are very raw exposing the lowest-level of functionality, and then there are static factories that have nicer APIs.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose there is precedent with the Set and Map constructors.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the varargs style better; I don't think we need this to take an array/iterable.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess while new Map, Promise.all and friends take iterables; Array is Array(1, 2, 3) (ignoring the footgun overload for 1 argument) and CompositeKey is somewhat like an Array and I can't think of future arguments we may want to pass to the constructor. Varargs does feel cleaner which is why I initially went with it.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #4

}
```

### Records and Tuples (follow on?)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing not mentioned in this readme is the difference with the current R&T proposal. As primitive values they round tripped through the ShadowRealm callable boundary, which had a lot of interesting properties.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be good to note, but if implementations do not want to add more primitives, then I think this is a good path through.

- ECMA262 aims to be as deterministic as possible (`Date.now()` and `Math.random()` being examples of the few exceptions) and backwards-compatible with previous versions; this would mean that built-in hash functions would most likely need to be fully specified and limited ability to evolve the hashing algorithm. Exposing these low level details _may_ also pose a security risk.
- What about membranes?
- More investigation required.
- Out of the box `CompositeKey` won't work across membranes because their uniqueness is encoded within an internal slot. Membranes would need to add explicit support for re-constructing CompositeKeys when used across a membrane.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah existing membranes would need to be updated to become fully transparent. I think we agreed that any new global API is fair game for introducing such a change, as long as there is a path to making the membrane transparent again.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so, in particular, if you pass the constructor over the membrane barrier, it will then wrap its parameters properly and all will be well, except that comparisons in maps may not work. An upgrade to the membrane system would be needed to recover the intended equality semantics, but not provide any sort of "security" issue, right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So full transparent membranes don't have any intrinsic security properties, the question is whether the membrane is transparent or not.

The problem is when one side expects to reach behavior of an object that isn't available on the object's "surface". In this case, it's whether a program using a Map/Set with keyBy on one side would expect to be able to use a CompositeKey instance from another side, or if the CompositeKey global from one side (as a constructor or its static methods) would expect to be able to use the instances from the other side.

I think it's likely fine to say right now that CompositeKey instances behave like plain objects through a membrane unaware of CompositeKey, and so we simply don't have the special equality semantics.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If CompositeKeys had Symbol.keyBy on them and this was looked up by Map's keyBy that could be where the membrane can add support for object's keys working across membranes. (objects from blue-realm work in the keyBy map of red's realm). The red-proxy-trap on Symbol.keyBy would get blue's compositeKey and use a Map to get a consistent red-object that represents that key, then put that object in a Red CompositeKey and return that.

So yeah, out-of-the-box CK across membranes would only have regular object identity when used as keys, but support could be added.

README.md Outdated
this.value = value; this.leafs = leafs;
}

getKey() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this isn't a realistic example...? Having getKey() be O(n) seems less than ideal.

It doesn't map very well, but our "interned value type" alternative to R&T does everything shallowly, which seems like a better approach. But maybe there's no possible analogue with this paradigm.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this example is slightly confusing more because of how early it is in the README, and people also might wonder what the relationship between getKey and Symbol.keyBy is. Maybe you could have a much simpler example here, showing that CompositeKey.equals(new CompositeKey(new CompositeKey(1, 2), 3), new CompositeKey(new CompositeKey(1, 2), 3)) === true. And save the classes for when you're actually overloading [Symbol.keyBy] (and then for something fixed-size and not recursive).

README.md Outdated

### Map and Set config (phase 1)

Allow `Map` and `Set` instances to be customized with a lookup function that will produce the value that represents the keys values for that collection. In this mode the representative values are considered equal if they are `SameValueZero` equal, or if they are both `CompositeKey`s and equal according to `CompositeKey.equal`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I don't disagree with this choice, I'll point out that maybe the answer should be "don't do that"? If CompositeKey just had 100% value semantics when used as a map key then that's one less point of complexity, and I'm struggling to come up with a reasonable use case where someone should want to distinguish. Also, SameValueZero already has differences from any other equality predicate (=== and Object.is, specifically) so the fact that maps are automatically collapsing two things that aren't strictly equal into a single key isn't inconsistent with existing behavior for ±0 or NaN. That said, I think this would actually make it quite a bit harder to polyfill as well. But I wanted to get both sides of the argument on paper.

README.md Outdated
return v;
}
}
new CompositeKey([lookupKey(position1), lookupKey(position2)]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a little surprising to me, actually. Given the analogue to Array.of, I wouldn't expect any transformation to happen on the elements. I see the value here, but I wonder if a different name wouldn't be prudent to avoid the assumption.

- This allows any hashing and equality operations to be done lazily on API access. Compared to `CompositeKey(0) === CompositeKey(0)`, which would require either an eager global intern-table or for `===` to be have a special overload for these objects.
- Why are the `Map` and `Set` changes opt-in, and do not work with existing default constructors `new Map()` and `new Set()`?
- Adding `Symbol.keyBy` to an object could invalidate existing code that assumes `Map` and `Set` will use object identity.
- The opt-in mode can be strict, and throw an Error if a value does not implement `Symbol.keyBy` rather than silently falling back to object identity.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to be able to add Symbol.keyBy to existing types without breaking the web then this would be the only way to have any chance at that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On one hand yes, this is the safest strategy. On the other hand, it might be OK if people don't depend on it too much (sometimes JSON serializability is added later, and that kinda has the same issue). Anyway I prefer this strategy.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still wondering whether we should require that the keyBy option of collection should be a CompositeKey.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps CompositeKey or primitive? Seems OK for something to want to keyBy a number or string without needed to put that value in a CK.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, I meant that. keyBy option of collections would require a primitive or CompositeKey, and similarly the R&T constructors would require that a nested object has a Symbol.keyBy that returns a primitive or CompositeKey. That way it'd be consistent for any Map.usingKeys()


Immutable values types such as those in Temporal could implement `Symbol.keyBy`, without requiring users to work out the best way to represent these types using a `CompositeKey`.

## Q+A
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see some discussion of mutability here. In particular, I'm convicted that keying a mutable object by any mutable property is a Bad Idea. This was impossible with the old R&T, but nothing here even mentions it as a concern. I don't see a good way to outright remove this footgun, but I wonder how the system will react if (when) it happens. Suppose an object is added to a set under one key and later keyBy returns something different - what happens if the first key is added again?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're talking about "stability of the keyBy function" and not mutability itself. R&T got around this by not exposing a user-defined function. But Bradley correctly determined that this functionality is needed, despite this risk. I think we should just live with this, define exactly what the algorithms do (to imply how they handle inconsistencies--probably by ignoring them) and key things by the returned keyBy.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we discussed we have 2 options with different trade-offs:

  • We allow keyBy to return different values for the same object used a collection key. This will result in the possibly of duplicate keys when iterating and potentially the inability to delete some entries.
  • We disallow keyBy to return a different value for keys already in used in the collection. This effectively requires the implementation to keep an internal map from the key to the previous keyBy result.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I don't see the issue as mutability, but mutation - iow, codebases and APIs that simply don't mutate work just fine without immutability. iow, using objects that are treated as immutable is the same as using actually immutable ones in practice, most of the time.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I'd like to support the use case of mutable exits in a record / tuple without symbol indirection like required for a primitive R&T proposal. This can be accomplished in the current state of this proposal by an object implementing the keyBy protocol, which returns CompositeKey([this]). As long as keyBy is required to return a primitive value or CompositeKey value, I think it would express sufficient opt-in to structural equality semantics.

- What about membranes?
- More investigation required.
- Out of the box `CompositeKey` won't work across membranes because their uniqueness is encoded within an internal slot. Membranes would need to add explicit support for re-constructing CompositeKeys when used across a membrane.
- What about `WeakMap` and `WeakSet`?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dumb question, but I suppose it's worth making explicit - is there any reason to allow WeakMaps to have keyBy? My gut says that defeats the purpose, but without working through the detailed implications, I don't have a solid explanation for why.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably something like, to avoid the need to build a trie for this purpose. Rick Button made a trie like that when implementing the R&T polyfill. I'm not sure how common a need this really is.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have one of those trie as well. Actually we have a few, none which would be needed if we had composite keys.
A common use case is memoization of a function based on multiple arguments.

README.md Outdated

### Map and Set config (phase 1)

Allow `Map` and `Set` instances to be customized with a lookup function that will produce the value that represents the keys values for that collection. In this mode the representative values are considered equal if they are `SameValueZero` equal, or if they are both `CompositeKey`s and equal according to `CompositeKey.equal`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think this could go either way, but really I'm not convinced application developers should be using so many CompositeKeys directly.


Immutable values types such as those in Temporal could implement `Symbol.keyBy`, without requiring users to work out the best way to represent these types using a `CompositeKey`.

## Q+A
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're talking about "stability of the keyBy function" and not mutability itself. R&T got around this by not exposing a user-defined function. But Bradley correctly determined that this functionality is needed, despite this risk. I think we should just live with this, define exactly what the algorithms do (to imply how they handle inconsistencies--probably by ignoring them) and key things by the returned keyBy.

- This allows any hashing and equality operations to be done lazily on API access. Compared to `CompositeKey(0) === CompositeKey(0)`, which would require either an eager global intern-table or for `===` to be have a special overload for these objects.
- Why are the `Map` and `Set` changes opt-in, and do not work with existing default constructors `new Map()` and `new Set()`?
- Adding `Symbol.keyBy` to an object could invalidate existing code that assumes `Map` and `Set` will use object identity.
- The opt-in mode can be strict, and throw an Error if a value does not implement `Symbol.keyBy` rather than silently falling back to object identity.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On one hand yes, this is the safest strategy. On the other hand, it might be OK if people don't depend on it too much (sometimes JSON serializability is added later, and that kinda has the same issue). Anyway I prefer this strategy.

- ECMA262 aims to be as deterministic as possible (`Date.now()` and `Math.random()` being examples of the few exceptions) and backwards-compatible with previous versions; this would mean that built-in hash functions would most likely need to be fully specified and limited ability to evolve the hashing algorithm. Exposing these low level details _may_ also pose a security risk.
- What about membranes?
- More investigation required.
- Out of the box `CompositeKey` won't work across membranes because their uniqueness is encoded within an internal slot. Membranes would need to add explicit support for re-constructing CompositeKeys when used across a membrane.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so, in particular, if you pass the constructor over the membrane barrier, it will then wrap its parameters properly and all will be well, except that comparisons in maps may not work. An upgrade to the membrane system would be needed to recover the intended equality semantics, but not provide any sort of "security" issue, right?

- What about membranes?
- More investigation required.
- Out of the box `CompositeKey` won't work across membranes because their uniqueness is encoded within an internal slot. Membranes would need to add explicit support for re-constructing CompositeKeys when used across a membrane.
- What about `WeakMap` and `WeakSet`?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably something like, to avoid the need to build a trie for this purpose. Rick Button made a trie like that when implementing the R&T polyfill. I'm not sure how common a need this really is.

README.md Outdated Show resolved Hide resolved
Copy link
Collaborator

@mhofman mhofman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An advantage of introducing all elements at once is that we can have stronger requirements on the values allowed for each feature, in particular require that [Symbol.keyBy]() returns a primitive or CompositeKey, that R&T similarly require every value to be a primitive or object with Symbol.keyBy, and possibly even that the keyBy option of Collections must return a primitive or CompositeKey.

As I mentioned last time, the program can still express all keying equivalence they wish were these restriction not present by simply wrapping a regular object they would have used directly, into a CompositeKey containing only that object.

- How does this compare to the original Record&Tuple proposal?
- Here R&T are just plain objects and arrays, not primitives
- They do not enforce deeply immutable structures
- They can contain any value, including functions
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we should make the R&T only accept primitives or objects that have a Symbol.keyBy to require explicitly opting to an exit of an immutable structure.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could allow R&T to include any value at construction time. It's unlikely that a function would implement Symbol.keyBy but might be common to want to reference a function directly.

Instead the error is delayed until the R/T's Symbol.keyBy is invoked, e.g. when it's put into a keyed Map/Set, and that would then throw a TypeError. This is similar to Python where Tuples can contain values than don't implement hashable, but when trying to get the hash of that tuple it then errors.

This allows users to still easily use R/T if they are only interested in the getting the shallow immutability Object.freeze sugar, and not making use of keyBy.

What do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I suppose that highlights another problem: the mutable value in these records/tuples may drop their @@keyBy at anytime, and even if that property doesn't change or is captured at creation, it could change its return value too.

I think we basically need to decide when the [Symbol.keyBy]() call is performed, whether at construction or lazily when the record's [Symbol.keyBy]() is itself called, in which case whether the values should be cached or recomputed every time.

acutmore and others added 2 commits August 17, 2023 13:59
Co-authored-by: Jordan Harband <ljharb@gmail.com>
CompositeKey.equal(key1, key3); // false (nesting keys does not flatten them)
```

### Symbol.keyBy (follow on?)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the last call we discussed what the proposal might look like not including a symbol protocol.

The reason for including the protocol would be to create a separation between the APIs that have this object based equality and the objects themselves. For example building a Set::<Temporal.PlainMonthDay>. The Set could be constructed with a generic call like Set.usingKeys(), and has no knowledge about Temporal.PlainMonthDay instances specifically; it only knows to call Symbol.keyBy and expect a CompositeKey (or primitive) in return. It is left to the Temporal.PlainMonthDay class to be responsible for providing a good implementation of the Symbol.keyBy method.

However a similar separation could also be achieved with an API like Set.usingKeyBy(Temporal.keyBy). Set on its own does not know about Temporal, and Temporal is responsible for providing the keyBy implementation. This API makes things very explicit. The main downside of this would be that it becomes harder to create Sets of a mixture of different domains. With the symbol protocol it would relatively straightforward to create a Set that contains a mixture of both Temporal and URL objects, (assuming they both implemented the protocol). However it might be that this type of use case is rarely required, and the majority of object based Sets would only need to contain a single domain of objects and this domain would be known at the point where it is constructed.

Another place where the Symbol protocol could be useful is with the Record and Tuple composite value discussed in this proposal. The generic protocol would allow other object types to participate in the built-in keyBy equality of the Record/Tuple. e.g. two Tuples containing two of the semantically same Temporal.PlainDate (maybe they mark the start and end dates of an event) objects, then the Symbol.keyBy of these two Tuples would be equal. The built-in Symbol.keyBy of a Record/Tuple would be there to ideally cover the vast majority of use-cases out-of-the-box without needing code authors to keep having to implement the protocol themselves. This is simpler to other languages that support record/tuple like values and have hash/equality protocols (e.g. Kotlin/Swift).

The downside of Record and Tuple looking up the Symbol.keyBy of their constituents is that this makes the Symbol.keyBy protocol Record and Tuple both more dynamic and observable. If Record and Tuple equality was limited to the the equality of privates and other Records and Tuples then much of how this was implemented exactly could be hidden, e.g. is it calculated eagerly on creation or lazily on demand. And it would be guaranteed to never change. If they look up the symbol then this can be hooked into to observe when the key is being generated, this means the spec would have to encode exactly when the values are looked up, and also if they are cached to avoid changing or called repeatedly risking a changing key. A different way to make it ergonomic to include non R/T values within a R/T and still produce a compositeKey for the structure could be with other helper functions (Object.recursvieKeyBy(obj, fn)) or maybe decorators.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symbol protocols is how this sort of thing is done in the language - why would it be worth exploring an entirely unprecedented form of interop?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ljharb I thought that @acutmore comment made a pretty good summary of the drawbacks of a symbol based approach, namely the intrinsic instability of an equality based on a dynamic deferral to the constituents.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s how all protocols work in the language.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, which is problematic for a feature that expresses equality. An instable mechanism has major issues for this specific use case, which warrants exploring alternative approaches.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like Symbol.hasInstance, for example.

A protocol means you're trusting, implicitly, anything that conforms to the protocol.

@acutmore
Copy link
Owner Author

Thanks all for the input into this initial PR. I think it has served its purpose of allowing any part of the initial text to be directly commented on. I will merge this as is, so discussions can continue in dedicated issues and PRs.

@acutmore acutmore merged commit f327715 into main Dec 15, 2023
@acutmore acutmore deleted the init branch December 15, 2023 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants