Skip to content
This repository has been archived by the owner on Jul 8, 2024. It is now read-only.

RFC: make isSubsetOf like isSupersetOf #41

Merged
merged 2 commits into from
Jan 14, 2019
Merged

Conversation

bakkot
Copy link
Collaborator

@bakkot bakkot commented Dec 29, 2018

My preferred resolution to #35, in PR form because my comment got a little long.

This makes isSubsetOf identical to isSupersetOf except that it iterates its receiver and gets has from its argument, instead of the other way around. Iterating its receiver matches all the other methods in this proposal except isDisjointWith and isSupersetOf, but getting has from its argument makes it unique among all methods in this proposal or in the spec (though isDisjointWith and isSupersetOf are unique in that they get has from their receiver).

@zloirock
Copy link
Contributor

It will break set.isSubsetOf(array). Now it works, other methods also work with non-sets.

@bakkot
Copy link
Collaborator Author

bakkot commented Dec 29, 2018

Yup, that's true. That seems fine to me, but there are other possible resolutions - for example, make all of isSubsetOf, isSupersetOf, and isDisjointWith fall back to the iteration protocol and the original Set.prototype.has when their argument or receiver (as appropriate) does not have a callable has.

@zloirock
Copy link
Contributor

@bakkot it will cause possible conflicts with non-standard .has methods on arrays which available in many old libraries, increasing complexity O(n) -> O(n^2) and many other problems. I think that would be better to leave it as-is.

1. Let _iter_ be ? GetIterator(_set_).
1. If _iter_ is undefined, throw a *TypeError* exception.
1. If Type(_O_) is not Object, throw a *TypeError* exception.
1. Let _hasCheck_ be ? Get(_O_, "has").
Copy link
Member

Choose a reason for hiding this comment

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

How much harder would it be to make it both iterate the receiver and the argument, and not use “has” at all?

@bakkot
Copy link
Collaborator Author

bakkot commented Dec 29, 2018

@zloirock That's true. I expect it to be quite rare; the far more common case will be to pass an array without .has and immediately get a (hopefully informative) error. That seems OK to me, but like I say, there are other alternatives. I don't think the current state is good.

@ljharb, very easy technically. The problem is that it would make this operation O(n+m) complexity, where n is the size of the receiver and m the size of the argument. In the current text (unless someone has overridden Set.prototype.has) and in this PR, it's O(n). That can be a big difference.

@ljharb
Copy link
Member

ljharb commented Dec 29, 2018

In the case where the receiver is a Set with an unmodified Symbol.iterator, would it not be O(m) at that point?

@bakkot
Copy link
Collaborator Author

bakkot commented Dec 29, 2018

I don't think so. It needs to perform an operation for each element of the receiver (i.e., check if it is contained in the argument).

When the argument is a Set with an unmodified Symbol.iterator then it can be done in O(n).

@gsathya
Copy link
Member

gsathya commented Jan 3, 2019

I really like how all these Set methods work out of the box for non Set arguments too. It'd be great if we could have this method be consistent with that.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 3, 2019

Non-Set arguments make sense for the other Set methods, because they only care about iteration of their respective arguments (at least given how they're currently defined). But isSubsetOf only cares about membership testing of its argument. So I think that difference makes sense, personally, especially given how easy it is to construct a Set from an iterable if that's what you want.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 3, 2019

Alternatively, isSubsetOf could fall back to iterating its argument when its argument does not have a callable has. If we did that, I think it would be nice if isSupersetOf and isDisjointWith did the same for their receiver.

@zloirock
Copy link
Contributor

zloirock commented Jan 3, 2019

@bakkot allowing non-set argument is an important case for me. As I wrote, adding new "hasable" protocol could cause too many problems. I can't understand, why you against of internal conversion non-set argument to Set (or it could be something else - anyway, it's unobservable, so it can be optimized) by iteration protocol like it's now in the spec text.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 3, 2019

@zloirock:

As I wrote, adding new "hasable" protocol could cause too many problems

The proposal already adds such a protocol; isSupersetOf and isDisjointWith use it on their receiver. If you just mean that it would lead to confusion when used with libraries which are adding Array.prototype.has, well, as I wrote, I am less concerned about this than you are.

I can't understand, why you against of internal conversion non-set argument to Set

