はじめに
マクロへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。
この実験では、Rust におけるマクロの概念を探り、macro_rules!
を使った宣言的マクロと、3 種類の手続き型マクロ:カスタム#[derive]
マクロ、属性のようなマクロ、および関数のようなマクロについて学びます。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
マクロへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。
この実験では、Rust におけるマクロの概念を探り、macro_rules!
を使った宣言的マクロと、3 種類の手続き型マクロ:カスタム#[derive]
マクロ、属性のようなマクロ、および関数のようなマクロについて学びます。
この本を通じて、println!
のようなマクロを使用してきましたが、マクロが何であり、どのように機能するかについては、まだ十分に掘り下げていません。「マクロ」という用語は、Rust の機能のグループを指します。macro_rules!
を使った「宣言的」マクロと、3 種類の「手続き型」マクロです。
derive
属性に追加されるコードを指定するカスタム#[derive]
マクロこれらの各項目について順に説明しますが、最初に、関数が既にあるのになぜマクロが必要なのか見てみましょう。
基本的に、マクロは他のコードを書くコードを書く方法であり、これは「メタプログラミング」と呼ばれます。付録 C では、derive
属性について説明しており、これはあなたに対してさまざまなトレイトの実装を生成します。また、本書を通じてprintln!
やvec!
マクロも使用してきました。これらのマクロはすべて、手動で書いたコードよりも多くのコードを生成するように「展開」されます。
メタプログラミングは、書かなければならないコード量と保守コストを削減するのに役立ち、これは関数の役割の 1 つでもあります。ただし、マクロには関数にはない追加の機能がいくつかあります。
関数のシグネチャは、関数が持つパラメータの数と型を宣言する必要があります。一方、マクロは可変数のパラメータを取ることができます。つまり、1 つの引数でprintln!("hello")
を呼び出したり、2 つの引数でprintln!("hello {}", name)
を呼び出したりすることができます。また、マクロはコンパイラがコードの意味を解釈する前に展開されるため、例えば、特定の型に対してトレイトを実装することができます。関数はできません。なぜなら、関数は実行時に呼び出され、トレイトはコンパイル時に実装する必要があるからです。
関数の代わりにマクロを実装する欠点は、マクロ定義が関数定義よりも複雑であるということです。なぜなら、あなたが Rust コードを書いているので、それが Rust コードを書くからです。この間接性のため、マクロ定義は一般的に関数定義よりも読みにくく、理解しにくく、保守しにくい傾向があります。
マクロと関数のもう 1 つの重要な違いは、関数はどこで定義してもどこで呼び出してもよいのに対し、マクロはファイル内で呼び出す前に定義するか、スコープに持ち込む必要があるということです。
macro_rules!
を使った宣言的マクロRust で最も広く使われるマクロの形式は「宣言的マクロ」です。これは、「例によるマクロ」、「macro_rules!
マクロ」、または単に「マクロ」とも呼ばれます。その核心は、宣言的マクロが Rust の match
式に似たものを書けるようにすることです。第 6 章で説明したように、match
式は制御構造であり、式を取り、その式の結果の値をパターンと比較し、その後、一致するパターンに関連付けられたコードを実行します。マクロもまた、特定のコードに関連付けられたパターンと値を比較します。この場合、値はマクロに渡されるリテラルの Rust ソースコードであり、パターンはそのソースコードの構造と比較され、各パターンに関連付けられたコードは一致したときに、マクロに渡されたコードを置き換えます。これらのすべてはコンパイル時に行われます。
マクロを定義するには、macro_rules!
構文を使用します。vec!
マクロがどのように定義されているかを見て、macro_rules!
をどのように使用するかを探りましょう。第 8 章では、特定の値を持つ新しいベクトルを作成するために vec!
マクロをどのように使用できるかを説明しました。たとえば、次のマクロは 3 つの整数を含む新しいベクトルを作成します。
let v: Vec<u32> = vec![1, 2, 3];
また、vec!
マクロを使って、2 つの整数のベクトルや 5 つの文字列スライスのベクトルを作成することもできます。事前に値の数や型を知らないため、同じことを関数で行うことはできません。
リスト 19-28 は、vec!
マクロのわずかに簡略化された定義を示しています。
ファイル名:src/lib.rs
1 #[macro_export]
2 macro_rules! vec {
3 ( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
4 $(
5 temp_vec.push(6 $x);
)*
7 temp_vec
}
};
}
リスト 19-28: vec!
マクロ定義の簡略化バージョン
注:標準ライブラリにおける
vec!
マクロの実際の定義には、事前に正しい量のメモリを割り当てるコードが含まれています。このコードは最適化であり、この例を簡単にするためにここには含めていません。
#[macro_export]
アノテーション [1] は、マクロが定義されているクレートがスコープに入るときに、このマクロが利用可能になるようにすることを示しています。このアノテーションがないと、マクロをスコープに持ち込むことができません。
次に、マクロ定義を macro_rules!
と定義するマクロの名前(感嘆符を付けない)で始めます [2]。この場合の名前は vec
で、マクロ定義の本体を表す波括弧が続きます。
vec!
の本体の構造は、match
式の構造に似ています。ここでは、( $( $x:expr ),* )
というパターンを持つ 1 つのアームがあり、その後に =>
とこのパターンに関連付けられたコードブロックが続きます [3]。パターンが一致すると、関連付けられたコードブロックが生成されます。このマクロにはこれが唯一のパターンなので、一致する有効な方法は 1 つだけで、他のパターンはエラーになります。より複雑なマクロには複数のアームがあります。
マクロ定義における有効なパターン構文は、第 18 章で説明したパターン構文とは異なります。なぜなら、マクロパターンは値ではなく Rust コード構造と照合されるからです。リスト 19-28 のパターンの部分が何を意味するか見てみましょう。完全なマクロパターン構文については、https://doc.rust-lang.org/reference/macros-by-example.html の Rust リファレンスを参照してください。
まず、全体的なパターンを囲むために一組の丸括弧を使用します。マクロシステム内の変数を宣言するには、ドル記号 ($
) を使用します。この変数は、パターンに一致する Rust コードを含みます。ドル記号は、これが通常の Rust 変数とは異なるマクロ変数であることを明確に示しています。次に、丸括弧のセットが続きます。これは、置換コードで使用するために、丸括弧内のパターンに一致する値をキャプチャします。$()
の中には $x:expr
があり、これは任意の Rust 式と一致し、式に $x
という名前を付けます。
$()
の後に続くコンマは、$()
内のコードに一致するコードの後に、リテラルのコンマ区切り文字が任意に現れることを示しています。*
は、*
の前にあるものと 0 回以上一致することを指定します。
このマクロを vec![1, 2, 3];
で呼び出すと、$x
パターンは 3 つの式 1
、2
、3
と 3 回一致します。
次に、このアームに関連付けられたコードの本体のパターンを見てみましょう。$()*
内の temp_vec.push()
[5] は、パターンが一致する回数に応じて、パターン内の $()
と一致する各部分に対して 0 回以上生成されます。$x
[6] は、一致した各式に置き換えられます。このマクロを vec![1, 2, 3];
で呼び出すと、このマクロ呼び出しを置き換える生成されるコードは次のようになります。
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
任意の型の任意の数の引数を取り、指定された要素を含むベクトルを作成するコードを生成できるマクロを定義しました。
マクロを書く方法について詳しく学ぶには、オンラインドキュメントや、Daniel Keep によって始まり Lukas Wirth によって続けられている https://veykril.github.io/tlborm の「The Little Book of Rust Macros」などの他のリソースを参照してください。
マクロの 2 番目の形式は手続き型マクロで、これはより関数に似た働きをします(手続きの一種です)。「手続き型マクロ」は、宣言的マクロのようにパターンと照合してコードを他のコードで置き換えるのではなく、入力としてコードを一部受け取り、そのコードに対して操作を行い、出力としてコードを生成します。3 種類の手続き型マクロは、カスタムderive
、属性のような、および関数のようなもので、すべて同様の方法で機能します。
手続き型マクロを作成する際、定義は特別なクレートタイプを持つ独自のクレートに存在しなければなりません。これは将来的に解消したい複雑な技術的理由によるものです。リスト 19-29 では、特定のマクロ種類を使用するためのプレースホルダであるsome_attribute
がある場合の手続き型マクロの定義方法を示しています。
ファイル名:src/lib.rs
use proc_macro::TokenStream;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
リスト 19-29: 手続き型マクロの定義の例
手続き型マクロを定義する関数は、入力としてTokenStream
を取り、出力としてTokenStream
を生成します。TokenStream
型は、Rust に含まれるproc_macro
クレートによって定義され、トークンのシーケンスを表します。これがマクロの核心です。マクロが操作するソースコードが入力TokenStream
を構成し、マクロが生成するコードが出力TokenStream
です。また、関数には、作成している手続き型マクロの種類を指定する属性が付けられています。同じクレートに複数種類の手続き型マクロを持つことができます。
さて、手続き型マクロの種類を見てみましょう。まずはカスタムderive
マクロから始めて、他の形式が異なる小さな違いを説明します。
derive
マクロを書く方法hello_macro
という名前のクレートを作成しましょう。このクレートには、HelloMacro
という名前のトレイトが定義されており、そのトレイトには hello_macro
という名前の 1 つの関連付けられた関数があります。ユーザーに対してそれぞれの型に対して HelloMacro
トレイトを実装させるのではなく、手続き型マクロを提供して、ユーザーが #[derive(HelloMacro)]
で型を注釈付けすることで、hello_macro
関数のデフォルトの実装を取得できるようにします。デフォルトの実装では、Hello, Macro! My name is
TypeName!
と出力されます。ここで、TypeName はこのトレイトが定義されている型の名前です。言い換えると、他のプログラマーが私たちのクレートを使ってリスト 19-30 のようなコードを書けるように、私たちはクレートを書きます。
ファイル名:src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
リスト 19-30: 私たちのクレートのユーザーが手続き型マクロを使用した場合に書けるようになるコード
このコードは、完了したときに Hello, Macro! My name is Pancakes!
と出力します。最初のステップは、新しいライブラリクレートを作成することです。次のようにします。
cargo new hello_macro --lib
次に、HelloMacro
トレイトとその関連付けられた関数を定義します。
ファイル名:src/lib.rs
pub trait HelloMacro {
fn hello_macro();
}
トレイトとその関数があります。この時点で、私たちのクレートのユーザーは、次のようにトレイトを実装して、望ましい機能を達成することができます。
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}
fn main() {
Pancakes::hello_macro();
}
しかし、彼らは hello_macro
で使用したい各型に対して実装ブロックを書かなければなりません。私たちは彼らにこの作業を強いたくありません。
また、私たちはまだ、トレイトが実装されている型の名前を出力するデフォルトの実装を hello_macro
関数に提供することができません。Rust にはリフレクション機能がないため、実行時に型の名前を参照することができません。コンパイル時にコードを生成するために、マクロが必要です。
次のステップは、手続き型マクロを定義することです。この記事を書いている時点で、手続き型マクロは独自のクレートに存在する必要があります。最終的に、この制限は解除されるかもしれません。クレートとマクロクレートを構成する規則は次の通りです。foo という名前のクレートに対して、カスタム derive
手続き型マクロクレートは foo_derive
と呼ばれます。hello_macro
プロジェクト内に新しい hello_macro_derive
という名前のクレートを作成しましょう。
cargo new hello_macro_derive --lib
私たちの 2 つのクレートは密接に関連しているため、hello_macro
クレートのディレクトリ内に手続き型マクロクレートを作成します。hello_macro
のトレイト定義を変更すると、hello_macro_derive
の手続き型マクロの実装も変更する必要があります。2 つのクレートは別々に公開する必要があり、これらのクレートを使用するプログラマーは、両方を依存関係として追加し、両方をスコープに持ち込む必要があります。代わりに、hello_macro
クレートが hello_macro_derive
を依存関係として使用し、手続き型マクロコードを再エクスポートすることもできます。ただし、私たちがプロジェクトを構成した方法により、プログラマーが derive
機能を必要としない場合でも、hello_macro
を使用できるようになります。
hello_macro_derive
クレートを手続き型マクロクレートとして宣言する必要があります。また、すぐに見るでしょうが、syn
と quote
クレートの機能が必要なので、依存関係として追加する必要があります。hello_macro_derive
の Cargo.toml
ファイルに次のように追加します。
ファイル名:hello_macro_derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
手続き型マクロを定義し始めるには、hello_macro_derive
クレートの src/lib.rs
ファイルにリスト 19-31 のコードを配置します。impl_hello_macro
関数の定義を追加するまで、このコードはコンパイルされません。
ファイル名:hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Rust コードを構文木として表現し、操作できるようにする
let ast = syn::parse(input).unwrap();
// トレイトの実装を構築する
impl_hello_macro(&ast)
}
リスト 19-31: Rust コードを処理するために必要な手続き型マクロクレートのほとんどのコード
hello_macro_derive
関数は、TokenStream
を解析する責任があり、impl_hello_macro
関数は、構文木を変換する責任があります。これにより、手続き型マクロを書くのが便利になります。外側の関数(この場合は hello_macro_derive
)のコードは、見たり作成したりするほとんどの手続き型マクロクレートで同じになります。内側の関数(この場合は impl_hello_macro
)の本体で指定するコードは、手続き型マクロの目的に応じて異なります。
私たちは 3 つの新しいクレートを紹介しました。proc_macro
、syn
(https://crates.io/crates/syn から入手可能)、および quote
(https://crates.io/crates/quote から入手可能)です。proc_macro
クレートは Rust に付属しているため、Cargo.toml
の依存関係に追加する必要はありませんでした。proc_macro
クレートは、コンパイラの API であり、コードから Rust コードを読み取り、操作することができます。
syn
クレートは、文字列を Rust コードに解析して、操作できるデータ構造にします。quote
クレートは、syn
データ構造を再び Rust コードに変換します。これらのクレートにより、取り扱いたい任意の種類の Rust コードを解析するのがはるかに簡単になります。Rust コード用の完全なパーサを書くのは簡単な作業ではありません。
hello_macro_derive
関数は、ライブラリのユーザーが型に #[derive(HelloMacro)]
を指定したときに呼び出されます。これは、ここで hello_macro_derive
関数に proc_macro_derive
アノテーションを付け、トレイト名と一致する HelloMacro
という名前を指定したからできます。これは、ほとんどの手続き型マクロが従う規則です。
hello_macro_derive
関数はまず、input
を TokenStream
から、その後で解釈して操作できるデータ構造に変換します。ここが syn
が登場するところです。syn
の parse
関数は TokenStream
を取り、解析された Rust コードを表す DeriveInput
構造体を返します。リスト 19-32 は、struct Pancakes;
文字列を解析することで得られる DeriveInput
構造体の関連部分を示しています。
DeriveInput {
--snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
リスト 19-32: リスト 19-30 のマクロ属性を持つコードを解析するときに得られる DeriveInput
インスタンス
この構造体のフィールドは、解析した Rust コードが Pancakes
の ident
(識別子、つまり名前)を持つユニット構造体であることを示しています。この構造体には、さまざまな種類の Rust コードを記述するためのさらに多くのフィールドがあります。詳細については、https://docs.rs/syn/1.0/syn/struct.DeriveInput.html の DeriveInput
の syn
ドキュメントを参照してください。
すぐに、新しい Rust コードを生成する impl_hello_macro
関数を定義します。しかし、その前に、derive
マクロの出力も TokenStream
であることに注意してください。返された TokenStream
は、クレートのユーザーが書いたコードに追加されます。したがって、彼らがクレートをコンパイルするとき、修正された TokenStream
に含まれる追加の機能を得ることができます。
syn::parse
関数の呼び出しがここで失敗した場合に、hello_macro_derive
関数がパニックするように unwrap
を呼んでいることに気付いたかもしれません。手続き型マクロがエラーでパニックする必要があるのは、proc_macro_derive
関数が手続き型マクロ API に準拠するために TokenStream
を返さなければならないからです。この例では unwrap
を使って簡略化していますが、本番コードでは、panic!
または expect
を使って何が問題だったかに関するより具体的なエラーメッセージを提供する必要があります。
ここで、注釈付きの Rust コードを TokenStream
から DeriveInput
インスタンスに変換するコードがあるので、注釈付きの型に対して HelloMacro
トレイトを実装するコードを生成しましょう。リスト 19-33 を参照してください。
ファイル名:hello_macro_derive/src/lib.rs
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!(
"Hello, Macro! My name is {}!",
stringify!(#name)
);
}
}
};
gen.into()
}
リスト 19-33: 解析された Rust コードを使用して HelloMacro
トレイトを実装する
ast.ident
を使用して、注釈付きの型の名前(識別子)を含む Ident
構造体インスタンスを取得します。リスト 19-32 の構造体は、リスト 19-30 のコードに対して impl_hello_macro
関数を実行したとき、取得する ident
が ident
フィールドに "Pancakes"
の値を持つことを示しています。したがって、リスト 19-33 の name
変数は、Ident
構造体インスタンスを含み、これを印刷すると、リスト 19-30 の構造体の名前である文字列 "Pancakes"
になります。
quote!
マクロを使って、返す Rust コードを定義します。コンパイラは、quote!
マクロの実行の直接の結果とは異なるものを期待しているため、TokenStream
に変換する必要があります。これは、この中間表現を消費して、必要な TokenStream
型の値を返す into
メソッドを呼び出すことで行います。
quote!
マクロはまた、非常に便利なテンプレート機能も提供します。#name
を入力すると、quote!
はそれを name
変数の値で置き換えます。通常のマクロと同じように、繰り返し処理もできます。詳細な紹介については、https://docs.rs/quote の quote
クレートのドキュメントを参照してください。
私たちの手続き型マクロは、ユーザーが注釈付けした型に対して HelloMacro
トレイトの実装を生成したいと思っています。これは、#name
を使用することで取得できます。トレイトの実装には、1 つの関数 hello_macro
があり、その本体には、私たちが提供したい機能が含まれています。つまり、Hello, Macro! My name is
と出力して、その後に注釈付きの型の名前を出力します。
ここで使用されている stringify!
マクロは、Rust に組み込まれています。これは、Rust 式(たとえば 1 + 2
)を取り、コンパイル時に式を文字列リテラル(たとえば "1 + 2"
)に変換します。これは、式を評価してから結果を String
に変換する format!
や println!
マクロとは異なります。#name
入力が文字通り出力する式である可能性があるため、stringify!
を使用します。stringify!
を使用することで、コンパイル時に #name
を文字列リテラルに変換することで、割り当てを節約することもできます。
この時点で、hello_macro
と hello_macro_derive
の両方で cargo build
が正常に完了するはずです。この手続き型マクロを実際に使ってみましょう!リスト 19-30 のコードにこれらのクレートを接続します。cargo new pancakes
を使って、project
ディレクトリに新しいバイナリプロジェクトを作成します。pancakes
クレートの Cargo.toml
に hello_macro
と hello_macro_derive
を依存関係として追加する必要があります。hello_macro
と hello_macro_derive
のバージョンを https://crates.io に公開する場合は、通常の依存関係になります。そうでない場合は、次のように path
依存関係として指定できます。
[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
リスト 19-30 のコードを src/main.rs
に入れ、cargo run
を実行します。Hello, Macro! My name is Pancakes!
と出力されるはずです。手続き型マクロによる HelloMacro
トレイトの実装が、pancakes
クレートが実装する必要なく含まれました。#[derive(HelloMacro)]
により、トレイトの実装が追加されました。
次に、他の種類の手続き型マクロがカスタム derive
マクロとどのように異なるかを調べましょう。
属性のようなマクロは、カスタム derive
マクロに似ていますが、derive
属性用のコードを生成する代わりに、新しい属性を作成できるようにします。また、もっと柔軟です。derive
は構造体と列挙体にのみ機能しますが、属性は関数など、他の項目にも適用できます。属性のようなマクロを使用する例を示しましょう。Web アプリケーションフレームワークを使用する際に、route
という名前の属性が関数に注釈付けされるとします。
#[route(GET, "/")]
fn index() {
この #[route]
属性は、フレームワークによって手続き型マクロとして定義されます。マクロ定義関数のシグネチャは次のようになります。
#[proc_macro_attribute]
pub fn route(
attr: TokenStream,
item: TokenStream
) -> TokenStream {
ここでは、TokenStream
型の 2 つのパラメータがあります。最初は属性の内容に対するもので、GET, "/"
の部分です。2 番目は、属性が付けられている項目の本体です。この場合、fn index() {}
と関数本体の残り部分です。
それ以外は、属性のようなマクロはカスタム derive
マクロと同じように機能します。proc-macro
クレートタイプのクレートを作成し、生成したいコードを生成する関数を実装します!
関数のようなマクロは、関数呼び出しのように見えるマクロを定義します。macro_rules!
マクロと同様に、関数よりも柔軟です。たとえば、未知の数の引数を取ることができます。ただし、macro_rules!
マクロは、「一般的なメタプログラミング用の macro_rules!
を使った宣言的マクロ」で説明したマッチのような構文を使ってのみ定義できます。関数のようなマクロは TokenStream
パラメータを取り、その定義は、他の 2 種類の手続き型マクロと同じように、Rust コードを使ってその TokenStream
を操作します。関数のようなマクロの例は、次のように呼び出される sql!
マクロです。
let sql = sql!(SELECT * FROM posts WHERE id=1);
このマクロは、内部の SQL 文を解析し、構文的に正しいことを確認します。これは、macro_rules!
マクロが行うことよりもはるかに複雑な処理です。sql!
マクロは次のように定義されます。
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
この定義は、カスタム derive
マクロのシグネチャに似ています。丸括弧内のトークンを受け取り、生成したかったコードを返します。
おめでとうございます!あなたはマクロの実験を完了しました。あなたのスキルを向上させるために、LabEx でさらに多くの実験を行って練習することができます。