Swift Digest

#Expression マクロと Expression

#Expression Macro and Type

Proposal
SF-0006
Authors
Jeremy Schonfeld
Review Manager
Tina Liu
Status
Accepted

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

01 何が問題だったのか

Foundation には、NSPredicate を Swift 向けに置き換える Predicate 型と、それを書きやすくするための #Predicate マクロが用意されています。#Predicate を使うと、Swift のクロージャ構文でフィルタ条件を書きながら、SendableCodable、かつ実行時にも構造を取り出せる述語を組み立てられます。SwiftData などのクエリでも採用されています。

ただし Predicate は名前のとおり「真偽値を返す式」に特化した型で、内部に持つ式は Bool を出力するものに制約されています。実用上は、真偽値ではなく任意の型を出力する式を、同じように SendableCodable、かつ introspection 可能な形で表現したい場面があります。たとえば次のようなケースです。

  • データベースから行全体を読み込まずに、特定のフィールドだけ(あるいは複数フィールドの組み合わせだけ)を取り出したい。NSFetchRequest.propertiesToFetch のように「どのプロパティを引いてくるか」を式として渡したい
  • あるモデルのプロパティ群を別のエンティティのプロパティ群へ写したい。たとえば CSSearchableItemAttributeSet のような Spotlight 用エンティティへのマッピングを Swift の式で書きたい

KeyPath でも単一のプロパティ参照は表せますが、min()max() のような集約関数や、条件分岐(if/else や三項演算子)で値を出し分ける式は KeyPath では表現できません。Predicate の表現力を保ったまま、出力型の制約だけを外したいというのがこの Proposal の出発点です。

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

Predicate と対になる新しい Expression 型と、それを書きやすくする #Expression マクロが追加されます。Expression は出力型を Bool に固定せず任意の型にできる以外、API は Predicate とほぼ同じ作りで、Sendable / Codable / CodableWithConfiguration に適合します。FoundationPreview 0.4 以降で利用できます。

Expression 型と #Expression マクロ

Expression は入力のパラメータパックと出力型でジェネリックです。#Expression マクロは渡されたクロージャを Expression 値へと変換します。

class Library {
    var albums: [Album]
}

class Album {
    var contents: [Photo]
    var isHidden: Bool
}

let libraryAlbumCountExpression = #Expression<Library, Int> { library in
    library.albums.filter {
        !$0.isHidden
    }.count
}

// その場で評価することも、API に渡して別の表現へ変換することもできる
let numberOfAlbums = try libraryAlbumCountExpression.evaluate(someLibrary)

Expression の型としての宣言は次のとおりです。Predicate と同じ StandardPredicateExpression プロトコルで内部の式を制約しつつ、OutputBool に縛らない点だけが異なります。サポートされる演算子の集合自体は Predicate と同一です。

public struct Expression<each Input, Output> : Sendable, Codable, CodableWithConfiguration, CustomStringConvertible, CustomDebugStringConvertible {
    public typealias EncodingConfiguration = PredicateCodableConfiguration
    public typealias DecodingConfiguration = PredicateCodableConfiguration

    public let expression: any StandardPredicateExpression<Output>
    public let variable: (repeat PredicateExpressions.Variable<each Input>)

    public init(_ builder: (repeat PredicateExpressions.Variable<each Input>) -> any StandardPredicateExpression<Output>)

    public func evaluate(_ input: repeat each Input) throws -> Output
}

@freestanding(expression)
public macro Expression<each Input, Output>(
    _ body: (repeat each Input) -> Output
) -> Expression<repeat each Input, Output>

Predicate の中で Expression を評価する

Predicate の中に Predicate を埋め込む PredicateEvaluate 演算子と対になる形で、Expression を評価する ExpressionEvaluate 演算子が追加されます。これにより、Predicate や別の Expression の内部で、別途定義した Expression を呼び出して結果を組み合わせられます。