See #35 for that discussion. Anyway, I'm not super against converting non-set arguments to sets (though I am a little against it because of time-complexity concerns, and it is observable, since invoking the Set constructor on an iterable performs an observable lookup of (and calls to the current value of) Set.prototype.add). (Edit: but see above for an alternative which would still allow that.) Mostly my concern is with using [[SetData]] directly when the argument (or receiver) is a set: if isSubsetOf reads the [[SetData]] slot directly if it's present, then it's impossible to write certain kinds of set subclasses and have those behave correctly when used with isSubsetOf. That's unfortunate, and inconsistent with all the other methods in this proposal.

@zloirock
Copy link
Contributor

zloirock commented Jan 3, 2019

The proposal already adds such a protocol;

It's not the same. Those methods are generics, but they are placed on Set prototype, so it's OK if they are work only with set-like objects as this argument. But all the rest methods accept iterables as an argument. Just iterables without extra methods. We should be able to do something like

new Set(iterable1).method(iterable2);

and don't think have those iterables .has methods or not. Array.prototype.has is just an example. We have another standard iterables with .has method, for example, Map and URLSearchParams and result will be different:

new Set([1, 2]).isSubsetOf(new Map([[1, 2], [2, 3], [3, 4]])); // -> ?
new Set([1, 2, 3]).isSupersetOf(new Map([[1, 2], [2, 3]])); // -> ?

Set constructor on an iterable performs an observable lookup

You are right, I forgot about it, however, a new set can be initialized not in the constructor to prevent it.

That's unfortunate, and inconsistent with all the other methods in this proposal.

Yes. However, the rest options look worse. As an option, we could convert an argument to internal Set or something else with O(1) lookup even if this argument already Set.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 3, 2019

We should be able to do something like new Set(iterable1).method(iterable2); and don't think have those iterables .has methods or not.

