WWDC Quick Look 💓 By SwiftGGTeam
Write Swift macros

Write Swift macros

元の動画を見る

ハイライト

Swift 5.9ではマクロ(Macro)機構が導入されました。開発者は、型安全性を維持し、IDE の拡張と表示、単体テストをサポートし、カスタム エラーをコンパイラに報告しながら、コンパイル時に繰り返しコードを生成するコンパイラ プラグインを作成できます。

主な内容

重複したコードを書くことは、すべての開発者が嫌うことです。たとえば、一連の算数演習を維持したい場合は、各質問で計算結果と対応する計算文字列を同時に記録する必要があります。

let calculations = [
    (2 + 3, "2 + 3"),
    (10 * 5, "10 * 5"),
]

この書き込み方法には、重複、冗長性、およびエラーが発生しやすいという 3 つの問題があります。結果と計算文字列は手動で完全に調整されます。一方を変更してもう一方を変更するのを忘れると、バグが埋もれてしまいます。

Swift 5.9 で提供される解決策はマクロです。#stringify(a + b)この書き方は自動的に次のように展開されます。(a + b, "a + b")、結果と計算式は常に一致します。

マクロの動作は C プリプロセッサとはまったく異なります。 C マクロは型チェックの前にテキスト置換を行いますが、Swift マクロは型チェック後にそれを行います。これは、コンパイラがマクロ展開を呼び出す前に、マクロ パラメータが型チェックに合格する必要があることを意味します。 1つ合格しますStringリクエストを与えるIntマクロの場合、コンパイラは直接エラーを報告し、マクロ展開段階には入りません。

01:15

独立した式マクロ

#stringify独立式マクロ (独立式マクロ) に属している場合は、次を使用例します。#最初は、式が必要などこでも使用例できます。

マクロの宣言は関数のように見えますが、次を使用例します。@freestanding(expression)マーク:

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

#externalMacroマクロ宣言を実装に関連付けます。実装は別のコンパイラ プラグインで記述され、独立したプロセスとして実行されます。

05:55

追加のメンバーマクロ

独立した式マクロに加えて、添付マクロ (添付マクロ) もあります。@冒頭では、ステートメントを強化するために使用例されます。

講演者はスキーシーンの例を挙げた。 1つありますSlope列挙はすべての雪道を表しており、EasySlope初心者に適した斜面のサブセットを表す列挙。EasySlope2 つの変換メソッドを手動で記述する必要があります。Slope初期化されたinit?そして転送して戻りますSlopeslope財産。

enum EasySlope {
    case beginnersParadise
    case practiceRun

    init?(_ slope: Slope) {
        switch slope {
        case .beginnersParadise: self = .beginnersParadise
        case .practiceRun: self = .practiceRun
        default: return nil
        }
    }

    var slope: Slope {
        switch self {
        case .beginnersParadise: return .beginnersParadise
        case .practiceRun: return .practiceRun
        }
    }
}

サブセット列挙が追加されるたびに、定型コードを再度記述する必要があります。使用例@SlopeSubsetマクロを自動生成できるinit?方法:

@SlopeSubset
enum EasySlope {
    case beginnersParadise
    case practiceRun
}

12:05

マクロのエラー報告

マクロは、その使用例シナリオが正しいかどうかを確認できます。例えば@SlopeSubset列挙にのみ使用例できます。開発者が構造上でこれを誤って使用例すると、マクロはコンパイル エラーをスローする可能性があります。

enum SlopeSubsetError: CustomStringConvertible, Error {
    case onlyApplicableToEnum

    var description: String {
        switch self {
        case .onlyApplicableToEnum:
            return "@SlopeSubset can only be applied to an enum"
        }
    }
}

マクロ実装で宣言された型が一致しない場合は、その型を確認してください。throw

guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
    throw SlopeSubsetError.onlyApplicableToEnum
}

このようにして、開発者が間違った場所でマクロを使用例すると、Xcode はわかりやすいエラー プロンプトを直接表示します。

28:00

##詳細

マクロプロジェクトを作成する

Xcode 15 は Swift マクロ テンプレートを提供します。 [ファイル] > [新規] > [パッケージ] で Swift マクロ テンプレートを選択し、マクロ定義、マクロ実装、テストを含む完全なプロジェクト構造を作成します。

生成されたテンプレート#stringifyマクロは完全なワークフローを示します。

let a = 17
let b = 25
let (result, code) = #stringify(a + b)
print("The value \(result) was produced by the code \"\(code)\"")

マクロ呼び出しを右クリックし、「マクロの展開」を選択して、展開されたコードを表示します。

(a + b, "a + b")

05:55

マクロの実装原理

コンパイラはマクロ呼び出しを検出すると、マクロ式全体のソース コードをコンパイラ プラグインに送信します。プラグインは SwiftSyntax を使用例してソース コードを構文ツリーに解析し、マクロ実装が構文ツリーで変換を実行し、結果をソース コードにシリアル化してコンパイラに返します。

StringifyMacroこの実装では、基本的なパターンを示します。

public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        guard let argument = node.argumentList.first?.expression else {
            fatalError("compiler bug: the macro does not have any arguments")
        }

        return "(\(argument), \(literal: argument.description))"
    }
}

キーポイント:

  • 実現するExpressionMacroプロトコル -expansion(of:in:)構文ツリーのノードとコンテキストを受信します
  • 引数リストから最初の式を抽出します
  • 新しいものを返すExprSyntax、文字列補間を使用例して展開結果を構築します -\(literal: ...)式の説明を文字列リテラルに変換します

07:10

メンバーマクロの実装

