マクロ
本全体を通じてprintln!
のようなマクロを使用してきましたが、マクロがなんなのかや、
どう動いているのかということは完全には探究していませんでした。
Rustにおいて、マクロという用語はある機能の集合のことを指します:macro_rules!
を使った 宣言的 (declarative) マクロと、3種類の 手続き的 (procedural) マクロ:
- 構造体とenumに
derive
属性を使ったときに追加されるコードを指定する、カスタムの#[derive]
マクロ - 任意の要素に使えるカスタムの属性を定義する、属性風のマクロ
- 関数のように見えるが、引数として指定されたトークンに対して作用する関数風のマクロ
です。
それぞれについて一つずつ話していきますが、その前にまず、どうして関数がすでにあるのにマクロなんてものが必要なのか見てみましょう。
マクロと関数の違い
基本的に、マクロは、他のコードを記述するコードを書く術であり、これはメタプログラミングとして知られています。
付録Cで、derive
属性を議論し、これは、色々なトレイトの実装を生成してくれるのでした。
また、本を通してprintln!
やvec!
マクロを使用してきました。これらのマクロは全て、展開され、
手で書いたよりも多くのコードを生成します。
メタプログラミングは、書いて管理しなければならないコード量を減らすのに有用で、これは、関数の役目の一つでもあります。 ですが、マクロには関数にはない追加の力があります。
関数シグニチャは、関数の引数の数と型を宣言しなければなりません。一方、マクロは可変長の引数を取れます:
println!("hello")
のように1引数で呼んだり、println!("hello {}", name)
のように2引数で呼んだりできるのです。
また、マクロは、コンパイラがコードの意味を解釈する前に展開されるので、例えば、
与えられた型にトレイトを実装できます。関数ではできません。何故なら、関数は実行時に呼ばれ、
トレイトはコンパイル時に実装される必要があるからです。
関数ではなくマクロを実装する欠点は、Rustコードを記述するRustコードを書いているので、 関数定義よりもマクロ定義は複雑になることです。この間接性のために、マクロ定義は一般的に、 関数定義よりも、読みにくく、わかりにくく、管理しづらいです。
マクロと関数にはもう一つ、重要な違いがあります: ファイル内で呼び出す前にマクロは定義したりスコープに導入しなければなりませんが、 一方で関数はどこにでも定義でき、どこでも呼び出せます。
一般的なメタプログラミングのためにmacro_rules!
で宣言的なマクロ
Rustにおいて、最もよく使用される形態のマクロは、宣言的マクロです。これらは時として、
例によるマクロ、macro_rules!
マクロ、あるいはただ単にマクロとも称されます。
核となるのは、宣言的マクロは、Rustのmatch
式に似た何かを書けるということです。第6章で議論したように、
match
式は、式を取り、式の結果の値をパターンと比較し、それからマッチしたパターンに紐づいたコードを実行する制御構造です。
マクロも、あるコードと紐付けられたパターンと値を比較します。ここで、値とは
マクロに渡されたリテラルのRustのソースコードそのもののこと。パターンがそのソースコードの構造と比較されます。
各パターンに紐づいたコードは、それがマッチしたときに、マクロに渡されたコードを置き換えます。これは全て、コンパイル時に起きます。
マクロを定義するには、macro_rules!
構文を使用します。vec!
マクロが定義されている方法を見て、
macro_rules!
を使用する方法を探究しましょう。vec!
マクロを使用して特定の値で新しいベクタを生成する方法は、
第8章で講義しました。例えば、以下のマクロは、3つの整数を持つ新しいベクタを生成します:
#![allow(unused)] fn main() { let v: Vec<u32> = vec![1, 2, 3]; }
また、vec!
マクロを使用して2整数のベクタや、5つの文字列スライスのベクタなども生成できます。
同じことを関数を使って行うことはできません。予め、値の数や型がわかっていないからです。
リスト19-28で些か簡略化されたvec!
マクロの定義を見かけましょう。
ファイル名: src/lib.rs
#![allow(unused)] 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_rules!
でマクロ定義と定義しているマクロの名前をビックリマークなしで始めています。
名前はこの場合vec
であり、マクロ定義の本体を意味する波括弧が続いています。
vec!
本体の構造は、match
式の構造に類似しています。ここではパターン( $( $x:expr ),* )
の1つのアーム、
=>
とこのパターンに紐づくコードのブロックが続きます。パターンが合致すれば、紐づいたコードのブロックが発されます。
これがこのマクロの唯一のパターンであることを踏まえると、合致する合法的な方法は一つしかありません;
それ以外は、全部エラーになるでしょう。より複雑なマクロには、2つ以上のアームがあるでしょう。
マクロ定義で合法なパターン記法は、第18章で講義したパターン記法とは異なります。というのも、 マクロのパターンは値ではなく、Rustコードの構造に対してマッチされるからです。リスト19-28のパターンの部品がどんな意味か見ていきましょう; マクロパターン記法全ては参考文献をご覧ください。
まず、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
}
任意の型のあらゆる数の引数を取り、指定した要素を含むベクタを生成するコードを生成できるマクロを定義しました。
macro_rules!
には、いくつかの奇妙なコーナーケースがあります。
将来、Rustには別種の宣言的マクロが登場する予定です。これは、同じように働くけれども、それらのコーナーケースのうちいくらかを修正します。
そのアップデート以降、macro_rules!
は事実上非推奨 (deprecated) となる予定です。
この事実と、ほとんどのRustプログラマーはマクロを書くよりも使うことが多いということを考えて、macro_rules!
についてはこれ以上語らないことにします。
もしマクロの書き方についてもっと知りたければ、オンラインのドキュメントや、“The Little Book of Rust Macros”のようなその他のリソースを参照してください。
属性からコードを生成する手続き的マクロ
2つ目のマクロの形は、手続き的マクロと呼ばれ、より関数のように働きます(そして一種の手続きです)。 宣言的マクロがパターンマッチングを行い、マッチしたコードを他のコードで置き換えていたのとは違い、 手続き的マクロは、コードを入力として受け取り、そのコードに対して作用し、出力としてコードを生成します。
3種の手続き的マクロ (カスタムのderiveマクロ, 属性風マクロ、関数風マクロ)はみな同じような挙動をします。
手続き的マクロを作る際は、その定義はそれ専用の特殊なクレート内に置かれる必要があります。
これは複雑な技術的理由によるものであり、将来的には解消したいです。
手続き的マクロを使うとListing 19-29のコードのようになります。some_attribute
がそのマクロを使うためのプレースホールダーです。
ファイル名: src/lib.rs
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
手続き的マクロを定義する関数はTokenStream
を入力として受け取り、TokenStream
を出力として生成します。
TokenStream
型はRustに内蔵されているproc_macro
クレートで定義されており、トークンの列を表します。
ここがマクロの一番重要なところなのですが、マクロが作用するソースコードは、入力のTokenStream
へと変換され、マクロが生成するコードが出力のTokenStream
なのです。
この関数には属性もつけられていますが、これはどの種類の手続き的マクロを作っているのかを指定します。
同じクレート内に複数の種類の手続き的マクロを持つことも可能です。
様々な種類の手続き的マクロを見てみましょう。カスタムのderiveマクロから始めて、そのあと他の種類との小さな相違点を説明します。
カスタムのderive
マクロの書き方
hello_macro
という名前のクレートを作成してみましょう。
このクレートは、hello_macro
という関連関数が1つあるHelloMacro
というトレイトを定義します。
クレートの使用者に使用者の型にHelloMacro
トレイトを実装することを強制するのではなく、
使用者が型を#[derive(HelloMacro)]
で注釈してhello_macro
関数の既定の実装を得られるように、
手続き的マクロを提供します。既定の実装は、Hello, Macro! My name is TypeName!
(訳注
: こんにちは、マクロ!僕の名前は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();
}
このコードは完成したら、Hello, Macro! My name is Pancakes!
(Pancakes
: ホットケーキ)と出力します。最初の手順は、
新しいライブラリクレートを作成することです。このように:
$ cargo new hello_macro --lib
次にHelloMacro
トレイトと関連関数を定義します:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { 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にはリフレクションの能力がないので、型の名前を実行時に検索することができないのです。
コンパイル時にコード生成するマクロが必要です。
注釈: リフレクションとは、実行時に型名や関数の中身などを取得する機能のことです。 言語によって提供されていたりいなかったりしますが、実行時にメタデータがないと取得できないので、 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 = "1.0"
quote = "1.0"
手続き的マクロの定義を開始するために、hello_macro_derive
クレートのsrc/lib.rsファイルにリスト19-31のコードを配置してください。
impl_hello_macro
関数の定義を追加するまでこのコードはコンパイルできないことに注意してください。
ファイル名: hello_macro_derive/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 操作可能な構文木としてのRustコードの表現を構築する
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// トレイトの実装内容を構築
// Build the trait implementation
impl_hello_macro(&ast)
}
TokenStream
をパースする役割を持つhello_macro_derive
関数と、構文木を変換する役割を持つimpl_hello_macro
関数にコードを分割したことに注目してください:これにより手続き的マクロを書くのがより簡単になります。
外側の関数(今回だとhello_macro_derive
)のコードは、あなたが見かけたり作ったりするであろうほとんどすべての手続き的マクロのクレートで同じです。
内側の関数(今回だとimpl_hello_macro
)の内部に書き込まれるコードは、手続き的マクロの目的によって異なってくるでしょう。
3つの新しいクレートを導入しました: proc_macro
、syn
、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
を指定したからです;
これは、ほとんどの手続き的マクロが倣う慣習です。
この関数はまず、TokenStream
からのinput
をデータ構造に変換し、解釈したり操作したりできるようにします。
ここでsyn
が登場します。
syn
のparse
関数はTokenStream
を受け取り、パースされたRustのコードを表現するDeriveInput
構造体を返します。
Listing 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
)
}
)
}
この構造体のフィールドは、構文解析したRustコードがPancakes
というident
(識別子、つまり名前)のユニット構造体であることを示しています。
この構造体にはRustコードのあらゆる部分を記述するフィールドがもっと多くあります;
DeriveInput
のsyn
ドキュメンテーションで詳細を確認してください。
まもなくimpl_hello_macro
関数を定義し、そこにインクルードしたい新しいRustコードを構築します。
でもその前に、私達のderiveマクロのための出力もまたTokenStream
であることに注目してください。
返されたTokenStream
をクレートの使用者が書いたコードに追加しているので、クレートをコンパイルすると、
我々が修正したTokenStream
で提供している追加の機能を得られます。
ここで、unwrap
を呼び出すことで、syn::parse
関数が失敗したときにhello_macro_derive
関数をパニックさせていることにお気付きかもしれません。
エラー時にパニックするのは、手続き的マクロコードでは必要なことです。何故なら、
proc_macro_derive
関数は、手続き的マクロのAPIに従うために、Result
ではなく
TokenStream
を返さなければならないからです。この例については、unwrap
を使用して簡略化することを選択しました;
プロダクションコードでは、panic!
かexpect
を使用して何が間違っていたのかより具体的なエラーメッセージを提供すべきです。
今や、TokenStream
からの注釈されたRustコードをDeriveInput
インスタンスに変換するコードができたので、
Listing 19-33のように、注釈された型にHelloMacro
トレイトを実装するコードを生成しましょう:
ファイル名: hello_macro_derive/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
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()
}
ast.ident
を使って、注釈された型の名前(識別子)を含むIdent
構造体インスタンスを得ています。
Listing 19-32の構造体を見ると、impl_hello_macro
関数をListing 19-30のコードに実行したときに私達の得るident
は、フィールドident
の値として"Pancakes"
を持つだろうとわかります。
従って、Listing 19-33における変数name
は構造体Ident
のインスタンスをもちます。このインスタンスは、printされた時は文字列"Pancakes"
、即ちListing 19-30の構造体の名前となります。
quote!
マクロを使うことで、私達が返したいRustコードを定義することができます。
ただ、コンパイラが期待しているものはquote!
マクロの実行結果とはちょっと違うものです。なので、TokenStream
に変換してやる必要があります。
マクロの出力する直接表現を受け取り、必要とされているTokenStream
型の値を返すinto
メソッドを呼ぶことでこれを行います。
このマクロはまた、非常にかっこいいテンプレート機構も提供してくれます; #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
の両方で成功するはずです。
これらのクレートをリスト19-30のコードにフックして、手続き的マクロが動くところを確認しましょう!
cargo new pancakes
であなたのプロジェクトのディレクトリ(訳注:これまでに作った2つのクレート内ではないということ)に新しいバイナリプロジェクトを作成してください。
hello_macro
とhello_macro_derive
を依存としてpancakes
クレートのCargo.tomlに追加する必要があります。
自分のバージョンのhello_macro
とhello_macro_derive
を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
は構造体とenumにしか使えませんでしたが、属性は関数のような他の要素に対しても使えるのです。
属性風マクロを使った例を以下に示しています:webアプリケーションフレームワークを使っているときに、route
という関数につける属性名があるとします:
#[route(GET, "/")]
fn index() {
この#[route]
属性はそのフレームワークによって手続き的マクロとして定義されたものなのでしょう。
マクロを定義する関数のシグネチャは以下のようになっているでしょう:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
ここで、2つTokenStream
型の引数がありますね。
1つ目は属性の中身:GET, "/"
に対応しており、2つ目は属性が付けられた要素の中身に対応しています。今回だとfn index() {}
と関数の本体の残りですね。
それ以外において、属性風マクロはカスタムのderiveマクロと同じ動きをします:
クレートタイプとしてproc-macro
を使ってクレートを作り、あなたのほしいコードを生成してくれる関数を実装すればよいです!
関数風マクロ
関数風マクロは、関数呼び出しのように見えるマクロを定義します。
これらは、macro_rules!
マクロのように、関数よりフレキシブルです。
たとえば、これらは任意の数の引数を取ることができます。
しかし、一般的なメタプログラミングのためにmacro_rules!
で宣言的なマクロで話したように、macro_rules!
マクロはmatch風の構文を使ってのみ定義できたのでした。
関数風マクロは引数としてTokenStream
をとり、そのTokenStream
を定義に従って操作します。操作には、他の2つの手続き的マクロと同じように、Rustコードが使われます。
例えば、sql!
マクロという関数風マクロで、以下のように呼び出されるものを考えてみましょう:
let sql = sql!(SELECT * FROM posts WHERE id=1);
このマクロは、中に入れられたSQL文をパースし、それが構文的に正しいことを確かめます。これはmacro_rules!
マクロが可能とするよりも遥かに複雑な処理です。
sql!
マクロは以下のように定義することができるでしょう:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
この定義はカスタムのderiveマクロのシグネチャと似ています:カッコの中のトークンを受け取り、生成したいコードを返すのです。
まとめ
ふう! あなたがいま手にしたRustの機能はあまり頻繁に使うものではありませんが、非常に特殊な状況ではその存在を思い出すことになるでしょう。 たくさんの難しいトピックを紹介しましたが、これは、もしあなたがエラー時の推奨メッセージや他の人のコードでそれらに遭遇した時、その概念と文法を理解できるようになっていてほしいからです。 この章を、解決策にたどり着くためのリファレンスとして活用してください。
次は、この本で話してきたすべてのことを実際に使って、もう一つプロジェクトをやってみましょう!