引き続き脱線をしまして Swift 5.9 で新たに導入された機能まわりを見ていきますね。そんな中から大々的に打ち出されている3機能のうちの マクロ
を今日は眺めてみます。とりあえず先に学んでみていておおよその概要は掴めているものの、いざ作ろうと思うと足りない知識が多そうなので、今回はそんなあたりを中心に探り探りに進めてみようと思っています。よろしくお願いしますね。
——————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #305
00:00 開始 00:32 今回の展望 01:09 Ownership と ParameterPacks は時期尚早? 05:41 将来 repeat each が Sequence 扱いに 07:11 Swift Macros 10:21 マクロを作ってみる 10:26 マクロの種類 11:15 マクロの特徴 14:08 マクロ制作のための準備 15:29 マクロのファイル構成とコードの雰囲気 23:08 結果をテキスト表現で組み立てていける 26:07 提供するマクロはリストアップが必要 27:15 クロージングと次回の展望 27:48 コード補完に挙がってこないのが悩ましい 30:12 改めて、クロージング ———————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #305
今日も引き続き、Swift 5.9の新機能について話していきます。ネット上では循環参照の話が挙がっていましたが、少し脱線しているので、それを見た後に戻る予定です。今日はSwift 5.9で大々的に取り上げられている三つの新機能のうち、最後の一つ「マクロ」について見ていこうと思います。
Swift 5.9では「マクロ」、「パラメータパック」、そして「オーナーシップ」の三つがテーマとして挙げられています。これまでに「オーナーシップ」と「パラメータパック」について取り上げましたが、その印象として、これらが大々的に掲げられていることには納得がいく部分もあります。しかし、まだ少し早いかなという印象も拭えません。
オーナーシップについてですが、ジェネリクスに弱すぎるため、特定の場面でしか問題なく使用できないという厄介な問題があります。もちろんオーナーシップの投入は、インスタンスの渡し方に新たな選択肢を提供することを意図しています。他の言語における参照渡しのようなイメージで、参照渡しが追加されたのですが、レガシーな参照渡しとは異なり、どっちに許容権があるかを明示する仕様が追加されました。これにより、オーナーシップの概念が導入されましたが、実際に活用するためにはもうちょっと準備期間が必要と感じます。
一方で、「パラメータパック」に関しても、多変調ジェネリクスの使い道がまだ微妙な感じです。使えることは使えますが、Swiftらしい書き方ではなく、どうしても昔ながら手続き型言語のような書き方になってしまいます。ただ、「パラメータパック」の一部である repeat
キーワードが新たに登場します。これにより、可変長ジェネリクスを受け取ってシーケンスとして使うことが可能になります。
新しい機能として「repeat」がアクセプトされたので、可変長ジェネリクスの本領を発揮する日は近いと感じます。それまでは静観してもよいし、現段階でできる範囲で使用してみてもいいでしょう。パラメータパックのテーマは以上のような感じです。
次に、Swift 5.9の目玉機能と感じる「マクロ」について見ていこうと思います。マクロの詳細についてはまた後で見てみましょう。 自分でマクロを定義するところまでは分かったけど、それ以上のことをやろうとすると、このマクロの構文を理解するだけでなく、「SwiftSyntax」という別のモジュールパッケージについても理解しないと扱えない感じがして、少し別の分野に入るような感覚です。HTMLのスクレーピングをJavaScriptなどでやったことがある人にとっては、似たような感じかもしれません。HTMLを知っている人がスクレーピングをするときほどややこしくはないですけどね。
幸い、SwiftはなるべくSwift言語でいろんなことをできるようにしようという方針があり、そのおかげでSwift言語内でマクロを組むことができます。そのため、学習コストはHTMLほどではないです。HTMLで例えると、HTMLのスクレーピングをHTMLで書くようなものです。Swiftの場合、それほど難しくはありません。ただし、感じが少し違うのはライブラリの使い方の問題で、初めてCombineを導入したときのような感覚に近いです。
準備には若干癖があり、今回はマクロの準備が少し珍しいので難しいです。とはいえ、慣れてしまえばそれほど難しくはないです。使うのにはそれなりの学習コストがありますが、これはSwift 5.9での非常に強力な機能の一つです。そのため、この三つの新機能のうちどれから押さえたほうが良いかを選ぶとしたら、マクロが良いでしょう。どれだけ一般のプログラマーがマクロを組むかは分かりませんが、実際に作っていくとしましょう。
マクロを作る上で大事になってくるのが、まずプロポーザルを見ておくことです。マクロにはExpressionマクロとAttachedマクロの二つがあります。フリースタンディングマクロもありますが、これらが内容としてまとまっていない場合、Swiftプログラミング言語の公式ドキュメントが分かりやすいかもしれません。
Swiftプログラミング言語のドキュメントの中にマクロについての記述があります。そこには導入が分かりやすく書いてあるので、まずはそれを見ると良いでしょう。マクロの特徴はコンパイルタイムにあります。あるソースコードのコンパイルタイムにマクロが含まれていた場合、それを全体をコンパイルする前にマクロをコンパイルして展開し、その後で全体をコンパイルするという流れです。そのため、目的のものをコンパイルするときには、マクロがすでに完成している必要があります。マクロは先に作っておく必要があるという制約があります。
これは他のフレームワークと同じです。フレームワークをビルドし、その上でそれを使うアプリをビルドするのと同じ感覚です。うまいマクロもパッケージとしてあらかじめビルドしておき、それを使うというような場面で利用します。ソースコードをコンパイルする際に必要なタイミングでそのパッケージがロードされ、そのパッケージの実装に従ってマクロの展開が行われてビルドが進行します。
さらに、マクロの場合はパッケージに実装を備えて、インターフェースを規定し、マクロとして使うものを定義していきます。Swiftの特徴としては宣言と実装が同じファイルに書かれるのですが、マクロについては実装と宣言が別々になります。Swiftの実装を書いた上で、マクロとしての宣言を別に書くという形です。
昔でいうヘッダーファイルと実装ファイルのようなものが必要になります。こういった感覚の違いはありますが、押さえておくべきポイントです。 まずはいろいろ書いていく。そのときにXcodeで作っていけるらしいんですけど、何か見つけられなかったんですよね。どこだっけ、こっちか。プロジェクトに関して、大事なところとしてSwiftSyntaxというパッケージに依存するんで、これをモジュール取り込んであげた上でプロジェクトの中でこのマクロのターゲットを追加していくみたいなんですけど、何かマクロを見たいような気がするんですけど、この間試したとき見つけられなかったんですよね。
まあいいや、その辺りはゆっくり探すとして。なので作っていくときに一番簡単なのがSwift Package Managerを使うことです。例えば、ディレクトリを作って "MacroSample" のように名前を付けて、この中で swift package init --type macro
を実行すると、Swift 5.9でこれをやると必要なファイルがいろいろできるんですね。こんな感じです。これをXcodeで開けばいいのかな、そうすると見やすくなります。
このパッケージを見ると、まずパッケージ名として "MacroSample" というパッケージが作られており、この中にまずライブラリとして "MacroSample" があります。ここでマクロのインターフェースがエクスポートされます。その上で今 Xcode
というのが作られるこの実行可能ファイルは "MacroSampleClient" で、これはマクロを使うテストコードです。これは置いておいて、大事なのはこのマクロのインターフェースを規定するところと、もう一つターゲットとして "MacroSampleMacros" というのが作られています。これがマクロの実装を置くところです。
プロダクトの依存関係としては、SwiftコンパイラブルなライブラリとしてSwiftSyntaxが指定されています。このインターフェースを記述するターゲットは実装のあるターゲットに依存しています。これらは自動生成されるので、内容を読めればOKです。
実際に見ていくと、マクロのインターフェースを規定するのは "freestanding macro" 独立型マクロです。例として #stringify
というマクロがありますが、よく見る #line
や #file
と同じような感覚で使えるマクロをFreestandingとして作成します。 public macro stringify<T>(_: T) -> (T, String)
という具合です。引数をジェネリックに取って、その引数と値をもとに文字列を返します。これはコードそのものが返ってくる作りになっています。
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MacroSampleMacros", type: "StringifyMacro")
このような感じですね。 #externalMacro
はSwiftの標準ライブラリにあるもので、どのモジュールのどの関数で実装されているかを示しています。このモジュール実装はここにあるよ、という内容です。
"MacroSampleMacros" というターゲットにはSwiftSyntaxがインポートされており、ここで StringifyMacro
が ExpressionMacro
プロトコルに準拠しています。この ExpressionMacro
というのはSwiftSyntaxにあるプロトコルで、マクロを定義するときにはこのプロトコルに従う形になっています。マクロを展開する際には expand
が呼ばれます。
import SwiftSyntax
import SwiftSyntaxMacros
public struct StringifyMacro: ExpressionMacro {
public static func expand(_ node: ExpressionSyntax, in context: MacroExpansionContext) throws -> ExprSyntax {
// 実装内容
}
}
このとき、ソースコードがAST(抽象構文木)というメモリ内で扱いやすい形のデータ形式で展開されます。これによりマクロの部分のソースコードが分かるので、あとは解析していきます。 こんなふうなコードになります。そしてSwiftのシンタックスを理解してくると、解析して実際のコードに反映させることができます。必要なパラメータはソースコードに含まれてくるわけです。例えば、MSXというコンピュータでIPL命令やCMT命令を使っていた経験がある人なら理解しやすいかもしれませんが、そのような人はあまりいないでしょう。とりあえず必要な部分だけ渡されて呼ばれるので、「よし、何をやればいいのか」といった感じですね。ソースコード自体はとてもシンプルに作られています。
次に、実際のコードが書かれたStringify
マクロですが、これがAPIのマクロのエントリーポイントになっています。どのモジュールのどこにこのStringify
マクロのエントリーポイントがあるかが書かれており、そのプロトコルに規定されたエクスパンション(展開)が呼び出されます。ノードのところにソースコードが入ってくると、ノードの引数リストの最初の評価式が取れるかどうかがポイントです。引数リストの一番目にあるのが評価式で、それが取れたら評価式としてそのまま渡されます。
また、リテラルとしてアグメント(引数)のディスクリプション(説明)をテキスト表現として返すことができます。さらに、ストリングインターポレーションを使ってソースコードを返すことができます。ここでモデル情報の方のコード保管が少し厄介です。たとえば、リテラルを打つと出てきますが、具体的に何かを打っても出てこないことがあります。ここでエスケープキーを押しても出てこないこともあります。
ただ、売りにもなっているのが、スイッチとマクロの表現がテキストリテラルで行えるようになっている点です。これにより、読みやすく、視覚的に理解しやすいコードを書けるようになっています。実際にテストコードを見て、Stringify
のテストを行う際に、バスワードマクロのエクスパンションが直ちに確認できる形で作られています。このようにテキスト表現でやりやすいという点が、スイッチとマクロのシンタックスの特徴です。慣れてくるとわかりやすいでしょう。 実際のところ、この辺の話をもう少し具体的に説明した方が良いかもしれませんね。リテラルを使わない場合、もう少し意外性が欲しくなってくると思います。こうした部分は普通のインターポレーションについて考えていますが、細かいところでは分からない部分もあります。しかしこのように色々と工夫して作っていく中では学習が必要です。
こう書いていけるようになると、最後にこのパッケージで提供するマクロを一覧に記載する必要があります。例えば Stringify
マクロが提供されていることを示します。これがマクロの基本的な実装方法で、エクスパンドマクロしか見ていないのですが、同じようにフィルタマクロやその他の型に対するマクロも書けるでしょう。必要なAST(抽象構文木)を理解した上で処理を行い、結果を返すという流れは変わりません。
このように作成したマクロをインポートすれば利用可能になります。これがマクロの基本的な概要です。さらに、ユニットテストも利用できるので、テストが非常にやりやすくなっている点も押さえておきましょう。このようにマクロの概要を理解しておけば、必要になったときに作成することができるでしょう。
次回は、またマクロについて話すことがあれば取り上げますが、なければ元に戻って循環参照の話やクローズアの循環参照辺りに進んでいこうと思います。この部分まで拾ってコメントしています。
小コンプリーションズに関する質問がありました。学校のプログラムで小コンプリーションが出たりしないのかという点についてです。確かに試してみる価値がありますね。メニューにあるので右クリックで試してみます。ここにあったボタンを押すと、SF(コード補完機能)の時とは少し違う感じがしますが、優先的には出てこなかった模様です。
少し様子を見て、学習コストが高い部分があるのは確かです。リテラルで解決しようとしなくても良い部分もあるかもしれませんが、頑張ってやっていく必要があります。ネットで検索すれば色々な情報が得られるはずなので怖がらずに進んでみましょう。
Swiftのスクレーピングも興味深いトピックです。なかなか奥が深いので挑戦してみると面白いかもしれません。
ここまできちんと説明できたので、今日はこのくらいにしておきましょう。お疲れ様でした。ありがとうございました。