Swift Macrosの始め方

こんにちは。マネックス・ラボの佐藤です。プログラミング言語の中では表現力の高いSwiftが好きなので、今回はWWDC2023ネタで書きたいと思います。

Swift5.9から、if elseブロックやswitchが値を返せるようになったりと、細かいところで記述の幅が広がっていて、ますますSwiftが使いやすくなっています。 その中でも一番気になったのが、Swift Macrosです。今回はまだベータ版であるXcode15をダウンロードしつつ、マクロの作り方を理解するためのヒントをまとめたいと思います。

この記事では最後に抽象構文木(AST)を見ながらswift-macro-examplesリポジトリの実装を読むことをゴールとし、最初にそれに必要な知識を簡潔に説明したいと思います。

C/C++のマクロとSwift Macros

特にC/C++を書いたことがあるプログラマーにとっては、マクロは可読性を下げたり、バグを追いづらくしたりと悪名高い存在ですが、Swiftではモダンな言語らしい実装になっていて、うまく活用すれば重複コードの排除やバグの削減に役立ちそうな作りになっています。

Swift Macrosの特徴は、C/C++に見られるようなただの文字列の連結とその置き換えではなく、ASTを元にしたコード生成ができることです。 Swiftの個々のマクロは、宣言とその実装で作られます。実装はSwift言語で記述することができ、その実装の中で、ASTを元にしてコードの構造を解釈しつつ、文字列の形でコードを生成します。

2種類のマクロ(Freestanding MacrosとAttached Macros)

Swift MacrosにはFreestanding MacrosAttached Macrosの2種類があります。 (このブログは日本語の記事であり読者は日本語の方が慣れていると思われるため、リンク先は日本語訳へのリンクを貼っています。一方でコードを読むときには英語の呼び方で覚えておいた方が理解しやすいと思うので、記事上の表記は英語のままにしています)

Freestanding Macrosの方は、それ単体で機能するマクロで、マクロが取った引数を元にコードを生成したい場合にFreestandingとして作成します。

もう一つのAttached Macrosは例えば構造体定義の前やメンバーの前に記述し、マクロが付与された構文を解析しつつ、コードを自動生成します。例えば構造体の前につけるマクロを実装すれば、構造体のメンバーを列挙して、そのメンバーごとに追加のコードを生成するようなことができます。 Attached Macrosについてはメンバーを生成するMember macrosや、プロトコルに準拠するComformance macrosなど複数の属性があるのですが、詳しくは後述します。

マクロ宣言

Swiftでは例えば構造体を書くときに、宣言とその実装をまとめて書くことができます。一方でマクロについては、宣言と、その実装に分けます。 宣言の書き方もThe Swift Programming Languageにわかりやすく記載があります。

リンク先の解説を見ると、Attached Macroの場合には、マクロ宣言の前に複数の@attached()を記述し、マクロが何をするのかを明示します。 例えば@attached(member, names: named(CodingKeys))と記述すると、このマクロは構造体などに対して、CodingKeysというメンバーを生成することを示すことができます。

Xcode15でマクロのパッケージを作成してみる

おそらくここからは、実際に手を動かしながら試してみた方が理解しやすいと思うので、Xcode15(beta)をダウンロードして試してみることをお勧めします。 いつも通り、AppleのMore DownloadsでXcodeキーワードで検索すれば見つけることができるはずです。 ダウンロードして解凍し、起動した後、File -> New -> Package...を開くとSwift Macroを選んでパッケージを作成することができます。

今回は名前にこだわる必要も無いので、とりあえずデフォルトのMyMacroでパッケージを作成しました。 Package.swiftを眺めながら確認するとわかりやすいと思いますが、ざっくりと4つのファイルが自動生成されます。

  • MyMacro.swift マクロの宣言を記述します。デフォルトでサンプルとしてstringifyというマクロ宣言が記述されています。
  • MyMacroMacro.swift ファイル名がとてもイマイチなのはさておき、マクロの実装を記述します。同じくサンプルであるStringifyMacroの実装があります。
  • main.swift ここで宣言と実装を行なったマクロの動作確認をすることができます。
  • MyMacroTests.swift 実装したマクロのテストを記述することができます。

