Swift 5.9 で新たに導入された マクロ
の機能について、前回は、それを使うのに不可欠な SwiftSyntax
の体験的な意味合いも兼ねて実際にマクロを組んでみました。その制作もひとまずマクロの概要を捉えられるところまで進んで良好な印象でしたけれども、もう少し感覚を掴んでいくとより実際に使っていき易くなりそうな部分が残されているので、せっかくなので今回はそのあたりを眺めつつ、マクロの完成を目指してみますね。よろしくお願いします。
——————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #307
00:00 開始 00:48 作ろうとしているマクロの概要と、これまでのおさらい 10:21 プロパティー定義を取り出していく 12:45 SwiftSyntax のビジターパターンで探索する 24:45 リストアップしたプロパティー定義から CodingKeys を作る 32:24 Result Builder で for - in が使える 33:53 生成コードの改行やインデントを整える 35:24 CodingKeys の生成マクロ完成 36:39 クロージングと次回の展望 ———————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #307
今日は、前回のSwiftのマクロ作成において、Swift Syntaxの使用がまだ不確実であることについて話しました。実際にマクロを作成する際に、この辺りを事前に体験しておけば、抵抗なく始められるかなという感覚で紹介を始めました。今日も同じ感じで進めていきます。せっかくなので、前回の内容を完成させてしまおうと思います。前回の続きから見ていきますが、前回に参加していない方もいらっしゃるので、ざっくりと概要を説明します。
前回はマクロを作り、同じようなパッケージを作成しました。その中で何を作ろうとしているかというところからお話しします。今回の目的は、初期値を指定したプロパティをエンコーディングから外す、ということです。標準では含まれてしまい、場合によっては警告が出ることがあります。特に初期値が指定されていると上書きできないので、それが原因で警告が毎回出てしまいます。この警告を抑えるために、自分でプライベートなCodingKey
を明記する必要があります。
例えば、CodingKey
を持つだけでは問題が解決しないので、enum CodingKeys: String, CodingKey
として、該当するプロパティを除外するようにします。しかし、これを毎回書くのは面倒です。そこで、マクロを使って自動的にこのコードを書かせることにしました。今回のマクロはNonInitializedValueCoding
と名づけ、初期値が指定されているものだけを除外して、残りのプロパティでCodingKey
を作成するというものです。
マクロ宣言として、NonInitializedValueCoding
はNonInitializedValueCodingMacros
パッケージの中のNonInitializedValueCoding
型で規定されています。このマクロは、他の何らかのものに付属させる属性を付与することでメンバーを追加するようにします。追加されるメンバーはCodingKeys
という名前です。
ここでプログラムが正常にパスすると、このマクロが何をするかが明確になります。NonInitializedValueCoding
マクロが呼ばれるとき、マクロ自身のノードと、それが付与された宣言部分、そしてどこに貼られているかというコードの場所が分かります。これを活用して、最終的には追加するメンバーをSwift Syntaxのシンタックス型として返します。
具体的には、異なるブロックの中から変数宣言のものだけを取り出し、それをcompactMap
で変数のメンバーだけに絞り込んでいきます。このvariables
には変数宣言シンタックス用のメンバーだけが含まれた状態になります。その中で、コメントとしてコードブロックを作成し、変数が何を取ったかを埋め込みます。最終的にはCodingKeys
を生成します。
ここまでの内容が全体の流れです。次回以降もこのマクロの完成を目指して進めていきます。 これ全体を返してあげている、つまり、これを挿入してもらうよという形でここまでが展開です。これによってビルドトリアーターが出てくるかな。これによってこのマクロを展開してみると、とりあえず空っぽのコーディングキーができていて、コメントとして let id =
とか var name =
とか、変数が取れてそうだねという状況になりました。
基本的にはこのノリでさらにコードを分析していって、IDだとか名前だとかを取っていって、かつ id = UUID
ってなっていた場合、このUUIDを今回は取らなくていいんです。=
の右側が求められる初期値であれば、コーディングキーの項目からは除外するという方針です。普通に書いていけばとりあえずはいいのですが、これがなかなか難しいです。難しいという部分は複雑だという話だけですけどね。
どこを見ればいいのか不明瞭ですし、不要な部分もあって整理が必要ですね。これはテストか、どれを取ればいいのか、実装部分を見てみましょう。特に何かをやることができなくはないです。例えば変数の選別では、最初のバインディングスの中に変数があるので、この中から必要なものを取り出します。例えば let
や var
の部分です。
この部分を掘り下げていって探すのは良いですが、探すところが深くて大変です。前のコードをチェックしてみましょう。Swift Abstract Syntax Tree Explorer
では選択した部分の詳細を確認できますが、これもかなり深いですよ。特に初期値があるとまた複雑になります。
この深さが問題で、初期値が設定されていない場合でも、構文解析が必要で、更に複雑になってしまう可能性があります。たとえば、XYZ
のような変数が登場すると、なおさら大変です。そこで、あらかじめ用意されている Visitor
パターンを利用します。
Visitor
パターンを利用することで、名前を取りたい場所に来たら Identifier
パターンを含まれていればそれを取り出します。この方法であれば、構文の深さに影響されずに名前を回収できます。
また、Identifier
パターンと一緒に初期値が設定されていた場合には、それをコーディングキーに含めたくないため、それも取り出します。Visitor
パターンを使う場合、初期値が設定されているかも判断する必要があります。このためには PatternBinding
を利用します。
今日やる作業としては、この Visitor
パターンと PatternBinding
を用いたコードを実際に書いていきます。Visitor
パターンを使う上で、結果をシンプルに取得するために構造体を1つ定義しておきます。
以上が今回の作業の概要です。具体的なコードを実装していきましょう。 まずは外に用意しておけるプライベートなストラクトを作ります。例えば、バリアブルインフォメーションのような名前で、ネームを持ち、それがストリング型であれば良さそうです。そして、初期値を設定します。例えば Google
としておきます。以下のような感じです。
private struct VariableInformation {
var name: String = "Google"
}
これをベースにして、Visitorパターンで実装を進めていきます。Swiftには SyntaxVisitor
というものがあります。これはオープンクラスとして宣言されているので、それを継承して実装していきます。オブジェクト指向プログラミングに馴染みがない人にとってはクラスの継承が馴染まないかもしれませんが、このパターンが最適です。
とにかく、クラスを持ち出してここから実装していきますが、もしオブジェクト指向に慣れていない方は今すぐ勉強する必要はありません。ただし、勉強することをお勧めします。
クラスを使って実装していく際に、例えば変数をビジットする部分を考えます。ここでパターンバインディングにフォーカスします。パターンバインディングリストの中にパターンバインディングがあるので、それをビジットするためのクラスを、例えば PatternBindingVisitor
として、 SyntaxVisitor
から継承します。
名前は重要なので、正確に取ってきます。次に visit
メソッドをオーバーライドします。 PatternBindingSyntax
が見つかったときに呼ばれるメソッドをオーバーライドしてカスタマイズします。
以下がその実装例です。
class PatternBindingVisitor: SyntaxVisitor {
private var informations: [VariableInformation] = []
override func visit(_ node: PatternBindingSyntax) -> SyntaxVisitorContinueKind {
if let pattern = node.pattern as? IdentifierPatternSyntax {
let info = VariableInformation(name: pattern.identifier.text)
informations.append(info)
}
return .visitChildren
}
}
この実装では、見つかったパターンを IdentifierPatternSyntax
としてキャストし、名前を取り出して VariableInformation
に格納しています。このように、Visitorパターンを使って文法ツリーを巡り、情報を抽出していくことができます。
一つ重要な点は、visit
メソッドにはどのノードをさらに巡るかという情報を返す必要があることです。例えば、 return .visitChildren
とすることで子ノードも巡るようになります。これにより、必要な情報を適切に抽出できるようになります。
まとめると、 Visitorパターンを使用する際には適切な SyntaxVisitor
クラスを継承し、必要な部分で visit
メソッドをオーバーライドしてカスタマイズします。これにより、特定の構文要素をターゲットにした情報抽出が可能になります。 次に進めますね。
まず、アイデンティファイアーに関するパターンの話ですが、気持ち悪いかもしれませんが、アイデンティファイアーパターンとしておきましょう。「初イニシャライズバリュー」ですが、これはインフォメーションではなく、「イニシャライザーパターン」が正しいです。ノードのイニシャライザーとなる部分を確認してから進めます。
ここで得た情報を information
(情報)リストに追加します。ここは「プッシュ」ではなく、「アップエンド」ですね。これで必要な情報を回収できました。もうチャイルドノードは見なくても良いでしょう。これでビジターパターンは完了です。
ビジターが完成したので、次にビジターを使って情報を取得していきます。取りたい情報は variables
(変数)です。ここからビジターパターンを使い、変数毎に情報を取り出す流れにします。まずは variables
を iterate(反復)して情報を集めます。
具体的にはこうします:Informations = variables.map
のように書いて、すべての variable
から情報を取得します。 Visitor
クラスを使って、各 variable
に対してビジターを作成します。このビジターが全ての variable
を巡り、情報を蓄積します。
次に、このビジターが巡るノードを指定します。今回の場合は variable
ですね。そして、ビジターが蓄積した情報を return
します。 Visitor
が持つ情報を取り出して各変数ごとにまとめ上げた情報が取得できました。
取得した情報を全てフラットにマッピング(flatMap
)します。これにより、どの変数がどの情報を持っているかをフラットに返すことができます。最終的に、変数の名前とイニシャライザーを含む情報を返すことができました。
ここまでくれば、コードは完成です。取得した情報を実際に見てみましょう。ビルドをかけると、それぞれの変数名とイニシャライザーが正しく取得できているはずです。実際に取得できていることを確認し、 Variable Informations
で各 ID が取れて、イニシャライザの情報も取れています。
あともう少しなのですが、コードブロックを作成する部分についても触れておきます。ここでディスクリプションがない場合には、コードブロックを追加する必要があります。これで全体が完成します。
ここまでお疲れさまでした。Swiftのビジターパターンの実装と情報収集プロセスについてしっかり学ぶことができましたね。 なので、ここで一回コードブロックのアイテムを作って展開してあげます。これが数回有限だったらこう書いていけばいいんですけど、ケースによっては違うかもしれません。このように書いていくことで理解できますが、中でループを回したいので、コードを次のように書いてあげます。
case someCase:
// 処理
あらかじめ CodeBlockItemSyntax
という形でアイテムを作ってあげて、このコードを SwiftSyntax
のコードブロックアイテムのシンタックスを作っていくわけです。このイニシャライザーでコードブロックを作っていくのですが、これは ResultBuilder
で作られているので、宣言的に SwiftSyntax
のフォークオープンを埋め込んでいけば完成します。
そこで Information
を使い、この中でコードを書いていきます。インフォメーション図の中で ResultBuilder
なんで文字列リテラルで簡単に作っていける仕組みができており、この方法でケースを文字列で作っていけます。次にストリングインターポレーションを使っていきます。
初期の頃は ResultBuilder
がうまく動かなかったこともありましたが、今は問題ないです。実行してうまくいかなかったら、また見直しましょう。ここで、アイデンティファイヤーの設定を確認します。これを行い、インフォメーションのアイテムがオッケーとなればいいのですが、もし何かおかしな点があれば修正していきます。
次に、初期化されたシンタックスを次のように書いてあげます。
let item = SyntaxFactory.makeCodeBlockItem(...)
これが SwiftSyntax
として解釈されるので、問題ないはずです。このように作成すると、イニシャライザーを持っているものがケースとして処理されますが、それを回避するために適切にラベル付けをしてあげます。これで問題なければ、実行時にエラーが発生せず、ケースが生成されます。ただし、インデントや改行が正しく処理されない場合があります。
これは SwiftSyntax
がコード構文として意味のあるもの以外は保持しないためです。必要なトリビア(インデント、コメントなど)を追加してあげることで、正しいフォーマットのコードが生成されます。例えば、次のようにフォーマティングを追加します。
let formattedItem = item.withLeadingTrivia(.spaces(4))
これによりインデントなどが正しく追加され、最終的な生成結果も適切なフォーマットになります。
実行してみると、次のように正しく展開されます。
private enum Example {
case someCase
case anotherCase
}
計算型プロパティなども対応する必要がある場合や、他の要素を追加する必要がある場合もありますが、基本はこの方法で問題ないです。後は問題を見つけて調整を行い、自動でうまく動くようにすれば完成します。
この作業を進めることで、属性の設定や自動生成が効率的に行えるようになります。他にも処理を追加する場合は、さらに工夫が必要となります。
以上で、今回のマクロに関する説明は終了です。次回はいよいよ話を元に戻して、クロージャーにおける循環参照について見ていきたいと思います。今日はこれで終わりにしますね。お疲れ様でした、ありがとうございました。