付録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 } }; } #}
リストD-1: vec!マクロ定義の簡略化されたバージョン
標準ライブラリの
vec!マクロの実際の定義は、予め正確なメモリ量を確保するコードを含みます。 そのコードは、ここでは簡略化のために含まない最適化です。
#[macro_export]注釈は、マクロを定義しているクレートがインポートされる度にこのマクロが利用可能になるべきということを示しています。
この注釈がなければ、このクレートに依存する誰かが#[macro_use]注釈を使用していても、
このマクロはスコープに導入されないでしょう。
それから、macro_rules!でマクロ定義と定義しているマクロの名前をビックリマークなしで始めています。
名前はこの場合vecであり、マクロ定義の本体を意味する波括弧が続いています。
vec!本体の構造は、match式の構造に類似しています。ここではパターン( $( $x:expr ),* )の1つのアーム、
=>とこのパターンに紐付くコードのブロックが続きます。パターンが合致すれば、紐付いたコードのブロックが発されます。
これがこのマクロの唯一のパターンであることを踏まえると、合致する合法的な方法は一つしかありません;
それ以外は、全部エラーになるでしょう。より複雑なマクロには、2つ以上のアームがあるでしょう。
マクロ定義で合法なパターン記法は、第18章で講義したパターン記法とは異なります。というのも、 マクロのパターンは値ではなく、Rustコードの構造に対してマッチされるからです。リストD-1のパターンの部品がどんな意味か見ていきましょう; マクロパターン記法全てはthe referenceをご覧ください。
まず、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などを調べてください。
独自の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はこのトレイトが定義されている型の名前です。言い換えると、他のプログラマに我々のクレートを使用して、
リスト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();
}
リストD-2: 我々のプロシージャルマクロを使用した時にクレートの使用者が書けるようになるコード
このコードは完成したら、Hello, Macro! My name is 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: Rustコードを処理するためにほとんどのプロシージャルマクロクレートに必要になるコード
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ドキュメンテーションをお調べください。