WWDC Quick Look 💓 By SwiftGGTeam
Expand on Swift macros

Expand on Swift macros

元の動画を見る

ハイライト

Swift マクロは、ロール システムを通じてマクロの機能を制限し、独立したマクロを提供します (#) および補助マクロ (@) 2 つのカテゴリに 7 つの役割があり、開発者は入力型のチェック、予測可能な展開、デバッグ可能な結果を​​維持しながら、コンパイル中にコードを安全に生成できます。

主な内容

Swift には、コードを自動生成するための多くの機能がすでに備わっています。 コード可能な自動合成CodingKeysおよびエンコードおよびデコードのメソッドを使用例して、Result Builder は宣言構文を命令型コードに拡張します。ただし、これらの機能はコンパイラーにハードコーディングされているため、同様の機能を追加する場合は、コンパイラーのソース コードを変更する必要があります。

Acer はこの機能を開発者に公開しています。コンパイラをフォークせずに、独自の「言語機能」を作成し、Swift パッケージとして配布できます。

00:50

4 つの設計原則

Swift マクロは、次の 4 つの中心的な目標を持って設計されています。

明示的な識別。スタンドアロンマクロと#から始まる@始まり。これら 2 つのシンボルが表示されない場合は、マクロが関与していないことがわかります。これにより、コードリーダーに明確な期待が与えられます。

タイプの安全性。マクロに渡されるパラメータは、正しい型の完全な式である必要があります。#unwrap(1 + )構文エラーは直接報告されます。@AddCompletionHandler(parameterName: 42)レポートの種類が一致しません。マクロの入力と出力は完全に型チェックされます。

予測可能な展開。マクロはコードを追加することのみが可能であり、既存のコードを削除または変更することはできません。マクロが関数呼び出しを密かに削除することを心配する必要はありません。

透過的にデバッグ可能。 Xcode は、右クリックによるマクロ展開結果の表示、展開コードでのブレークポイントの設定、およびシングルステップのデバッグをサポートしています。これらのツールは、マクロがクローズド ソース ライブラリからのものであっても引き続き機能します。

03:16

独立したマクロ

スタンドアロンマクロの使用例#通話には 2 つの役割があります。

式マクロ(@freestanding(expression)) は式に展開されます。#stringify(a + b)に拡張する(a + b, "a + b")、式が必要な場所ならどこでも使用例できます。

マクロを宣言(@freestanding(declaration, names: arbitrary)) は 1 つ以上のステートメントに展開されます。例えば#makeArrayND(n: 5)に拡張できますArray5D構造の完全な定義。

04:51

補助マクロ

補助マクロ@声明には次の 5 つの役割が記されています。

peer@attached(peer, names: overloaded)) ターゲット宣言の隣に新しい宣言を追加します。@AddCompletionHandlerできるasyncこの関数は、完了ハンドラーを備えたオーバーロードされたバージョンを自動的に生成します。

@AddCompletionHandler(parameterName: "onCompletion")
func fetchAvatar(_ username: String) async -> Image? {
    // ...
}

// マクロ展開後に自動生成:
func fetchAvatar(
    _ username: String,
    onCompletion: @escaping (Image?) -> Void
) {
    Task.detached {
        onCompletion(await fetchAvatar(username))
    }
}

accessor@attached(accessor)) 保存されたプロパティのゲッター/セッターを追加します。@DictionaryStorage属性の読み取りと書き込みは、自動的に辞書に委任できます。

memberAttribute@attached(memberAttribute)) 型のメンバーにプロパティを追加します。@DictionaryStorage構造に注釈を付けると、各属性が自動的に追加されます@DictionaryStorage

member@attached(member, names: named(dictionary), named(init(dictionary:)))) タイプに新しいメンバーを追加します。@DictionaryStorage自動的に生成されますdictionary属性とinit(dictionary:)コンストラクタ。

conformance@attached(conformance)) タイプにプロトコル準拠を追加します。@DictionaryStorage構造が自動的に従うようになりますDictionaryRepresentableプロトコル。

マクロは同時に複数の役割を持つことができます。@DictionaryStorageアクセサー、メンバー属性、メンバー、適合性の 4 つの役割を同時に実装し、数十行の定型コードを 1 行のアノテーションに圧縮します。

11:23

実装メカニズム

マクロの実装は、別のコンパイラ プラグインで記述されます。申告用#externalMacro実装に関連するもの:

