Swift 6.0 beta 日本語化計画 : Swift 6.0 beta
マクロ
マクロを使用すると、コンパイル時にソースコードが変換されるため、手動で繰り返しコードを作成する必要がなくなります。コンパイル中、Swift は通常どおりコードをビルドする前に、あなたのコード内のマクロを展開します。
マクロの展開は常に追加操作です。マクロは新しいコードを追加しますが、既存のコードを削除したり変更したりすることは決してありません。
マクロへの入力とマクロ展開の出力の両方がチェックされ、構文的に有効な Swift コードであることが確認されます。同様に、マクロにあなたが渡す値とマクロによって生成されたコード内の値がチェックされ、正しい型であることが確認されます。さらに、マクロの展開時にマクロの実装でエラーが発生した場合、コンパイラはこれをコンパイルエラーとして扱います。これらの保証により、マクロを使用するコードについての推論が容易になり、マクロの誤った使用やバグのあるマクロ実装などの問題を特定しやすくなります。
Swift には 2 種類のマクロがあります。
付属したマクロと独立したマクロの呼び出し方法は少し異なりますが、どちらもマクロ拡張の同じモデルに従い、同じアプローチを使用して実装します。以下のセクションでは、両方の種類のマクロについて詳しく説明します。
独立したマクロを呼び出すには、その名前の前にシャープ記号 (#) を書き、そのマクロの引数をその名前の後にかっこ内に書きます。例えば:
最初の行で、#function は Swift の標準ライブラリから function() (function()) マクロを呼び出します。このコードをコンパイルすると、Swift はそのマクロの実装を呼び出し、#function を現在の関数の名前に置き換えます。このコードを実行して myFunction() を呼び出すと、"現在 myFunction() を実行中" と表示されます。2 行目で #warning は、Swift の標準ライブラリから warning(_:) (warning(_:)) マクロを呼び出して、カスタムのコンパイル時警告を生成します。
独立したマクロは、#function のように値を生成したり、#warning のようにコンパイル時にアクションを実行したりできます。
付属したマクロを呼び出すには、その名前の前にアットマーク (@) を書き、そのマクロの引数をその名前の後のかっこ内に書きます。
付属したたマクロは、付属している宣言を変更します。新しいメソッドの定義やプロトコルへの準拠の追加など、その宣言にコードを追加します。
たとえば、マクロを使用しない以下のコードを考えてみましょう。
このコードでは、SundaeToppings オプションセットの各オプションにはイニシャライザへの呼び出しが含まれており、これは反復的かつ手動です。新しいオプションを追加するときは、行末に間違った数字を入力するなど、間違いを犯しやすくなります。
代わりにマクロを使用する以下のコードのバージョンを示します。
このバージョンの SundaeToppings は @OptionSet マクロを呼び出しています。このマクロは、private 列挙内の case のリストを読み取り、各オプションの定数のリストを生成し、OptionSet プロトコルへの準拠を追加します。
比較のために、@OptionSet マクロの拡張バージョンは以下のようになります。このコードはあなたが作成したものではなく、マクロの展開を表示するように Swift に具体的に要求した場合にのみ表示されます。
private の列挙の後のコードはすべて @OptionSet マクロからのものです。マクロを使用してすべての静的変数を生成する SundaeToppings のバージョンは、以前の手動でコード化されたバージョンよりも読みやすく、保守も容易です。
ほとんどの Swift コードでは、関数や型などのシンボルを実装するときに、別個の宣言はありません。ただし、マクロの場合、宣言と実装は別になります。マクロの宣言には、マクロの名前、マクロが受け取るパラメータ、使用できる場所、生成されるコードの種類が含まれます。マクロの実装には、Swift コードを生成してマクロを展開するコードが含まれます。
macro キーワードを使用してマクロ宣言を導入します。たとえば、前の例で使用した @OptionSet マクロの宣言の一部を以下に示します。
最初の行はマクロの名前とその引数を指定します。名前は OptionSet で、引数は全く取りません。2 行目は、Swift 標準ライブラリの externalMacro(module:type:) (externalMacro(module:type:)) マクロを使用して、マクロの実装の場所を Swift に伝えます。この場合、SwiftMacros モジュールには、@OptionSet マクロを実装する OptionSetMacro という名前の型が含まれます。
OptionSet は付属したマクロであるため、その名前には構造体やクラスの名前と同様に大文字のキャメルケース (upper camel case) が使用されます。独立したマクロには、変数や関数の名前と同様に、小文字のキャメルケース (lower camel case) の名が付けられます。
マクロ宣言は、マクロの 役割、つまりそのマクロを呼び出すことができるソースコード内の場所、およびマクロが生成できるコードの種類を定義します。すべてのマクロには 1 つ以上の役割があり、マクロ宣言の先頭に属性の一部として記述します。@OptionSet の宣言をもう少し詳しく説明します。これには、その役割の属性も含まれます。
@attached 属性は、この宣言内で 2 回 (マクロの役割ごとに 1 回ずつ) 現れます。最初の使用 @attached(member) は、マクロが適用先の型に新しいメンバを追加することを示します。@OptionSet マクロは、OptionSet プロトコルに必要な init(rawValue:) イニシャライザといくつかの追加メンバを追加します。2 番目の使用である @attached(extension, conformances: OptionSet) は、@OptionSet が OptionSet プロトコルに準拠を追加することを示します。@OptionSet マクロは、マクロを適用する型を拡張して、OptionSet プロトコルへの準拠を追加します。
独立したマクロの場合は、@freestanding 属性を記述してその役割を指定します。
上記の #line マクロには expression の役割があります。expression マクロは値を生成するか、警告の生成などのコンパイル時のアクションを実行します。
マクロの役割に加えて、マクロの宣言は、マクロが生成するシンボルの名前に関する情報を提供します。マクロ宣言で名前のリストが提供されている場合、それらの名前を使用する宣言のみが生成されることが保証されているため、生成されたコードの理解とデバッグに役立ちます。@OptionSet の完全な宣言は以下のとおりです。
上記の宣言では、@attached(member) マクロには、@OptionSet マクロが生成する各シンボルの named: ラベルの後に引数が含まれています。マクロは、RawValue、rawValue、 および init という名前のシンボルの宣言を追加します。これらの名前は事前にわかっているため、マクロ宣言でそれらを明示的にリストします。
マクロ宣言には、名前のリストの後に arbitrary も含まれているため、マクロを使用するまで名前が分からない宣言をマクロで生成できます。たとえば、@OptionSet マクロが上記の SundaeToppings に適用されると、列挙 case である、nuts、cherry、fudge に対応する型プロパティが生成されます。
マクロの役割の完全なリストを含む詳細については、属性 の Attached (付属した) と freestanding (独立した) を参照してください。
マクロを使用する Swift コードをビルドする場合、コンパイラはマクロの実装を呼び出してマクロを展開します。
具体的には、Swift は以下の方法でマクロを展開します。
具体的な手順を実行するには、次の点を考慮してください。
#fourCharacterCode マクロは、4 文字の長さの文字列を受け取り、結合された文字列内の ASCII 値に対応する符号なし 32 ビット整数を返します。一部のファイル形式では、コンパクトでありながらデバッガで読み取り可能なため、このような整数を使用してデータを識別します。以下の マクロの実装 セクションでは、このマクロを実装する方法を示します。
上記のコード内のマクロを展開するために、コンパイラは Swift ファイルを読み取り、抽象構文ツリー (abstract syntax tree、AST) として知られるそのコードのメモリ内表現を作成します。AST はコードの構造を明示的にするため、コンパイラやマクロの実装など、その構造を操作するコードの記述が容易になります。上記のコードの AST を以下に示します。余分な詳細を省略して少し簡略化しています。
上記の図は、このコードの構造がメモリ内でどのように表現されるかを示しています。AST 内の各要素はソースコードの一部に対応します。"定数宣言" の AST 要素の下には 2 つの子要素があり、定数宣言の 2 つの部分 (その名前と値) を表します。"マクロ呼び出し(Macro call)" 要素には、マクロの名前とマクロに渡される引数のリストを表す子要素があります。
この AST の構築の一環として、コンパイラはソースコードが有効な Swift であることを確認します。たとえば、#fourCharacterCode は 1 つの引数を取りますが、これは文字列でなければなりません。整数の引数を渡そうとした場合、または文字列リテラルの末尾の引用符 (") を忘れた場合は、プロセスのこの時点でエラーが発生します。
コンパイラは、コード内であなたがマクロを呼び出す場所を見つけ、それらのマクロを実装する外部バイナリをロードします。マクロ呼び出しごとに、コンパイラは AST の一部をそのマクロの実装に渡します。以下はその部分的な AST の表現です。
#fourCharacterCode マクロの実装は、マクロを展開するときに、この部分的な AST をその入力として読み取ります。マクロの実装は、その入力として受け取る部分的な AST でのみ動作します。つまり、マクロは、その前後にどのようなコードがあるかに関係なく、常に同じ方法で展開されます。この制限により、マクロの展開が理解しやすくなり、Swift が、変更されていないマクロの展開を回避できるため、コードのビルドが高速化されます。
Swift は、マクロを実装するコードを制限することで、マクロの作成者が誤って他の入力を読み取ることを避けるのに役立ちます。
これらの安全対策に加えて、マクロの作成者には、マクロの入力以外のものを読み取ったり変更したりしない責任があります。たとえば、マクロの展開は現在の時刻に依存してはなりません。
#fourCharacterCode を実装すると、拡張されたコードを含む新しい AST が生成されます。そのコードがコンパイラに返す内容は以下のとおりです。
コンパイラはこの展開を受け取ると、マクロ呼び出しを含む AST 要素をマクロの展開を含む要素に置き換えます。マクロ展開後、コンパイラはプログラムが依然として構文的に有効な Swift であり、すべての型が正しいことを再度チェックします。これにより、通常どおりコンパイルできる最終的な AST が生成されます。
この AST は、以下のような Swift コードに対応します。
この例では、入力ソースコードにはマクロが 1 つしかありませんが、実際のプログラムには同じマクロのインスタンスが複数存在し、異なるマクロへの呼び出しが複数存在する可能性があります。コンパイラはマクロを一度に 1 つずつ展開します。
あるマクロが別のマクロの中にある場合、外側のマクロが最初に展開されます。これにより、外側のマクロはそれが展開される前に内側のマクロを変更できます。
マクロを実装するには、2 つのコンポーネントを作成して下さい。1 つはマクロ展開を実行する型、もう 1 つはマクロを宣言して API として公開するライブラリです。マクロの実装はマクロのクライアントのビルドの一部として実行されるため、マクロとそのクライアントを一緒に開発している場合でも、これらの部分はマクロを使用するコードとは別にビルドされます。
Swift Package Manager を使用して新しいマクロを作成するには、swift package init --type Macro を実行します。これにより、マクロの実装と宣言のテンプレートを含むいくつかのファイルが作成されます。
既存のプロジェクトにマクロを追加するには、Package.swift ファイルの先頭を以下のように編集します。
以下のコードは、Package.swift ファイルの例の先頭を示しています。
次に、マクロ実装のターゲットとマクロライブラリのターゲットを既存の Package.swift ファイルに追加します。たとえば、あなたのプロジェクトに合わせて名前を変更して、以下のようなものを追加できます。
上記のコードは 2 つのターゲットを定義しています。MyProjectMacros にはマクロの実装が含まれており、MyProject はこれらのマクロを使用可能にします。
マクロの実装では、SwiftSyntax モジュールを使用して、AST を使用して構造化された方法で Swift コードを操作します。Swift Package Manager を使用して新しいマクロパッケージを作成した場合、生成された Package.swift ファイルには、SwiftSyntax への従属 (dependency) が自動的に含まれます。既存のプロジェクトにマクロを追加する場合は、Package.swift ファイルに SwiftSyntax への従属を追加します。
マクロの役割に応じて、マクロの実装が準拠する SwiftSyntax の対応するプロトコルがあります。たとえば、前のセクションの #fourCharacterCode について考えてみましょう。そのマクロを実装する構造体は次のとおりです。
このマクロを既存の Swift Package Manager プロジェクトに追加する場合は、マクロターゲット用のエントリポイントとして機能し、ターゲットが定義するマクロをリストする型を追加します。
#fourCharacterCode マクロは式を生成する独立したマクロであるため、それを実装する FourCharacterCode 型は ExpressionMacro プロトコルに準拠します。この ExpressionMacro プロトコルには、AST を展開する expansion(of:in:) メソッドという要件が 1 つあります。マクロの役割とそれに対応する SwiftSyntax プロトコルのリストについては、属性 の Attached (付属した) と freestanding (独立した) を参照してください。
#fourCharacterCode マクロを展開するために、Swift はこのマクロを使用するコードの AST をマクロの実装を含むライブラリに送信します。ライブラリ内で、Swift は FourCharacterCode.expansion(of:in:) を呼び出し、AST とコンテキストを引数としてメソッドに渡します。expandation(of:in:) の実装は、#fourCharacterCode に引数として渡された文字列を検索し、対応する 32 ビット符号なし整数リテラル値を計算します。
上記の例では、最初の guard ブロックは AST から文字列リテラルを抽出し、その AST 要素を literalSegment に代入します。2 番目の guard ブロックは、private の fourCharacterCode(for:) 関数を呼び出します。マクロが正しく使用されない場合、これらのブロックは両方ともエラーを throw します。エラーメッセージは、不正な形式のサイト呼び出しでコンパイラエラーになります。たとえば、マクロを #fourCharacterCode("AB" + "CD") として呼び出そうとすると、コンパイラは "静的文字列が必要です" というエラーを表示します。
expanded(of:in:) メソッドは、AST 内の式を表す SwiftSyntax の型である ExprSyntax のインスタンスを返します。この型は StringLiteralConvertible プロトコルに準拠しているため、マクロの実装は結果を作成するための軽量構文として文字列リテラルを使用します。マクロの実装から返される SwiftSyntax 型はすべて StringLiteralConvertible に準拠しているため、あらゆる種類のマクロを実装するときにこのアプローチを使用できます。
マクロは、テストを使用した開発に適しています。マクロは、外部状態に依存せず、外部状態を変更することなく、ある AST を別の AST に変換します。さらに、文字列リテラルから構文ノードを作成できるため、テストの入力のセットアップが簡素化されます。AST の description プロパティをも読み取り、期待値と比較する文字列を取得することもできます。たとえば、以下は前のセクションの #fourCharacterCode マクロのテストです。
上記の例では前提条件を使用してマクロをテストしていますが、代わりにテスト用フレームワークを使用することもできます。