tc39 / proposal-collection-normalization

MIT License
41 stars 8 forks source link

What happens when iterating over a rekeyed Map? #7

Closed zenparsing closed 5 years ago

zenparsing commented 5 years ago

If I call Symbol.iterator on a Map where rekey has been specified, what does the list contain?

let userMap = new Map(undefined, {
  rekey({email}) {
    return email;
  }
});

userMap.set({ email: "x@y.z" }, 42);
userMap.has({ email: "x@y.z" }); // true, correct?

let users = [...userMap];

Am I correct in assuming that it would result in the following?

[ ["x@y.z", 42] ];

If so, do we think that the following would be an issue?

let user = { email: "x@y.z" };
userMap.has(user); // true
[...userMap.keys()].includes(user); // false

Thanks!

ljharb commented 5 years ago

I would expect has to use the rekey as well - along with every operation that takes a key as input or provides one as output.

bmeck commented 5 years ago

I would expect has to use the rekey as well - along with every operation that takes a key as input or provides one as output.

I'm not sure what this means regarding the .includes example above.

bmeck commented 5 years ago

@zenparsing I assume key/value hooks in this proposal are only applied to when performing operations that are seeking to generate / inspect internal data, not when extracting the data. I would indeed expect this to mean the result of [...userMap.entries()] to be [ ["x@y.z", 42] ];.

If so, do we think that the following would be an issue?

[...userMap.keys()].includes(user); // false

I do not think this is an issue because they are different operations. They are not intended to my knowledge to be consistent and are not enforced by any part of the language to be consistent amongst types of maps or even if a user mutates the builtin. I would expect code trying to extract the internal data structures to be treated separately from those working on the Map (or subclass of Map), which could contain more logic around the internal structures like .entries().

ljharb commented 5 years ago

Although a subclass can alter that consistency, I would expect that a builtin collection that reports has for a key would absolutely include the key in the iteration - why would we want the rekeying to not apply to everything?

One use case I have is to make a base Map/Set that distinguishes negative and positive zero, but this is only possible if i get to entirely control and encapsulate the "normalized" key away from consumers, so they only see and interact with the "outer" or "unwrapped" key.

(to clarify, this would mean no subclass and no overriding methods, and all builtin methods could be reliably .call-ed on the instance and it would work the same)

bmeck commented 5 years ago

@ljharb given:

let rekeyOps = 0;
function rekey(person) {
  rekeyOps++;
  return person.name;
}
const bobA = {name: 'bob'};
const bobB = {name: 'bob'};
const ppl = new Map([], {rekey});
ppl.set(bobA, 'is here');
ppl.set(bobB, 'is missing');
const key = [...ppl.keys()][0];

What would you expect key and rekeyOps to be? Ideally I would like to be able to prevent us from having to run rekey for all extraction operations, and not prevent us from being able to GC values passed to rekey operations.

bmeck commented 5 years ago

To clarify, I think if extraction hooks are desired they can/should be done in a different proposal.

ljharb commented 5 years ago

I think conceptually that "rekey" must apply to every single place the key is touched; whether it's insertion or extraction has no bearing on that.

In that example, I would expect rekeyOps to be 3, and key to be 'bob'. In other words, each .set runs rekey, as does each iteration, as would .has, .delete, etc.

bmeck commented 5 years ago

@ljharb if key is "bob" wouldn't that invalidate your ability to rewrite the internals to have different key when inspecting from extracting? I don't understand how given your comment above you could use the the hook behavior you describe to make a map with -0 and 0 differentiated.

ljharb commented 5 years ago

In your example, you're reducing the information available (down to the "name"). For my case, I'd envision something like:

function rekey(key) {
  if (typeof key === 'string') {
    return `s-${key}`
  }
  if (key === 0) {
    return `zero-${Object.is(key, -0) ? 'neg' : 'pos'}`;
  }
  return key;
}

but of course, now that i type that, I see that for it to truly work as I'd expect i'd also need to provide another function in reverse :-/

domenic commented 5 years ago

It does seem any truly encapsulated re-keying functionality needs to be able to map in both directions.

For my use cases, I would prefer the has() call to return false; I actually want a "normalize key" hook. Maybe worth a separate issue :/

bmeck commented 5 years ago

@domenic can you explain that in a separate issue, I'm not sure what is being requested.

bmeck commented 5 years ago

@zenparsing does https://github.com/tc39/proposal-richer-keys/tree/master/collection-rekey#when-are-the-normalization-steps-applied in the FAQ satisfy this question? Since it explains that normalization is only applied when looking for the entry Record in [[*Data]] or when updating underlying [[*Data]] not when getting values out of the underlying [[*Data]].

domenic commented 5 years ago

I guess it might be good to add the output of .keys() to that code example, to make it really clear.

bmeck commented 5 years ago

Added a clarifying example in https://github.com/tc39/proposal-richer-keys/commit/610f5d01ebe1c7445959827c929a8d0fc5d7102e