@freestanding(expression)
macro stringify<T>(_ expr: T) -> (T, String) = #externalMacro(
    module: "MyLibMacros",
    type: "StringifyMacro"
)

実装には、SwiftSyntax、SwiftSyntaxMacros、および SwiftSyntaxBuilder をインポートする必要があります。

import SwiftSyntax
import SwiftSyntaxMacros
import SwiftSyntaxBuilder

struct DictionaryStorageMacro: MemberMacro {
    static func expansion(
        of attribute: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        return [
           "init(dictionary: [String: Any]) { self.dictionary = dictionary }",
           "var dictionary: [String: Any]"
        ]
    }
}

18:01

エラー報告

マクロを渡すことができるcontext.diagnose()コンパイラにエラーを報告します。

struct DictionaryStorageMacro: MemberMacro {
    static func expansion(
        of attribute: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard declaration.is(StructDeclSyntax.self) else {
            let structError = Diagnostic(
                node: attribute,
                message: MyLibDiagnostic.notAStruct
            )
            context.diagnose(structError)
            return []
        }
        return [
            "init(dictionary: [String: Any]) { self.dictionary = dictionary }",
            "var dictionary: [String: Any]"
        ]
    }
}

enum MyLibDiagnostic: String, DiagnosticMessage {
    case notAStruct

    var severity: DiagnosticSeverity { return .error }

    var message: String {
        switch self {
        case .notAStruct:
            return "'@DictionaryStorage' can only be applied to a 'struct'"
        }
    }

    var diagnosticID: MessageID {
        MessageID(domain: "MyLibMacros", id: rawValue)
    }
}

このように、開発者としては@DictionaryStorage列挙型で使用例すると、Xcode はエラー メッセージを直接表示します。

25:17

命名宣言

マクロの展開中に生成された新しい宣言は、マクロの中に含める必要があります。names:パラメータで予測されます。これは、マクロを展開する前にコンパイラがプログラム構造を理解するのに役立ちます。

  • named(xxx)— xxx という名前のステートメントを生成します -overloaded— ターゲットと同じ名前のオーバーロード宣言を生成します。 -arbitrary— 任意の名前の宣言を生成します (明示的な宣言が必要です)
@attached(conformance)
@attached(member, names: named(dictionary), named(init(dictionary:)))
@attached(memberAttribute)
@attached(accessor)
macro DictionaryStorage(key: String? = nil)

@attached(peer, names: overloaded)
macro AddCompletionHandler(parameterName: String = "completionHandler")

@freestanding(declaration, names: arbitrary)
macro makeArrayND(n: Int)

35:44

##詳細

#ラップ解除マクロの実装

#unwrap危険な表現を変換する実用的な表現マクロです。!強制的に解凍し、詳細なエラー情報に置き換えますpreconditionFailure

let image = #unwrap(downloadedImage, message: "was already checked")

// 展开后:
{ [downloadedImage] in
    guard let downloadedImage else {
        preconditionFailure(
            "Unexpectedly found nil: 'downloadedImage' was already checked",
            file: "main/ImageLoader.swift",
            line: 42
        )
    }
    return downloadedImage
}()

実装中には、いくつかの詳細に対処する必要があります。

  1. キャプチャ変数名は外部スコープと競合する可能性があるため、次を使用例する必要があります。context.makeUniqueName()一意の名前を生成する
  2. エラー メッセージには、元の式の説明を含める必要があります。originalWrapped.description3. 渡されたファイルと行番号context.location(of:)得る
static func makeGuardStmt(
    wrapped: TokenSyntax,
    originalWrapped: ExprSyntax,
    message: ExprSyntax,
    in context: some MacroExpansionContext
) -> StmtSyntax {
    let messagePrefix = "Unexpectedly found nil: '\(originalWrapped.description)' "
    let originalLoc = context.location(of: originalWrapped)!

    return """
        guard let \(wrapped) else {
            preconditionFailure(
                \(literal: messagePrefix) + \(message),
                file: \(originalLoc.file),
                line: \(originalLoc.line)
            )
        }
    """
}

キーポイント:

  • \(literal: messagePrefix)エスケープ問題を避けるために文字列をリテラルとして挿入します -context.location(of:)元の式のソース コードの場所を返します。 -context.makeUniqueName()外部変数と競合しない一意の識別子を生成する

30:17

@DictionaryStorage の完全な進化

@DictionaryStorage複数の役割を組み合わせてコードを徐々に簡素化する方法を示します。

元のコード:

struct Person: DictionaryRepresentable {
    init(dictionary: [String: Any]) { self.dictionary = dictionary }
    var dictionary: [String: Any]

    var name: String {
        get { dictionary["name"]! as! String }
        set { dictionary["name"] = newValue }
    }
    var height: Measurement<UnitLength> {
        get { dictionary["height"]! as! Measurement<UnitLength> }
        set { dictionary["height"] = newValue }
    }
    var birthDate: Date? {
        get { dictionary["birth_date"] as! Date? }
        set { dictionary["birth_date"] = newValue as Any? }
    }
}

ステップ 1: アクセサー ロールを使用例して、各プロパティのゲッター/セッターを追加します。

struct Person: DictionaryRepresentable {
    init(dictionary: [String: Any]) { self.dictionary = dictionary }
    var dictionary: [String: Any]

    @DictionaryStorage var name: String
    @DictionaryStorage var height: Measurement<UnitLength>
    @DictionaryStorage(key: "birth_date") var birthDate: Date?
}

ステップ 2: memberAttribute ロールを追加して、@DictionaryStorageプロパティごとに自動的に追加されます@DictionaryStorage

@DictionaryStorage
struct Person: DictionaryRepresentable {
    init(dictionary: [String: Any]) { self.dictionary = dictionary }
    var dictionary: [String: Any]

    var name: String
    var height: Measurement<UnitLength>
    @DictionaryStorage(key: "birth_date") var birthDate: Date?
}

ステップ 3: メンバー役割を追加して自動的に生成するinit(dictionary:)そしてdictionary財産:

@DictionaryStorage
struct Person {
    var name: String
    var height: Measurement<UnitLength>
    @DictionaryStorage(key: "birth_date") var birthDate: Date?
}

ステップ 4: 自動的に従う適合ロールを追加するDictionaryRepresentable

@DictionaryStorage
struct Person {
    var name: String
    var height: Measurement<UnitLength>
    @DictionaryStorage(key: "birth_date") var birthDate: Date?
}

20 行を超える元の定型コードは、最終的に 5 行に圧縮されました。

17:28

テストマクロ

SwiftSyntaxMacrosTestSupport が提供するassertMacroExpansion

import MyLibMacros
import XCTest
import SwiftSyntaxMacrosTestSupport

final class MyLibTests: XCTestCase {
    func testMacro() {
        assertMacroExpansion(
            """
            @DictionaryStorage var name: String
            """,
            expandedSource: """
            var name: String {
                get { dictionary["name"]! as! String }
                set { dictionary["name"] = newValue }
            }
            """,
            macros: ["DictionaryStorage": DictionaryStorageMacro.self]
        )
    }
}

38:28

重要なポイント

非同期関数のコールバック バージョンを自動的に生成

@AddCompletionHandlerアイデアを広げることができます。多くのプロジェクトを同時に維持する必要があるasyncバージョンとコールバックのバージョン。自動的に開始するマクロを作成するasyncこの関数は完了ハンドラー バージョンを生成します。これにより、多くの重複コードを削除できます。

入り口:@attached(peer, names: overloaded)。関数のパラメーターと戻り値の型を読み取り、元の関数を呼び出す新しい関数を生成し、タスクで結果を待ちます。

Equatable/Hashable のカスタム実装を自動的に生成

Swiftが自動合成してくれるEquatable時には十分ではありません。 1つ書いてもいいよ@CustomEquatable比較に参加するフィールドと、属性タグに基づいて比較する方法を決定するマクロ。

入り口:@attached(member) + MemberMacro。属性を調べてマークを付ける@CompareBy属性は、対応する比較ロジックを生成します。

SwiftUI のプレビュー コードを生成

書く@PreviewVariantsビュー上でマークされたマクロは、複数のプレビュー バリエーション (さまざまなサイズ、さまざまなカラー モード、さまざまなダイナミック フォント) を自動的に生成します。

入り口:@attached(member)。 View タイプを読み取り、複数のビューを生成します。PreviewProvider成し遂げる。

ビルダー モード コードを自動的に生成

多くのクラスでは、複雑なオブジェクトを構築するために Builder パターンが必要です。書く@Builderマクロは、対応する Builder クラスとクラスの連鎖呼び出しメソッドを自動的に生成します。

入り口:@attached(peer)。クラスのすべての属性を読み取り、同じ名前の Builder クラスを生成します。各属性は、withXxx()方法とbuild()方法。

関連セッション

コメント

GitHub Issues · utterances