I understand the appeal, but... not all methods are the same. Some methods need to iterate their argument; others need to query membership in it. (And there are other kinds of method too, like .has - is it similarly bad that .has doesn't iterate its argument?) Different operations and have different requirements.

As an option, we could convert an argument to internal Set or something else with O(1) lookup even if this argument already Set

Yeah, that's proposed above. As I say, I think making what should be an O(size of receiver) into an O(size of receiver + size of argument) operation is bad.


We have another standard iterables with .has method, for example, Map and URLSearchParams and result will be different

I mean, yes, but the current proposal's behavior for those objects is strictly less useful than this PR's, so I'm not sure what point you're making.


As yet another alternative, how would you feel about a symbol-based protocol for membership querying? I'm not proposing we depend on that proposal, just that we add a Set.has symbol (or something) and have Set.prototype[Set.has] === Set.prototype.has, then change all uses of thing.has in the proposal to thing[Set.has]. Then you could say that .isSubsetOf would use this method if available and fall back to the iteration protocol otherwise. This would let classes explicitly opt-in to working with .isSubsetOf if they supported membership querying.

@ljharb
Copy link
Member

ljharb commented Jan 3, 2019

A symbol-based protocol seems much more robust, at least - especially if it could be genericized to work on Map, Set, and Array.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 3, 2019

One more point I keep forgetting to mention: in the current proposal, set.isSubsetOf(weakSet) doesn't work. It does under this PR (or this alternative or this one). To me it feels like it should.

@zloirock
Copy link
Contributor

zloirock commented Jan 3, 2019

I understand the appeal, but... not all methods are the same. ... Different operations and have different requirements.

At least, it should be consistent with the paired method - isSupersetOf.

One more point I keep forgetting to mention: in the current proposal, set.isSubsetOf(weakSet) doesn't work.

It should not work anyway because weak collections are not iterable and we can't make work set.isSupersetOf(weakSet).

I mean, yes, but the current proposal's behavior for those objects is strictly less useful than this PR's, so I'm not sure what point you're making.

I think that it at least more useful than removing [[SetData]] dependency.

As yet another alternative, how would you feel about a symbol-based protocol for membership querying?

I'm not a fan of adding well-known symbols and methods to many built-ins for this too narrow case. It's over-complication of this method.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 3, 2019

At least, it should be consistent with the paired method - isSupersetOf.

It's already inconsistent. As it must be, because isSupersetOf reverses the requirements for the argument and receiver from isSubsetOf. With this PR they would at least use the same mechanisms, rather than one and only one of them relying on internal slots.

It should not work anyway because weak collections are not iterable and we can't make work set.isSupersetOf(weakSet).

I don't share this intuition at all. isSupersetOf obviously cannot work. But isSubsetOf just as obviously can, and should.

I think that it at least more useful than removing [[SetData]] dependency.

I'm sorry, I don't know what this means.

@gsathya
Copy link
Member

gsathya commented Jan 3, 2019

So I think that difference makes sense, personally, especially given how easy it is to construct a Set from an iterable if that's what you want.

I'd rather have this be part of the spec. It's pretty much impossible to optimize away the new Set in the baseline tier if we ask the user to create a Set.

Mostly my concern is with using [[SetData]] directly when the argument (or receiver) is a set: if isSubsetOf reads the [[SetData]] slot directly if it's present, then it's impossible to write certain kinds of set subclasses and have those behave correctly when used with isSubsetOf.

Can you give me an example of this? We're pulling out the has method from the sub class so that should make this work for all sub classes right?

@zloirock I'm hiding your comment as it's really not helping the discussion.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 3, 2019

@gsathya:

I'd rather have this be part of the spec. It's pretty much impossible to optimize away to the new Set in the baseline tier if we ask the user to create a Set.

How do you feel about this alternative, or the one at the end of this comment? I think they'd be about as optimizable as the current spec, except with one more observable lookup first.

Also, how bad is it to not optimize that construction away? It means an extra object allocation, but I would naively expect that the main bottleneck would be iterating the argument and creating a set structure holding its contents, which you presumably want to do in either case. I would expect that the only difference would be whether you create a JS object which exposes the the underlying set. But this is just speculation on my part.

Can you give me an example of this? We're pulling out the has method from the sub class so that should make this work for all sub classes right?

Sorry, yes, it'll work for subclasses as currently specified. It won't work for, say, WeakSets, or anything else which behaves like a set but lacks the relevant internal slot.

Edit: Oh, and the other half of it is that .isSubsetOf can't work when the subclass is the receiver rather than the argument, because it uses its receiver's [[SetData]] directly rather than merely checking for it. (Assuming the subclass overrides Symbol.iterator.) That's not so bad, because the subclass can just override .isSubsetOf, but I don't see any reason for it not to use the iteration protocol on its receiver instead of doing anything with the internal slot, like .union does.

@gsathya
Copy link
Member

gsathya commented Jan 3, 2019

How do you feel about this alternative, or the one at the end of this comment? I think they'd be about as optimizable as the current spec, except with one more observable lookup first.

Just to confirm -- #41 (comment) would mean that instead of the current [[SetData]] slot check, we have a has method check right? Otherwise it's pretty much the same since we do the iteration protocol when creating the new Set currently.

From an implementation POV, the [[SetData]] slot check is much better than the has method check. The has method check would require us to install a protector cell that gets invalidated when the has method is changed. Aside from the cost of checking that protector cell hasn't been invalidated for the fast path, we'd have to check if the target is Set.prototype every time someone writes to a property named has (on any object) to invalidate this. In the slow path, we need to do a full property lookup for this has method. The [[SetData]] slot check only pays when we use this method and is similar to the cost of checking the protector cell.

The Symbol approach has pretty much the same performance characteristics as the has method check.

Also, how bad is it to not optimize that construction away? It means an extra object allocation, but I would naively expect that the main bottleneck would be iterating the argument and creating a set structure holding its contents, which you presumably want to do in either case. I would expect that the only difference would be whether you create a JS object which exposes the the underlying set. But this is just speculation on my part.

Maybe it's not too bad but if the only difference is that we support WeakSet by making this change, then I'm not sure if it's worth it to pay for both performance and memory.

Sorry, yes, it'll work for subclasses as currently specified. It won't work for, say, WeakSets, or anything else which behaves like a set but lacks the relevant internal slot.

I agree that it wouldn't work for WeakSets, but personally I think that's fine since WeakSets are not iterable, although I agree that they are a Set kind-of object. I don't see why your frozen set wouldn't work though -- we'd just create a new Set when it fails the internal slot check, right?

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 3, 2019

Just to confirm -- #41 (comment) would mean that instead of the current [[SetData]] slot check, we have a has method check right? Otherwise it's pretty much the same since we do the iteration protocol when creating the new Set currently.

Right.

I agree that it wouldn't work for WeakSets, but personally I think that's fine since WeakSets are not iterable.

Whether something is iterable is completely unrelated to whether it makes sense to ask if something is a subset of it, though. I don't get the intuition that this is relevant.

I don't see why your frozen set wouldn't work though -- we'd just create a new Set when it fails the internal slot check, right?

Sorry, I guess I'm overloading "work" slightly. Yes, it would end up returning the right result. But it would take time which is linear in both the size of the receiver and the size of the frozen set, which can be dramatically worse than it should be (which is just linear in the size of the receiver).

@gsathya
Copy link
Member

gsathya commented Jan 3, 2019

Whether something is iterable is completely unrelated to whether it makes sense to ask if something is a subset of it, though. I don't get the intuition that this is relevant.

Fair enough, I see your point. Do you think your use case could be better solved by having a WeakSet.prototype.isSubsetOf (and other such similar methods on WeakSet.prototype)?

But it would take time which is linear in both the size of the receiver and the size of the frozen set, which can be dramatically worse than it should be (which is just linear in the size of the receiver).

Ah, I see. Do you think this is something we should be optimizing for though? I would say that we should optimize for the 99% case which is probably something like a Set or even an Array sometimes.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 3, 2019

The has method check would require us to install a protector cell that gets invalidated when the has method is changed.

Sorry, missed this initially: why is that not already the case for the case for Set.prototype.isSupersetOf wrt its receiver? It seems like "get has from the receiver, if it's not callable then throw" shouldn't be any easier than "get has from the argument, if it's not callable then invoke the iteration protocol".

Sorry for my lack of familiarity with this part of engines.

Do you think your use case could be better solved by having a WeakSet.prototype.isSubsetOf (and other such similar methods on WeakSet.prototype)?

You can't have WeakSet.prototype.isSubsetOf without exposing GC. (For example: let o = {}; let w = new WeakSet([o]); w.isSubsetOf(new WeakSet); // true iff o has been GC'd by the time isSubsetOf is called.) So, no, I don't think so.

