Swift Digest

述語列を連結するためのイニシャライザ

Initializers for joining a sequence of predicates

Proposal
SF-0036
Authors
Matthew Turk
Review Manager
Jeremy S
Status
Approved

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

01 何が問題だったのか

Foundation の Predicate#Predicate マクロにビルダクロージャを渡して組み立てる API です。固定数の述語を &&|| でつなぐだけなら、ビルダ内で各述語を evaluate するだけで足ります。

#Predicate<Message> { fooPredicate.evaluate($0) && barPredicate.evaluate($0) }

しかし実際のアプリケーションでは、ユーザーが指定した複数のフィルタを動的に連結したいケースが多くあります。たとえば書籍データベースに対して、著者名・出版日の範囲・ジャンルなど、入力されるかも事前にはわからないし、いくつ入力されるかもわからないフィルタを連結して検索する、といった場面です。NSPredicate には andPredicateWithSubpredicates: / orPredicateWithSubpredicates: が用意されており、配列に集めた述語をまとめて連結できました。

Predicate には対応するメソッドが無いため、開発者は次のような込み入ったコードを書くか、自前のヘルパーを用意する必要がありました。

let predicates = [
    #Predicate<Book> { $0.title.localizedStandardContains("Swift") },
    #Predicate<Book> { $0.rating > 4 / 5 },
    #Predicate<Book> { $0.series != nil }
]

let conjunction = Predicate<Book> {
    var pieces: any StandardPredicateExpression<Bool> = PredicateExpressions.build_evaluate(PredicateExpressions.build_Arg(predicates[0]), $0)

    func append<T: StandardPredicateExpression<Bool>, U: StandardPredicateExpression<Bool>>(_ leftPredicate: T, _ rightPredicate: U) {
        // The generic context is required for this line to compile.
        pieces = PredicateExpressions.build_Conjunction(lhs: leftPredicate, rhs: rightPredicate)
    }

    for next in predicates.dropFirst() {
        append(pieces, PredicateExpressions.build_evaluate(PredicateExpressions.build_Arg(next), $0))
    }

    return pieces
}

最初の要素を取り出して特別扱いし、#Predicate のビルダ展開に頼らずに直接 PredicateExpressions を組み立てるなど、わかりにくい点が多くあります。SDK の中には別の書き方もありますが、いずれも公開ドキュメントには載っていない挙動に頼るもので、推奨できる手段とは言えません。

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

Predicate に2つの新しいイニシャライザ Predicate(all:)Predicate(any:) が追加されます。それぞれ、同じ入力型の述語の集まりを受け取り、すべての述語を満たす論理積(conjunction)と、いずれかの述語を満たす論理和(disjunction)を組み立てます。

let predicates = [
    #Predicate<Int> { $0 > 2 },
    #Predicate<Int> { $0 <= 9 },
    #Predicate<Int> { $0 != 5 },
    #Predicate<Int> { $0 % 2 != 0 }
]
let conjunction = Predicate(all: predicates)
let disjunction = Predicate(any: predicates)

冒頭の書籍検索の例も、次のように簡潔に書けます。

let predicates = [
    #Predicate<Book> { $0.title.localizedStandardContains("Swift") },
    #Predicate<Book> { $0.rating > 4 / 5 },
    #Predicate<Book> { $0.series != nil }
]
let conjunction = Predicate(all: predicates)

空コレクションの扱い

要素が空のコレクションを渡したときの挙動は、論理学の慣例に従います。

  • Predicate(all:) に空のコレクションを渡すと、常に真となる Predicate.true を返します。
  • Predicate(any:) に空のコレクションを渡すと、常に偽となる Predicate.false を返します。

API のかたち

イニシャライザは BidirectionalCollection を受け取る形で定義されます。FoundationPreview 6.4 以降で利用できます。

@available(FoundationPreview 6.4, *)
extension Predicate {
    public init(all subpredicates: some BidirectionalCollection<Self>)
    public init(any subpredicates: some BidirectionalCollection<Self>)
}

引数ラベルの all / any は、Predicate(all: filters) のような呼び出し形でそのまま「すべて満たす/いずれかを満たす」と読めるよう選ばれています。any は Swift の any キーワードと字面が重なりますが、引数ラベルとして使われる文脈ではキーワードと衝突せず、Predicate(allOf:) / Predicate(anyOf:) のような冗長な綴りに比べて簡潔さが優先されています。

追加は純粋に additive で、既存のコードへの影響はありません。