マクロ: 体系的説明
本章ではRustの宣言的なMacro-By-Exampleのシステムについて、全体の概略を説明することによって紹介していきます。 まず構成要素の文法と鍵となるパーツについて説明したあと、最低限知っておくべきより全般的な情報を補足します。
macro_rules!
以上のことを念頭に置いて、macro_rules!
自体の説明に入ります。
先述したとおり、macro_rules!
はそれ自体が構文拡張のひとつであり、これはmacro_rules!
が技術的にはRustの文法に含まれないということを意味します。
macro_rules! $name {
$rule0 ;
$rule1 ;
// …
$ruleN ;
}
少なくとも1つのルールが必要で、最後のルールの後ろのセミコロンは省略できます。
かっこは、角かっこ([]
)、丸かっこ(()
)、波かっこ({}
)のどれを使ってもかまいません。
それぞれの「ルール」は次のような見た目になります:
($マッチパターン) => {$展開形}
先程と同様かっこの種類はどれでもかまいませんが、マッチパターンは丸かっこで、展開形は波かっこで囲む慣習があります。
ここでどのかっこを選ぶかは、MBEマクロの呼び出し方には影響しないということを指摘しておきます。
実際のところ、関数形式マクロはどの種類のかっこを使っても呼び出せます。ただし、{ .. }
や( ... );
(末尾のセミコロンに注意)という形の呼び出しは、常にアイテムとしてパースされるという点で特別です。
不思議に思われるかもしれませんが、macro_rules!
自体の呼び出しは… 何にも展開されません。
少なくとも、抽象構文木には何の変化も起きません。むしろ、その呼び出しはMBEマクロを登録するためにコンパイラ内部のデータ構造を操作します。
そういうわけで、技術的には macro_rules!
は空の展開が許される全ての場所で利用できます。
マッチング
macro_rules!
マクロが呼び出されると、macro_rules!
のインタプリタはルールを1つ1つ定義順に調べていきます。
各ルールについて、インタプリタは入力トークン木の内容とルールの「マッチパターン」のマッチングを試みます。
マッチパターンが入力の全体に一致している場合のみ、マッチしたとみなされます。
入力とマッチパターンがマッチしたら、マクロの呼び出しを「展開形」で置き換えます。マッチしなければ、次のルールを試します。 どのルールにもマッチしなかった場合、展開はエラーとともに失敗します。
最も単純な例は空のマッチパターンです:
macro_rules! four {
() => { 1 + 3 };
}
これは、入力が同様に空のとき、かつそのときに限りマッチします(例: four!()
, four![]
, four!{}
)。
関数形式マクロを呼び出す際に用いるグルーピング用トークン1はマッチ対象ではないことに注意してください。実際のところ、それらのトークンはそもそも入力として渡されません。
つまり、上記のマクロをfour![]
のように呼び出しても、やはりマッチします。
入力トークン木の中身だけが考慮されるということです。
訳注: マクロ呼び出しにおける一番外側のかっこのこと。
マッチパターンには生の(literal)トークン木を含めることもできます。生のトークン木にマッチさせるには、入力が厳密に一致している必要があります。
これを行うには、単にトークン木をそのまま書けばよいです。
例えば、4 fn ['spang "whammo"] @_@
という並びにマッチさせたければ、次のように書けます:
macro_rules! gibberish {
(4 fn ['spang "whammo"] @_@) => {...};
}
トークン木は、書けるものならなんでも使えます。
メタ変数 (Metavariables)
マッチパターンはキャプチャを含むこともできます。 キャプチャは、入力に対する概括的な文法上のカテゴリに基づくマッチングを可能にします。その結果はメタ変数(metavariable)に捕捉され、出力においてメタ変数を捕捉された中身に置換することができます。
キャプチャはドル記号($
)に続けて識別子、コロン(:
)、そしてフラグメント指定子(fragment-specifier)とも呼ばれるキャプチャの種類という形で書きます。フラグメント指定子は以下のどれかでなければなりません:
block
: ブロック (波かっこで囲まれた、文や式からなるかたまり)expr
: 式 (expression)ident
: 識別子 (予約語を含む)item
: アイテム (関数、構造体、モジュール、implなど)lifetime
: ライフタイム (例:'foo
,'static
, ...)literal
: リテラル (例:"Hello World!"
,3.14
,'🦀'
, ...)meta
: メタアイテム。#[...]
や#![...]
といった属性の中にくるものpat
: パターンpath
: パス (path) (例:foo
,::std::mem::replace
,transmute::<_, int>
, …)stmt
: 文 (statement)tt
: 単一のトークン木ty
: 型vis
: 可視性修飾子(visibility qualifier)。空でもよい (例:pub
,pub(in crate)
, ...)
フラグメント指定子のより詳しい説明を見るには、フラグメント指定子の章を参照してください。
例えば、以下のmacro_rules!
マクロは、入力を式として$e
という名前のメタ変数にキャプチャします:
macro_rules! one_expression {
($e:expr) => {...};
}
これらのメタ変数はRustコンパイラのパーサを活用しており、そのため常に「正しい」ことが保証されています。
expr
のメタ変数は常に、コンパイル時のRustバーションにおける完全かつ妥当な式をキャプチャします。
生のトークン木とメタ変数を組み合わせて使うこともできますが、一定の制限(メタ変数と展開・再考で説明します)があります。
メタ変数を参照するには単に$name
と書きます。変数の型はマッチパターンの中で指定済みのため書く必要はありません。例えば次のようになります:
macro_rules! times_five {
($e:expr) => { 5 * $e };
}
マクロの展開と同様に、メタ変数は完全な抽象構文木のノードとして置換されます。
これは、たとえメタ変数$e
にどんなトークンが捕捉されているとしても、単一の完全な式として解釈されるということを意味します。
一つのマッチパターンに複数のメタ変数を書くことができます:
macro_rules! multiply_add {
($a:expr, $b:expr, $c:expr) => { $a * ($b + $c) };
}
また、一つのメタ変数を展開形の中で何度でも使うことができます:
macro_rules! discard {
($e:expr) => {};
}
macro_rules! repeat {
($e:expr) => { $e; $e; $e; };
}
さらに、$crate
という特別なメタ変数があり、現在のクレートを参照するのに使えます。
繰り返し
マッチパターンは「繰り返し」を含むことができます。これによりトークンの列へのマッチングが可能になります。
繰り返しは $ ( ... ) sep rep
という一般形式を持ちます。
-
$
は文字通りのドル記号のトークン。 -
( ... )
は繰り返し対象となるかっこで括られたマッチパターン。 -
sep
は省略可能な区切りのトークン。区切り文字(delimiter)2や繰り返し演算子は使えない。よく使われるのは,
や;
。 -
rep
は必須の繰り返し演算子。現時点で以下のものが使える:?
: 最大1回の繰り返し*
: 0回以上の繰り返し+
: 1回以上の繰り返し
?
は最大1回の出現を表すので、区切りトークンと一緒に使うことはできない。
訳注: ここでの区切り文字(delimiter)は、いわゆる「かっこ」に使われる文字: ( ) [ ] { }
を指す。
繰り返しの中では、生のトークン木、メタ変数、任意にネストした他の繰り返しを含む、任意の正当なマッチパターンを使えます。
繰り返しは展開形の中でも同じ構文を用います。繰り返されるメタ変数は展開形の中の繰り返しの内部からしかアクセスできません。
例えば、以下は各要素を文字列にフォーマットするMBEマクロです。 0個以上のコンマ区切りの式にマッチし、ベクタを生成する式に展開されます。
macro_rules! vec_strs { ( // Start a repetition: // 繰り返しの開始: $( // Each repeat must contain an expression... // 各繰り返しは式を含み... $element:expr ) // ...separated by commas... // ...コンマで区切られ... , // ...zero or more times. // ...0回以上繰り返される * ) => { // Enclose the expansion in a block so that we can use // multiple statements. // 複数の式を使うため、展開形をブロックで囲む { let mut v = Vec::new(); // Start a repetition: // 繰り返しの開始: $( // Each repeat will contain the following statement, with // $element replaced with the corresponding expression. // 各繰り返しは次のような文に展開される。ここで $element は対応する式に置き換えられる v.push(format!("{}", $element)); )* v } }; } fn main() { let s = vec_strs![1, "a", true, 3.14159f32]; assert_eq!(s, &["1", "a", "true", "3.14159"]); }
すべてのメタ変数が同じ回数だけ繰り返される場合に限り、一つの繰り返しの中で複数のメタ変数を繰り返すことができます。 よって、次のようなマクロ呼び出しは動作します:
#![allow(unused)] fn main() { macro_rules! repeat_two { ($($i:ident)*, $($i2:ident)*) => { $( let $i: (); let $i2: (); )* } } repeat_two!( a b c d e f, u v w x y z ); }
しかしこれは動作しません:
#![allow(unused)] fn main() { macro_rules! repeat_two { ($($i:ident)*, $($i2:ident)*) => { $( let $i: (); let $i2: (); )* } } repeat_two!( a b c d e f, x y z ); }
これは次のようなエラーで失敗します。
error: meta-variable `i` repeats 6 times, but `i2` repeats 3 times
--> src/main.rs:6:10
|
6 | $( let $i: (); let $i2: (); )*
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
メタ変数式(Metavariable Expressions)
RFC: rfcs#1584
トラッキングIssue: rust#83527
機能フラグ:#![feature(macro_metavar_expr)]
マクロの書き換え先(transcriber)には、メタ変数式(metavariable expressions)と呼ばれるものを書くことができます。
メタ変数式は書き換え先に対し、他の方法では簡単に得られないメタ変数に関する情報を提供します。
$$
式という例外を除き、メタ変数式は $ { op(...) }
という一般形式を持ちます。
現時点で $$
式を除く全てのメタ変数式が繰り返しに対応しています。
以下のような式が利用できます。ここで、ident
は束縛済みメタ変数の名前、depth
は整数リテラルです:
${count(ident)}
: 最も内側の繰り返しにおける$ident
の繰り返し回数。${count(ident, 0)}
と等価。${count(ident, depth)}
:depth
の深さにある繰り返しにおける$ident
の繰り返し回数。${index()}
: 最も内側の繰り返しにおける、現在の繰り返しインデックス。${index(0)}
と等価。${index(depth)}
: 深さdepth
にある繰り返しにおける、現在の繰り返しインデックス。深さは内側から外側に向かって数える。${length()}
: 最も内側の繰り返しが繰り返される回数。${length(0)}
と等価。${length(depth)}
: 深さdepth
にある繰り返しが繰り返される回数。深さは内側から外側に向かって数える。${ignore(ident)}
: 繰り返しのために$ident
を束縛するが、何にも展開しない。$$
: 単一の$
記号に展開される。実質的に、$
が書き換わらないようエスケープする。
メタ変数式の完全な文法定義については、RustリファレンスのMacros By Exampleの章を参照してください。