Home > OS >  Why Swift Tuples can be compared only when the number of elements is less than or equal to 6?
Why Swift Tuples can be compared only when the number of elements is less than or equal to 6?

Time:05-09

I read in HackingWithSwift that Swift tuples can be compared with == operator only when the number of elements is less than or equal to 6. What is the reason behind this limitation ?

CodePudding user response:

Background: Tuples aren't Equatable

Swift's tuples aren't Equatable, and they actually can't be (for now). It's impossible to write something like:

extension (T1, T2): Equatable { // Invalid
    // ...
}

This is because Swift's tuples are structural types: Their identity is derived from their structure. Your (Int, String) is the same as my (Int, String).

You can contrast this from nominal types, whose identity is solely based off their name (well, and the name of the module that defines them), and whose structure is irrelevant. An enum E1 { case a, b } is different from an enum E2 { case a, b }, despite being structurally equivalent.

In Swift, only nominal types can conform to protocols (like Equatble), which precludes tuples from being able to participate.

...but == operators exist

Despite this, == operators for comparing tuples are provided by the standard library. (But remember, since there is still no conformance to Equatable, you can't pass a tuple to a function where an Equatable type is expected, e.g. func f<T: Equatable>(input: T).)

One == operator has to be manually be defined for every tuple arity, like:

public func == <A: Equatable, B: Equatable,                                                       >(lhs: (A,B        ), rhs: (A,B        )) -> Bool { ... }
public func == <A: Equatable, B: Equatable, C: Equatable,                                         >(lhs: (A,B,C      ), rhs: (A,B,C      )) -> Bool { ... }
public func == <A: Equatable, B: Equatable, C: Equatable, D: Equatable,                           >(lhs: (A,B,C,D    ), rhs: (A,B,C,D    )) -> Bool { ... }
public func == <A: Equatable, B: Equatable, C: Equatable, D: Equatable, E: Equatable,             >(lhs: (A,B,C,D,E  ), rhs: (A,B,C,D,E  )) -> Bool { ... }
public func == <A: Equatable, B: Equatable, C: Equatable, D: Equatable, E: Equatable, F: Equatable>(lhs: (A,B,C,D,E,F), rhs: (A,B,C,D,E,F)) -> Bool { ... }

Of course, this would be really tedious to write-out by hand. Instead, it's written using GYB ("Generate your Boilerplate"), a light-weight Python templating tool. It allows the library authors to implement == using just:

% for arity in range(2,7):
%   typeParams = [chr(ord("A")   i) for i in range(arity)]
%   tupleT = "({})".format(",".join(typeParams))
%   equatableTypeParams = ", ".join(["{}: Equatable".format(c) for c in typeParams])

// ...

@inlinable // trivial-implementation
public func == <${equatableTypeParams}>(lhs: ${tupleT}, rhs: ${tupleT}) -> Bool {
  guard lhs.0 == rhs.0 else { return false }
  /*tail*/ return (
    ${", ".join("lhs.{}".format(i) for i in range(1, arity))}
  ) == (
    ${", ".join("rhs.{}".format(i) for i in range(1, arity))}
  )
}

Which then gets expanded out by GYB to:

@inlinable // trivial-implementation
public func == <A: Equatable, B: Equatable>(lhs: (A,B), rhs: (A,B)) -> Bool {
  guard lhs.0 == rhs.0 else { return false }
  /*tail*/ return (
    lhs.1
  ) == (
    rhs.1
  )
}

@inlinable // trivial-implementation
public func == <A: Equatable, B: Equatable, C: Equatable>(lhs: (A,B,C), rhs: (A,B,C)) -> Bool {
  guard lhs.0 == rhs.0 else { return false }
  /*tail*/ return (
    lhs.1, lhs.2
  ) == (
    rhs.1, rhs.2
  )
}

@inlinable // trivial-implementation
public func == <A: Equatable, B: Equatable, C: Equatable, D: Equatable>(lhs: (A,B,C,D), rhs: (A,B,C,D)) -> Bool {
  guard lhs.0 == rhs.0 else { return false }
  /*tail*/ return (
    lhs.1, lhs.2, lhs.3
  ) == (
    rhs.1, rhs.2, rhs.3
  )
}

@inlinable // trivial-implementation
public func == <A: Equatable, B: Equatable, C: Equatable, D: Equatable, E: Equatable>(lhs: (A,B,C,D,E), rhs: (A,B,C,D,E)) -> Bool {
  guard lhs.0 == rhs.0 else { return false }
  /*tail*/ return (
    lhs.1, lhs.2, lhs.3, lhs.4
  ) == (
    rhs.1, rhs.2, rhs.3, rhs.4
  )
}

@inlinable // trivial-implementation
public func == <A: Equatable, B: Equatable, C: Equatable, D: Equatable, E: Equatable, F: Equatable>(lhs: (A,B,C,D,E,F), rhs: (A,B,C,D,E,F)) -> Bool {
  guard lhs.0 == rhs.0 else { return false }
  /*tail*/ return (
    lhs.1, lhs.2, lhs.3, lhs.4, lhs.5
  ) == (
    rhs.1, rhs.2, rhs.3, rhs.4, rhs.5
  )
}

Even though they automated this boilerplate and could theoretically change for arity in range(2,7): to for arity in range(2,999):, there is still a cost: All of these implementations have to be compiled and produce machine code that ends up bloating the standard library. Thus, there's still a need for a cutoff. The library authors chose 6, though I don't know how they settled on that number in particular.

Future

There's two ways this might improve in the future:

  1. There is a Swift Evolution pitch (not yet implemented, so there's no official proposal yet) to introduce Variadic generics, which explicitly mentions this as one of the motivating examples:

    Finally, tuples have always held a special place in the Swift language, but working with arbitrary tuples remains a challenge today. In particular, there is no way to extend tuples, and so clients like the Swift Standard Library must take a similarly boilerplate-heavy approach and define special overloads at each arity for the comparison operators. There, the Standard Library chooses to artificially limit its overload set to tuples of length between 2 and 7, with each additional overload placing ever more strain on the type checker. Of particular note: This proposal lays the ground work for non-nominal conformances, but syntax for such conformances are out of scope.

    This proposed language feature would allow one to write:

    public func == <T...>(lhs: T..., rhs: T...) where T: Equatable -> Bool { 
        for (l, r) in zip(lhs, rhs) {
            guard l == r else { return false }
        }
        return true
    }
    

    Which would be a general-purpose == operator that can handle tuples or any arity.

  2. There is also interest in potentially supporting non-nominal conformances, allowing structural types like Tuples to conform to protocols (like Equatable).

    That would allow one to something like:

    extension<T...> (T...): Equatable where T: Equatable {
        public static func == (lhs: Self, rhs: Self) -> Bool { 
            for (l, r) in zip(lhs, rhs) {
                guard l == r else { return false }
            }
            return true
        }
    }
    
  • Related