実際にコーディングしながらマクロの書き方を調べるときには、上記の自動生成されたファイルに追記しながら作業すると効率が良いと思います。

サンプルを読み解く

ありがたいことに、Swift Macrosを提案し、実装されているDouglas_Gregorさんがswift-macro-examplesというリポジトリを用意してくれています。 このサンプルもXcode15で自動生成したプロジェクトと似たような構造になっていて、構造を理解してから読み解くのがお勧めです。

  • MacroExamplesLib/Macros.swift サンプルマクロの宣言が列挙されています。
  • MacroExamplesPlugin/ディレクトリ サンプルマクロの実装が書かれています。いきなり実装を読んでもよくわからないと思うので、最初はあまり読まなくていいと思います。
  • MacroExamples/main.swift サンプルマクロの使用例が書かれています。おそらく、このファイルから読んでいくのがお勧めです。これを見ると、サンプルのマクロをどういうときに使って、何ができるのかだいたいのイメージを掴むことができると思います。

まずはmain.swiftを眺めていくのが良いのですが、リポジトリをcloneするかダウンロードしてきて、Xcode15で開いてみると良いです。マクロの上で右クリックをしてExpand Macroを選ぶと、マクロが生成したコードを見ることができます。

なお、このリポジトリを最初に開いたときは、ビルドエラー(External macro implementation type 'MacroExamplesPlugin.DictionaryStorageMacro' could not be found for macro ')が出るかもしれません。 その場合には、Xcode上でスキーマをMacroExamplePlugin, MacroExampleLibの順に切り替えながらそれぞれをビルドしてあげると解消できると思います。

main.swiftをExpand Macroを使いながら見てみると、Swift Macrosでどんなことができるのか、イメージが湧いてくると思います。 私の場合には@CustomCodable@CodableKeyがとても気になったので、これらの実装を読み解きながら、自分で実装するときにどんなコードを書けば良いのかを見ていきたいと思います。

下記がswift-macro-examplesリポジトリからのコードの抜粋です。

@CustomCodable
struct CustomCodableString: Codable {  
  @CodableKey(name: "OtherName")
  var propertyWithOtherName: String  
  var propertyWithSameName: Bool 
  func randomFunction() {
  }
}

マクロを使わずに自分でCodingKeysを書くとしたら、下記のようになるはずです。構造体が持つプロパティの数だけcaseを書かないといけないので、プロパティが増えると無駄な記述が増えがちです。マクロがこれを生成してくれると記述量も減りますし、どのプロパティがKeyを置き換えたいのかが分かりやすくなるはずです。

struct CustomeCodableString: Codable {
  var propertyWithOtherName: String
  var propertyWithSameName: Bool
  
  enum CodingKeys: String, CodingKey {
      case propertyWithOtherName = "OtherName"
      case propertyWithSameName
  }
  
  func randomFunction() {
  }
}

ということで、@CustomCodable@CodableKeyがどのようにしてプロパティを読み取り、自動的にCodingKeysを生成しているのかを読み解いていきたいと思います。

CodableKeyマクロ

実装は、swift-macro-examples/MacroExamplesPlugin/CodableKey.swiftにあります。 中身を見てみるととてもシンプルで、コメントにもある通り、何もしないマクロになっています。これは外側の@CustomCodable側で@CodableKeyの存在を確認し、@CodableKeyの代わりにコードを生成する実装になっているからです。

ちなみに、マクロは外側から展開されていくので、実際には@CustomCodableによるコード生成が行われるだけで、@CodableKeyの実装はダミーだといえます。

CustomCodableマクロ

swift-macro-examples/MacroExamplesPlugin/CustomCodable.swiftに実装があります。 こちらはそれなりに中身のあるコードになっています。main.swiftにて生成されるコードの検討がついていることが多いので、returnしている行から逆に追っていくのが読みやすいと思います。 returnで使っている変数を逆に追っていけば、概ねマクロの実装がわかるはずです。

キャストしながらの長いプロパティ参照がよくわからない問題

メタプログラミングではよく見かける、メンバーの属性をたどって、そこからさらに識別子を辿って・・・ということがSwift Macrosにも当てはまります。 プロパティの抽出を行っている行CodableKeyマクロの有無を調べている行 がそれです。

Swift Macrosを支えているSwift Syntaxに熟知しているならまだしも、いきなりこのコードを書くのは至難の技です。要は想定しているコードを表すASTの構造がわかればよく、 Swift Ast Explorer.comを使うと、ブラウザ上で簡単に任意のコードのASTを知ることができます。 main.swift@CustomCodableを使っているコードを投入した結果が以下のキャプチャのようになります。

かなり長いので、今回は使わないrandomFunction()を削除し、構造体定義を表すASTの部分まで少しスクロールした状態でキャプチャをとっています。

では、プロパティの抽出を行っている行について、 ASTのどこに対応しているのかを見てみます。といっても、文章で書くよりもコードとASTを並べて図を書いた方がわかりやすいので貼り付けておきます。

ざっと説明すると、memberがASTのMemberDeclListにぶら下がっている個々のMemberDeclListItemを指していて、as(VariableDeclSyntax.self)?のダウンキャストを使ってVariableDeclであるmemberをフィルタしています。 さらにbindingsプロパティがASTでいうところのPatternBindingListに該当するので、このリストの先頭がIdentifierPatternSyntaxであるかをダウンキャストすることで判定しています。 IdentifierPatternSyntaxであれば、.identifier.textプロパティが構造体のプロパティ名を表すので、これをpropertyNameに代入しています。

CodableKeyマクロの有無を調べている行の各プロパティがAST上のどこに該当するかはこちら。

こちらについてはもはや説明はいらないと思うので、割愛します。

サンプルマクロの実装コードとASTの対比を読み解くコツは、まずマクロ実装のコード上でプロパティごとに改行を入れることと、Swift Ast Explorerでマウスカーソルを重ねると元のコードのどこに対応してくれるのかを示してくれるので、これを参考にすると良いと思います。

自分でマクロを作るには

今回は主に@CustomCodableを読み解きましたが、swift-macro-examplesリポジトリには他にもたくさんのサンプル実装があります。 自分が作りたいマクロの性質になるべく近いサンプルを探し、そのコードとASTを見比べながら、自分のマクロに取り込んでいくのが良いと思います。

また、Swift Macrosを支えているSwiftSyntaxのリポジトリを覗いてみるのも良いと思います。特にswift-syntax/Sources/SwiftSyntaxMacros/MacroProtocols/ ディレクトリに、マクロ実装をするときに指定できるプロトコルが列挙されています。気になるプロトコルを見つけたら、Xcode上でそのプロパティに準拠させた構造体を書くと、おなじみの"Type 'MyMacroSample' does not conform to protocol 'ConformanceMacro'"といったエラーとともに自動で必要なコードを補完してくれるので、これを元にして調査したり、実装してみても良いと思います。

おわりに

マクロはコードの自動生成に他ならず、コードの可読性を低下させる場合もあります。一方で、今回見てきたサンプルのCodableKeyマクロは、大きな構造体の一部のプロパティだけのために長いCodingKeysを書かなければならないような場合に、とても簡潔な記述をもたらしてくれます。swift-macro-examplesリポジトリには他にも有用そうなサンプルが多々あり、マクロによって少ないコード量で安全なコードを書けるようになる可能性を秘めています。ただし、チームで独自にマクロを作る場合には、最初にマクロの使い方を書いてみて、チーム内の合意をとってから実装をするのが良いと思います。

参考にしたドキュメント

The Swift Programming Languageのドキュメント

すでにSwift5.9(beta)のドキュメントが公開されており、入門用にちょうどいい程度の詳しさでまとまっているので、まずはこちらを読むのが良いと思います。

docs.swift.org

ありがたいことに、すでに日本語訳も出ています。

www.swiftlangjp.com

ボリュームがそれほどあるわけではないので、一読するのがおすすめです。

WWDC2023のセッション

developer.apple.com マクロが想定している位置に書かれているかどうかを検査するコードも含まれています。使い勝手のいいマクロを書くためには必要なテクニックであろうと思います。

プロポーザル

github.com 提案時点からの情報が詰め込まれているので、最終的な仕様を知るには遠回りになってしまいますが、言語に新機能が追加されるまでのプロセスを垣間見ることができるので面白いです。

佐藤 俊介マネックス・ラボ