本日ももう少し脱線をしまして、前回に見てみた Swift 5.9 で新たに導入された
マクロ
の機能、それを使うのに不可欠な SwiftSyntax
まわりについて、実際にマクロを組みながら眺めてみようと思います。使ってみればなんとかなると思いきや、確かにそうなような癖があって迷うような感じだったので、慣れる気持ちで軽く取り組んでみますね。よろしくお願いします。——————————————————————————————
熊谷さんのやさしい Swift 勉強会 #306
00:00 開始
01:48 マクロで CodingKeys を自動生成
06:16 CodingKeys は private で良い
06:39 マクロの制作開始
08:57 今回のマクロで必要な機能
09:22 付属型マクロで実現する
15:21 MemberMacro の実装時に扱える値
17:53 MemberMacro の実装の仕方
19:49 構造体だけを対象とするなら
20:04 マクロでコンパイルエラーとしたいときは
21:48 プロパティー定義を抽出するには
24:35 ビジターパターンで探索可能
25:31 構造体直下のプロパティー定義を取得するには
27:59 目的のものが取得できたか確かめるには
29:25 次回の展望とクロージング
——————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #306
では、始めていきますね。今日はSwift 5.9のマクロについて話します。前回も紹介しましたが、今回はこのマクロを使用するプロジェクトやパッケージの作り方を紹介します。
まず、Swift Syntaxについてです。これは以前から存在しており、その道に進んでいる人もいるので取り組みやすいと思われます。しかし、実際に調べてみると検索ワードが「Swift Syntax」ではないかと思うのですが、検索すると普通のSwiftオーブンに関する情報が多数引っかかります。意外とこの検索が面倒で、その中でも何とか一つ具体的な例を見つけたので、今日はそれをもとにマクロの作り方の雰囲気をつかんでいただければと思います。そして、実際に作ってみようと思った時の知識の足しになれば嬉しいです。
さて、実際に自分で作成してみて得た知見も多々あるので、比較的スムーズに進むと思いますが、それでも自分もまだ完全に慣れていない部分もあり、多少引っかかりつつ進めていく感じになりそうです。
まず、今日やろうと思うのは、ゆめみのスラックで見かけた面白い題材を元に、それをマクロで解決するという話です。具体的には、以下のような例を考えています。
例えば、以下のような
Struct
が存在するとします。struct MyStruct {
var id: UUID // UUIDが自動で設定されるようにしたい
var name: String
var summary: String
}
この例では、
id
は自動で生成されるようにしたいのですが、毎回新しいインスタンスを作成するたびに必ずid
がセットされてほしいという要件があります。例えば、let id: UUID
のように設定していると、初期化されていないプロパティがあると警告が出ることがあります。このような場合にどうするかという問題が生じます。通常、
id
はエンコード時にはUUID
が含まれているものの、デコード時には復元されないというケースがあります。これを回避するために、独自のキーを用意し、CodingKeys
に定義することができます。private enum CodingKeys: String, CodingKey {
case name
case summary
}
この方法を使用することで、エンコード対象から
id
を除外し、毎回新しい値が設定されるようにすることができます。ですが、プロパティが多くなると対応が難しいことがあります。ですので、ここでマクロを使って自動化できると良いのではないかという話です。マクロを使用することで、こういった繰り返しの作業を省略し、より簡潔にコードを書くことができるようになります。
ちなみに、
CodingKeys
をプライベートにすることで、外部からのアクセスを制限することもできます。このようにすることで、セキュリティを確保しつつ必要な機能を実装することができます。このような考え方で、マクロを使った開発を進めていければと思います。それでは、具体的なプロジェクト作成の手順に移ります。 基本的にはこんな感じでマクロを作っていくわけですが、これを実行するためには、まずXcodeでもできるっぽいんですけど、この前見つからなかったので改めてターミナルからやってみます。
まず名前を何にしようかな、まあいいか。名前は「Non-Initialized Value Coding」みたいな感じで付けてあげます。
swift package init --type macro
として基本テンプレートを揃えます。次に、Xcodeで開いて編集していきましょう。クライアントの実際にマクロを使う部分にさっきのコードを持ってきます。このページはIDをバーにしてしまうと復元されるので永続化されてしまいます。これを避けるために、IDを自動で毎回振りたいですね。多分これを振るのが正しい方法だと思います。このようにして、例えばコードを作成するときに必要なプロパティの名前と初期値が設定されていないプロパティを取り扱います。
さらに、このメンバーに対して「Non-Initialized Value Coding」みたいに名前を付けていくことでうまく動きます。これを通すためには、まずマクロをどちらにするか決めます。インターフェースのほうから先に進めましょう。
ここで、
attaches macro
のパラメーターが必要になります。型の中のメンバーに対してあれこれ操作を行います。例えば、マクロで追加されるのはメンバーの追加やプロトコル準拠です。メンバーに対する
attaches macro
を作成した上で、追加される要素をリスト化します。これはマクロを使う側が何がされるのかを把握するためです。例えば、Named CodingKeys
という名前のメンバーが追加されるという具合に書いてあげます。次に、定義するものとしてメンバーを追加します。それが
CodingKeys
というマクロであることを明示します。さらに、パブリックマクロとしてNonInitializedCodingKeys
を定義します。そして、関数の実装部分を書きます。External Macro Module
でこれがどのテンプレートを使ったものかを明示した上で、マクロの実装を進めます。例えば、構造体でpublic struct
にする部分などはSwiftのコードで書いていきます。このように進めることで、Swiftでのマクロ実装が進んでいきます。 とりあえず、構造体を書いてみます。これはアタッチドマクロの中で評価したものをコードに埋め込む意味があります。エクスプレッションマクロというグローバルバージョンに準拠させてあげることで、スタティックファンクエクスプレッションをマクロを展開するときに利用できる状況になります。
例えば、以下のようにコードを書くとします。
struct ExampleStruct {
// これはパブリックなメソッドになります
public func testMethod() -> String {
return "テスト"
}
}
この前、Sweet Syntax系はリテラルで表現できるので、String Interpolationを使うのが難しいという話がありましたが、コメント文を出すなどの良点もあります。
ビルドをかけると、以下のような書き方をしてあげれば、ビルドが通るか確認できます。
ノンイニシャライズドバリューコーティングマクロをエクスポートするという書き方です。ビルドが通るようになればOKですが、ここでエラーが発生する場合もあります。例えば、エクスプレッションマクロの代わりにメンバーマクロが適用されていることがあります。
アタッチドマクロから来るメンバーマクロを適用することで、メンバーをいじるマクロになります。これでビルドが通ればOKですが、コンパイルエラーが発生する場合もあります。その場合は、エンドキー変換がうまくいっていない可能性があるので、別のファイルを選んで再度試みることが求められます。
以下のようなコードで、レッド型の作成を試みることもできます。
struct AnotherStruct {
private enum CodingKeys: String, CodingKey {
case exampleKey
}
func exampleMethod() -> [String] {
let result = ["example"]
return result
}
}
ビルドに失敗することもありますが、メイン関数内でエクスパンドマクロを利用することもできます。コーディングキーが持つストリングの歴史なども考慮する必要がありますが、全体的にこのようなコード書き方で進めていきます。
この内容でさらに実装を進め、例えば以下のように変数を定義してみます。
let resultSyntax: [String] = []
最終的に以下のようにすることで、ビルドが通るか再度確認します。
return resultSyntax
このようにして、ビルドが成功するまでさまざまな試行錯誤を繰り返し、最終的に目的の機能を実装していきます。 次に構造体の値を解釈して取得する方法について説明します。ここから少し難しくなってきますが、まずは構造体に適用する方法を見ていきましょう。ガード文で適用されている場合、宣言部分に進みます。
たとえば
isStructEqualSyntax
でなければ、iterfaitable
でないフローとしてエラーハンドリングでエラーを返すことになります。具体的には、エラーマクロエラーとしてエラープロトコルに準拠させ、メッセージとしてストリングを持たせます。そして、ストリングコンバティブルにしておきます。まず、ストリングメッセージを持たせるようにして、スローをマクロエラーとしてメッセージに
notStruct
と書くようにします。これでメタレービルドが可能になります。エラーハンドリングを使ってエラーを返すと、カスタムストリングコンバティブルを使って description
としてメッセージを返すようにします。このようにすると、エラーメッセージがテキストとして表示されます。エラーを出す部分まで確認できればOKです。ここでコンパイルエラーが発生していることがわかりますが、面白いですね。
次に、この中からパラメータ、つまりプロパティのうち初期化されていないものを探していきます。これは少々癖があり、慣れるまでに時間がかかるかもしれません。
let
も var
も、Swiftの内部構造ではバリアブルという形で表現されており、その種類として let
なのか var
なのかが決まります。この抽象構文木がどのようになっているかは、ウェブ上のツールを使って調査できます。たとえば、
SwiftASTUR
というツールを使えば、Swiftのコードを解釈してくれます。このツールにコードを貼り付けると、どのように解釈されているかが分かります。この中から目的のものを探していきます。メンバーブロックがリストの中にあり、そのメンバーリストの中にバリアブルが含まれているのがわかります。そして、名前の後ろに Type
を付けるとSwift Syntaxの型になります。次に、バリアブル(
let
や var
)を探して、その中のモディファイアリストやパラメータ、パターンバインディングリストとして認識されている項目を確認します。アイデンティファイア(識別子)パターンとしてIDが用意されており、アイデンティファイアパターンのネームで初期化があるかどうかを確認します。これは初期化されているかどうかを判断するためです。このように構造体のメンバーを探していく際に、メンバーブロックアイテムリストの中から適切な項目を見つけ出します。ただし、Swiftの言語構文によって階層が変わることがあるため、この記事では構文に応じた処理方法を説明しています。
アイデンティファイアパターンを認識したら、それに基づいて何かをするようなコードを書くことになります。以上が、構造体の値を解釈して取得する方法の基本的な流れです。 なので、とりあえずこのメンバーの中から変数一覧を出すというのを、まずはちょっとやってみようと思います。
そうするためには、マクロのさっきの実装の中で、ここで例えば
let
を探します。まずは何を探そうか。特定のものだけを探すのは後にして、とりあえず変数を探しましょう。この中でデクラレーションの中にメンバーブロックというプロパティがあります。そしてその中に members
というのがあって、ここから変数だけを取ってくる形になります。ここから map
を使って、メンバーの定義のものをリストアップします。基本的には、構造体の直下にあるメンバーから変数を取ってくるということですね。ビジュアルパターンとか使う必要はなく、こうやって取ることができます。ただし、こうすると
map
で全ての定義を VariableDeclSyntax
に変えているので、変えられなかったものは nil
が返ってきます。ここで is
演算を使う方法もありますが、as
メソッドを使うともっと明確に型を指定できます。as
にすると戻り値が指定した型にキャストされるので、なるべく as
を使いたいですね。ここで compactMap
にして nil
を捨てることで、変数のリストが取れます。これで本当に取れているのかが気になるところですが、例えばここでコメントを埋め込んで確認します。インターポレーションを使って
variables
の内容を出力します。普通の配列にして map
にして、0
の名前が取れないか確認し、description
で出力します。こうすることで、構造体に対してビルドをかけて展開すると、let
や var
がちゃんと取れていることが確認できました。簡単にプリントデバッグのようなことができるので、こんな感じで進めます。ここまで変数を取得できたので、次にその名前や初期化式を取得して、
let
が生成される流れになります。ただし、identifier
パターンが常に同一の階層に出てくるとは限らないので、この後のメンバーリストを取って探して見つけるのは大変です。そのため、ビジターパターンを使ってデータを揃えていきます。この部分に関しては次回続きをやりますね。今日はここまでです。次回は祝日で月曜日はお休みなので、火曜日にこの続きをやります。そして、次回多分完成すると思います。
今日はこれで終わりにします。お疲れ様でした。ありがとうございました。


