1 of 22

Set Methods

Part III

2 of 22

Previously:

decided instance methods should use

internal slots on `this`, but should use the

public API on `arg`.

3 of 22

But how exactly do you access the public API?

4 of 22

Set.prototype.union = function(arg) {

let argIterator = %GetIterator(arg)%;

let resultList = new %List%;

for (let item of argIterator)

%AddItemToList%(resultList, item);

for (let item of this.[[SetData]])

%AddItemToList%(resultList, item);

let result = new %Set%;

result.[[SetData]] = resultList;

return result;

};

Set.prototype.union

5 of 22

Set.prototype.intersection = function(arg) {

let thisSize = this.[[SetData]].length;

let argSize = %ToNumber%(arg.size);

let argHas = %BoundFunctionCreate%(arg.has, arg, []);

let argIterator = %GetIterator%(arg);

let resultList = new %List%;

if (thisSize > argSize) {

for (let item of argIterator)

if (%ListContainsItem%(this.[[SetData]], item))

%AddItemToList%(resultList, item);

} else {

for (let item of this.[[SetData]])

if (argHas(item))

%AddItemToList%(resultList, item);

}

let result = new %Set%;

result.[[SetData]] = resultList;

return result;

};

Set.prototype.intersection

6 of 22

Why does the `.intersection` algorithm vary by size?

Because it's big-O worse if you don't.

Consider `small.intersect(large)` or `large.intersect(small)`.

Always iterating the argument means the first is slow.

Always doing membership tests on the argument means the second is slow.

7 of 22

We can't just convert the argument to a Set, because that requires iterating the whole argument, which is a potentially expensive operation we're trying to avoid.

8 of 22

So how do we access the public API?

9 of 22

Several options.

10 of 22

0: Using internal slots on the argument

Previously decided this was not acceptable (means you can't pass a Proxy for a Set, among other downsides).

Not revisiting this option.

11 of 22

1: `[Symbol.iterator]` and `.has`

This has IMO unacceptable behavior if you pass a Map.

map.has acts as if a Map is a Set of keys.

map[Symbol.iterator] does not: it returns entry objects.

First behavior means you get the intersection with the set of keys of the map. Second means you get the empty set.

12 of 22

1: `[Symbol.iterator]` and `.has`

So `set.intersection(map)` works sometimes:

only if `map.size >= set.size`. (!!!)

"Does it quack like a duck" should be a property of the interface of the argument, not its precise value.

13 of 22

2: `.keys` and `.has`

This means `union` and `intersection` perform iteration in a different way: seems bad.

Also has the same problem as Map for at least some userland types.

14 of 22

class IndexedSet {

#set = new Set;

#list = [];

get size() { return this.#set.size; }

add(v) {

if (!this.#set.has(v)) {

this.#set.add(v);

this.#list.push(v === 0 ? 0 : v);

}

}

// membership, consistent with `Symbol.iterator` but not `keys`

has(v) { return this.#set.has(v); }

at(i) { return this.#list.at(i); }

// indices, consistent with `at` but not `has`

keys() { return this.#list.keys(); }

[Symbol.iterator]() { return this.#list.values(); }

}

15 of 22

It's fine for Map or IndexedSet not to work

as an argument at all, but not for them

to only work sometimes.

16 of 22

This is a fundamental problem with using String-named methods for this API.

17 of 22

3: `[Symbol.iterator]` and `[Symbol.SetHas]`

Add a new Symbol (name/location TBD) for Set membership testing.

Map would not implement this symbol.

Now everything works.

18 of 22

3: `[Symbol.iterator]` and `[Symbol.SetHas]`

Maybe also do `[Symbol.SetSize]`?

19 of 22

4: Some other way to declare an object supports being passed to `.intersection` and `.difference`?

For example, a [Symbol.isSet] whose presence promises that .keys and .has are consistent.

This adds another observable property access.

20 of 22

Discussion

21 of 22

Where do the new symbols live?

Please not `Symbol`.

Maybe `Set.protocol.has`?

This will set precedent going forward.

22 of 22