述語列を連結するためのイニシャライザ
Initializers for joining a sequence of predicates
このダイジェストは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 で、既存のコードへの影響はありません。