extension PredicateExpressions {
    public struct ExpressionEvaluate<
        Transformation : PredicateExpression,
        each Input : PredicateExpression,
        Output
    > : PredicateExpression, CustomStringConvertible
    where
        Transformation.Output == Expression<repeat (each Input).Output, Output>
    {
        public let expression: Transformation
        public let input: (repeat each Input)

        public init(expression: Transformation, input: repeat each Input)
    }

    public static func build_evaluate<Transformation, each Input, Output>(
        _ expression: Transformation,
        _ input: repeat each Input
    ) -> ExpressionEvaluate<Transformation, repeat each Input, Output>
}

ExpressionEvaluate は内側の Transformation と各 Input の性質に応じて、StandardPredicateExpression / Codable / Sendable に条件付きで適合します。

Predicate との相互変換

出力型が BoolExpression は意味的に Predicate と等価なので、両者の間で失敗しないイニシャライザによる相互変換が用意されます。

extension Predicate {
    public init(_ expression: Expression<repeat each Input, Bool>)
}

extension Expression {
    public init(_ predicate: Predicate<repeat each Input>) where Output == Bool
}

なお、概念上は PredicateExpression where Output == Booltypealias として定義し直すこともできますが、Predicate はすでに ABI として SDK に存在しており、定義を差し替えるコストに見合うメリットがないため、両者は別の型として並列に提供されます。

NSExpression への変換

PredicateNSPredicate に変換できるのと同様に、Expression から NSExpression への失敗しうる変換イニシャライザも追加されます。サポートされる演算子と制約は Predicate から NSPredicate への変換と同じで、特に keypath は @objc プロパティに限られ、定数として扱える値も限定的です。

extension NSExpression {
    public init?<Input, Output>(_ expression: Expression<Input, Output>) where Input : NSObject
}

Codable 用のヘルパー

Predicate を独自型として Codable 実装するためのサポート関数((Un)Keyed{Encoding,Decoding}Container への拡張)はこれまで Output == Bool に縛られており、任意出力型の Expression を含む型には使えませんでした。これを解消するため、出力型を制約しない版のメソッドが追加されます。エンコード側は既存 API を優先するために @_disfavoredOverload が付き、デコード側は output: 引数で出力型を明示することで既存 API と曖昧にならないようになっています。

extension KeyedEncodingContainer {
    @_disfavoredOverload
    public mutating func encodePredicateExpression<T: PredicateExpression & Encodable, each Input>(
        _ expression: T,
        forKey key: Self.Key,
        variable: repeat PredicateExpressions.Variable<each Input>,
        predicateConfiguration: PredicateCodableConfiguration
    ) throws

    @_disfavoredOverload
    public mutating func encodePredicateExpressionIfPresent<T: PredicateExpression & Encodable, each Input>(
        _ expression: T?,
        forKey key: Self.Key,
        variable: repeat PredicateExpressions.Variable<each Input>,
        predicateConfiguration: PredicateCodableConfiguration
    ) throws
}

extension KeyedDecodingContainer {
    public mutating func decodePredicateExpression<each Input, Output>(
        forKey key: Self.Key,
        input: repeat (each Input).Type,
        output: Output.Type,
        predicateConfiguration: PredicateCodableConfiguration
    ) throws -> (
        expression: any PredicateExpression<Output>,
        variable: (repeat PredicateExpressions.Variable<each Input>)
    )

    public mutating func decodePredicateExpressionIfPresent<each Input, Output>(
        forKey key: Self.Key,
        input: repeat (each Input).Type,
        output: Output.Type,
        predicateConfiguration: PredicateCodableConfiguration
    ) throws -> (
        expression: any PredicateExpression<Output>,
        variable: (repeat PredicateExpressions.Variable<each Input>)
    )?
}

UnkeyedEncodingContainer / UnkeyedDecodingContainer 側にも、同じ方針でキー無し版のオーバーロードが追加されます。