SlopeSubsetMacro成し遂げるMemberMacro列挙用に生成されたプロトコルinit?方法:

public struct SlopeSubsetMacro: MemberMacro {
    public static func expansion(
        of attribute: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
            throw SlopeSubsetError.onlyApplicableToEnum
        }

        let members = enumDecl.memberBlock.members
        let caseDecls = members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
        let elements = caseDecls.flatMap { $0.elements }

        let initializer = try InitializerDeclSyntax("init?(_ slope: Slope)") {
            try SwitchExprSyntax("switch slope") {
                for element in elements {
                    SwitchCaseSyntax("""
                    case .\(element.identifier):
                        self = .\(element.identifier)
                    """)
                }
                SwitchCaseSyntax("default: return nil")
            }
        }

        return [DeclSyntax(initializer)]
    }
}

キーポイント:

  • 使用例するdeclaration.as(EnumDeclSyntax.self)宣言を列挙型宣言に変換する -memberBlock.membersメンバーリストを取得する -compactMapenum case ステートメントをフィルターで除外する -flatMap { $0.elements }すべての case 要素を抽出する
  • 使用例するInitializerDeclSyntaxそしてSwitchExprSyntaxSwiftSyntax Builder からの構文ツリーの構築
  • case 要素をトラバースして、対応する switch ブランチを生成します。
  • 戻る[DeclSyntax]配列

19:25

マクロプラグインの登録

すべてのマクロ実装は、コンパイラ プラグイン エントリに登録する必要があります。

@main
struct WWDCPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        StringifyMacro.self,
        SlopeSubsetMacro.self
    ]
}

16:23

テストマクロ

SwiftSyntaxMacrosTestSupport が提供するassertMacroExpansionマクロ展開の結果をテストするには:

final class WWDCTests: XCTestCase {
    func testMacro() {
        assertMacroExpansion(
            """
            #stringify(a + b)
            """,
            expandedSource: """
            (a + b, "a + b")
            """,
            macros: testMacros
        )
    }
}

let testMacros: [String: Macro.Type] = [
    "stringify": StringifyMacro.self
]

テストメンバーマクロ:

func testSlopeSubset() {
    assertMacroExpansion(
        """
        @SlopeSubset
        enum EasySlope {
            case beginnersParadise
            case practiceRun
        }
        """,
        expandedSource: """
        enum EasySlope {
            case beginnersParadise
            case practiceRun
            init?(_ slope: Slope) {
                switch slope {
                case .beginnersParadise:
                    self = .beginnersParadise
                case .practiceRun:
                    self = .practiceRun
                default:
                    return nil
                }
            }
        }
        """,
        macros: testMacros
    )
}

エラー シナリオをテストすることもできます。

func testSlopeSubsetOnStruct() throws {
    assertMacroExpansion(
        """
        @SlopeSubset
        struct Skier {
        }
        """,
        expandedSource: """
        struct Skier {
        }
        """,
        diagnostics: [
            DiagnosticSpec(message: "@SlopeSubset can only be applied to an enum", line: 1, column: 1)
        ],
        macros: testMacros
    )
}

09:12

一般化されたマクロ

SlopeSubsetに一般化することができますEnumSubset、親の列挙を指定する汎用パラメータを受け入れます。

@attached(member, names: named(init))
public macro EnumSubset<Superset>() = #externalMacro(
    module: "WWDCMacros",
    type: "SlopeSubsetMacro"
)

マクロ実装で汎用パラメータを抽出します。

guard let supersetType = attribute
    .attributeName.as(SimpleTypeIdentifierSyntax.self)?
    .genericArgumentClause?
    .arguments.first?
    .argumentType else {
    return []
}

それで@EnumSubset<Slope>任意の列挙サブセットに使用例できます。

31:03

重要なポイント

安全な強制解凍マクロを作成する

チームには常に執筆者がいます!強制的に解凍すると、問題の特定が困難になります。一つ作ることができます#unwrapマクロ。ファイルと行番号を含むように展開されます。preconditionFailure

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

入り口:@freestanding(expression) + ExpressionMacroプロトコル。参照#stringify実現するには、使用例しますcontext.location(of:)ソースコードの場所を取得します。

Codable の代替実装を自動的に生成

一部のモデルにはカスタムのコーディングキーが必要ですが、ほとんどのフィールドは単なる単純なキーマッピングです。 1つ書いてもいいよ@CustomCodableマクロ、自動生成init(from:)そしてencode(to:)一方で、属性タグを使用例して特定のフィールドの動作をオーバーライドできるようになります。

入り口:@attached(member) + MemberMacroプロトコル。属性宣言を調べて、各属性に対応するデコード/エンコード コードを生成します。

ステート マシンの変換コードを生成

ゲームやワークフローにはステート マシンが存在することがよくあり、各状態は遷移先のターゲット状態を定義する必要があります。 1つ書いてもいいよ@StateMachineマクロ。状態宣言に基づいて状態遷移メソッドと有効性チェックを自動的に生成します。

入り口:@attached(member, names: arbitrary) + MemberMacroプロトコル。すべてのケースを読み取り、状態ごとに生成するcanTransition(to:)そしてtransition(to:)方法。

SwiftUI プレビュー用のマルチデバイス スナップショットを生成

1つ書いてもいいよ#previewDevicesマクロを宣言して複数に展開する.previewDeviceモディファイアを組み合わせて、さまざまなデバイスへの効果を一度に確認できます。

入り口:@freestanding(declaration)+ 複数生成PreviewProvider声明。

関連セッション

コメント

GitHub Issues · utterances