Ah, I see. Do you think this something we should be optimizing for though? I would say that we should optimize for the 99% case which is probably something like a Set or even an Array sometimes.

I agree that our first concern should be the case where both are Sets (and not subclasses, and no one has messed with any primordials, and so on). Though it's still worth the cost of a couple observable lookups to avoid directly depending on the internal slot, I think, if that's all it takes. (That's what we're doing for the other methods already, after all!)

But for cases outside that, it's less clear. In particular, I am not at all sure it's worth optimizing (in the sense of avoiding a baseline call to the Set constructor) for the case where the argument is an array if that means that it would not work (with appropriate time complexity) on cases where it ought to (like WeakSet or FrozenSet). The tradeoff seems to be "users have to write very slightly more code in cases where they pass something which does not support membership testing and those cases are a small constant amount slower" vs "passing things like FrozenSet will silently have significantly worse time complexity and passing things like WeakSet won't work at all". I prefer the former cost to the latter, myself, if that's an accurate summary.

@gsathya
Copy link
Member

gsathya commented Jan 3, 2019

Sorry, missed this initially: why is that not already the case for the case for Set.prototype.isSupersetOf wrt its receiver? It seems like "get has from the receiver, if it's not callable then throw" shouldn't be any easier than "get has from the argument, if it's not callable then invoke the iteration protocol".

That's correct. I'm just comparing the internal slot check with the has method check. Given a choice between the two I'd prefer the internal slot check.

You can't have WeakSet.prototype.isSubsetOf without exposing GC. (For example: let o = {}; let w = new WeakSet([o]); w.isSubsetOf(new WeakSet); // true iff o has been GC'd by the time isSubsetOf is called.) So, no, I don't think so.

I haven't thought too much of the GC issues here but isn't this already true with the WeakRefs proposal?

The tradeoff seems to be "users have to write very slightly more code in cases where they pass something which does not support membership testing and those cases are a small constant amount slower" vs "passing things like FrozenSet will silently have significantly worse time complexity and passing things like WeakSet won't work at all". I prefer the former cost to the latter, myself, if that's an accurate summary.

