ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swiftマクロの拡張
Swift マクロがどのようにコードベースの冗長な部分を削減し、複雑な機能をより簡単に取り入れるのに役立つのかを考えましょう。マクロがコードを分析し、正しい使用方法に導くための豊富なコンパイラエラーを出力し、新しいコードを生成して自動的にプロジェクトに組み込む方法を学びます。また、マクロの役割、コンパイラプラグイン、構文木などの重要な概念も解説します。
関連する章
- 0:00 - Introduction
- 0:51 - Why macros?
- 2:13 - Design philosophy
- 4:48 - Translation model
- 6:18 - Macro roles
- 17:48 - Macro implementation
- 33:36 - Writing correct macros
- 38:42 - Wrap up
リソース
関連ビデオ
WWDC23
-
ダウンロード
♪♪
SwiftチームのBeccaです 今日はSwiftマクロの カスタマイズできる 機能を紹介します まずはマクロの説明をします 次にSwiftマクロの 設計で考慮した いくつかの原則について 説明します そして Swiftマクロの動作方法や 他のコードとの 対話方法を説明します
その後 マクロの実装方法と 正しい動作の 確認方法を紹介します
では Swiftマクロの サポートから教えます 表現豊かなコードやAPIを 書くのが得意です そのため 派生させたものや Result builderなどの ユーザーが冗長なコードを 何度も書かなくて済む 機能を提供しています これらの機能は基本的に 同じ方法で動作します 例えば Codable に 準拠するとき メンバの実装を 提供しない場合 自動的にその準拠を 一連のメンバに展開し プログラムに挿入します グレーボックスにその準拠の展開を 表示しています このコード生成により Codableを使用する際には 具体的な動作方法を 知る必要がなく 有益なサポートで簡潔なコードを 書く価値があります Swiftには 多くの機能があります 簡単な構文を書くと コンパイラが自動的に 複雑なコードに展開します では 既存の機能が思うように 動作しない場合はどうでしょう? Swiftコンパイラは オープンソースなので 新しい機能を追加できます それには私自身が ビデオ会議で 他のSwiftプロジェクトリーダーと 機能について話し合う 必要があります ここにマクロを導入した 背景があります。 冗長さを排除しつつ 独自の言語機能を 追加できるようにしていますが コンパイラを変更せずに Swiftパッケージとして配布可能です 他の言語ではマクロを使った 経験がない人もいます でも経験者は複雑な感情を 抱くかもしれません Swiftデベロッパの多くは Objective-Cや Cプリプロセッサを使用する 他言語に精通している為 マクロの制約や問題点を 知っているからです でもSwiftのマクロは問題を 回避する方法が異なります 設計する際 4つの目標を掲げました 第一の目標は マクロ使用時の明示性です マクロには2種類あります Freestandingマクロは コード内の他要素の代用 常に (#) で始まります Attachedマクロはコード内の 宣言に属性として使用 常に (@) で始まります Swiftでは既に (#) と (@) を 使用しています マクロは 拡張可能にするだけです それらの記号が 見当たらなければ マクロは使用されていないと 確信できます 第二の目標は マクロに 渡されるコードが完全であり エラーがないか 確認されることです 引数は完全な式である 必要があるため 1+ のようなものは マクロに渡せません またマクロの引数と結果は 関数の引数と同様に 型チェックされるため 誤った型の引数を渡せません マクロの実装では 入力を検証し 問題がある場合は エラーを発生できる為 マクロ使用方法を確実に することが容易になります 第三の目標は マクロの展開が予測可能で 追加的にプログラムに 組み込まれることです プログラム内の可視コードに のみ追加できます 削除や変更はできません someUnknownMacro が 何をするか分からなくても finishDoingThingy の呼出を 削除したり 新しい関数に移動しないと 確信できます なのでコードの 読みやすさが向上します 最後の目標はマクロが 不可解ではないことです マクロはプログラムに コードを追加するだけ Xcodeで直接確認できます 右クリックして 展開内容を確認できます デバッガでステップ実行や ブレークポイント設定もできます マクロ展開内のコードが コンパイルできない場合 展開内のエラー箇所と ソースコード内でどこに 行くかが表示されます これらのツールは ライブラリが提供する クローズドソースの マクロでも動作します マクロの作成者は ユニットテストを作成して 期待どおりに動作することを 確認できます Swiftのマクロは デベロッパにとって 理解しやすく メンテナンスしやすいかと思います ここからはSwiftマクロが どう目標を達成するかについて 説明します まず 基本的な概念を 押さえましょう Swiftがコード内で マクロを呼び出すと Xcode のマクロ パッケージテンプレートの stringify マクロなど コードから抽出され 処理する為のマクロ実装が 含まれた 特別なコンパイラプラグインに 送ります セキュアなサンドボックス内で 別プロセスとして実行し マクロ作者が書いた独自の Swiftコードが含まれます マクロの使用を処理し 生成された新しいコードの 断片である拡張を返します Swiftコンパイラは その拡張をプログラムに追加し コードと拡張を一緒に コンパイルします プログラムを実行すると まるで自分で拡張を 書いたかのように機能します 重要なポイントがあります Swiftは stringify マクロを どう把握したのか 答えは マクロ宣言です マクロ宣言は API を提供します マクロ宣言は独自の モジュールに直接書けて ライブラリやフレームワークから インポートできます 名前とシグネチャーを指定し パラメータ数やラベルや型 そしてマクロがある場合の 結果の型を指定でき 関数宣言と同様です そして1つ以上の属性があり マクロの役割を指定します マクロの役割を考えずに 作成するのは不可能です では 役割とは何か 異なる種類のマクロの 作成方法を説明します 役割はマクロに対する 一連のルールです マクロを適用する場所と方法 展開されるコードの種類 そして展開がコードに 挿入される場所を規定します 最終的にはマクロの役割が 拡張を予測可能で 追加可能な方法で 挿入する責任があります freestandingマクロを 作成する2つの役割とは Expression と Declaration です そして Attachedマクロを 作成する5つの役割とは "Peer" "Accessor" "member attribute" "member" そして "conformance"です 早速この役割と使用した場合について 見ましょう freestanding expression の 役割から始めます "expression" と聞いて ピンとこない場合は 実行されて結果を生成する コードの単位を指します この "let" 文では 等号後の算術演算が式です しかし 式は再帰的な構造を持ち 小さな式で構成されます したがって "x + width" だけでも式となります 単語 "width" 自体も式です "freestanding expression" 式に展開されるマクロです 使用方法とは 強制的にアンラップすると 想像して下さい Swiftには強制的にアンラップ する演算子がありますが 容易すぎると感じ 安全性を考慮せず スタイルガイドでは nil にならないと示す為に 複雑なコードを書くよう 指示してきます ただし代替案はほぼ "guard let" を使用して "else" ブランチで "preconditionFailure"を 呼び出すなどはやや冗長です さて バランスを取る マクロを設計しましょう このマクロは値を計算して 返すようにしたいので "freestanding(expression)” マクロにします それに "unwrap" という名前と ジェネリック型を与えます 更に渡される文字列も unwrap に失敗した場合に 表示されます 従って 関数のように 呼び出すしますが "guard let" を含む式に 展開されるマクロができます またエラーメッセージには 変数名も含まれ これは通常の関数では 不可能です freestanding expression の次は freestanding declaration を見てみましょう 1つ以上の宣言に 展開されます 関数 変数 または 型 何に使えるでしょうか 例えば2次元配列の必要な 統計解析を書いているとします 配列内の行が同じ列数を 持つようにしたいので 行ごとに同じ列数を持つ 単一の配列が必要です
代わりに要素をフラットな 一次元配列に格納し デベロッパが渡す 二次元インデックスから 一次元インデックスを 計算します 次のような型を作成できます "makeIndex" 関数は 2つの整数を受取り 1次元のインデックスに 変換します しかし プログラムの 別の部分で 3次元配列が必要だと 気付きます 2次元配列とほぼ同じです いくつかの追加の インデックスがあり 計算も少し複雑です そして4次元配列 5次元配列が必要になり やがてほぼ同じ配列型に 取り囲まれますが ジェネリックやプロトコル拡張 又はサブクラスなどの Swiftが提供する 他の機能では うまく対応できません 幸いにも各構造体は 宣言であるため 宣言マクロを使用して 作成できます では "makeArrayND"という freestanding declaration マクロを宣言しす これは次元のN配列型を 作成する為のものです 次元の数を Int パラメータ として渡し 結果の型は宣言せず 宣言を追加する為であり 他のコードで 使用される結果を 計算するものではありません マクロを4回呼び出すことで 2次元 3次元 4次元 5次元の 適切な数の引数と そのサイズに対する 計算を含む完全な 多次元配列型に展開されます freestanding Macrosについて 紹介しました 次は attached macrosを 見ていきましょう 名前の通り特定の宣言に 関連付けられます つまり関連付けた宣言に アクセスできます Freestanding の場合 渡された引数のみ使用し Attached は関連付けられた 宣言にアクセスできます その宣言を調査し 内部から名前や型 及び その他の 情報を取得します では Peerロールから 始めましょう Peerマクロは 変数や関数 型だけでなく インポートや オペレータの宣言など どんな宣言にも アタッチすることができ 新しい宣言を 一緒に挿入できます メソッドやプロパティに 使用すると その型のメンバーが作成され トップレベルの関数や型に 使用すると 新しいトップレベルの宣言が 作成されます 従って非常に柔軟となります では使用方法を教えます Swift の 並行性のテクニックを使う クライアント向けに 完了ハンドラを使用する APIのバージョンを 提供したいとします メソッド作成は 難しくないです 単純に "async" キーワードを削除し 完了ハンドラの パラメータを追加し タスク内で非同期バージョン を呼び出すだけです しかし これを頻繁に 行う必要があり 手作業で書くことは 避けたいです 正にアタッチされた Peerマクロに最適な仕事です AddCompletionHandler という名前のマクロを宣言し 引数としてCompletion Handlerの 引数ラベルを受取 それを非同期メソッドの 宣言にアタッチします マクロは元の メソッドと同等の Completion Handlerの シグネチャを作成し メソッドの本体を記述し ドキュメンテーション コメントも追加します いい感じですよね 次にaccessorロールを 見てみましょう アタッチされた accessor ロールは 以下のaccessor を 追加できます "get" や "set" "willSet" または "didSet" どう便利なのか 辞書をラップし プロパティを使用して アクセスできるタイプが 幾つかあるとしましょう 例えば "Person" 構造体では "name" "height" 及び "birth_date" に アクセスできますが 3つのフィールド以外にも 辞書に他情報がある場合 それは保持され プログラムでは無視されます 計算されたゲッターと セッターが必要ですが 手動で書くのは面倒ですし 他の格納プロパティに アクセスできないため プロパティラッパーを 使用することもできません アタッチされたアクセサ マクロを作成しましょう アタッチされたマクロは "DictionaryStorage" キーパラメータは "key" "birth_date" は⸺ アンダースコアで スペルしていますが キーを省略しても構わず デフォルトで nil になり プロパティの名前を キーとして使用します これで大きなアクセサ ブロックを書く代わりに 各プロパティの前に “@DictionaryStorage" と記述するだけで マクロがアクセサを生成する まだ幾つかの 定型文がありますね "DictionaryStorage" の 属性が同じであることです 確かに それらはまだ煩雑です 一部の組み込み属性は 型や拡張全体に適用する事で このような状況に 対処できます attached member attribute 制限可能なマクロです マクロは特定の型や拡張に 関連付けられ それに関連付けられた メンバに属性を追加できます 実際にやってみましょう 異なる方法を試してみます 新しいマクロを 宣言する代わりに DictionaryStorageマクロに もう一つの役割属性を追加 これはattached accessor に既存する役割属性です マクロ作成に有効な テクニックです freestanding以外 組合わせ可能ですが 一部ではSwiftが 判断できない場合があります Swift は意味がある 役割を展開しますが 少なくとも1つの役割が 必要です もし DictionaryStorageを 型に付加すると member attribute の 役割をSwiftが展開します プロパティに付加すると accessorの役割を展開します しかし関数に付加すると コンパイラエラーが発生 DictionaryStorage には関数にアタッチする為の 役割が存在しないからです
DictionaryStorageに 2番目のロールが追加され 個々のプロパティに 別々に付加するのではなく 型全体に一度に付加できます イニシャライザや dictionaryプロパティ 既にDictionaryStorage 属性を持つ birth_date 等の プロパティなど 特定メンバをスキップする ロジックがあります でも他にDictionaryStorage 属性を追加し それらの属性は既に 見たアクセサに展開されます まだ削除できる 冗長な部分があります イニシャライザと ストアドプロパティです DictionaryRepresentable プロトコルで要求され プロパティはaccessorにより 使用されますが DictionaryStorageでの 型は同一です 確かに手動で記述する 必要がないように 自動的にこれらを 追加するようにしましょう attached memberを 使用して実現できます Member Attributeマクロと同様に これらのマクロは新しい メンバを追加するため 型や拡張に 適用することができます イニシャライザなどを 追加することができます ストアドプロパティを 列挙型にケース追加できます 再び DictionaryStorage マクロに新しい attached member を 追加し組合わせます イニシャライザとdictionary 名前のプロパティを追加
異なるマクロが同じコードに 適用される場合 どちらのマクロが最初に 展開されるか 気になるかもしれません その答えは 重要ではありません 各マクロは他のマクロにより 展開されたものではない 元の宣言の バージョンを見ます 従って順序を気にする 必要はありません タイミングに関わらず 同じ結果が得られます attached memberが 追加された事で 2つのメンバを手動で 記述する必要はありません DictionaryStorageを使えば 自動的に追加されます 他の役割はプロパティに DictionaryStorageを追加し それらの属性はaccessorに 展開されます
しかし まだ排除するべき 冗長な部分があります DictionaryRepresentable プロトコルへの適合です attached conformance これで完璧に自動化できます 適用先の型や拡張に 適合性を追加します attached conformance ロールを DictionaryStorageに追加し 他3つと組合わせます 新しいロールに適合性を追加 DictionaryRepresentation 従って適合性を手動で 書く必要はありません アクセサと生成された メンバに追加した DictionaryStorage属性は 自動的に動作に加えて conformanceも追加します さて時間が経ちましたので 改めてお伝えすると 煩雑で繰り返しの 多い大規模な型を取り 大部分を強力なマクロの いくつかの役割に移動させました その結果 残されたコードは特定の 型に特化した部分のみを 簡潔に指定できる様に なりました もし DictionaryStorage を 使える多数の型があったら? すべて扱うのが どれだけ簡単になるのか 宣言や役割について 話しましたが そのコードがどの様に 現れるのかはまだ不明です そのギャップを埋めて マクロ実装方法を説明します 今までマクロ宣言について 説明した際に 重要な要素を省いていました それは実装です それは等号の後にあり 常に別のマクロです パラメータを再配置したり 追加のパラメータを リテラルとして指定する事で 時には別のマクロとなります しかし通常は外部の マクロを使用します 外部のマクロはコンパイラの プラグインによる実装です 以前コンパイラのプラグインについて 話しましたね マクロの使用が検出されると コンパイラは 別のプロセスでプラグインを起動 マクロの展開を依頼します #externalMacro は その関係を定義するものです それはコンパイラが 起動すべきプラグインと そのプラグイン内の型の 名前を指定します Swift が このマクロを展開する際 MyLibMacros という名前の プラグインを起動し StringifyMacro という型に 展開を依頼します マクロの宣言は通常の ライブラリ内に配置され 実装はコンパイラの プラグインモジュールに配置 #externalMacroは 宣言とそれを実装する 型との間のリンクを 作成します マクロの実装は どのようなものか DictionaryStorage の 実装方法を見ましょう DictionaryStorageマクロは attached memberという 役割を持ち 格納プロパティ とイニシャライザが追加 こちらが実装例です 実装を見て 動作を学びましょう まず上でSwiftSyntaxという ライブラリをインポート Swiftプロジェクトにより メンテナスされたもので 解析 検査 操作 生成するのに役立ちます SwiftSyntax 言語仕様の 進化に合わせアップデートし 全ての機能を サポートしています SwiftSyntax は特別な Tree構造として表現します 例えばコードサンプルの Person という構造体は StructDeclSyntaxという型の インスタンスとして表示 しかし そのインスタンスに プロパティがあり それぞれのプロパティは 構造体の宣言の一部を 表します 属性リストはこのプロパティ attributes にあります 実際の struct の キーワードは structKeyword プロパティにあります 構造体の名前は identifier にあります そして 波括弧と構造体のメンバが 含まれる本体は memberBlock プロパティにあります また一部の構造体宣言に関連する modifiers等のプロパティも あります この例では存在しませんので これらは nil です これらに含まれる一部は tokens と呼ばれます これらはソースファイル内の 特定のテキストを表し 名前やキーワード 句読点などが含まれ そのテキストと 周囲のスペースや コメントなどの情報が 含まれます 構文木を深く掘り下げると ソースファイルの各バイトを カバーするトークンノードが 見つかります しかし attributes 内の AttributeListSyntaxノードや memberBlock内の MemberDeclBlockSyntax それらの一部ノードは トークンではありません これらにはプロパティに 子ノードが存在します 例えば memberBlock プロパティ内を見ると 開始波括弧に 対応するトークン メンバリストに対応する MemberDeclListSyntax 終了波括弧に対応する トークンが含まれています MemberDeclListSyntax ノードの中身を探求するなら 各プロパティに対応する ノードを見つけられます SwiftSyntaxを扱うのは 非常に広範なトピックであり この動画を2倍に 長くする代わりに 他の2つの リソースを紹介します 1つ目は Write Swift Macros 特定のソースコードが構文Tree としてどう表現されるか 理解する為の実践的な ヒントが提供されています 2つ目は SwiftSyntax パッケージのドキュメント オンラインでも見れます 又はマクロパッケージ内で Xcodeの Build Documentation コマンドを使用すると Developer Documentation ウィンドウに表示されます メインの SwiftSyntax ライブラリに加えて 2つの他のモジュールも インポートしています 1つはSwiftSyntaxMacros マクロを書くために 必要なプロトコルや 型を提供します 2つ目のモジュールは SwiftSyntaxBuilder このライブラリは 新しく生成されたコードを 表現するの構文Treeを構築する 便利なAPIを提供します このモジュールを使わず マクロ記述できますが 便利なので積極的に 活用する事を推奨します では実際にプラグインが 提供する DictionaryStorageMacro という型の記述は始めます 留意点は MemberMacro プロトコルに適合してること 各役割には対応する プロトコルがあり 実装はマクロが提供し 各役割のために対応する プロトコルに適合する 必要があります DictionaryStorageには 役割が4つあります DictionaryStorageMacro型は 4つの対応するプロトコルに 適合する必要があります ただし現時点では簡単にするために MemberMacroに対する 適合性のみ考えています この型の本体に進むと expansion of, providingMembersOf, in というメソッドがあります MemberMacroプロトコルにより 要求され マクロが使用されたときに マクロのメンバー役割を 展開するために Swift コンパイラが呼び出します まだ引数を未使用ですが 後で話します 現時点では静的メソッドです 全ての展開メソッドは 静的なため Swift は DictionaryStorageMacroの インスタンスを作成しません 代わりにメソッドを含む コンテナとして使用 展開メソッドの各々は ソースコードに挿入される SwiftSyntax ノードを 返します メンバーマクロは型に 追加する宣言のリストとして 展開される為 メンバの展開メソッドは DeclSyntax ノードの配列を返します 本体を見れば 配列作成されたと分かります これはイニシャライザと ストアドプロパティです この var dictionary は 通常の文字列に見えますが 実際には違います この文字列リテラルは DeclSyntax が 必要な場所に書かれており 実際には その文字列を DeclSyntaxノードに 変換する様に要求します SwiftSyntaxBuilder ライブラリが提供する 便利な機能のひとつです 先ほどインポートして 良かったですね 従ってこれと他の3つの 役割に対するプロトコルへの 適合性を持つことで DictionaryStorageの 動作する実装が完成します ただし正常には動作しますが 間違って使用した場合 どうなるのか struct ではなく enum に 適用しようとした場合は? dictionary を 追加しようとします しかし enum はストアドプロパティを 持てません 列挙型はストアドプロパティを 含める事はできません Swiftはコンパイルを停止して コードを防げるが エラーメッセージは 少し分かりにくいですよね DictionaryStorageが ストアドプロパティを 作成しようとした理由など 明確ではありません 前述した様にSwiftの目標は マクロが入力のミスを検出し カスタムエラーを 生成できるようにすること そこでマクロの実装を 変更して この場合のより明確な エラーメッセージ @DictionaryStorageのみ適用できます と出力しましょう デベロッパは何が間違っているか これで把握できます これを行うための鍵は これまで無視してきた 展開メソッドの パラメータです 異なる役割によって 正確な引数が わずかに異なりますが memberマクロの場合は3つあります 1つ目は attribute型は AttributeSyntax これは デベロッパがマクロを 使用するために書いた DictionaryStorage属性です 2つ目は declaration DeclGroupSyntaxに 準拠する型です DeclGroupSyntaxは struct enum class actor protocol extension が 準拠するプロトコルです 従ってこのパラメータは デベロッパが 属性を添付した宣言を 取得するものです 最後のパラメータは context と呼ばれ MacroExpansionContextに 準拠する型です コンテキストオブジェクトは マクロの実装が コンパイラと通信する為に 使用されます エラーや警告の発生など 異なる事ができます これら3つのパラメータを使って エラーを発生させます では 見てみましょう まず問題を検出する 必要があります declarationパラメータの型を チェックします 各種の宣言には 異なる型があるので struct であれば型は StructDeclSyntax enum であれば EnumDeclSyntax その為 declaration の is メソッドを呼び出し StructDeclSyntax を渡す guard-else を記述します もし宣言がstructでない場合 else ブロックに入ります つまりマクロはプロジェクトに コードを追加しませんが 本当にやりたいことは エラーを出力することです では簡単な方法として 通常の Swift エラーを スローする方法がありますが 細かく制御できません より洗練されたエラーを 作成するために もっと複雑な方法を紹介します 最初は Diagnostic 型の インスタンスを作成 これはコンパイラ用語の一部です 貴方の骨折のX線を見て 骨折を診断する医師のように コンパイラやマクロは あなたの壊れたコードの 構文Treeを見て エラーや警告を診断します エラーを表すインスタンスを Diagnostic と呼びます Diagnostic には少なくとも 2つの情報が含まれます まずエラーが発生した 構文ノードが必要で どの行を間違っていると マークするかを知れます ユーザーが書いた DictionaryStorage を 指し示したいので メソッドに渡された attribute パラメータが 提供してくれます 2つ目はコンパイラに 出力させたいメッセージ カスタム型を作成し インスタンスを渡して 見てみましょう このモジュールが生成できる 全ての診断を定義しています enum を使用し各診断に対して ケースを提供する事を選びましたが 別の種類の型を使用する事も できます これはスローアブルな Swiftエラーのように機能します DiagnosticMessage プロトコルに準拠し 診断情報を提供する幾つかの プロパティを持っています 最も重要なのは Severity プロパティです 診断がエラーか警告かを 指定します
次に実際のエラーを生成する message と diagnosticID があります ドメインにはプラグインの モジュール名を使用し IDには一意の文字列を 使用する必要があります このenumでは文字列の生の値を 使用することを選択しましたが これは便利なものです メッセージが用意できたら 診断を作成します それからコンテキストに 診断する様に指示して完了です
これはかなり基本的な診断ですが 必要に応じて更に 洗練されたものにもできます 例えば Fix-Its を診断に追加し Xcodeの修正ボタンで 自動的に適用することができます またハイライトを追加し コード内の他の場所を指し示す ノートを添付する事もできます つまりデベロッパにとって エラー体験を提供できます しかしマクロが正しく適用 されている事を確認した後も 実際に展開を作成する 必要があります SwiftSyntax には それを行うツールがあります 構文ノードは不変ですが 新しいノードを作成したり 既存ノードの変更版を 返す為の多くのAPIがあります SwiftSyntaxBuilder ライブラリは幾つかの トレーリングクロージャで 指定する構文ビルダーを追加 例えば多次元配列マクロは 生成するタイプに適切な数の パラメータを持つ 構文ビルダを使用できます また DictionaryStorage と イニシャライザ作成する為の 使用した文字列リテラル機能も 補間をサポートしています
これらの機能は 異なる状況で有用で 特に複雑なマクロでは いくつかを組合わせることに なるでしょう ただし文字列リテラル機能は 大量のコードの構文Treeを 生成するのに特に適しており その補間機能について 学ぶ必要があります ではこれを使用して コードの生成方法を見ましょう 先程 unwrap という マクロについて話しました オプショナルな値と メッセージ文字列を受け取り クロージャで囲まれた guard let に展開されます このコードの一般的な形状は 常に同じであり 内容の多くは使用箇所に応じて カスタマイズされます guard letに焦点を当て 文を生成する為の関数を 作成する方法を見てみましょう 始めにちょうど見た コードサンプルを取り makeGuardStatement ヘルパーメソッドに入れて Statement Syntax ノードを返すようにします そして使用箇所に応じて 異なる部分を置き換える為 徐々に補間を 追加していきます まずは適切なメッセージ 文字列を追加すること メッセージ文字列は 任意の式なので ExprSyntaxノード として渡し補間します 通常の補間は構文ノードを コードに追加できますが 単純な文字列を 追加することはできません 間違って無効なコードを挿入を 防ぐための安全機能です guard-let の条件も 似たようなものですが 変数名だけなので 式ではなくトークンです どちらにせよTokenSyntax パラメータを追加し 式の補間と同様に補間します 次にアンラップされる式を エラーメッセージに 追加する場合が少し複雑です マクロの特徴のひとつは 失敗した場合に アンラップしようとしていた コードを出力することです 構文ノードの文字列化版を含む 文字列リテラルを 作成する必要があります
まず Statement Syntax リテラルから接頭辞を抽出し 単なるプレーンな文字列としての 変数に格納します その文字列を補間に 追加しますが literal: で始まる特殊な補間を 使用します これを行うとSwiftSyntaxは 文字列の内容を 文字列リテラルとして追加します これはマクロによって 計算される他の種類の情報は リテラルを作成する場合も 機能します 数値 ブール値 配列 辞書 オプショナルなどです 文字列を変数で 構築しているので メッセージに適切なコードを 含めるように変更可能です 元の式のパラメータを追加し descriptionプロパティを 文字列に補間します エスケープする為に特別な処理は 必要はありません literal: 補間は文字列に 特殊な文字が含かを 自動的に検出しエスケープ追加したり コードが有効であることを 確認するために 生のリテラルに切替えたりします その為 literal: 正しい処理が 非常に簡単に行えます 最後に扱うのはファイルと行番号です 少し紛らわしいですが コンパイラはマクロに 展開されるソースの位置を マクロに伝えないからです マクロ展開コンテキストには コンパイラが ソース位置情報を持つ リテラルに変換する 特殊な構文ノードを 生成する為のAPIがあります では見てみましょう マクロ展開コンテキストの 引数を追加し それを使用して location of を呼び出します これにより提供したノードの 位置の構文ノードを 生成できるオブジェクトが 返されます マクロが生成したノードではなく コンパイラがマクロに渡した ノードの場合は nil が返されますが originalWrapped は 常に nil ではなく 安全にアンラップできます あとはファイルと行番号の 構文ノードを補間するだけです 今は正しい guard 文を 生成しています これまでマクロが どう動作するか説明しました 次はうまく機能する為の方法を 話します まず名前の衝突について 話しましょう 以前に unwrap という マクロを見たとき 単純な変数名を アンラップする例を見ました しかし 複雑な式を アンラップしようとすると マクロは異なる展開を行う 必要があります 式の結果を wrappedValueという 変数にキャプチャし それをアンラップするコードを 生成します
しかしメッセージで wrappedValueという変数を 使用しようとした 場合はどうなるのか コンパイラがwrappedValueを 探しに行くと より近いものを見つける為 意図したものではなく 代わりにそれを使用します
ユーザーが使用しないと 思われる名前を選ぶと 修正しようとすることも できますが それを不可能にする方がよいでしょう
makeUniqueName それはメソッドが行うこと これはユーザーコードや 他のマクロ展開で 使用されていないことが 保証された変数名を返すため メッセージ文字列が誤って 参照しないことを確認できます なぜ Swift 自体がそれを 自動的に防ぐのではないのか と思っている人もいるかも しれませんね 一部の言語にはマクロ内の 名前と外部の名前が異なる為 衝突することがない 衛生的なマクロシステムがあります
Swift は違います なぜなら多くのマクロが 外部の名前を使用する必要があると わかっているからです DictionaryStorage マクロは型上の dictionaryという プロパティを使用します もしマクロ内のdictionaryと 外部のdictionaryが 異なる意味を持つなら 機能するのは難しくなります
そして非マクロコードが アクセスできる新しい名前を 導入したい場合もあります peerマクロ memberマクロ declaration マクロは 基本的にはこれを行う為に 存在しています その場合 追加している 名前を宣言する必要があり コンパイラが それらを認識します そしてロール属性の 内部で行われます 以前 宣言を見たことに 気付いてないかもしれないが 実際にはこれらの 宣言をすべて見てきました DictionaryStorage の member 役割には names: パラメータがあり その中で dictionary と init という名前が 指定されていました 実際 見てきた ほとんどのマクロは names 引数を持つ役割が 少なくとも1つあります
使用できるname specifiersは 5つあります Overloadedは マクロがアタッチされたものと 同じベース名の宣言を 追加することを意味します Prefixed は指定された 接頭辞を追加したものを含め 同じベース名の宣言を追加します Suffixedは同じことですが 接尾辞を使用します Named は特定の固定ベース名の 宣言を追加することを意味します そして arbitrary は これらのルールを使って 説明できない他の名前で 宣言追加を意味します arbitraryを使用する事は 非常に一般的です 例えば多次元配列のマクロで パラメータの1つから 計算される名前を持つ 型を宣言する必要がある為 arbitrary を指定する 必要があります 但し ほかのspecifiersのいずれかを 使える場合はそちらを使って下さい コンパイラやコード補完など 他ツール動作が高速化します さてこの時点で皆さんは 最初のマクロを書く準備が 整っている事と思います すでに良いアイデアが あるかもしれません 展開された日時を挿入する マクロを作成するとかね 良いと思ったでしょ 間違っています 実際にはこのマクロを 書いてはいけません なぜかを説明します コンパイラが提供する情報を 使う必要があるからです コンパイラはマクロ実装が 純粋な関数であると想定し 提供されたデータが 変わらない限り 展開も変わらないと 考えています 回避すると一貫性のない 動作が発生する可能性がある マクロシステムは このルールに違反します つまり可能性のある一部の動作を 防ぐように設計されています コンパイラプラグインは ディスク上のファイルを読み マクロ実装を停止する サンドボックスで実行します 但し不正なアクションを ブロックする訳ではありません 日付やランダムな数値など 情報を取得するためのAPIを使用したり 1つの展開での情報を グローバル変数に保存して 別展開で使う事ができます しかし 動作が正しく ならない可能性があるので やらないで下さい 最後テストについて話します 通常のSwiftモジュールであり 普通にユニットテストを 書くことができます テスト駆動開発は Swift マクロを開発する 非常に効果的なアプローチです assertMacroExpansion ヘルパーを使用すると マクロが正しい展開を 生成するかを確認できます マクロの例と展開されるべき コードを指定するだけで それらが一致するかを 確認してくれます 今日は Swiftマクロについて たくさん学びました 小さな使用箇所をより 複雑なコードに展開する事で ボイラープレートを 削減することができます 通常はライブラリ内など 他のAPIと共に宣言するが 実際の実装は セキュアなサンドボックス内で 実行される別のプラグインになります マクロの役割はそれを 使用できる場所と展開が プログラムの他の部分に どう統合されるかを表します そして期待通り動作するか 確認するために ユニットテストを 書くことができます まだ視聴していない場合は 次に 「Write Swift Macros」のセッションを ご覧ください Xcodeの開発ツールや パッケージテンプレ使用方法 SwiftSyntaxツリーの調査と 情報の抽出方法 フローの構築方法などが 紹介されています ご視聴ありがとうございます ♪♪
-
-
0:44 - The #unwrap expression macro, with a more complicated argument
let image = #unwrap(request.downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [wrappedValue = request.downloadedImage] in guard let wrappedValue else { preconditionFailure( "Unexpectedly found nil: ‘request.downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return wrappedValue }() // End expansion for "#unwrap"
-
0:50 - Existing features using expansions (1)
struct Smoothie: Codable { var id, title, description: String var measuredIngredients: [MeasuredIngredient] static let berryBlue = Smoothie(id: "berry-blue", title: "Berry Blue") { """ Filling and refreshing, this smoothie \ will fill you with joy! """ Ingredient.orange .measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry .measured(with: .cups) Ingredient.avocado .measured(with: .cups).scaled(by: 0.2) } }
-
1:11 - Existing features using expansions (2)
struct Smoothie: Codable { var id, title, description: String var measuredIngredients: [MeasuredIngredient] // Begin expansion for Codable private enum CodingKeys: String, CodingKey { case id, title, description, measuredIngredients } init(from decoder: Decoder) throws { … } func encode(to encoder Encoder) throws { … } // End expansion for Codable static let berryBlue = Smoothie(id: "berry-blue", title: "Berry Blue") { """ Filling and refreshing, this smoothie \ will fill you with joy! """ Ingredient.orange .measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry .measured(with: .cups) Ingredient.avocado .measured(with: .cups).scaled(by: 0.2) } }
-
3:16 - Macros inputs are complete, type-checked, and validated
#unwrap(1 + ) // error: expected expression after operator @AddCompletionHandler(parameterName: 42) // error: cannot convert argument of type 'Int' to expected type 'String' func sendRequest() async throws -> Response @DictionaryStorage class Options { … } // error: '@DictionaryStorage' can only be applied to a 'struct'
-
3:45 - Macro expansions are inserted in predictable ways
func doThingy() { startDoingThingy() #someUnknownMacro() finishDoingThingy() }
-
4:51 - How macros work, featuring #stringify
func printAdd(_ a: Int, _ b: Int) { let (result, str) = #stringify(a + b) // Begin expansion for "#stringify" (a + b, "a + b") // End expansion for "#stringify" print("\(str) = \(result)") } printAdd(1, 2) // prints "a + b = 3"
-
5:43 - Macro declaration for #stringify
/// Creates a tuple containing both the result of `expr` and its source code represented as a /// `String`. @freestanding(expression) macro stringify<T>(_ expr: T) -> (T, String)
-
7:11 - What’s an expression?
let numPixels = (x + width) * (y + height) // ^~~~~~~~~~~~~~~~~~~~~~~~~~ This is an expression // ^~~~~~~~~ But so is this // ^~~~~ And this
-
7:34 - The #unwrap expression macro: motivation
// Some teams are nervous about this: let image = downloadedImage! // Alternatives are super wordy: guard let image = downloadedImage else { preconditionFailure("Unexpectedly found nil: downloadedImage was already checked") }
-
8:03 - The #unwrap expression macro: macro declaration
/// Force-unwraps the optional value passed to `expr`. /// - Parameter message: Failure message, followed by `expr` in single quotes @freestanding(expression) macro unwrap<Wrapped>(_ expr: Wrapped?, message: String) -> Wrapped
-
8:21 - The #unwrap expression macro: usage
let image = #unwrap(downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [downloadedImage] in guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return downloadedImage }() // End expansion for "#unwrap"
-
9:09 - The #makeArrayND declaration macro: motivation
public struct Array2D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1: Int public func makeIndex(_ i0: Int, _ i1: Int) -> Index { Index(storageIndex: i0 * width1 + i1) } public subscript (_ i0: Int, _ i1: Int) -> Element { get { self[makeIndex(i0, i1)] } set { self[makeIndex(i0, i1)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } // Note: Omitted additional members needed for 'Collection' conformance } public struct Array3D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1, width2: Int public func makeIndex(_ i0: Int, _ i1: Int, _ i2: Int) -> Index { Index(storageIndex: (i0 * width1 + i1) * width2 + i2) } public subscript (_ i0: Int, _ i1: Int, _ i2: Int) -> Element { get { self[makeIndex(i0, i1, i2)] } set { self[makeIndex(i0, i1, i2)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } // Note: Omitted additional members needed for 'Collection' conformance }
-
10:03 - The #makeArrayND declaration macro: macro declaration
/// Declares an `n`-dimensional array type named `Array<n>D`. /// - Parameter n: The number of dimensions in the array. @freestanding(declaration, names: arbitrary) macro makeArrayND(n: Int)
-
10:15 - The #makeArrayND declaration macro: usage
#makeArrayND(n: 2) // Begin expansion for "#makeArrayND" public struct Array2D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1: Int public func makeIndex(_ i0: Int, _ i1: Int) -> Index { Index(storageIndex: i0 * width1 + i1) } public subscript (_ i0: Int, _ i1: Int) -> Element { get { self[makeIndex(i0, i1)] } set { self[makeIndex(i0, i1)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } } // End expansion for "#makeArrayND" #makeArrayND(n: 3) #makeArrayND(n: 4) #makeArrayND(n: 5)
-
11:23 - The @AddCompletionHandler peer macro: motivation
/// Fetch the avatar for the user with `username`. func fetchAvatar(_ username: String) async -> Image? { ... } func fetchAvatar(_ username: String, onCompletion: @escaping (Image?) -> Void) { Task.detached { onCompletion(await fetchAvatar(username)) } }
-
11:51 - The @AddCompletionHandler peer macro: macro declaration
/// Overload an `async` function to add a variant that takes a completion handler closure as /// a parameter. @attached(peer, names: overloaded) macro AddCompletionHandler(parameterName: String = "completionHandler")
-
11:59 - The @AddCompletionHandler peer macro: usage
/// Fetch the avatar for the user with `username`. @AddCompletionHandler(parameterName: "onCompletion") func fetchAvatar(_ username: String) async -> Image? { ... } // Begin expansion for "@AddCompletionHandler" /// Fetch the avatar for the user with `username`. /// Equivalent to ``fetchAvatar(username:)`` with /// a completion handler. func fetchAvatar( _ username: String, onCompletion: @escaping (Image?) -> Void ) { Task.detached { onCompletion(await fetchAvatar(username)) } } // End expansion for "@AddCompletionHandler"
-
12:36 - The @DictionaryStorage accessor macro: motivation
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? } } }
-
13:04 - The @DictionaryStorage accessor macro: declaration
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
13:20 - The @DictionaryStorage accessor macro: usage
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] @DictionaryStorage var name: String // Begin expansion for "@DictionaryStorage" { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage var height: Measurement<UnitLength> // Begin expansion for "@DictionaryStorage" { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage(key: "birth_date") var birthDate: Date? // Begin expansion for "@DictionaryStorage" { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } // End expansion for "@DictionaryStorage" }
-
13:56 - The @DictionaryStorage member attribute macro: macro declaration
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
14:46 - The @DictionaryStorage member attribute macro: usage
@DictionaryStorage struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var name: String // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
15:52 - The @DictionaryStorage member macro: macro definition
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
16:26 - The @DictionaryStorage member macro: usage
// The @DictionaryStorage member macro @DictionaryStorage struct Person: DictionaryRepresentable { // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
16:59 - The @DictionaryStorage conformance macro: macro definition
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(conformance) @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
17:09 - The @DictionaryStorage conformance macro: usage
struct Person // Begin expansion for "@DictionaryStorage" : DictionaryRepresentable // End expansion for "@DictionaryStorage" { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
17:28 - @DictionaryStorage starting point
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? } } }
-
17:32 - @DictionaryStorage ending point
@DictionaryStorage struct Person // Begin expansion for "@DictionaryStorage" : DictionaryRepresentable // End expansion for "@DictionaryStorage" { // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var name: String // Begin expansion for "@DictionaryStorage" { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } // End expansion for "@DictionaryStorage" // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var height: Measurement<UnitLength> // Begin expansion for "@DictionaryStorage" { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage(key: "birth_date") var birthDate: Date? // Begin expansion for "@DictionaryStorage" { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } // End expansion for "@DictionaryStorage" }
-
17:35 - @DictionaryStorage ending point (without expansions)
@DictionaryStorage struct Person { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
18:01 - Macro implementations
/// Creates a tuple containing both the result of `expr` and its source code represented as a /// `String`. @freestanding(expression) macro stringify<T>(_ expr: T) -> (T, String) = #externalMacro( module: "MyLibMacros", type: "StringifyMacro" )
-
19:18 - Implementing @DictionaryStorage’s @attached(member) role (1)
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]" ] } }
-
19:52 - Code used to demonstrate SwiftSyntax trees
@DictionaryStorage struct Person { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
22:00 - Implementing @DictionaryStorage’s @attached(member) role (2)
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]" ] } }
-
24:29 - A type that @DictionaryStorage isn’t compatible with
@DictionaryStorage enum Gender { case other(String) case female case male // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" }
-
25:17 - Expansion method with error checking
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] { 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) } }
-
29:32 - Parameter list for `ArrayND.makeIndex`
FunctionParameterListSyntax { for dimension in 0 ..< numDimensions { FunctionParameterSyntax( firstName: .wildcardToken(), secondName: .identifier("i\(dimension)"), type: TypeSyntax("Int") ) } }
-
30:17 - The #unwrap expression macro: revisited
let image = #unwrap(downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [downloadedImage] in guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return downloadedImage }() // End expansion for "#unwrap"
-
30:38 - Implementing the #unwrap expression macro: start
static func makeGuardStmt() -> StmtSyntax { return """ guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } """ }
-
30:57 - Implementing the #unwrap expression macro: the message string
static func makeGuardStmt(message: ExprSyntax) -> StmtSyntax { return """ guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
31:21 - Implementing the #unwrap expression macro: the variable name
static func makeGuardStmt(wrapped: TokenSyntax, message: ExprSyntax) -> StmtSyntax { return """ guard let \(wrapped) else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
31:44 - Implementing the #unwrap expression macro: interpolating a string as a literal
static func makeGuardStmt(wrapped: TokenSyntax, message: ExprSyntax) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘downloadedImage’ " return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
32:11 - Implementing the #unwrap expression macro: adding an expression as a string
static func makeGuardStmt(wrapped: TokenSyntax, originalWrapped: ExprSyntax, message: ExprSyntax) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘\(originalWrapped.description)’ " return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
33:00 - Implementing the #unwrap expression macro: inserting the file and line numbers
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) ) } """ }
-
34:05 - The #unwrap expression macro, with a name conflict
let wrappedValue = "🎁" let image = #unwrap(request.downloadedImage, message: "was \(wrappedValue)") // Begin expansion for "#unwrap" { [wrappedValue = request.downloadedImage] in guard let wrappedValue else { preconditionFailure( "Unexpectedly found nil: ‘request.downloadedImage’ " + "was \(wrappedValue)", file: "main/ImageLoader.swift", line: 42 ) } return wrappedValue }() // End expansion for "#unwrap"
-
34:30 - The MacroExpansion.makeUniqueName() method
let captureVar = context.makeUniqueName() return """ { [\(captureVar) = \(originalWrapped)] in \(makeGuardStmt(wrapped: captureVar, …)) \(makeReturnStmt(wrapped: captureVar)) } """
-
35:44 - Declaring a macro’s names
@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)
-
38:28 - Macros are testable
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]) } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。