Swift Digest

Equatable 適合なしでの nil 比較サポート

Support for nil comparisons without Equatable conformance

Proposal
SF-0035
Authors
Matthew Turk
Review Manager
Jeremy S
Status
Accepted

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

Foundation の #Predicate マクロでは、Swift のクロージャ構文でフィルタ条件を書きながら、SendableCodable、かつ実行時にも構造を取り出せる述語を組み立てられます。

しかし、#Predicate の中でオプショナルな値を nil と比較するとき、その Wrapped 型が Equatable に適合していないとコンパイルエラーになっていました。これは == / != を構築する PredicateExpressions.build_Equal(lhs:rhs:) / build_NotEqual(lhs:rhs:) が、左右の式の出力型に常に Equatable 適合を要求していたためです。

struct Message {
    struct Subject {
        let value: String
    }

    let subject: Subject?
}

let predicate = #Predicate<Message> { $0.subject == nil }
// Referencing static method 'build_Equal(lhs:rhs:)' on 'Optional' requires that 'Message.Subject' conform to 'Equatable'

この挙動は Swift 標準ライブラリのセマンティクスとは食い違っています。標準ライブラリでは、WrappedEquatable に適合していなくてもオプショナルを nil と比較できるからです。#Predicate の中でも同じように書けることが期待されます。

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

PredicateExpressionsbuild_Equal(lhs:rhs:) / build_NotEqual(lhs:rhs:) のオーバーロードを追加し、NilLiteral を相手にする比較については WrappedEquatable 適合を要求しないようにします。これにより、冒頭の例のように non-Equatable なオプショナルを nil と比較する #Predicate がそのままコンパイルできるようになります。

let predicate = #Predicate<Message> { $0.subject == nil } // OK

追加されるオーバーロード

左右どちらが nil 側でも書けるよう、== / != のそれぞれに NilLiteral を一方に取る2種類のオーバーロードが用意され、合計4つが追加されます。FoundationPreview 6.4 以降で利用できます。

@available(FoundationPreview 6.4, *)
extension PredicateExpressions {
    public static func build_Equal<LHS, Wrapped>(
        lhs: LHS,
        rhs: NilLiteral<Wrapped>
    ) -> Equal<OptionalFlatMap<LHS, Wrapped, Value<Bool>, Bool>, Value<Bool?>>

    public static func build_Equal<Wrapped, RHS>(
        lhs: NilLiteral<Wrapped>,
        rhs: RHS
    ) -> Equal<Value<Bool?>, OptionalFlatMap<RHS, Wrapped, Value<Bool>, Bool>>

    public static func build_NotEqual<LHS, Wrapped>(
        lhs: LHS,
        rhs: NilLiteral<Wrapped>
    ) -> NotEqual<OptionalFlatMap<LHS, Wrapped, Value<Bool>, Bool>, Value<Bool?>>

    public static func build_NotEqual<Wrapped, RHS>(
        lhs: NilLiteral<Wrapped>,
        rhs: RHS
    ) -> NotEqual<Value<Bool?>, OptionalFlatMap<RHS, Wrapped, Value<Bool>, Bool>>
}

仕組み

各オーバーロードは、Equatable でないかもしれない式を OptionalFlatMap で包み、値があるかどうかだけを Bool? に写してから nil と比較します。値がある側のクロージャは中身を捨てて Value(true) を返し、無ければそのまま nil が伝播します。これは if let で値の有無だけを見る書き方と同じ発想で、Wrapped 自体の値どうしを比較する必要が無いため、Equatable 適合は不要になります。

オーバーロードは既存のものより具体的なシグネチャを持つので、Swift のメソッド解決により nil との比較ではこちらが優先的に選ばれます。WrappedEquatable に適合している場合の挙動は変わりません。

追加は純粋に additive で、#Predicate マクロが生成するコード自体も従来と同じです。Foundation のチームによる計測では、述語を多く含むファイルの型検査時間にも目立った変化は見られませんでした。