Personally, I weigh the Array use case way more compared to the WeakSet so this trade off is fine with me. I understand your argument about not having this method work with an iterable, but this is still important to me. To better understand why I keep going to back to my should work with iterable argument, I looked into other languages to see how they implement these methods. Swift, C#, Python allow an iterable to be used as an argument; whereas Rust and Ruby require the argument to be a Set -- but they also mandate all the other Set methods take a Set too. I don't think this is conclusive but there's precedence either ways.

I'm coming around to the idea of just checking for the has method and if not we iterate over the argument. I think that's a reasonable compromise here as this method is different from the other two (isDisjointWith, isSupersetOf) in that, we need membership checking of the argument, not the receiver.

@ljharb
Copy link
Member

ljharb commented Jan 3, 2019

It still seems to me that, in general, for both receiver and argument, it seems the most versatile to have a check on all the methods like "if it has the slot, proceed, else SpeciesConstruct with the receiver/arg; then call the has method while iterating through it". I'm still not super clear on why that's slow in the common case.

@gsathya
Copy link
Member

gsathya commented Jan 3, 2019

It still seems to me that, in general, for both receiver and argument, it seems the most versatile to have a check on all the methods like "if it has the slot, proceed, else SpeciesConstruct with the receiver/arg; then call the has method while iterating through it

This wouldn't work for @bakkot's WeakSet use case right?

@zloirock
Copy link
Contributor

zloirock commented Jan 3, 2019

@gsathya maybe for this use case it would be better to add WeakSet.prototype.isSupersetOf?

@ljharb
Copy link
Member

ljharb commented Jan 7, 2019

The latter option seems best to me as well.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 7, 2019

I'd be fine with any of those. I don't think we should shy away from adding new Symbol-based protocols even in the absence of the protocols proposal, but am OK with delaying that to a separate proposal. I do think it's important that we do it fairly soon after this proposal, because silently falling back to worse-than-expected time complexity is bad.

With a few caveats about the last item:

