Passing a Collection to Set.contains(_:) leads to confusing results

There is a confusing behavior of Set.contains(_:) when passing a collection that contains more than one element:

let s: Set = [1, 2, 3]
s.contains([1]) // true
s.contains([1] as Set) // true
s.contains([1, 2]) // false 🤨
s.contains([1, 2] as Set) // false 🤨
s.contains([1, 2, 3]) // false 🤨
s.contains([1, 2, 3] as Set) // false 🤨

I would have expect this to behave identical to Set.isSuperset(of:):

s.isSuperset(of: [1, 2]) // true
s.isSuperset(of: [1, 2] as Set) // true
s.isSuperset(of: [1, 2, 3]) // true
s.isSuperset(of: [1, 2, 3] as Set) // true

I think this confusion is introduced by a Collection extension in the _StringProcessing module:

extension Collection where Self.Element : Equatable {

    /// Returns a Boolean value indicating whether the collection contains the
    /// given sequence.
    /// - Parameter other: A sequence to search for within this collection.
    /// - Returns: `true` if the collection contains the specified sequence,
    /// otherwise `false`.
    @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
    public func contains<C>(_ other: C) -> Bool where C : Collection, Self.Element == C.Element
}

What this means is that a collection can check if it contains another collection. I think this was introduced along with the regex features, mainly to check if a String contains a given substring.

Note that this works for an Array as expected:

let a: Array = [1, 2, 3]
a.contains([1]) // true
a.contains([1, 2]) // true
a.contains([1, 2, 3]) // true
a.contains([1, 2, 3, 4]) // false

But as mentioned above, this surprisingly does not work for Set if the passed in collection contains more than one element.

I think I understand where the issue lies on a technical level, but I find this behavior highly confusing from a Swift user’s perspective and we actually ran into this situation in our code base.

Should I file an issue for this on GitHub - swiftlang/swift: The Swift Programming Language?

2 Likes

It seems the keyword here is that this overload tests "whether the collection contains the given sequence" (my emphasis)

This works:

let s: Set = [1, 2, 3]
print(Array(s))
// [3, 1, 2] (on my computer using the current hashing seed)

s.contains([1, 2]) // true
s.contains([1, 2, 3]) // false
s.contains([3, 1, 2]) // true

I agree that this is confusing when treating sets as sequences.

1 Like

That is an excellent point – it seems the Collection is treated as a sequence of elements here, not in the sense of the type Sequence, but as a bunch of ordered elements. And a Set is weird in that its elements have a random order when looked at it that way.

But there are other areas in the Standard Library that paper over this randomness, e.g.:

let s1: Set = [1, 2, 3]
let s2: Set = [1, 2, 3]

s1 == s2 // true
Array(s1) == Array(s2) // often false, but sometimes also true

So Set equality does not care about the internal order of elements of the two Sets it compares. Granted, this is an overload of == that knows what kind of types it’s dealing with.

So maybe a better question would be: Should the Standard Library contain an overload of the contains(_:) method that when you call it on a Set and give it a Set, it forwards to isSuperset(of:)?

5 Likes