Tuesday, May 24, 2016

Swift Tuples Aren’t Equatable

Erica Sadun notes that Swift will let you create an array of tuples:

let a: [(Int, Int)] = [(1, 2), (3, 4)]

But you can’t use the contains() method:

a.contains((3, 4)) // no!

Its definition looks like:

extension SequenceType where Generator.Element : Equatable {
    /// Returns `true` iff `element` is in `self`.
    public func contains(element: Self.Generator.Element) -> Bool

The problem is that it requires the element to conform to the Equatable protocol. Equatable only includes one method:

func ==(lhs: Self, rhs: Self) -> Bool

And the tuple does implement ==. But the compiler can’t use that fact because Swift doesn’t allow tuples to conform to protocols. This is the sort of thing that annoys people about static languages. The workaround is to use a different version of contains():

extension SequenceType {
    /// Returns `true` iff an element in `self` satisfies `predicate`.
    public func contains(@noescape predicate: (Self.Generator.Element) throws -> Bool) rethrows -> Bool

to call == directly via a closure:

a.contains({ $0 == (3, 4) })

12 Comments RSS · Twitter

What does this issue have to do with the static vs. dynamic discussion? If a Swift tuple was just a simple struct with syntactic sugar rather than a bespoke "compound type" built directly into the language then there would be no problem with making it Equatable, right?

@Jon Right. It’s not exactly the same issue, but it’s on the general theme of “The compiler is preventing me from doing something for seemingly arbitrary reasons. It’s not actually protecting me here, just making the code more cumbersome.” On the surface, this seems like an incredibly simple example that should just work. And if you look at an oldish dynamic language like Python, it does:

a = [(1, 2), (3, 4)]
(3, 4) in a # or a.__contains__((3, 4))

That makes sense. I actually searched around a bit to see if I could find an explanation for why tuples aren't just syntactic sugar for a normal Tuple type and I couldn't find anything. I assume there is some sort of esoteric performance reason for the way it is, which goes to your point.

The issue is that a tuple can only be Equatable if all its member types are also Equatable.

You can trivially write a generic implementation of == for a 1-tuple with an Equatable member type, or a 2-tuple with both member types Equatable, etc. up to N-tuple for any finite N, but I don't think Swift's type system can handle a generic function with type constraints on an arbitrary number of types.

On a similar note:

“The Swift standard library includes tuple comparison operators for tuples with less than seven elements. To compare tuples with seven or more elements, you must implement the comparison operators yourself.”

Excerpt From: Apple Inc. “The Swift Programming Language (Swift 2.2).” iBooks. https://itun.es/us/jEUH0.l

Another consequence of this: tuples can't be Hashable, so you can't use them as Swift dictionary keys.

NSDictionary only requires that keys be NSCopying, so you can use NSArray as an NSDictionary key. Now, it's common in other languages for arrays to not be allowed as keys (they're often mutable, for one, which makes things tricky), so it's not surprising that you can't use a Swift Array as a Dictionary key, but a common solution to that is to use a tuple or immutable array as a dictionary key.

Unfortunately, Swift requires that Dictionary keys be Hashable, and Tuples (like Arrays) are structs, so they can't implement any protocols, so you can't use them as dictionary keys.

Strangely, NSArray is (Swift) Hashable, so you can use NSArray as a Swift dictionary key. In fact, a common suggestion is to simply use NSArray in Swift: http://stackoverflow.com/a/30188581/5937636

It's not the compiler disallowing anything though, is it? At least, not in the directed and intentional manner that "for seemingly arbitrary reasons. It's not actually protecting me here" implies. This aspect of "Tuples are a non-nominal type" as a quirk of implementation that bleeds through is the same reason that we can't extend them. https://twitter.com/jckarter/status/611656674391097344 My point is that this isn't really a 'static vs dynamic" or a "the compiler is trying to protect me" issue. We can't do it yet and that sucks but it isn't a design decision beyond 'we haven't had time to design it and make it so'.

@Kyle Yes, that’s another area where Python just works. Using NSArray as a dictionary key seems like a terrible idea; isn’t its hash code only based on its length?

@TJ I mean that it seems arbitrary from the point of view of the non-compiler developer. Some of these type issues, like my other example today, seem like an issue of the design or implementation being too difficult, which is a risk that you run when you depend on the compiler being able to figure everything out. Perhaps you’re right that in this case it’s more about tuples being a special case in the implementation.

Swift tuples have always felt like second-class citizens to me, and with strange behavior to boot, e.g. http://wanderingcoder.net/2014/10/21/swift-thoughts/ and search for "tuple type conversion" (and yes, I need to add anchors to this post); why is it possible to add them to collections but are they so otherwise limited? If it came down to me I wouldn't even allow tuple variables (use structs for that). They should make up their mind and either make tuples a first class citizen (equivalent to structs and enumerations), or disallow most of their uses.

Whoa, crazy. I had no idea that NSArray -hashValue was just its -count.

It'll still work, of course, but it probably won't be very efficient.

[…] Previously: Swift Tuples Aren’t Equatable. […]

[…] Swift tuples aren’t Equatable, so you can’t use them with […]

Leave a Comment