Just do a internal slot check and fall back to creating a Set for now; in the future, add a 'has' Symbol once the protocols proposal is in.

  • I am ok with this in the cases where what we need to do with the thing is query membership rather than iterate (e.g. the argument of isSubsetOf and receiver of isSupersetOf). For cases where we need to iterate, I continue to want to use the iteration protocol unconditionally, since we already have that protocol in the language. There's no reason isSubsetOf should need to check the internal slot on its receiver.

  • If we were adding the symbol-based protocol now, I'd probably want to do something like Set.prototype[Set.has] = Set.prototype.has. But because it would be a breaking change to subclasses to introduce the protocol in that way later, I think if we introduce it later we'd have to add it as a getter for the symbol which would return this.has. (This is a bit brief; I can give some code examples if it doesn't make sense.)

  • I don't actually think "fall back to creating a Set" is the right behavior; see Querying membership in iterables #43.

Still interested in your thoughts on the "Also:" and "Also also:" questions in my comment above.


Would you as an implementor be OK with a later proposal which did the following:

  • Introduce a new Set.has symbol (name/location TBD; just asking about implementation concerns)
  • Make Set.prototype[Set.has] a getter which did return this.has;
  • Make WeakSet.prototype[Set.has] a getter which did return this.has;
  • Replace any "if the [[SetData]] slot is present on obj, use obj.has" language introduced by this proposal with something like "if obj[Set.has] is not undefined, use obj[Set.has]"

? Because that's what I'd want from a follow-on.

@gsathya
Copy link
Member

gsathya commented Jan 9, 2019

After thinking about this some more, the internal slot check wouldn't work as you'd expect if you use a Map as an argument (even though it's an iterable).

With the current spec text, we'd create a Set of arrays from the Map (by iterating the Map) and use that to do the isSubsetOf check which seems completely useless.

> new Set([1, 2]).isSubsetOf(new Map([1, 'a'], [2, 'b']))
// false

But in Python, this would evaluate to true as we'd just use the keys of the Map.

> set([1, 2]).issubset({1: "a", 2: "b"})
// True

I know this isn't super intuitive but I have used this before. With the has method check instead of the internal slot check, it would work nicely for Maps. This makes me like the 'has' method check more than the internal slot check and be in favor of the second option I've listed above.

Also: is there a reason not to extend the fall-back-to-iteration behavior to isSupersetOf and isDisjointWith (and I guess intersection) for their receiver? Those cases are less important to me, but if there's no reason for them not to, it'd be nice for them to match.

The reason I didn't do it for these methods is that I think it's reasonable to assume that the receiver is a Set, as these are not static methods. It seems needlessly complicated to allow any kind of receiver too.

Also also: is there a reason for isSubsetOf not to iterate its receiver rather than accessing [[SetData]] directly, as it currently does? That's a mostly-independent question from what it does with its argument. I know it's a little slower from an implementation perspective, but union is already doing iteration of its receiver with the iteration protocol.

This follows from my earlier point about it being reasonable to assume that the receiver is also a Set.

I don't actually think "fall back to creating a Set" is the right behavior; see #43.

Looks pretty complicated but I think this is a reasonable request. I'd have to think more about the spec text to see if it's optimizable.

Replace any "if the [[SetData]] slot is present on obj, use obj.has" language introduced by this proposal with something like "if obj[Set.has] is not undefined, use obj[Set.has]"

isSubsetOf is the only place where we do this [[SetData]] check so just replacing this slot check in this one method is fine.

@ljharb
Copy link
Member

ljharb commented Jan 9, 2019

Passing a Map anywhere that uses the iterable protocol should only use the values of the default iterator; you’d use .keys() if you wanted the keys, at which point it’d be a generic iterator. It seems super weird to me to rely on “has”, especially since adding doesn’t have a consistent method name.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 9, 2019

Passing a Map

Sounds like a good use case for a symbol-based protocol! :D

I think it would still be OK to introduce such a protocol on top of option 3 later, even including adding it to Map.prototype, because as you point it's totally useless to iterate the map right now and so unlikely anyone would rely on that behavior continuing. But it's a little riskier than just adding it to Set and WeakSet.

The reason I didn't do it for these methods is that I think it's reasonable to assume that the receiver is a Set, as these are not static methods. It seems needlessly complicated to allow any kind of receiver too.

I agree it's a reasonable assumption, but it seems like it would be strictly more useful to make them more generic, and shouldn't slow down the common case. We do the same sort of thing for Array methods and I think it's beneficial there. (Plus it means this proposal only has two different ways it can treat an object, rather than three.) I know I at least would find use for Set.prototype.isDisjointWith.call([2, 3, 4], [0, 1, 2]).

But I don't feel especially strongly about this.

This follows from my earlier point about it being reasonable to assume that the receiver is also a Set.

Well, sure, but other methods which can make a similar assumption don't. (union, difference, and symmetricDifference, in particular.) Why the difference here? And it makes it harder to subclass.

@tc39 tc39 deleted a comment from zloirock Jan 10, 2019
@gsathya
Copy link
Member

gsathya commented Jan 10, 2019

@bakkot, one more quick idea -- what if we use the SpeciesConstructor to construct the Set in this method? We'd just create a new instance of FrozenSet and then lookup and use the has method on this leading to O(1) lookup?

I agree it's a reasonable assumption, but it seems like it would be strictly more useful to make them more generic, and shouldn't slow down the common case.

There's tons more we could do to make all these APIs more flexible, but I want to ship this sooner rather than later and we need to draw a line on what's really useful here.

I know I at least would find use for Set.prototype.isDisjointWith.call([2, 3, 4], [0, 1, 2]).

Then you should patch Array.prototype.has = Array.prototype.contains and it'll work. I think it's fine to distinguish between the receiver and argument having different types and thus have different behaviors. Looking at the Google codebase, I don't see a single instance of a Set.prototype method being called with a different receiver, but I see calls to Set.prototype methods with Arrays as arguments. I think it's a reasonable expectation to have these Set methods not work out of the box for non Set receivers.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 10, 2019

what if we use the SpeciesConstructor to construct the Set in this method?

Wouldn't that mean that you'd construct an Array when passed an Array? That seems like not what you want. Or am I not understanding your suggestion?

but I want to ship this sooner rather than later and we need to draw a line on what's really useful here

I mean... these APIs are going to be around for decades and used by millions of people. I think it's worth making them broadly more consistent and useful, where it's possible to do so without costs other than taking slightly longer to spec and implement.

Looking at the Google codebase, I don't see a single instance of a Set.prototype method being called with a different receiver

Well, yes, because none of them make sense for non-Set receivers at the moment. But I'm pretty sure there are lots of instances of Array.prototype methods being called with different receivers.

@gsathya
Copy link
Member

gsathya commented Jan 10, 2019

Wouldn't that mean that you'd construct an Array when passed an Array? That seems like not what you want. Or am I not understanding your suggestion?

Ah, yes that's true :'(

I mean... these APIs are going to be around for decades and used by millions of people. I think it's worth making them broadly more consistent and useful, where it's possible to do so without costs other than taking slightly longer to spec and implement.

I'm fully aware of this, but it just seems like we're nitpicking on every little detail here, most of which are just really subjective.

Well, yes, because none of them make sense for non-Set receivers at the moment. But I'm pretty sure there are lots of instances of Array.prototype methods being called with different receivers.

I meant closure's Set.prototype methods

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 10, 2019

I think it's important to get details right. If you think there's some user-facing reason that isDisjointWith should not fall back to the iteration protocol for its receiver, that's fine. On the other hand, if you agree that it would be strictly more useful/consistent and without runtime penalty and just worry that it will be slightly harder to spec and/or implement, I don't know if that's all that compelling. But I agree it would be fairly rare, so I'm not that concerned either way.

Also, regardless of whether isDisjointWith falls back to iteration, I do continue think it's important that isSubsetOf iterate its receiver using the iteration protocol rather than reading [[SetData]] directly. That affects its behavior when invoked on actual Set subclasses, not just when .call-ing it on non-set receivers, and really is an inconsistency with union etc. I can submit a PR for just that change if you like.

@michaelficarra
Copy link
Member

but it just seems like we're nitpicking on every little detail here

This is our responsibility as champions and reviewers. We should think deeply and discuss at length every detail of a proposal.

@gsathya
Copy link
Member

gsathya commented Jan 11, 2019

Okay, I don't think there's one objectively correct answer here. Given that, let's try to come to consensus here.

I think it's important to get details right. If you think there's some user-facing reason that isDisjointWith should not fall back to the iteration protocol for its receiver, that's fine. On the other hand, if you agree that it would be strictly more useful/consistent and without runtime penalty and just worry that it will be slightly harder to spec and/or implement, I don't know if that's all that compelling. But I agree it would be fairly rare, so I'm not that concerned either way.

I don't think it's useful, it's just unnecessary fallback behavior. But, it's easy to optimize away and spec it so I'll add it.

Also, regardless of whether isDisjointWith falls back to iteration, I do continue think it's important that isSubsetOf iterate its receiver using the iteration protocol rather than reading [[SetData]] directly. That affects its behavior when invoked on actual Set subclasses, not just when .call-ing it on non-set receivers, and really is an inconsistency with union etc. I can submit a PR for just that change if you like.

Ok. Please submit a PR for this I'll merge it. Can you also update your current PR to construct a new Set instead of throwing if the has check fails? I'll update isDisjointWith and isSupersetOf to do the same once we land this.

@gsathya
Copy link
Member

gsathya commented Jan 14, 2019

Can you also update your current PR to construct a new Set instead of throwing if the has check fails?

I'll land this PR once it's updated to do this.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 14, 2019

@gsathya Since #45 landed, this PR basically amounts to a switch from the third to the second of the options you laid out above. It sounded above like you preferred your third option above (use [[SetData]] check for now, maybe introduce a Symbol later), which I think corresponds to the current spec. Do you still want to merge this?

Like I say I'm fine either way (though @michaelficarra expressed a preference not to take option 2); happy to update this if you do want to make that switch.

@bakkot
Copy link
Collaborator Author

bakkot commented Jan 14, 2019

Updated anyway, though.

@gsathya
Copy link
Member

gsathya commented Jan 14, 2019

Do you still want to merge this?

Yeah, I think I changed my mind in #41 (comment) and agree with you about membership testing being the core here.

@gsathya gsathya merged commit 95badc1 into tc39:master Jan 14, 2019
@bakkot bakkot deleted the rm-subset-checks branch January 14, 2019 23:38
@gsathya gsathya mentioned this pull request Jan 14, 2019
4 tasks
This was referenced Jan 15, 2019
zloirock added a commit to zloirock/core-js that referenced this pull request Jan 15, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants