为什么Swift元组只有元素个数小于等于6时才能比较?

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

我在HackingWithSwift中读到Swift元组只有在元素个数小于或等于6时才可以与==运算符进行比较,这背后的原因是什么限制?

背景:元组不是 Equatable

Swift 的元组不是 Equatable,而且它们实际上不可能(目前)。写这样的东西是不可能的:

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

这是因为Swift的元组是结构类型:它们的身份是从它们的结构派生出来的。你的 (Int, String) 和我的 (Int, String) 一样。

您可以将其与 名义类型 进行对比,后者的身份完全基于它们的名称(好吧,以及定义它们的模块的名称),并且其结构无关紧要. enum E1 { case a, b } 不同于 enum E2 { case a, b },尽管它们在结构上是等价的。

在Swift中,只有名义类型可以符合协议(如Equatble),这使得元组无法参与。

...但存在 == 个运算符

尽管如此,== 用于比较元组的运算符由标准库提供。 (但请记住,由于仍然不符合 Equatable,您不能将元组传递给需要 Equatable 类型的函数,例如 func f<T: Equatable>(input: T)。)

必须为每个元组元数手动定义一个 == 运算符,例如:

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 { ... }

当然,这对于手工 write-out 来说真的很乏味。相反,它是使用 GYB(“生成样板”)编写的,这是一种 light-weight Python 模板工具。它允许库作者仅使用:

来实现 ==
% 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))}
  )
}

然后由 GYB 扩展为:

@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
  )
}

即使他们自动化了这个样板文件并且理论上可以将 for arity in range(2,7): 更改为 for arity in range(2,999):,但仍然存在成本:所有这些实现都必须编译并生成最终导致膨胀的机器代码标准库。因此,仍然需要中断。库作者选择了 6,但我不知道他们是如何特别确定这个数字的。

未来

未来可能会通过两种方式改进:

  1. 有一个Swift Evolution pitch(尚未实施,所以还没有官方提案)来介绍Variadic generics,它明确提到这是激励示例之一:

    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.

    这一提议的语言功能将允许编写:

    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
    }
    

    这将是一个可以处理元组或任何元数的 general-purpose == 运算符。

  2. 也有兴趣潜在支持 non-nominal conformances,允许元组等结构类型符合协议(如 Equatable)。

    这将允许一个像这样的东西:

    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
        }
    }