Swift Digest
SE-0015 | Swift Evolution

Tuple comparison operators

Proposal
SE-0015
Authors
Lily Ballard
Review Manager
Dave Abrahams
Status
Implemented (Swift 2.2)

01 何が問題だったのか

Swift 2.x 当時、タプル同士を比較する演算子は標準ライブラリに用意されていませんでした。IntString のように EquatableComparable に適合する値であっても、それらを組み合わせたタプル同士を比較しようとするとコンパイルエラーになってしまっていたのです。

タプル同士を比較できない

要素がすべて Equatable なタプルに対して == / != を、Comparable なタプルに対して < / <= / > / >= を使えそうなものですが、Swift 2.x ではそれらが定義されていませんでした。結果として、たとえば座標を表す (Int, Int) のタプルを2つ比較したいだけでも、自分で比較コードを書く必要がありました。

let a = (1, 2)
let b = (1, 3)
a == b // error: 当時はこの比較がコンパイルできない

== / != の自然な定義(要素ごとの比較を && / || でつなぐ)も、大小比較の自然な定義(先頭の要素から順に比べていく辞書式比較)も、利用者の期待がほぼ一致しているにもかかわらず、毎回手書きする必要があるのは冗長でした。

タプル類似の構造体にも影響する

タプルの比較が使えないと、タプル的な構造体(複数のプロパティを束ねただけの型)で EquatableComparable に適合させるときにも手間が増えます。もしタプル比較が使えれば、構造体の比較演算子の実装として「各プロパティをタプルにまとめて比較するだけ」と書けますが、それもできないため、比較ロジックを要素ごとに手で展開する必要がありました。

02 どのように解決されるのか

標準ライブラリに、一定のアリティ(要素数)までのタプルに対する比較演算子のジェネリックな実装を追加します。対象となる演算子は == / != / < / <= / > / >= の6つで、アリティは 最大6 までがサポートされます。

アリティの上限は、利便性(ほとんどのタプルを網羅できる)と標準ライブラリのコードサイズ増加(アリティを増やすと急激にふくらむ)のバランスから選ばれています。アリティ6 までであればコードサイズ増加は約 1.4%、アリティ12 まで広げると約 5.5% になるという試算が根拠になっています。

等価比較

要素がすべて Equatable に適合するタプル同士に対して、要素ごとの == / !=&& / || でつないだ定義が提供されます。アリティ3 の場合は次のような実装が生成されます。

public func == <A: Equatable, B: Equatable, C: Equatable>(lhs: (A,B,C), rhs: (A,B,C)) -> Bool {
  return lhs.0 == rhs.0 && lhs.1 == rhs.1 && lhs.2 == rhs.2
}

public func != <A: Equatable, B: Equatable, C: Equatable>(lhs: (A,B,C), rhs: (A,B,C)) -> Bool {
  return lhs.0 != rhs.0 || lhs.1 != rhs.1 || lhs.2 != rhs.2
}

大小比較(辞書式比較)

要素がすべて Comparable に適合するタプル同士に対しては、先頭の要素から順に見ていき、最初に差がついた要素の比較結果を採用する辞書式比較が定義されます。アリティ3 の < は次のような実装です。

public func < <A: Comparable, B: Comparable, C: Comparable>(lhs: (A,B,C), rhs: (A,B,C)) -> Bool {
  if lhs.0 != rhs.0 { return lhs.0 < rhs.0 }
  if lhs.1 != rhs.1 { return lhs.1 < rhs.1 }
  return lhs.2 < rhs.2
}

<= / > / >= も同様で、最後の要素だけ対応する演算子(<= / > / >=)を使う形になっています。

使い方

これにより、たとえば次のようにタプルをそのまま比較できるようになります。

(1, 2) == (1, 2)        // true
(1, 2) != (1, 3)        // true
(1, 2) < (1, 3)         // true(先頭が同じ、次の要素で 2 < 3)
(2, 0) < (1, 9)         // false(先頭で 2 < 1 が false)

タプル類似の構造体で比較演算子を実装する際にも、プロパティをタプルにまとめて委譲する形で簡潔に書けます。

struct Version {
  let major: Int
  let minor: Int
  let patch: Int
}

func < (lhs: Version, rhs: Version) -> Bool {
  return (lhs.major, lhs.minor, lhs.patch) < (rhs.major, rhs.minor, rhs.patch)
}

アリティ7 以上や Equatable / Comparable 適合について

アリティ7 以上のタプルについては、この提案のスコープ外です。将来的にタプルへのプロトコル適合(Equatable / Comparable への conditional conformance)や可変長ジェネリクスがSwiftに導入されれば、それらを使ってより一般化した形で置き換えることが展望として挙げられていますが、この提案自体はあくまで演算子のオーバーロードとして固定アリティ分を提供するものです。