Predicate の Regex サポート
Predicate Regex Support
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
Foundation には、データのフィルタリング条件などを表現するための新しい Swift 製の Predicate 型が導入されています。これは、従来の NSPredicate を Swift 向けに置き換えるための型で、SwiftData などのクエリでも利用されています。
NSPredicate には正規表現による文字列マッチングの仕組みがあり、たとえば次のように書けば、zipcode プロパティが米国の郵便番号の形式に一致するレコードを抽出できました。
NSPredicate(format: "zipcode MATCHES %@", "\\d{5}(-\\d{4})?")
一方、新しい Predicate は == や contains、localizedStandardContains、localizedCompare、caseInsensitiveCompare といった基本的な文字列比較こそサポートしていたものの、正規表現のような複雑なパターンマッチングを行う手段を持っていませんでした。そのため、NSPredicate から Predicate への移行を進める上では、正規表現を使ったクエリを書き換えられず、機能面で取り残されてしまうケースがありました。
02 どのように解決されるのか
Predicate の中で Swift の Regex 型(および RegexComponent プロトコルに適合する任意の正規表現)を使えるようにするための API が追加されます。FoundationPreview 0.4 以降で利用できます。
Predicate 内での正規表現マッチ
文字列に対する contains(_:) を、正規表現を引数として呼び出せるようになります。Regex の DSL でも、正規表現リテラルでも書けます。
let regex = Regex {
Anchor.startOfSubject
Repeat(.digit, count: 5)
Optionally {
"-"
Repeat(.digit, count: 4)
}
Anchor.endOfSubject
}
let predicate = #Predicate<Address> {
$0.zipcode.contains(regex)
}
// または
let predicate = #Predicate<Address> {
$0.zipcode.contains(/^\d{5}(-\d{4})?$/)
}
実装としては、PredicateExpressions に新しい式型 StringContainsRegex と、それを構築する build_contains のオーバーロードが追加されます。Subject 側は BidirectionalCollection で SubSequence == Substring を満たす型、Regex 側は出力が RegexComponent に適合する PredicateExpression であれば渡せます。
extension PredicateExpressions {
public struct StringContainsRegex<
Subject : PredicateExpression,
Regex : PredicateExpression
> : PredicateExpression, CustomStringConvertible
where
Subject.Output : BidirectionalCollection,
Subject.Output.SubSequence == Substring,
Regex.Output : RegexComponent
{
public typealias Output = Bool
public let subject: Subject
public let regex: Regex
public init(subject: Subject, regex: Regex)
}
public func build_contains<Subject, Regex>(
_ subject: Subject,
_ regex: Regex
) -> StringContainsRegex<Subject, Regex>
}
StringContainsRegex は、Subject と Regex がそれぞれ Sendable / Codable / StandardPredicateExpression であれば、自身もそれらに条件付きで適合します。Predicate の他の式と同じく、SwiftData クエリへの変換や NSPredicate への変換、ネットワーク越しの受け渡しなどに乗せられる作りになっています。
Predicate 自体は丸ごとの一致を返す API(wholeMatch のような Bool 版)を持っていません。文字列全体に一致させたい場合は、上の例のように ^ と $ の anchor を付けた正規表現を contains に渡すパターンで書きます。
正規表現の定数値の格納
Predicate 内で参照される正規表現の「定数値」を扱うため、PredicateRegex という新しい型が追加されます。これは任意の RegexComponent を Codable & Sendable な形でラップするための型です。
extension PredicateExpressions {
public struct PredicateRegex : Sendable, Codable, RegexComponent, CustomStringConvertible {
var regex: Regex<AnyRegexOutput> { get }
var stringRepresentation: String { get }
public init?(_ component: some RegexComponent)
}
public func build_Arg(_ component: some RegexComponent) -> Value<PredicateRegex>
}
Regex 自体は Codable でも Sendable でもないため、PredicateExpressions.Value にそのまま入れることはできません。代わりに、build_Arg のオーバーロードが渡された RegexComponent を PredicateRegex に包み、Value<PredicateRegex> として保持します。Predicate を String として書き出したり、別のクエリ表現に変換したりするときには、PredicateRegex の stringRepresentation がその文字列表現を提供します。表現の構文は SE-0355 で定義されている Swift の正規表現リテラル構文に従い、一般的な正規表現エンジンの構文の上位互換になっています。
サポートされるのは、文字列表現に変換できる正規表現です。capture transform クロージャやカスタムパーサを使って組み立てた正規表現のように、文字列に書き戻せないものは Predicate では扱えません。
そのような表現が混入したときの挙動は、API によって異なります。
#Predicateマクロやbuild_Arg経由でPredicateを組み立てる場合は、fatalErrorで停止します。Predicateの構築はthrowできないため、構築時に検出した不正な正規表現はランタイムエラーとして即座に知らせる方針です。これは「不正なリテラルが書かれているコードがあるならコンパイル直後にクラッシュさせて気付かせる」「Predicateが評価されるのは構築場所から遠い場所(別ライブラリやプロセス)になりうるため、エラーは構築時に出すのが望ましい」「Predicateの文字列化や別表現への変換が常に成功することを保てる」といった理由によります。Predicateを手動で構築するコードでは、PredicateRegex(_:)の failable イニシャライザを使って、不正な正規表現をnilとして安全に検出し、独自のフォールバックを書けます。