Swift Digest
SE-0215 | Swift Evolution

Conform Never to Equatable and Hashable

Proposal
SE-0215
Authors
Matt Diephouse
Review Manager
Ted Kremenek
Status
Implemented (Swift 5.0)

01 何が問題だったのか

Never は、到達し得ないコードを表すための特殊な型です。値を持たない(uninhabited な)型で、fatalError() のようにリターンしない関数の戻り値型として古くから知られています。

加えて Never は、ジェネリック型のパラメータとして「絶対に起こらない」ことを型レベルで表現する用途にも使われます。代表的なのが Result<Success, Failure> 型で、FailureNever を指定すれば「失敗し得ない Result」を、SuccessNever を指定すれば「成功し得ない Result」を表現できます。

// 失敗しない Result
let alwaysOk: Result<Int, Never> = .success(42)

// 成功しない Result
let alwaysError: Result<Never, MyError> = .failure(.somethingWrong)

一方、Result のようなジェネリック型は、型パラメータが EquatableHashable に適合しているときだけ自身も Equatable / Hashable になるという conditional conformance の形で提供されるのが自然です。ところが Swift 4.2 以前の NeverEquatable にも Hashable にも適合していなかったため、FailureNever を入れた途端、外側の Result まで Equatable / Hashable ではなくなってしまう、という不便さがありました。

// Never が Equatable でないので、この Result 全体も Equatable にならない
let a: Result<Int, Never> = .success(1)
let b: Result<Int, Never> = .success(1)
a == b // error: Never does not conform to Equatable

Never は値を持たない型なので、「二つの Never 値が等しいか」という問いには本来意味がありません。それでもプロトコル適合が無いというだけの理由で、Never を含むジェネリック型の比較やハッシュが軒並みできなくなるのは、ライブラリ作者にも利用者にも大きな負担でした。

ライブラリ側で回避しようとすると、Result のようなジェネリック型の作者が Value == NeverError == Never、その両方、といった組み合わせごとに大量の conditional conformance を自前で書く必要があり、現実的ではありません。

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

標準ライブラリ側で NeverEquatableHashable への適合を追加します。さらにレビューの結論として Core Team の判断により、ComparableError への適合もあわせて追加されました。

extension Never: Equatable {
    public static func == (lhs: Never, rhs: Never) -> Bool {
        switch (lhs, rhs) {
        }
    }
}

extension Never: Hashable {
    public func hash(into hasher: inout Hasher) {
    }
}

== の実装が成立する理由

Never は値を持たない型なので、lhsrhs として実際に渡ってくる値は存在しません。そのため switch(lhs, rhs) を分岐しても網羅すべきケースが一つも無く、Bool を返す case が一つも無くても関数全体がきちんと型チェックを通ります。呼び出されることがあり得ない関数なので、中身は空でよいわけです。

Hashable 側はさらに単純で、hash(into:) の本体は空のままで済みます。

利用者側で得られる効果

この適合が入ったことで、Never をパラメータに持つジェネリック型の conditional conformance が素直に機能するようになります。Result のように FailureNever を入れるユースケースはもちろん、自作のジェネリック型でも同様の恩恵を受けられます。

let a: Result<Int, Never> = .success(1)
let b: Result<Int, Never> = .success(1)
a == b // true (Never が Equatable なので Result も Equatable)

let set: Set<Result<Int, Never>> = [.success(1), .success(2)]

ComparableError の追加

レビューの過程で、同じ論法(Never には値が無いのだから、値に対する要件はすべて空の実装で満たせる)が Comparable にも Error にも当てはまることが指摘され、これらも追加されました。

Error への適合は実用上の意味も大きく、ジェネリックな API で「throws するが、この具体型では絶対にエラーにならない」ことを Failure == Never のような形で表すときに、Never をそのままエラー型として流用できるようになります。

既存コードへの影響

標準ライブラリが同じ適合を提供するようになるため、自前で Never: Equatable などを宣言していたコードでは redundant conformance のエラーが出ます。自前の適合を削除すれば済む、ごく局所的な書き換えです。