Swift Digest

PredicateRegex サポート

Predicate Regex Support

Proposal
SF-0004
Authors
Jeremy Schonfeld
Review Manager
Charles Hu
Status
Accepted

このダイジェストは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==containslocalizedStandardContainslocalizedComparecaseInsensitiveCompare といった基本的な文字列比較こそサポートしていたものの、正規表現のような複雑なパターンマッチングを行う手段を持っていませんでした。そのため、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 側は BidirectionalCollectionSubSequence == 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 は、SubjectRegex がそれぞれ Sendable / Codable / StandardPredicateExpression であれば、自身もそれらに条件付きで適合します。Predicate の他の式と同じく、SwiftData クエリへの変換や NSPredicate への変換、ネットワーク越しの受け渡しなどに乗せられる作りになっています。

Predicate 自体は丸ごとの一致を返す API(wholeMatch のような Bool 版)を持っていません。文字列全体に一致させたい場合は、上の例のように ^$ の anchor を付けた正規表現を contains に渡すパターンで書きます。

正規表現の定数値の格納

Predicate 内で参照される正規表現の「定数値」を扱うため、PredicateRegex という新しい型が追加されます。これは任意の RegexComponentCodable & 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 のオーバーロードが渡された RegexComponentPredicateRegex に包み、Value<PredicateRegex> として保持します。PredicateString として書き出したり、別のクエリ表現に変換したりするときには、PredicateRegexstringRepresentation がその文字列表現を提供します。表現の構文は SE-0355 で定義されている Swift の正規表現リテラル構文に従い、一般的な正規表現エンジンの構文の上位互換になっています。

サポートされるのは、文字列表現に変換できる正規表現です。capture transform クロージャやカスタムパーサを使って組み立てた正規表現のように、文字列に書き戻せないものは Predicate では扱えません。

そのような表現が混入したときの挙動は、API によって異なります。

  • #Predicate マクロや build_Arg 経由で Predicate を組み立てる場合は、fatalError で停止します。Predicate の構築は throw できないため、構築時に検出した不正な正規表現はランタイムエラーとして即座に知らせる方針です。これは「不正なリテラルが書かれているコードがあるならコンパイル直後にクラッシュさせて気付かせる」「Predicate が評価されるのは構築場所から遠い場所(別ライブラリやプロセス)になりうるため、エラーは構築時に出すのが望ましい」「Predicate の文字列化や別表現への変換が常に成功することを保てる」といった理由によります。
  • Predicate を手動で構築するコードでは、PredicateRegex(_:) の failable イニシャライザを使って、不正な正規表現を nil として安全に検出し、独自のフォールバックを書けます。