Swift Digest
SE-0121 | Swift Evolution

Remove Optional Comparison Operators

Proposal
SE-0121
Authors
Jacob Bandes-Storch
Review Manager
Chris Lattner
Status
Implemented (Swift 3.0)

01 何が問題だったのか

Swift 3 以前の標準ライブラリには、Optional を比較するための次の4つの演算子が定義されていました。

public func <  <T : Comparable>(lhs: T?, rhs: T?) -> Bool
public func <= <T : Comparable>(lhs: T?, rhs: T?) -> Bool
public func >  <T : Comparable>(lhs: T?, rhs: T?) -> Bool
public func >= <T : Comparable>(lhs: T?, rhs: T?) -> Bool

これらは .nonenil)を .some(_) よりも「小さい」ものとして扱う順序を提供していました。

期待されていた用途と、実際には役に立たなかった理由

もともとこれらの演算子は、ジェネリックなアルゴリズムを Optional 値に対しても適用できるようにすることを目的としたものでした。たとえば次のように Optional の配列をそのままソートできる、という使い道が想定されていました。

[3, nil, 1, 2].sorted()  // [nil, 1, 2, 3] を返す想定

しかし当時の Swift のジェネリクスは conditional conformance(extension Optional: Comparable where Wrapped: Comparable のような条件付き適合)をサポートしていなかったため、Optional 自体は Comparable に適合できませんでした。結果として、本来期待されていたジェネリックプログラミング上の用途では、これらの演算子はそもそも機能していませんでした。

暗黙の昇格によって生まれていた驚き

実際にこれらの演算子が使われる典型的な場面は、非 Optional の値が Optional 型へ暗黙に昇格(coercion)するケースでした。

let a: Int? = 4
let b: Int = 5
a < b  // b が Int から Int? に昇格してから比較される

この挙動は、次のようなコードで直感に反するバグを生みやすいものでした。

struct Pet {
  let age: Int
}

struct Person {
  let name: String
  let pet: Pet?
}

let peeps = [
  Person(name: "Fred", pet: Pet(age: 5)),
  Person(name: "Jill", pet: .none), // ペットを飼っていない
  Person(name: "Burt", pet: Pet(age: 10)),
]

// 6歳未満のペットを持つ人を抽出したいつもりが…
let ps = peeps.filter { $0.pet?.age < 6 }
// 結果は [Fred, Jill]
// ペットを飼っていない Jill も含まれてしまう
// (nil は任意の非 nil より「小さい」とみなされるため)

ここでは $0.pet?.ageInt? であり、petnil の人では式全体が nil になります。nil < 6true と評価されてしまうため、ペットを飼っていない人まで「6歳未満のペットを持つ人」として抽出されてしまいます。

このように、Optional の比較演算子は、

  • 本来の目的であったジェネリックな用途では機能していない
  • 一方で暗黙の昇格と組み合わさって、気付きにくいバグの温床になっている

という状態にありました。ジェネリクスが十分に成熟して OptionalComparable への conditional conformance が書けるようになるまでは、いったんこれらの演算子を取り除いておくのが安全だ、という判断が下されました。

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

Optional を引数に取る < / <= / > / >= の4つの演算子を標準ライブラリから削除します。等価性を比較する ==!=Optional 版は結果が直感的で有用なため、こちらは残されます。

既存コードへの影響

これまで Optional 値と非 Optional 値を直接 < などで比較していたコードはコンパイルエラーになり、明示的にアンラップしてから比較する形に書き換える必要があります。

let a: Int?
let b: Int

// これまでは暗黙の昇格で動いていた
if a < b { ... } // error: コンパイルできない

// 書き換え例 1: if let でアンラップしてから比較する
if let a = a, a < b { ... }

// 書き換え例 2: guard let で早期 return する
guard let a = a else { return }
if a < b { ... }

// 書き換え例 3: nil でないと確信できる場合は強制アンラップ
if a! < b { ... }

書き換えが必要な箇所は多くなる可能性がありますが、それらは SE-0121 以前には気付かれにくかったバグを含んでいる可能性が高い箇所でもあるため、見直しの機会として価値があります。なお強制アンラップ(a!)を使う書き換えは、anil のときにトラップ(実行時クラッシュ)する点がそれ以前の a < b(黙って false を返す)とは挙動が異なるため、意味を変えてしまう書き換えになることに注意してください。

将来の見通し

Swift のジェネリクスが成熟し、Optional を条件付きで Comparable に適合させる(extension Optional: Comparable where Wrapped: Comparable のような)記述が可能になれば、その枠組みの上で Optional の比較を再導入することが検討され得ます。その場合は純粋な追加的変更となるため、このタイミングでいったん削除しておいても将来の選択肢は狭まりません。