付録D: マクロ
本全体でprintln!
のようなマクロを使用してきましたが、マクロがなんなのかや、
どう動いているのかということは完全には探究していません。この付録は、マクロを以下のように説明します:
- マクロとはなんなのかと関数とどう違うのか
- 宣言的なマクロを定義してメタプログラミングをする方法
- プロシージャルなマクロを定義して独自の
derive
トレイトを生成する方法
マクロは今でも、Rustにおいては発展中なので、付録でマクロの詳細を講義します。マクロは変わってきましたし、 近い将来、Rust1.0からの言語の他の機能や標準ライブラリに比べて速いスピードで変化するので、 この節は、本の残りの部分よりも時代遅れになる可能性が高いです。Rustの安定性保証により、 ここで示したコードは、将来のバージョンでも動き続けますが、この本の出版時点では利用可能ではないマクロを書くための追加の能力や、 より簡単な方法があるかもしれません。この付録から何かを実装しようとする場合には、そのことを肝に銘じておいてください。
マクロと関数の違い
基本的に、マクロは、他のコードを記述するコードを書く術であり、これはメタプログラミングとして知られています。
付録Cで、derive
属性を議論し、これは、色々なトレイトの実装を生成してくれるのでした。
また、本を通してprintln!
やvec!
マクロを使用してきました。これらのマクロは全て、展開され、
手で書いたよりも多くのコードを生成します。
メタプログラミングは、書いて管理しなければならないコード量を減らすのに有用で、これは、関数の役目の一つでもあります。 ですが、マクロには関数にはない追加の力があります。
関数シグニチャは、関数の引数の数と型を宣言しなければなりません。一方、マクロは可変長の引数を取れます:
println!("hello")
のように1引数で呼んだり、println!("hello {}", name)
のように2引数で呼んだりできるのです。
また、マクロは、コンパイラがコードの意味を解釈する前に展開されるので、例えば、
与えられた型にトレイトを実装できます。関数ではできません。何故なら、関数は実行時に呼ばれ、
トレイトはコンパイル時に実装される必要があるからです。
関数ではなくマクロを実装する欠点は、Rustコードを記述するRustコードを書いているので、 関数定義よりもマクロ定義は複雑になることです。この間接性のために、マクロ定義は一般的に、 関数定義よりも、読みにくく、わかりにくく、管理しづらいです。
マクロと関数の別の違いは、マクロ定義は、関数定義のようには、モジュール内で名前空間分けされないことです。
外部クレートを使用する際に予期しない名前衝突を回避するために、#[macro_use]
注釈を使用して、
外部クレートをスコープに導入するのと同時に、自分のプロジェクトのスコープにマクロを明示的に導入しなければなりません。
以下の例は、serde
クレートに定義されているマクロ全部を現在のクレートのスコープに導入するでしょう:
#[macro_use]
extern crate serde;
この明示的注釈なしにextern crate
が既定でスコープにマクロを導入できたら、偶然同じ名前のマクロを定義している2つのクレートを使用できなくなるでしょう。
現実的には、この衝突はあまり起きませんが、使用するクレートが増えるほど、可能性は高まります。
マクロと関数にはもう一つ、重要な違いがあります: ファイル内で呼び出す前にマクロはスコープに導入しなければなりませんが、 一方で関数はどこにでも定義でき、どこでも呼び出せます。
一般的なメタプログラミングのためにmacro_rules!
で宣言的なマクロ
Rustにおいて、最もよく使用される形態のマクロは、宣言的マクロです。これらは時として、
例によるマクロ、macro_rules!
マクロ、あるいはただ単にマクロとも称されます。
核となるのは、宣言的マクロは、Rustのmatch
式に似た何かを書けるということです。第6章で議論したように、
match
式は、式を取り、式の結果の値をパターンと比較し、それからマッチしたパターンに紐づいたコードを実行する制御構造です。
マクロも自身に紐づいたコードがあるパターンと値を比較します; この場面で値とは、
マクロに渡されたリテラルのRustのソースコードそのもの、パターンは、そのソースコードの構造と比較され、
各パターンに紐づいたコードは、マクロに渡されたコードを置き換えるコードです。これは全て、コンパイル時に起きます。
マクロを定義するには、macro_rules!
構文を使用します。vec!
マクロが定義されている方法を見て、
macro_rules!
を使用する方法を探究しましょう。vec!
マクロを使用して特定の値で新しいベクタを生成する方法は、
第8章で講義しました。例えば、以下のマクロは、3つの整数を中身にする新しいベクタを生成します:
# #![allow(unused_variables)] #fn main() { let v: Vec<u32> = vec![1, 2, 3]; #}
また、vec!
マクロを使用して2整数のベクタや、5つの文字列スライスのベクタなども生成できます。
同じことを関数を使って行うことはできません。予め、値の数や型がわかっていないからです。
リストD-1で些か簡略化されたvec!
マクロの定義を見かけましょう。
# #![allow(unused_variables)] #fn main() { #[macro_export] macro_rules! vec { ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } }; } #}
標準ライブラリの
vec!
マクロの実際の定義は、予め正確なメモリ量を確保するコードを含みます。 そのコードは、ここでは簡略化のために含まない最適化です。
#[macro_export]
注釈は、マクロを定義しているクレートがインポートされる度にこのマクロが利用可能になるべきということを示しています。
この注釈がなければ、このクレートに依存する誰かが#[macro_use]
注釈を使用していても、
このマクロはスコープに導入されないでしょう。
それから、macro_rules!
でマクロ定義と定義しているマクロの名前をビックリマークなしで始めています。
名前はこの場合vec
であり、マクロ定義の本体を意味する波括弧が続いています。
vec!
本体の構造は、match
式の構造に類似しています。ここではパターン( $( $x:expr ),* )
の1つのアーム、
=>
とこのパターンに紐づくコードのブロックが続きます。パターンが合致すれば、紐づいたコードのブロックが発されます。
これがこのマクロの唯一のパターンであることを踏まえると、合致する合法的な方法は一つしかありません;
それ以外は、全部エラーになるでしょう。より複雑なマクロには、2つ以上のアームがあるでしょう。
マクロ定義で合法なパターン記法は、第18章で講義したパターン記法とは異なります。というのも、 マクロのパターンは値ではなく、Rustコードの構造に対してマッチされるからです。リストD-1のパターンの部品がどんな意味か見ていきましょう; マクロパターン記法全ては参考文献をご覧ください。
まず、1組のカッコがパターン全体を囲んでいます。次にドル記号($
)、そして1組のカッコが続き、
このかっこは、置き換えるコードで使用するためにかっこ内でパターンにマッチする値をキャプチャします。
$()
の内部には、$x:expr
があり、これは任意のRust式にマッチし、その式に$x
という名前を与えます。
$()
に続くカンマは、$()
にキャプチャされるコードにマッチするコードの後に、区別を意味するリテラルのカンマ文字が現れるという選択肢もあることを示唆しています。
カンマに続く*
は、パターンが*
の前にあるもの0個以上にマッチすることを指定しています。
このマクロをvec![1, 2, 3];
と呼び出すと、$x
パターンは、3つの式1
、2
、3
で3回マッチします。
さて、このアームに紐づくコードの本体のパターンに目を向けましょう: $()*
部分内部のtemp_vec.push()
コードは、
パターンがマッチした回数に応じて0回以上パターン内で$()
にマッチする箇所ごとに生成されます。
$x
はマッチした式それぞれに置き換えられます。このマクロをvec![1, 2, 3];
と呼び出すと、
このマクロ呼び出しを置き換え、生成されるコードは以下のようになるでしょう:
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
任意の型のあらゆる数の引数を取り、指定した要素を含むベクタを生成するコードを生成できるマクロを定義しました。
多くのRustプログラマは、マクロを書くよりも使う方が多いことを踏まえて、これ以上macro_rules!
を議論しません。
マクロの書き方をもっと学ぶには、オンラインドキュメンテーションか他のリソース、
“The Little Book of Rust Macros(訳注
: Rustのマクロの小さな本)などを調べてください。
独自のderive
のためのプロシージャルマクロ
2番目の形態のマクロは、より関数(1種の手続きです)に似ているので、プロシージャル・マクロ(procedural macro; 訳注
:
手続きマクロ)と呼ばれます。プロシージャルマクロは、宣言的マクロのようにパターンにマッチさせ、
そのコードを他のコードと置き換えるのではなく、入力として何らかのRustコードを受け付け、そのコードを処理し、
出力として何らかのRustコードを生成します。これを執筆している時点では、derive
注釈にトレイト名を指定することで、
型に自分のトレイトを実装できるプロシージャルマクロを定義できるだけです。
hello_macro
という関連関数が1つあるHelloMacro
というトレイトを定義するhello_macro
というクレートを作成します。
クレートの使用者に使用者の型にHelloMacro
トレイトを実装することを強制するのではなく、
使用者が型を#[derive(HelloMacro)]
で注釈してhello_macro
関数の既定の実装を得られるように、
プロシージャルマクロを提供します。既定の実装は、Hello, Macro! My name is TypeName!
(訳注
: こんにちは、マクロ!僕の名前はTypeNameだよ!)と出力し、
ここでTypeName
はこのトレイトが定義されている型の名前です。言い換えると、他のプログラマに我々のクレートを使用して、
リストD-2のようなコードを書けるようにするクレートを記述します。
ファイル名: src/main.rs
extern crate hello_macro;
#[macro_use]
extern crate hello_macro_derive;
use hello_macro::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
このコードは完成したら、Hello, Macro! My name is Pancakes!
(Pancakes
: ホットケーキ)と出力します。最初の手順は、
新しいライブラリクレートを作成することです。このように:
$ cargo new hello_macro --lib
次にHelloMacro
トレイトと関連関数を定義します:
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { pub trait HelloMacro { fn hello_macro(); } #}
トレイトと関数があります。この時点でクレートの使用者は、以下のように、 このトレイトを実装して所望の機能を達成できるでしょう。
extern crate 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にはリフレクションの能力がないので、型の名前を実行時に検索することができないのです。
コンパイル時にコード生成するマクロが必要です。
注釈: リフレクションとは、実行時に型名や関数の中身などを取得する機能のことです。 言語によって提供されていたりいなかったりしますが、実行時にメタデータがないと取得できないので、 RustやC++のようなアセンブリコードに翻訳され、パフォーマンスを要求される高級言語では、提供されないのが一般的と思われます。
次の手順は、プロシージャルマクロを定義することです。これを執筆している時点では、プロシージャルマクロは、
独自のクレートに存在する必要があります。最終的には、この制限は持ち上げられる可能性があります。
クレートとマクロクレートを構成する慣習は以下の通りです: 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 = "0.11.11"
quote = "0.3.15"
プロシージャルマクロの定義を開始するために、hello_macro_derive
クレートのsrc/lib.rsファイルにリストD-3のコードを配置してください。
impl_hello_macro
関数の定義を追加するまでこのコードはコンパイルできないことに注意してください。
ファイル名: hello_macro_derive/src/lib.rs
extern crate proc_macro;
extern crate syn;
#[macro_use]
extern crate quote;
use proc_macro::TokenStream;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 型定義の文字列表現を構築する
// Construct a string representation of the type definition
let s = input.to_string();
// 文字列表現を構文解析する
// Parse the string representation
let ast = syn::parse_derive_input(&s).unwrap();
// implを構築する
// Build the impl
let gen = impl_hello_macro(&ast);
// 生成されたimplを返す
// Return the generated impl
gen.parse().unwrap()
}
D-3での関数の分け方に注目してください; これは、目撃あるいは作成するほとんどのプロシージャルマクロクレートで同じになるでしょう。
プロシージャルマクロを書くのが便利になるからです。impl_hello_macro
関数が呼ばれる箇所で行うことを選ぶものは、
プロシージャルマクロの目的によって異なるでしょう。
3つの新しいクレートを導入しました: proc_macro
、syn
、quote
です。proc_macro
クレートは、
Rustに付随してくるので、Cargo.tomlの依存に追加する必要はありませんでした。proc_macro
クレートにより、
RustコードをRustコードを含む文字列に変換できます。syn
クレートは、文字列からRustコードを構文解析し、
処理を行えるデータ構造にします。quote
クレートは、syn
データ構造を取り、Rustコードに変換し直します。
これらのクレートにより、扱いたい可能性のあるあらゆる種類のRustコードを構文解析するのがはるかに単純になります:
Rustコードの完全なパーサを書くのは、単純な作業ではないのです。
hello_macro_derive
関数は、ライブラリの使用者が型に#[derive(HelloMacro)]
を指定した時に呼び出されます。
その理由は、ここでhello_macro_derive
関数をproc_macro_derive
で注釈し、トレイト名に一致するHelloMacro
を指定したからです;
これは、ほとんどのプロシージャルマクロが倣う慣習です。
この関数はまず、TokenStream
からのinput
をto_string
を呼び出してString
に変換します。
このString
は、HelloMacro
を導出しているRustコードの文字列表現になります。
リストD-2の例で、s
はstruct Pancakes;
というString
値になります。
それが#[derive(HelloMacro)]
注釈を追加したRustコードだからです。
注釈: これを執筆している時点では、
TokenStream
は文字列にしか変換できません。 将来的にはよりリッチなAPIになるでしょう。
さて、RustコードのString
をそれから解釈して処理を実行できるデータ構造に構文解析する必要があります。
ここでsyn
が登場します。syn
のparse_derive_input
関数は、String
を取り、
構文解析されたRustコードを表すDeriveInput
構造体を返します。以下のコードは、
文字列struct Pancakes;
を構文解析して得られるDeriveInput
構造体の関係のある部分を表示しています:
DeriveInput {
// --snip--
ident: Ident(
"Pancakes"
),
body: Struct(
Unit
)
}
この構造体のフィールドは、構文解析したRustコードがPancakes
というident
(識別子、つまり名前)のユニット構造体であることを示しています。
この構造体にはRustコードのあらゆる部分を記述するフィールドがもっと多くあります;
DeriveInput
のsyn
ドキュメンテーションで詳細を確認してください。
この時点では、含みたい新しいRustコードを構築するimpl_hello_macro
関数を定義していません。
でもその前に、このhello_macro_derive
関数の最後の部分でquote
クレートのparse
関数を使用して、
impl_hello_macro
関数の出力をTokenStream
に変換し直していることに注目してください。
返されたTokenStream
をクレートの使用者が書いたコードに追加しているので、クレートをコンパイルすると、
我々が提供している追加の機能を得られます。
parse_derive_input
かparse
関数がここで失敗したら、unwrap
を呼び出してパニックしていることにお気付きかもしれません。
エラー時にパニックするのは、プロシージャルマクロコードでは必要なことです。何故なら、
proc_macro_derive
関数は、プロシージャルマクロAPIに従うようにResult
ではなく、
TokenStream
を返さなければならないからです。unwrap
を使用してこの例を簡略化することを選択しました;
プロダクションコードでは、panic!
かexpect
を使用して何が間違っていたのかより具体的なエラーメッセージを提供すべきです。
今や、TokenStream
からの注釈されたRustコードをString
とDeriveInput
インスタンスに変換するコードができたので、
注釈された型にHelloMacro
トレイトを実装するコードを生成しましょう:
ファイル名: hello_macro_derive/src/lib.rs
fn impl_hello_macro(ast: &syn::DeriveInput) -> quote::Tokens {
let name = &ast.ident;
quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}", stringify!(#name));
}
}
}
}
ast.ident
で注釈された型の名前(識別子)を含むIdent
構造体インスタンスを得ています。
リストD-2のコードは、name
がIdent("Pancakes")
になることを指定しています。
quote!
マクロは、返却しquote::Tokens
に変換したいRustコードを書かせてくれます。このマクロはまた、
非常にかっこいいテンプレート機構も提供してくれます; #name
と書け、quote!
は、
それをname
という変数の値と置き換えます。普通のマクロが動作するのと似た繰り返しさえ行えます。
完全なイントロダクションは、quote
クレートのdocをご確認ください。
プロシージャルマクロに使用者が注釈した型に対してHelloMacro
トレイトの実装を生成してほしく、
これは#name
を使用することで得られます。トレイトの実装には1つの関数hello_macro
があり、
この本体に提供したい機能が含まれています: Hello, Macro! My name is
、そして、注釈した型の名前を出力する機能です。
ここで使用したstringify!
マクロは、言語に埋め込まれています。1 + 2
などのようなRustの式を取り、
コンパイル時に"1 + 2"
のような文字列リテラルにその式を変換します。これは、format!
やprintln!
とは異なります。
こちらは、式を評価し、そしてその結果をString
に変換します。#name
入力が文字通り出力される式という可能性もあるので、
stringify!
を使用しています。stringify!
を使用すると、コンパイル時に#name
を文字列リテラルに変換することで、
メモリ確保しなくても済みます。
この時点で、cargo build
はhello_macro
とhello_macro_derive
の両方で成功するはずです。
これらのクレートをリストD-2のコードにフックして、プロシージャルマクロが動くところを確認しましょう!
cargo new --bin pancakes
でprojectsディレクトリに新しいバイナリプロジェクトを作成してください。
hello_macro
とhello_macro_derive
を依存としてpancakes
クレートのCargo.tomlに追加する必要があります。
自分のバージョンのhello_macro
とhello_macro_derive
をhttps://crates.io/ に公開するつもりなら、
普通の依存になるでしょう; そうでなければ、以下のようにpath
依存として指定できます:
[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
リストD-2のコードをsrc/main.rsに配置し、cargo run
を実行してください: Hello, Macro! My name is Pancakes
と出力するはずです。
プロシージャルマクロのHelloMacro
トレイトの実装は、pancakes
クレートが実装する必要なく、包含されました;
#[derive(HelloMacro)]
がトレイトの実装を追加したのです。
マクロの未来
将来的にRustは、宣言的マクロとプロシージャルマクロを拡張するでしょう。macro
キーワードでより良い宣言的マクロシステムを使用し、
derive
だけよりもよりパワフルな作業のより多くの種類のプロシージャルマクロを追加するでしょう。
この本の出版時点ではこれらのシステムはまだ開発中です; 最新の情報は、オンラインのRustドキュメンテーションをお調べください。