% マクロ、体系的な入門
この章では、Rust の Macro-By-Example システムである macro_rules! を紹介します。実用的な例に基づいて説明するのではなく、このシステムがどのように機能するのかを完全かつ徹底的に説明することを試みます。そのため、これは手順を追って案内されるよりも、システム全体の説明を求めている人を対象としています。
より取り組みやすい高レベルの説明として Rust Book のマクロの章 もあります。また、この本の 実践的な入門 の章では、単一のマクロを実装しながら解説しています。
% 構文拡張
マクロについて説明する前に、それらが構築されている一般的な仕組み、つまり構文拡張について説明しておく価値があります。そのためには、Rust のソースがコンパイラによってどのように処理されるのか、そしてユーザー定義マクロが構築される一般的な仕組みについて説明しなければなりません。
% ソース解析
Rustプログラムのコンパイルにおける最初の段階は、トークン化です。ここでは、ソーステキストがトークン(すなわち 分割不可能な字句単位。プログラミング言語における「単語」に相当するもの)の列へと変換されます。Rustには、次のようなさまざまな種類のトークンがあります。
- 識別子:
foo,Bambous,self,we_can_dance,LaCaravane, … - 整数:
42,72u32,0_______0, … - キーワード:
_,fn,self,match,yield,macro, … - ライフタイム:
'a,'b,'a_rare_long_lifetime_name, … - 文字列:
"","Leicester",r##"venezuelan beaver"##, … - シンボル:
[,:,::,->,@,<-, …
…などです。上記について、いくつか注意すべき点があります。まず、selfは識別子でもあり、キーワードでもあります。ほとんどすべての場合において、selfはキーワードですが、識別子として扱われることも可能であり、これについては後ほど(多くの罵倒とともに)出てきます。次に、キーワードの一覧には、yieldやmacroのような、言語には実際には存在しないものの、コンパイラによって解析される不審な項目が含まれています。これらは将来の利用のために予約されています。第三に、シンボルの一覧にも、その言語で使われていない項目が含まれています。<-の場合、それは痕跡的なものです。文法からは削除されましたが、レキサーからは削除されていません。最後の点として、::は独立したトークンであり、単に2つの隣接した:トークンではないことに注意してください。Rust 1.2時点では、Rustにおけるすべての複数文字シンボルトークンについて同じことが当てはまります。1
比較対象として、一部の言語では、この段階にマクロ層がありますが、Rustにはありません。たとえば、C/C++のマクロは、この時点で実質的に処理されます。2 これが、次のコードが機能する理由です。3
#define SUB void
#define BEGIN {
#define END }
SUB main() BEGIN
printf("Oh, the horror!\n");
END
次の段階は解析であり、トークンのストリームが抽象構文木(AST)に変換されます。これには、プログラムの構文構造をメモリ上に構築することが含まれます。たとえば、トークン列1 + 2は、次のものに相当する形へと変換されます。
┌─────────┐ ┌─────────┐
│ BinOp │ ┌╴│ LitInt │
│ op: Add │ │ │ val: 1 │
│ lhs: ◌ │╶┘ └─────────┘
│ rhs: ◌ │╶┐ ┌─────────┐
└─────────┘ └╴│ LitInt │
│ val: 2 │
└─────────┘
ASTにはプログラム全体の構造が含まれますが、それは純粋に字句的な情報に基づいています。たとえば、コンパイラは特定の式が「a」という名前の変数を参照していることを知っているかもしれませんが、この段階では、「a」が何であるか、あるいはそれがどこから来たのかさえ知る手段がありません。
マクロが処理されるのは、ASTが構築された後です。しかし、それについて議論する前に、トークンツリーについて話す必要があります。
トークンツリー
トークンツリーは、トークンとASTの中間に位置するものです。第一に、ほとんどすべてのトークンはトークンツリーでもあります。より具体的には、それらは葉です。トークンツリーの葉になり得るものはもう1種類ありますが、それについては後で戻ってきます。
葉ではない唯一の基本トークンは、「グループ化」トークン、すなわち(...)、[...]、{...}です。これら3つはトークンツリーの内部ノードであり、それらに構造を与えるものです。具体例を挙げると、このトークン列は次のようになります。
a + b + (c + d[0]) + e
次のトークンツリーへと解析されます。
«a» «+» «b» «+» «( )» «+» «e»
╭────────┴──────────╮
«c» «+» «d» «[ ]»
╭─┴─╮
«0»
これは、その式が生成するASTとは何の関係もないことに注意してください。単一のルートノードではなく、ルートレベルには9個のトークンツリーがあります。参考までに、ASTは次のようになります。
┌─────────┐
│ BinOp │
│ op: Add │
┌╴│ lhs: ◌ │
┌─────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ Var │╶┘ └─────────┘ └╴│ BinOp │
│ name: a │ │ op: Add │
└─────────┘ ┌╴│ lhs: ◌ │
┌─────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ Var │╶┘ └─────────┘ └╴│ BinOp │
│ name: b │ │ op: Add │
└─────────┘ ┌╴│ lhs: ◌ │
┌─────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ BinOp │╶┘ └─────────┘ └╴│ Var │
│ op: Add │ │ name: e │
┌╴│ lhs: ◌ │ └─────────┘
┌─────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ Var │╶┘ └─────────┘ └╴│ Index │
│ name: c │ ┌╴│ arr: ◌ │
└─────────┘ ┌─────────┐ │ │ ind: ◌ │╶┐ ┌─────────┐
│ Var │╶┘ └─────────┘ └╴│ LitInt │
│ name: d │ │ val: 0 │
└─────────┘ └─────────┘
ASTとトークンツリーの違いを理解することは重要です。マクロを書くときには、両方を別々のものとして扱う必要があります。
これについて注意すべきもう1つの側面があります。対応のない丸括弧、角括弧、中括弧を持つことは不可能です。また、トークンツリー内でグループが誤ってネストされることも不可能です。
-
@には目的がありますが、ほとんどの人はそれを完全に忘れているようです。これはパターン内で、パターンの非終端部分を名前に束縛するために使われます。この章を校正していたRustコアチームのメンバーでさえ、まさにこの節を話題にしていたにもかかわらず、@に目的があることを覚えていませんでした。かわいそうな、かわいそうな渦巻きくん。 ↩ -
実際には、CプリプロセッサはC自体とは異なる字句構造を使用しますが、その違いは大まかには重要ではありません。 ↩
-
それが機能すべきかどうかは、まったく別の問題です。 ↩
% AST 内のマクロ
前述のとおり、Rust におけるマクロ処理は AST の構築の後に行われます。したがって、マクロを呼び出すために使われる構文は、言語構文の正式な一部でなければなりません。実際、Rust の構文の一部である「構文拡張」形式はいくつか存在します。具体的には、次の形式です(例として):
# [ $arg ]; 例#[derive(Clone)],#[no_mangle], …# ! [ $arg ]; 例#![allow(dead_code)],#![crate_name="blang"], …$name ! $arg; 例println!("Hi!"),concat!("a", "b"), …$name ! $arg0 $arg1; 例macro_rules! dummy { () => {}; }.
最初の 2 つは「属性」であり、言語固有の構成要素(ユーザー定義型に C 互換 ABI を要求するために使われる #[repr(C)] など)と構文拡張(#[derive(Clone)] など)の両方で共有されています。現在、これらの形式を使うマクロを定義する方法はありません。
3 つ目が、私たちの関心対象です。これはマクロで使用できる形式です。この形式はマクロに限定されているわけではない点に注意してください。これは汎用的な構文拡張形式です。たとえば、format! はマクロですが、format! を実装するために使われる format_args! はマクロではありません。
4 つ目は本質的にはバリエーションであり、マクロでは使用できません。実際、この形式が少しでも使われる唯一のケースは macro_rules! であり、これについても後で再び取り上げます。
3 つ目の形式($name ! $arg)以外をすべて無視すると、疑問は次のようになります。Rust パーサーは、あらゆる可能な構文拡張について $arg がどのような形をしているのかを、どのように知るのでしょうか。答えは、知る必要がないということです。代わりに、構文拡張呼び出しの引数は単一のトークンツリーです。より具体的には、単一の非リーフトークンツリー、つまり (...)、[...]、または {...} です。このことを知っていれば、パーサーが次のすべての呼び出し形式をどのように理解できるのかが明らかになるはずです。
bitflags! {
flags Color: u8 {
const RED = 0b0001,
const GREEN = 0b0010,
const BLUE = 0b0100,
const BRIGHT = 0b1000,
}
}
lazy_static! {
static ref FIB_100: u32 = {
fn fib(a: u32) -> u32 {
match a {
0 => 0,
1 => 1,
a => fib(a-1) + fib(a-2)
}
}
fib(100)
};
}
fn main() {
let colors = vec![RED, GREEN, BLUE];
println!("Hello, World!");
}
上記の呼び出しはさまざまな種類の Rust コードを含んでいるように見えるかもしれませんが、パーサーは単に意味のないトークンツリーの集まりとして見ています。これをより明確にするために、これらすべての構文上の「ブラックボックス」を ⬚ に置き換えると、次のようになります。
bitflags! ⬚
lazy_static! ⬚
fn main() {
let colors = vec! ⬚;
println! ⬚;
}
繰り返しますが、パーサーは ⬚ について何も仮定しません。その中に含まれるトークンは記憶しますが、それらを理解しようとはしません。
重要なポイントは次のとおりです。
- Rust には複数種類の構文拡張があります。ここでは
macro_rules!構文によって定義されるマクロについてのみ扱います。 $name! $argという形式のものを見たからといって、それが実際にマクロであるとは限りません。別の種類の構文拡張かもしれません。- すべてのマクロへの入力は、単一の非リーフトークンツリーです。
- マクロ(正確には、構文拡張一般)は、抽象構文木の一部としてパースされます。
余談: 最初の点により、以下で述べることの一部(次の段落を含む)は構文拡張一般に当てはまります。1
最後の点が最も重要です。なぜなら、これには重大な意味合いがあるからです。マクロは AST にパースされるため、明示的にサポートされている位置にのみ出現できます。具体的には、マクロは次のものの代わりに出現できます。
- パターン
- 文
- 式
- アイテム
implアイテム
この一覧に含まれていないものの例:
- 識別子
- Match アーム
- 構造体フィールド
- 型2
最初の一覧にない位置でマクロを使う方法は、まったく、絶対にありません。
-
これは、「syntax extension」よりも「macro」のほうがはるかに素早く簡単に入力できるため、かなり都合がよいです。 ↩
-
型マクロは、不安定版 Rust で
#![feature(type_macros)]を指定すると利用できます。Issue #27336 を参照してください。 ↩
% 展開
展開は比較的単純な処理です。AST の構築が終わった後、かつコンパイラーがプログラムの意味的な理解を構築し始める前のどこかの時点で、コンパイラーはすべてのマクロを展開します。
これには、AST を走査し、マクロ呼び出しを見つけ、それらをその展開結果で置き換えることが含まれます。非マクロの構文拡張の場合、これがどのように行われるかは、その特定の構文拡張次第です。とはいえ、構文拡張は、その展開が完了すると、マクロが通るプロセスとまったく同じプロセスを通ります。
コンパイラーが構文拡張を実行すると、その結果は、コンテキストに基づいて、限られた構文要素のいずれかとしてパース可能であることを期待します。たとえば、モジュールスコープでマクロを呼び出した場合、コンパイラーはその結果をアイテムを表す AST ノードへとパースします。式位置でマクロを呼び出した場合、コンパイラーはその結果を式の AST ノードへとパースします。
実際には、構文拡張の結果を次のいずれかに変換できます。
- 式、
- パターン、
- 0 個以上のアイテム、
- 0 個以上の
implアイテム、または - 0 個以上の文。
言い換えると、マクロを呼び出せる場所によって、その結果が何として解釈されるかが決まります。
コンパイラーはこの AST ノードを受け取り、マクロの呼び出しノードを出力ノードで完全に置き換えます。これは構造上の操作であり、テキスト上の操作ではありません!
たとえば、次を考えてみましょう。
let eight = 2 * four!();
この部分的な AST は次のように可視化できます。
┌─────────────┐
│ Let │
│ name: eight │ ┌─────────┐
│ init: ◌ │╶─╴│ BinOp │
└─────────────┘ │ op: Mul │
┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌────────────┐
│ LitInt │╶┘ └─────────┘ └╴│ Macro │
│ val: 2 │ │ name: four │
└────────┘ │ body: () │
└────────────┘
コンテキストから、four!() は式へ展開される必要があります(初期化子は式であることしかできません)。したがって、実際の展開結果が何であれ、それは完全な式として解釈されます。この場合、four! は式 1 + 3 へ展開されるように定義されていると仮定します。その結果、この呼び出しを展開すると AST は次のように変化します。
┌─────────────┐
│ Let │
│ name: eight │ ┌─────────┐
│ init: ◌ │╶─╴│ BinOp │
└─────────────┘ │ op: Mul │
┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ LitInt │╶┘ └─────────┘ └╴│ BinOp │
│ val: 2 │ │ op: Add │
└────────┘ ┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌────────┐
│ LitInt │╶┘ └─────────┘ └╴│ LitInt │
│ val: 1 │ │ val: 3 │
└────────┘ └────────┘
これは次のように書き表せます。
let eight = 2 * (1 + 3);
展開結果には含まれていなかったにもかかわらず、括弧を追加している点に注意してください。コンパイラーは常に、マクロの展開結果を単なるトークン列としてではなく、完全な AST ノードとして扱うことを思い出してください。別の言い方をすれば、複雑な式を明示的に括弧で囲まなかったとしても、コンパイラーがその結果を「誤解釈」したり、評価順序を変えたりすることはありません。
マクロ展開が AST ノードとして扱われることを理解するのは重要です。この設計にはさらに 2 つの意味があるためです。
- 呼び出し位置の数が限られていることに加えて、マクロはその位置でパーサーが期待する種類の AST ノードにしか展開できません。
- 上記の結果として、マクロは不完全な構造や構文的に不正な構造へ展開することは絶対にできません。
展開について、さらにもう 1 つ注意すべきことがあります。構文拡張が、別の構文拡張呼び出しを含むものへ展開された場合に何が起こるかです。たとえば、four! の別の定義を考えてみましょう。これが 1 + three!() へ展開されたらどうなるでしょうか?
let x = four!();
これは次へ展開されます。
let x = 1 + three!();
これは、コンパイラーが展開結果に追加のマクロ呼び出しがないかを確認し、それらを展開することで解決されます。したがって、2 回目の展開ステップによって、上記は次のようになります。
let x = 1 + 3;
ここでの要点は、展開は「パス」で行われるということです。すべての呼び出しを完全に展開するために必要なだけ実行されます。
ただし、厳密にはそうではありません。実際には、コンパイラーは、諦める前に実行するこのような再帰的パスの回数に上限を課しています。これはマクロ再帰制限として知られており、デフォルトは 32 です。32 回目の展開にマクロ呼び出しが含まれている場合、コンパイラーは再帰制限を超えたことを示すエラーで中止します。
この制限は #![recursion_limit="…"] 属性を使用して引き上げることができますが、クレート全体に対して行われる必要があります。一般に、可能な限りマクロをこの制限未満に保つようにすることが推奨されます。
% macro_rules!
以上を踏まえて、macro_rules! そのものを紹介できます。前述のとおり、macro_rules! はそれ自体が構文拡張であり、つまり技術的には Rust の構文の一部ではありません。これは次の形式を使います。
macro_rules! $name {
$rule0 ;
$rule1 ;
// …
$ruleN ;
}
ルールは少なくとも 1 つ必要で、最後のルールの後のセミコロンは省略できます。
各「rule」は次のようになります。
($pattern) => {$expansion}
実際には、丸括弧と波括弧はどの種類のグループでもかまいませんが、パターンを丸括弧で囲み、展開を波括弧で囲むのがある程度慣例です。
気になるかもしれませんが、macro_rules! の呼び出しは……何にも展開されません。少なくとも、AST に現れるものには展開されません。そうではなく、マクロを登録するためにコンパイラー内部の構造を操作します。そのため、技術的には空の展開が有効な任意の位置で macro_rules! を使えます。
マッチング
マクロが呼び出されると、macro_rules! インタープリターはルールを字句上の順序で 1 つずつ処理します。各ルールについて、入力トークンツリーの内容をそのルールの pattern と照合しようとします。パターンは、マッチしたと見なされるためには入力の全体にマッチしなければなりません。
入力がパターンにマッチすると、その呼び出しは expansion に置き換えられます。そうでなければ、次のルールが試されます。すべてのルールがマッチに失敗すると、マクロ展開はエラーで失敗します。
最も単純な例は、空のパターンです。
macro_rules! four {
() => {1 + 3};
}
これは、入力も空である場合に限りマッチします(つまり four!()、four![]、または four!{})。
マクロを呼び出すときに使う特定のグループ化トークンはマッチ対象にならないことに注意してください。つまり、上のマクロは four![] として呼び出しても依然としてマッチします。入力トークンツリーの内容だけが考慮されます。
パターンにはリテラルのトークンツリーも含めることができ、それらは正確にマッチしなければなりません。これは、単に通常どおりトークンツリーを書くことで行います。たとえば、シーケンス 4 fn ['spang "whammo"] @_@ にマッチさせるには、次のようにします。
macro_rules! gibberish {
(4 fn ['spang "whammo"] @_@) => {...};
}
書くことができる任意のトークンツリーを使えます。
キャプチャ
パターンにはキャプチャも含めることができます。これにより、入力をある一般的な文法カテゴリに基づいてマッチさせ、その結果を変数にキャプチャして、出力に代入できるようになります。
キャプチャは、ドル記号($)、識別子、コロン(:)、そして最後にキャプチャの種類を続けて書きます。キャプチャの種類は次のいずれかでなければなりません。
item: 関数、構造体、モジュールなどのアイテムblock: ブロック(つまり、波括弧で囲まれた、文および/または式のブロック)stmt: 文pat: パターンexpr: 式ty: 型ident: 識別子path: パス(例:foo、::std::mem::replace、transmute::<_, int>、…)meta: メタアイテム。#[...]属性および#![...]属性の内側に入るものtt: 単一のトークンツリー
たとえば、入力を式としてキャプチャするマクロは次のとおりです。
macro_rules! one_expression {
($e:expr) => {...};
}
これらのキャプチャは Rust コンパイラーのパーサーを活用し、常に「正しい」ことを保証します。expr キャプチャは、コンパイル対象の Rust のバージョンにおいて、常に完全で有効な式をキャプチャします。
制限内であれば(後述)、リテラルのトークンツリーとキャプチャを混在させることができます。
キャプチャ $name:kind は、$name と書くことで展開に代入できます。例:
macro_rules! times_five {
($e:expr) => {5 * $e};
}
マクロ展開とよく似て、キャプチャは完全な AST ノードとして代入されます。これは、$e にどのようなトークン列がキャプチャされても、それが単一の完全な式として解釈されることを意味します。
1 つのパターンに複数のキャプチャを持たせることもできます。
macro_rules! multiply_add {
($a:expr, $b:expr, $c:expr) => {$a * ($b + $c)};
}
繰り返し
パターンには繰り返しを含めることができます。これにより、トークンのシーケンスをマッチさせることができます。繰り返しは一般に $ ( ... ) sep rep という形式です。
$はリテラルのドル記号トークンです。( ... )は、繰り返される丸括弧でグループ化されたパターンです。sepは任意の区切りトークンです。一般的な例は,や;です。repは必須の繰り返し制御です。現在、これは*(0 回以上の繰り返しを示す)または+(1 回以上の繰り返しを示す)のどちらかです。「0 回または 1 回」や、その他のより具体的な回数や範囲を書くことはできません。
繰り返しには、リテラルのトークンツリー、キャプチャ、他の繰り返しなど、任意の有効なパターンを含めることができます。
繰り返しは展開でも同じ構文を使います。
たとえば、以下は各要素を文字列としてフォーマットするマクロです。0 個以上のカンマ区切りの式にマッチし、ベクターを構築する式に展開されます。
macro_rules! vec_strs { ( // 繰り返しを開始: $( // 各繰り返しには式が含まれていなければなりません... $element:expr ) // ...カンマで区切られ... , // ...0 回以上繰り返されます。 * ) => { // 複数の文を使えるように、展開をブロックで囲みます。 { let mut v = Vec::new(); // 繰り返しを開始: $( // 各繰り返しには次の文が含まれ、 // $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"]); }
% 細部
このセクションでは、マクロシステムのより細かな詳細について説明します。少なくとも、これらの詳細や問題について最低限認識しておくようにしてください。
% キャプチャと展開の再考
パーサーがキャプチャのためにトークンの消費を開始すると、停止したりバックトラックしたりすることはできません。これは、どのような入力が与えられても、次のマクロの 2 番目のルールが決してマッチできないことを意味します。
macro_rules! dead_rule {
($e:expr) => { ... };
($i:ident +) => { ... };
}
このマクロが dead_rule!(x+) として呼び出された場合に何が起こるかを考えてみましょう。インタープリターは最初のルールから開始し、入力を式としてパースしようとします。最初のトークン(x)は式として有効です。2 番目のトークンも式の中で同様に有効であり、二項加算ノードを形成します。
この時点で、加算の右辺が存在しないため、パーサーは諦めて次のルールを試す、と予想するかもしれません。しかし実際には、パーサーはパニックを起こし、構文エラーを示してコンパイル全体を中止します。
そのため、一般的にマクロルールは最も具体的なものから最も一般的なものへと記述することが重要です。
将来の構文変更によってマクロ入力の解釈が変わることを防ぐために、macro_rules! は各種キャプチャの後に続けられるものを制限しています。Rust 1.3 時点での完全な一覧は次のとおりです。
item: 任意。block: 任意。stmt:=>,;pat:=>,=ifinexpr:=>,;ty:,=>:=>;asident: 任意。path:,=>:=>;asmeta: 任意。tt: 任意。
さらに、macro_rules! は一般に、内容が衝突しない場合であっても、繰り返しの後に別の繰り返しが続くことを禁止します。
置換に関してよく人を驚かせる点の 1 つは、置換は非常にそう見えるにもかかわらず、トークンベースではないということです。簡単な実演を示します。
macro_rules! capture_expr_then_stringify { ($e:expr) => { stringify!($e) }; } fn main() { println!("{:?}", stringify!(dummy(2 * (1 + (3))))); println!("{:?}", capture_expr_then_stringify!(dummy(2 * (1 + (3))))); }
stringify! は組み込みの構文拡張であり、単に与えられたすべてのトークンを受け取り、それらを 1 つの大きな文字列に連結します。
実行時の出力は次のとおりです。
"dummy ( 2 * ( 1 + ( 3 ) ) )"
"dummy(2 * (1 + (3)))"
同じ入力であるにもかかわらず、出力が異なることに注目してください。これは、最初の呼び出しがトークンツリーの列を文字列化しているのに対し、2 番目はAST の式ノードを文字列化しているためです。
違いを別の形で可視化すると、最初のケースで stringify! マクロが呼び出される際に受け取るものは次のようになります。
«dummy» «( )»
╭───────┴───────╮
«2» «*» «( )»
╭───────┴───────╮
«1» «+» «( )»
╭─┴─╮
«3»
…そして、2 番目のケースで呼び出される際に受け取るものは次のようになります。
« »
│ ┌─────────────┐
└╴│ Call │
│ fn: dummy │ ┌─────────┐
│ args: ◌ │╶─╴│ BinOp │
└─────────────┘ │ op: Mul │
┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ LitInt │╶┘ └─────────┘ └╴│ BinOp │
│ val: 2 │ │ op: Add │
└────────┘ ┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌────────┐
│ LitInt │╶┘ └─────────┘ └╴│ LitInt │
│ val: 1 │ │ val: 3 │
└────────┘ └────────┘
見てのとおり、トークンツリーは正確に1 つだけであり、それには capture_expr_then_stringify! 呼び出しへの入力からパースされた AST が含まれています。したがって、出力に見えているものは文字列化されたトークンではなく、文字列化された AST ノードです。
これにはさらに別の影響があります。次の例を考えてみましょう。
macro_rules! capture_then_match_tokens { ($e:expr) => {match_tokens!($e)}; } macro_rules! match_tokens { ($a:tt + $b:tt) => {"got an addition"}; (($i:ident)) => {"got an identifier"}; ($($other:tt)*) => {"got something else"}; } fn main() { println!("{}\n{}\n{}\n", match_tokens!((caravan)), match_tokens!(3 + 6), match_tokens!(5)); println!("{}\n{}\n{}", capture_then_match_tokens!((caravan)), capture_then_match_tokens!(3 + 6), capture_then_match_tokens!(5)); }
出力は次のとおりです。
got an identifier
got an addition
got something else
got something else
got something else
got something else
入力を AST ノードとしてパースすることで、置換された結果は分解不能になります。すなわち、その内容を調べたり、それに対して再びマッチングしたりすることはできません。
特に混乱しやすい別の例を示します。
macro_rules! capture_then_what_is { (#[$m:meta]) => {what_is!(#[$m])}; } macro_rules! what_is { (#[no_mangle]) => {"no_mangle attribute"}; (#[inline]) => {"inline attribute"}; ($($tts:tt)*) => {concat!("something else (", stringify!($($tts)*), ")")}; } fn main() { println!( "{}\n{}\n{}\n{}", what_is!(#[no_mangle]), what_is!(#[inline]), capture_then_what_is!(#[no_mangle]), capture_then_what_is!(#[inline]), ); }
出力は次のとおりです。
no_mangle attribute
inline attribute
something else (# [ no_mangle ])
something else (# [ inline ])
これを避ける唯一の方法は、tt または ident の種類を使ってキャプチャすることです。それ以外のものでキャプチャした場合、その後その結果に対してできる唯一のことは、それを直接出力へ置換することだけです。
% 衛生性
Rust のマクロは部分的に衛生的です。具体的には、ほとんどの識別子については衛生的ですが、ジェネリック型パラメーターやライフタイムについてはそうではありません。
衛生性は、すべての識別子に不可視の「構文コンテキスト」値を付与することで機能します。2 つの識別子を比較する際には、その 2 つが等しいと見なされるために、識別子のテキスト上の名前と構文コンテキストの両方が同一でなければなりません。
これを説明するために、次のコードを考えてみましょう。
macro_rules! using_a {
($e:expr) => {
{
let a = 42;
$e
}
}
}
let four = using_a!(a / 10);
背景色を使って構文コンテキストを表します。では、マクロ呼び出しを展開してみましょう。
let four = { let a = 42; a / 10 };
まず、macro_rules! 呼び出しは展開中に事実上消えることを思い出してください。
次に、このコードをコンパイルしようとすると、コンパイラーはおおよそ次のような内容で応答します。
<anon>:11:21: 11:22 error: unresolved name `a`
<anon>:11 let four = using_a!(a / 10);
展開されたマクロの背景色(すなわち構文コンテキスト)は、展開の一部として変化することに注意してください。各マクロ展開には、その内容に対して新しい一意の構文コンテキストが与えられます。その結果、展開後のコードには2 つの異なる a が存在します。一方は最初の構文コンテキストにあり、もう一方は別の構文コンテキストにあります。言い換えると、a は a と同じ識別子ではありません。見た目がどれほど似ていてもです。
とはいえ、展開後の出力に代入されたトークンは、元の構文コンテキストを保持します(マクロ自体の一部であったのではなく、マクロに提供されたものであるためです)。したがって、解決策は次のようにマクロを修正することです。
macro_rules! using_a {
($a:ident, $e:expr) => {
{
let $a = 42;
$e
}
}
}
let four = using_a!(a, a / 10);
これは展開すると次のようになります。
let four = { let a = 42; a / 10 };
使用されている a が 1 つだけであるため、コンパイラーはこのコードを受け入れます。
% 識別子ではない識別子
いずれ遭遇する可能性が高い、識別子のように見えるものの、実際にはそうではないトークンが2つあります。ただし、識別子である場合は別です。
1つ目は self です。これは間違いなくキーワードです。しかし、識別子の定義にもたまたま適合します。通常の Rust コードでは、self が識別子として解釈されることはありませんが、マクロでは起こり得ます:
macro_rules! what_is { (self) => {"the keyword `self`"}; ($i:ident) => {concat!("the identifier `", stringify!($i), "`")}; } macro_rules! call_with_ident { ($c:ident($i:ident)) => {$c!($i)}; } fn main() { println!("{}", what_is!(self)); println!("{}", call_with_ident!(what_is(self))); }
上記は次を出力します:
the keyword `self`
the keyword `self`
しかし、これは筋が通りません。call_with_ident! は識別子を要求し、それにマッチして、それを置換したのです! つまり、self はキーワードであり、同時にキーワードではないということになります。これがいったいどのように重要なのか疑問に思うかもしれません。次の例を見てください:
macro_rules! make_mutable {
($i:ident) => {let mut $i = $i;};
}
struct Dummy(i32);
impl Dummy {
fn double(self) -> Dummy {
make_mutable!(self);
self.0 *= 2;
self
}
}
#
# fn main() {
# println!("{:?}", Dummy(4).double().0);
# }
これは次のエラーでコンパイルに失敗します:
<anon>:2:28: 2:30 error: expected identifier, found keyword `self`
<anon>:2 ($i:ident) => {let mut $i = $i;};
^~
つまり、このマクロは self を識別子として問題なくマッチし、実際には使用できない場所で使うことを許してしまいます。とはいえ、まあよいでしょう。self は識別子であるときでさえ、どういうわけかそれがキーワードであることを覚えているわけです。なら、これもできるはずですよね?
macro_rules! make_self_mutable {
($i:ident) => {let mut $i = self;};
}
struct Dummy(i32);
impl Dummy {
fn double(self) -> Dummy {
make_self_mutable!(mut_self);
mut_self.0 *= 2;
mut_self
}
}
#
# fn main() {
# println!("{:?}", Dummy(4).double().0);
# }
これは次のエラーで失敗します:
<anon>:2:33: 2:37 error: `self` is not available in a static method. Maybe a `self` argument is missing? [E0424]
<anon>:2 ($i:ident) => {let mut $i = self;};
^~~~
これもやはり筋が通りません。これは static メソッドの中ではありません。使おうとしている self が同じ self ではない、と文句を言っているかのようです……まるで self キーワードが、識別子のように衛生性を持っているかのように。
macro_rules! double_method {
($body:expr) => {
fn double(mut self) -> Dummy {
$body
}
};
}
struct Dummy(i32);
impl Dummy {
double_method! {{
self.0 *= 2;
self
}}
}
#
# fn main() {
# println!("{:?}", Dummy(4).double().0);
# }
同じエラーです。では、これはどうでしょうか……
macro_rules! double_method { ($self_:ident, $body:expr) => { fn double(mut $self_) -> Dummy { $body } }; } struct Dummy(i32); impl Dummy { double_method! {self, { self.0 *= 2; self }} } fn main() { println!("{:?}", Dummy(4).double().0); }
ついに、これは動作します。つまり、self は都合のよいときにはキーワードかつ識別子なのです。きっと、これは他の似た構文にも使えるはずですよね?
macro_rules! double_method {
($self_:ident, $body:expr) => {
fn double($self_) -> Dummy {
$body
}
};
}
struct Dummy(i32);
impl Dummy {
double_method! {_, 0}
}
#
# fn main() {
# println!("{:?}", Dummy(4).double().0);
# }
<anon>:12:21: 12:22 error: expected ident, found _
<anon>:12 double_method! {_, 0}
^
いいえ、もちろん違います。_ はパターンや式で有効なキーワードですが、どういうわけかキーワード self のような識別子ではありません。識別子の定義にはまったく同じようにマッチするにもかかわらずです。
代わりに $self_:pat を使えばこれを回避できると思うかもしれません。そうすれば _ はマッチします! ところが、そうはいきません。なぜなら self はパターンではないからです。なんとも楽しいですね。
この回避策は、(これらのトークンの何らかの組み合わせを受け付けたい場合には)代わりに tt マッチャーを使うことだけです。
% デバッグ
rustc は、マクロをデバッグするためのツールを数多く提供しています。最も便利なものの 1 つが trace_macros! です。これは、展開前のすべてのマクロ呼び出しをダンプするようコンパイラに指示するディレクティブです。たとえば、次のようなコードがあるとします。
// 注: nightly チャネルのコンパイラを使用してください。 #![feature(trace_macros)] macro_rules! each_tt { () => {}; ($_tt:tt $($rest:tt)*) => {each_tt!($($rest)*);}; } each_tt!(foo bar baz quux); trace_macros!(true); each_tt!(spim wak plee whum); trace_macros!(false); each_tt!(trom qlip winp xod); fn main() {}
出力は次のようになります。
each_tt! { spim wak plee whum }
each_tt! { wak plee whum }
each_tt! { plee whum }
each_tt! { whum }
each_tt! { }
これは、深く再帰するマクロをデバッグする際に特に非常に有用です。コンパイラのコマンドラインに -Z trace-macros を追加することで、コマンドラインからこれを有効にすることもできます。
次に、log_syntax! があります。これは、渡されたすべてのトークンをコンパイラに出力させます。たとえば、これはコンパイラに歌を歌わせます。
// 注: nightly チャネルのコンパイラを使用してください。 #![feature(log_syntax)] macro_rules! sing { () => {}; ($tt:tt $($rest:tt)*) => {log_syntax!($tt); sing!($($rest)*);}; } sing! { ^ < @ < . @ * '\x08' '{' '"' _ # ' ' - @ '$' && / _ % ! ( '\t' @ | = > ; '\x08' '\'' + '$' ? '\x7f' , # '"' ~ | ) '\x07' } fn main() {}
これは、trace_macros! よりもやや対象を絞ったデバッグを行うために使用できます。
時には、問題があることが判明するのは、マクロが展開された結果です。このためには、コンパイラの --pretty 引数を使用できます。次のコードがあるとします。
// `String` を初期化するための省略表記。
macro_rules! S {
($e:expr) => {String::from($e)};
}
fn main() {
let world = S!("World");
println!("Hello, {}!", world);
}
次のコマンドでコンパイルすると、
rustc -Z unstable-options --pretty expanded hello.rs
次の出力が生成されます(整形のために変更されています)。
#![feature(no_std, prelude_import)]
#![no_std]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std as std;
// `String` を初期化するための省略表記。
fn main() {
let world = String::from("World");
::std::io::_print(::std::fmt::Arguments::new_v1(
{
static __STATIC_FMTSTR: &'static [&'static str]
= &["Hello, ", "!\n"];
__STATIC_FMTSTR
},
&match (&world,) {
(__arg0,) => [
::std::fmt::ArgumentV1::new(__arg0, ::std::fmt::Display::fmt)
],
}
));
}
--pretty のその他のオプションは、rustc -Z unstable-options --help -v を使用して一覧表示できます。名前が示すとおり、そのような一覧はいつでも変更される可能性があるため、完全な一覧は提供していません。
% スコープ
マクロのスコープの決まり方は、やや直感に反することがあります。まず、言語内の他のすべてのものとは異なり、マクロはサブモジュール内でも可視のままです。
macro_rules! X { () => {}; } mod a { X!(); // 定義済み } mod b { X!(); // 定義済み } mod c { X!(); // 定義済み } fn main() {}
注: これらの例では、モジュールの内容が別々のファイルにある場合でも、すべてが同じ振る舞いをすることを覚えておいてください。
次に、これもまた言語内の他のすべてのものとは異なり、マクロはその定義の後でのみアクセスできます。また、この例は、マクロがその定義スコープの外へ「漏れ出さない」ことを示している点にも注意してください。
mod a { // X!(); // 未定義 } mod b { // X!(); // 未定義 macro_rules! X { () => {}; } X!(); // 定義済み } mod c { // X!(); // 未定義 } fn main() {}
明確にしておくと、この字句順序への依存性は、マクロを外側のスコープに移動した場合でも適用されます。
mod a { // X!(); // 未定義 } macro_rules! X { () => {}; } mod b { X!(); // 定義済み } mod c { X!(); // 定義済み } fn main() {}
しかし、この依存性はマクロ自体には適用されません。
mod a { // X!(); // 未定義 } macro_rules! X { () => { Y!(); }; } mod b { // X!(); // 定義済みだが、Y! は未定義 } macro_rules! Y { () => {}; } mod c { X!(); // 定義済みであり、Y! も同様 } fn main() {}
マクロは #[macro_use] 属性を使用してモジュールからエクスポートできます。
mod a { // X!(); // 未定義 } #[macro_use] mod b { macro_rules! X { () => {}; } X!(); // 定義済み } mod c { X!(); // 定義済み } fn main() {}
マクロ内の識別子(他のマクロを含む)は展開時にのみ解決されるため、これがやや奇妙な形で相互作用することがある点に注意してください。
mod a { // X!(); // 未定義 } #[macro_use] mod b { macro_rules! X { () => { Y!(); }; } // X!(); // 定義済みだが、Y! は未定義 } macro_rules! Y { () => {}; } mod c { X!(); // 定義済みであり、Y! も同様 } fn main() {}
もう一つの複雑な点は、extern crate に適用された #[macro_use] はこのようには振る舞わないことです。このような宣言は実質的にモジュールの先頭へ巻き上げられます。したがって、X! が mac という外部クレートで定義されていると仮定すると、次のことが成り立ちます。
mod a {
// X!(); // 定義済みだが、Y! は未定義
}
macro_rules! Y { () => {}; }
mod b {
X!(); // 定義済みであり、Y! も同様
}
#[macro_use] extern crate macs;
mod c {
X!(); // 定義済みであり、Y! も同様
}
# fn main() {}
最後に、これらのスコープの振る舞いは、#[macro_use](これは適用できません)を除いて、関数にも同様に適用される点に注意してください。
macro_rules! X { () => { Y!() }; } fn a() { macro_rules! Y { () => {"Hi!"} } assert_eq!(X!(), "Hi!"); { assert_eq!(X!(), "Hi!"); macro_rules! Y { () => {"Bye!"} } assert_eq!(X!(), "Bye!"); } assert_eq!(X!(), "Hi!"); } fn b() { macro_rules! Y { () => {"One more"} } assert_eq!(X!(), "One more"); } fn main() { a(); b(); }
こうしたスコープ規則があるため、一般的な助言として、「クレート全体」でアクセス可能にすべきすべてのマクロは、ルートモジュールの最上部、他のどのモジュールよりも前に置くべきだとされています。これにより、それらが一貫して利用可能であることが保証されます。
% インポート/エクスポート
マクロをより広いスコープに公開する方法は2つあります。1つ目は #[macro_use] 属性です。これはモジュールと外部クレートのどちらにも適用できます。例:
#[macro_use] mod macros { macro_rules! X { () => { Y!(); } } macro_rules! Y { () => {} } } X!(); fn main() {}
マクロは、#[macro_export] を使って現在のクレートからエクスポートできます。これはすべての可視性を無視することに注意してください。
ライブラリパッケージ macs に対して次の定義があるとします。
mod macros {
#[macro_export] macro_rules! X { () => { Y!(); } }
#[macro_export] macro_rules! Y { () => {} }
}
// X! と Y! はここでは定義されて*いません*が、
// `macros` がプライベートであるにもかかわらず、エクスポート*されています*。
次のコードは期待どおりに動作します。
X!(); // X は定義されている
#[macro_use] extern crate macs;
X!();
#
# fn main() {}
外部クレートに対して #[macro_use] できるのは、ルートモジュールからのみであることに注意してください。
最後に、外部クレートからマクロをインポートする場合、どのマクロをインポートするかを制御できます。これを使って名前空間の汚染を制限したり、次のように特定のマクロをオーバーライドしたりできます。
// `X!` マクロ*のみ*をインポートする。
#[macro_use(X)] extern crate macs;
// X!(); // X は定義されているが、Y! は未定義
macro_rules! Y { () => {} }
X!(); // X は定義されており、Y も定義されている!
fn main() {}
マクロをエクスポートする場合、定義元クレート内の非マクロシンボルを参照したいことがよくあります。クレートはリネームできるため、利用可能な特別な置換変数があります: $crate。これは、含んでいるクレートへの絶対パス接頭辞に常に展開されます(例 :: macs)。
これはマクロには機能しないことに注意してください。マクロはいかなる方法でも通常の名前解決と相互作用しないためです。つまり、クレート内の特定のマクロを参照するために $crate::Y! のようなものを使うことはできません。これが #[macro_use] による選択的インポートと組み合わさると、現在のところ、別のクレートによってインポートされたときに任意のマクロが利用可能になることを保証する方法はない、ということになります。
競合を避けるため、標準ライブラリ内の名前も含めて、非マクロ名には常に絶対パスを使うことが推奨されます。
% マクロ、実践的な入門
この章では、比較的単純で実践的な例を使って、Rust の macro-by-example システムを紹介します。これは、このシステムの複雑な細部をすべて説明しようとするものではありません。目的は、マクロがどのように、そしてなぜ書かれるのかに慣れてもらうことです。
高レベルの別の説明として Rust Book のマクロの章 もあります。また、この本の methodical introduction の章では、マクロシステムを詳細に説明しています。
少しの背景
注記: 慌てないでください! 以下が、ここで話す唯一の数学です。記事の本題に入りたいだけなら、このセクションはまったく問題なくスキップできます。
ご存じない場合のために説明すると、漸化式とは、各値が 1 つ以上の以前の値に基づいて定義され、全体を始めるための 1 つ以上の初期値を持つ数列です。たとえば、フィボナッチ数列 は次の関係で定義できます。
したがって、この数列の最初の 2 つの数は 0 と 1 であり、3 番目は F0 + F1 = 0 + 1 = 1、4 番目は F1 + F2 = 1 + 1 = 2 であり、このように永遠に続きます。
さて、そのような数列は永遠に続き得るため、fibonacci 関数を定義するのは少し厄介になります。完全なベクターを返そうとは、明らかに思わないはずだからです。欲しいのは、必要に応じて数列の要素を遅延評価で計算するものを返すことです。
Rust では、それは Iterator を生成することを意味します。これは特に難しいわけではありませんが、それなりの量のボイラープレートが伴います。独自の型を定義し、その中に保存する必要がある状態を考え出し、それからその型に対して Iterator トレイトを実装する必要があります。
しかし、漸化式は十分に単純なので、これらの詳細のほとんどすべては、少しのマクロベースのコード生成によって抽象化できます。
以上を踏まえて、始めましょう。
構築
通常、新しいマクロに取り組むとき、私が最初に行うのは、マクロ呼び出しがどのように見えるべきかを決めることです。この具体的なケースでは、最初の試みは次のようになりました。
let fib = recurrence![a[n] = 0, 1, ..., a[n-1] + a[n-2]];
for e in fib.take(10) { println!("{}", e) }
そこから、実際の展開がどうなるか確信がなくても、マクロをどのように定義すべきかを試しに考えることができます。これは便利です。なぜなら、入力構文をどのように解析すればよいか分からないなら、おそらくそれを変更する必要があるからです。
macro_rules! recurrence { ( a[n] = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ }; } fn main() {}
この構文に慣れていないと仮定して、説明しましょう。これは macro_rules! システムを使って、recurrence! というマクロを定義しています。このマクロには単一の解析ルールがあります。そのルールは、マクロへの入力が次に一致しなければならないことを示しています。
- リテラルトークン列 `a` `[` `n` `]` `=`、
- `,` を区切り文字として使用し、以下を1回以上(`+`)繰り返す、繰り返し(`$( ... )`)シーケンス:
- 変数 `inits` にキャプチャされる有効な*式*(`$inits:expr`)
- リテラルトークン列 `,` `...` `,`、
- 変数 `recur` にキャプチャされる有効な*式*(`$recur:expr`)。
最後に、このルールは、入力がこのルールに一致*した場合*、マクロ呼び出しをトークン列 `/* ... */` に置き換えるべきだ、と述べています。
名前から示唆されるように、`inits` には実際には、この位置で一致する*すべて*の式が含まれ、最初や最後のものだけではありません。さらに、それらを、たとえばすべてを不可逆的に貼り合わせるのではなく、*シーケンスとして*キャプチャします。また、`+` の代わりに `*` を使うことで、繰り返しで「0回以上」を表現できることにも注意してください。「0回または1回」や、より具体的な繰り返し回数のサポートはありません。
演習として、提案された入力を取り上げ、それがどのように処理されるかを見るために、ルールに通してみましょう。「Position」列は、構文パターンのどの部分を次に照合する必要があるかを示し、「⌂」で表します。場合によっては、照合対象となる「次の」要素が複数存在する可能性があることに注意してください。「Input」には、まだ消費されて*いない*すべてのトークンが含まれます。`inits` と `recur` には、それぞれの束縛の内容が含まれます。
<style type="text/css">
/* カスタマイズ。 */
.small-code code {
font-size: 60%;
}
table pre.rust {
margin: 0;
border: 0;
}
table.parse-table code {
white-space: pre-wrap;
background-color: transparent;
border: none;
}
table.parse-table tbody > tr > td:nth-child(1) > code:nth-of-type(2) {
color: red;
margin-top: -0.7em;
margin-bottom: -0.6em;
}
table.parse-table tbody > tr > td:nth-child(1) > code {
display: block;
}
table.parse-table tbody > tr > td:nth-child(2) > code {
display: block;
}
</style>
<table class="parse-table">
<thead>
<tr>
<th>位置</th>
<th>入力</th>
<th><code>inits</code></th>
<th><code>recur</code></th>
</tr>
</thead>
<tbody class="small-code">
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code>⌂</code></td>
<td><code>a[n] = 0, 1, ..., a[n-1] + a[n-2]</code></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> ⌂</code></td>
<td><code>[n] = 0, 1, ..., a[n-1] + a[n-2]</code></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> ⌂</code></td>
<td><code>n] = 0, 1, ..., a[n-1] + a[n-2]</code></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> ⌂</code></td>
<td><code>] = 0, 1, ..., a[n-1] + a[n-2]</code></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> ⌂</code></td>
<td><code>= 0, 1, ..., a[n-1] + a[n-2]</code></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> ⌂</code></td>
<td><code>0, 1, ..., a[n-1] + a[n-2]</code></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> ⌂</code></td>
<td><code>0, 1, ..., a[n-1] + a[n-2]</code></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> ⌂ ⌂</code></td>
<td><code>, 1, ..., a[n-1] + a[n-2]</code></td>
<td><code>0</code></td>
<td></td>
</tr>
<tr>
<td colspan="4" style="font-size:.7em;">
<em>注</em>: ここには2つの ⌂ があります。これは、次の入力トークンが、繰り返し内の要素<em>間</em>em>のカンマ区切り、<em>または</em>繰り返しの<em>後</em>のカンマの<em>いずれか</em>に一致する可能性があるためです。マクロシステムは、どちらをたどるべきか判断できるようになるまで、両方の可能性を追跡し続けます。
</td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> ⌂ ⌂</code></td>
<td><code>1, ..., a[n-1] + a[n-2]</code></td>
<td><code>0</code></td>
<td></td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> ⌂ ⌂ <s>⌂</s></code></td>
<td><code>, ..., a[n-1] + a[n-2]</code></td>
<td><code>0</code>, <code>1</code></td>
<td></td>
</tr>
<tr>
<td colspan="4" style="font-size:.7em;">
<em>注</em>: 3つ目の取り消し線付きマーカーは、最後に消費されたトークンの結果として、マクロシステムが以前の可能な分岐の1つを除外したことを示します。
</td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> ⌂ ⌂</code></td>
<td><code>..., a[n-1] + a[n-2]</code></td>
<td><code>0</code>, <code>1</code></td>
<td></td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> <s>⌂</s> ⌂</code></td>
<td><code>, a[n-1] + a[n-2]</code></td>
<td><code>0</code>, <code>1</code></td>
<td></td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> ⌂</code></td>
<td><code>a[n-1] + a[n-2]</code></td>
<td><code>0</code>, <code>1</code></td>
<td></td>
</tr>
<tr>
<td><code>a[n] = $($inits:expr),+ , ... , $recur:expr</code>
<code> ⌂</code></td>
<td></td>
<td><code>0</code>, <code>1</code></td>
<td><code>a[n-1] + a[n-2]</code></td>
</tr>
<tr>
<td colspan="4" style="font-size:.7em;">
<em>注</em>: この特定のステップから、<tt>$recur:expr</tt> のようなバインディングは、何が有効な式を構成するかについてのコンパイラの知識を使って、<em>式全体</em>を消費することが明らかになるはずです。後で述べるように、他の言語構成要素に対してもこれを行えます。
</td>
</tr>
</tbody>
</table>
<p></p>
ここから得られる重要なポイントは、マクロシステムは、マクロへの入力として与えられたトークンを、与えられたルールに対してインクリメンタルに一致させようと*試みる*ということです。「試みる」の部分については後で戻ってきます。
では、最終的な完全展開形を書き始めましょう。この展開では、私は次のようなものを求めていました。
```ignore
let fib = {
struct Recurrence {
mem: [u64; 2],
pos: usize,
}
これが実際のイテレータ型になります。mem は、漸化式を計算できるように直近のいくつかの値を保持するメモバッファになります。pos は n の値を追跡するためのものです。
余談: 私はこのシーケンスの要素として「十分に大きい」型として
u64を選びました。これが他のシーケンスでどう機能するかは心配しないでください。そこには後で触れます。
impl Iterator for Recurrence {
type Item = u64;
#[inline]
fn next(&mut self) -> Option<u64> {
if self.pos < 2 {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
シーケンスの初期値を生成するための分岐が必要です。難しいことはありません。
} else {
let a = /* 何か */;
let n = self.pos;
let next_val = (a[n-1] + a[n-2]);
self.mem.TODO_shuffle_down_and_append(next_val);
self.pos += 1;
Some(next_val)
}
}
}
これは少し難しくなります。a を正確にどのように定義するかについては、後で戻って見ていきます。また、TODO_shuffle_down_and_append は別のプレースホルダーです。私は、next_val を配列の末尾に配置し、残りを 1 つ分ずつ下へずらして、0 番目の要素を捨てるものが欲しいのです。
Recurrence { mem: [0, 1], pos: 0 }
};
for e in fib.take(10) { println!("{}", e) }
最後に、新しい構造体のインスタンスを返します。これにより、その後イテレーションできるようになります。まとめると、完全な展開は次のとおりです。
let fib = {
struct Recurrence {
mem: [u64; 2],
pos: usize,
}
impl Iterator for Recurrence {
type Item = u64;
#[inline]
fn next(&mut self) -> Option<u64> {
if self.pos < 2 {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
} else {
let a = /* 何か */;
let n = self.pos;
let next_val = (a[n-1] + a[n-2]);
self.mem.TODO_shuffle_down_and_append(next_val.clone());
self.pos += 1;
Some(next_val)
}
}
}
Recurrence { mem: [0, 1], pos: 0 }
};
for e in fib.take(10) { println!("{}", e) }
余談: はい、これはマクロの各呼び出しごとに異なる
Recurrence構造体とその実装を定義していることを意味します。この大部分は、#[inline]属性をうまく使うことで、最終的なバイナリでは最適化により消えるでしょう。
展開を書いているときに、その展開を確認することも有用です。呼び出しによって変わる必要があるものが展開内にあるのに、実際のマクロ構文には含まれていない場合は、それをどこに導入するかを考えるべきです。この場合、u64 を追加しましたが、それが必ずしもユーザーの望むものとは限りませんし、マクロ構文にも含まれていません。そこで、これを修正しましょう。
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ }; } /* let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]]; for e in fib.take(10) { println!("{}", e) } */ fn main() {}
ここでは、新しいキャプチャ sty を追加しました。これは型であるべきです。
余談: 気になっている方のために説明すると、キャプチャ内のコロンの後の部分には、いくつかの種類の構文マッチャーのうちの 1 つを指定できます。最も一般的なものは
item、expr、tyです。完全な説明は Macros, A Methodical Introduction;macro_rules!(キャプチャ) にあります。もう 1 つ注意すべきことがあります。言語の将来互換性を保つために、コンパイラは、マッチャーの種類に応じて、その後に置くことが許されるトークンを制限します。典型的には、式や文に一致させようとするときに問題になります。これらの後には
=>、,、;のいずれかしか続けることができません。完全な一覧は Macros, A Methodical Introduction; Minutiae; Captures and Expansion Redux にあります。
インデックス付けとシャッフル
この部分はマクロの話からは実質的に外れるので、少し流して説明します。ユーザーが a にインデックスを付けることで、シーケンス内の以前の値にアクセスできるようにしたいと考えています。つまり、シーケンスの直近のいくつか(この場合は 2 つ)の要素を保持するスライディングウィンドウとして動作させたいのです。
これはラッパー型を使えばかなり簡単に実現できます。
struct IndexOffset<'a> {
slice: &'a [u64; 2],
offset: usize,
}
impl<'a> Index<usize> for IndexOffset<'a> {
type Output = u64;
#[inline(always)]
fn index<'b>(&'b self, index: usize) -> &'b u64 {
use std::num::Wrapping;
let index = Wrapping(index);
let offset = Wrapping(self.offset);
let window = Wrapping(2);
let real_index = index - offset + window;
&self.slice[real_index.0]
}
}
余談: Rust に不慣れな人にとってライフタイムは非常によく出てくる話題なので、簡単に説明しておきます。
'aと'bはライフタイムパラメータであり、参照(すなわち 何らかのデータへの借用ポインタ)がどこで有効かを追跡するために使われます。この場合、IndexOffsetはイテレータのデータへの参照を借用するため、'aを使って、その参照をどれだけの期間保持してよいかを追跡する必要があります。
'bが使われているのは、Index::index関数(添字構文が実際に実装されている仕組み)も、借用参照を返す都合上、ライフタイムでパラメータ化されているためです。'aと'bは、すべての場合に必ずしも同じものではありません。借用チェッカーは、私たちが'aと'bを明示的に関連付けていなくても、誤ってメモリ安全性を破らないように保証します。
これにより、a の定義は次のように変わります。
let a = IndexOffset { slice: &self.mem, offset: n };
残る問題は TODO_shuffle_down_and_append をどうするかだけです。標準ライブラリに、私が欲しいセマンティクスを正確に持つメソッドは見つけられませんでしたが、手で書くのは難しくありません。
{
use std::mem::swap;
let mut swap_tmp = next_val;
for i in (0..2).rev() {
swap(&mut swap_tmp, &mut self.mem[i]);
}
}
これは新しい値を配列の末尾に入れ、他の要素を 1 つ分下へずらします。
余談: この方法で行うということは、このコードがコピーできない型に対しても機能するということです。
ここまでの動作するコードは、次のようになります。
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ }; } fn main() { /* let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]]; for e in fib.take(10) { println!("{}", e) } */ let fib = { use std::ops::Index; struct Recurrence { mem: [u64; 2], pos: usize, } struct IndexOffset<'a> { slice: &'a [u64; 2], offset: usize, } impl<'a> Index<usize> for IndexOffset<'a> { type Output = u64; #[inline(always)] fn index<'b>(&'b self, index: usize) -> &'b u64 { use std::num::Wrapping; let index = Wrapping(index); let offset = Wrapping(self.offset); let window = Wrapping(2); let real_index = index - offset + window; &self.slice[real_index.0] } } impl Iterator for Recurrence { type Item = u64; #[inline] fn next(&mut self) -> Option<u64> { if self.pos < 2 { let next_val = self.mem[self.pos]; self.pos += 1; Some(next_val) } else { let next_val = { let n = self.pos; let a = IndexOffset { slice: &self.mem, offset: n }; (a[n-1] + a[n-2]) }; { use std::mem::swap; let mut swap_tmp = next_val; for i in (0..2).rev() { swap(&mut swap_tmp, &mut self.mem[i]); } } self.pos += 1; Some(next_val) } } } Recurrence { mem: [0, 1], pos: 0 } }; for e in fib.take(10) { println!("{}", e) } }
n と a の宣言順を変更し、それらを(再帰式とともに)ブロックで囲んでいることに注意してください。前者の理由は明らかなはずです(a で使えるように、先に n を定義しておく必要があります)。後者の理由は、借用された参照 &self.mem が、後で行うスワップを妨げるためです(他の場所でエイリアスされているものをミューテートすることはできません)。このブロックにより、その時点までに &self.mem の借用が終了することが保証されます。
ちなみに、mem のスワップを行うコードがブロック内にある唯一の理由は、整理のために、std::mem::swap が利用可能なスコープを狭めることです。
このコードを実行すると、次の結果が得られます。
0
1
2
3
5
8
13
21
34
成功です!では、これをマクロ展開にコピー&ペーストし、展開後のコードを呼び出しに置き換えましょう。すると次のようになります。
macro_rules! recurrence {
( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => {
{
/*
ここに続くのは、前のコードを*文字通り*
新しい位置に切り貼りしたものです。その他の変更は
一切加えていません。
*/
use std::ops::Index;
struct Recurrence {
mem: [u64; 2],
pos: usize,
}
struct IndexOffset<'a> {
slice: &'a [u64; 2],
offset: usize,
}
impl<'a> Index<usize> for IndexOffset<'a> {
type Output = u64;
#[inline(always)]
fn index<'b>(&'b self, index: usize) -> &'b u64 {
use std::num::Wrapping;
let index = Wrapping(index);
let offset = Wrapping(self.offset);
let window = Wrapping(2);
let real_index = index - offset + window;
&self.slice[real_index.0]
}
}
impl Iterator for Recurrence {
type Item = u64;
#[inline]
fn next(&mut self) -> Option<u64> {
if self.pos < 2 {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
} else {
let next_val = {
let n = self.pos;
let a = IndexOffset { slice: &self.mem, offset: n };
(a[n-1] + a[n-2])
};
{
use std::mem::swap;
let mut swap_tmp = next_val;
for i in (0..2).rev() {
swap(&mut swap_tmp, &mut self.mem[i]);
}
}
self.pos += 1;
Some(next_val)
}
}
}
Recurrence { mem: [0, 1], pos: 0 }
}
};
}
fn main() {
let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]];
for e in fib.take(10) { println!("{}", e) }
}
明らかに、まだキャプチャを使用してはいませんが、それはかなり簡単に変更できます。しかし、これをコンパイルしようとすると、rustc は中止し、次のように表示します。
recurrence.rs:69:45: 69:48 error: local ambiguity: multiple parsing options: built-in NTs expr ('inits') or 1 other options.
recurrence.rs:69 let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]];
^~~
ここで、macro_rules の制限にぶつかりました。問題は 2 つ目のカンマです。展開中にそれを見たとき、macro_rules は inits 用に別の式をパースすべきなのか、それとも ... なのかを判断できません。残念ながら、... が有効な式ではないことに気づけるほど賢くはないため、諦めてしまいます。理論的には、これは望みどおりに動作するはずですが、現時点ではそうなっていません。
補足: マクロシステムがこのルールをどう解釈するかについて、私は少しだけ嘘をついていました。一般的には説明したとおりに動作するはずですが、このケースではそうなりません。現状の
macro_rulesの仕組みには癖があり、うまく動かすために少し形をひねる必要がある場合もある、ということは覚えておく価値があります。この特定のケースでは、問題が 2 つあります。まず、マクロシステムは何がさまざまな文法要素(たとえば式)を構成し、何が構成しないのかを知りません。それはパーサーの仕事です。そのため、
...が式ではないことを知りません。次に、式のような複合的な文法要素をキャプチャしようとするとき、そのキャプチャに 100% コミットせずに試す方法がありません。言い換えると、マクロシステムは入力の一部を式としてパースするようパーサーに依頼できますが、パーサーは問題があると中断することで応答します。現時点でマクロシステムがこれに対処できる唯一の方法は、これが問題になり得る状況を単に禁止しようとすることです。
明るい面としては、これは文字どおり誰も歓迎していない状況です。
macroキーワードは、より厳密に定義された将来のマクロシステムのためにすでに予約されています。それまでは、必要なら仕方ありません。
幸い、修正は比較的簡単です。構文からカンマを取り除きます。バランスを保つため、... の周囲のカンマを両方とも取り除きます。
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => { // ^~~ 変更 /* ... */ // ズル :D (vec![0u64, 1, 2, 3, 5, 8, 13, 21, 34]).into_iter() }; } fn main() { let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]]; // ^~~ 変更 for e in fib.take(10) { println!("{}", e) } }
成功です! これで、展開内のものをキャプチャしたものに置き換え始めることができます。
置換
マクロでキャプチャしたものを置換するのはとても簡単です。$sty を使うことで、キャプチャ $sty:ty の内容を挿入できます。では、順番に u64 を修正していきましょう。
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => { { use std::ops::Index; struct Recurrence { mem: [$sty; 2], // ^~~~ 変更 pos: usize, } struct IndexOffset<'a> { slice: &'a [$sty; 2], // ^~~~ 変更 offset: usize, } impl<'a> Index<usize> for IndexOffset<'a> { type Output = $sty; // ^~~~ 変更 #[inline(always)] fn index<'b>(&'b self, index: usize) -> &'b $sty { // ^~~~ 変更 use std::num::Wrapping; let index = Wrapping(index); let offset = Wrapping(self.offset); let window = Wrapping(2); let real_index = index - offset + window; &self.slice[real_index.0] } } impl Iterator for Recurrence { type Item = $sty; // ^~~~ 変更 #[inline] fn next(&mut self) -> Option<$sty> { // ^~~~ 変更 /* ... */ if self.pos < 2 { let next_val = self.mem[self.pos]; self.pos += 1; Some(next_val) } else { let next_val = { let n = self.pos; let a = IndexOffset { slice: &self.mem, offset: n }; (a[n-1] + a[n-2]) }; { use std::mem::swap; let mut swap_tmp = next_val; for i in (0..2).rev() { swap(&mut swap_tmp, &mut self.mem[i]); } } self.pos += 1; Some(next_val) } } } Recurrence { mem: [1, 1], pos: 0 } } }; } fn main() { let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]]; for e in fib.take(10) { println!("{}", e) } }
より難しいものに取り組みましょう。inits を配列リテラル [0, 1] と、配列型 [$sty; 2] の両方に変換する方法です。前者は次のようにできます。
Recurrence { mem: [$($inits),+], pos: 0 }
// ^~~~~~~~~~~ 変更
これは実質的に、キャプチャの逆を行います。つまり、inits を 1 回以上繰り返し、それぞれをカンマで区切ります。これは期待どおりのトークン列 0, 1 に展開されます。
inits をなんとかリテラル 2 に変換するのは少し厄介です。これを直接行う方法はないことがわかっていますが、2 つ目のマクロを使えば可能です。1 ステップずつ進めましょう。
macro_rules! count_exprs { /* ??? */ () => {} } fn main() {}
自明なケースは、式が 0 個与えられた場合、count_exprs はリテラル 0 に展開されるはずだというものです。
macro_rules! count_exprs { () => (0); // ^~~~~~~~~~ 追加 } fn main() { const _0: usize = count_exprs!(); assert_eq!(_0, 0); }
補足: ここでは展開に波かっこの代わりに丸かっこを使っていることに気付いたかもしれません。
macro_rulesは、何を使うかは本当に気にしません。ただし、それが「マッチャー」のペアのいずれか、つまり( )、{ }、または[ ]である必要があります。実際、マクロ自体のマッチャー(つまりマクロ名の直後のマッチャー)、構文ルールの周囲のマッチャー、そして対応する展開の周囲のマッチャーを入れ替えることができます。マクロを呼び出すときに使うマッチャーも入れ替えることができますが、こちらはより制限されています。
{ ... }または( ... );として呼び出されたマクロは、常に**アイテム(つまりstructやfn宣言のようなもの)としてパースされます。これは関数本体内でマクロを使うときに重要です。「式のようにパースする」と「文のようにパースする」の曖昧さを解消するのに役立ちます。
1 つの式がある場合はどうでしょうか。それはリテラル 1 になるはずです。
macro_rules! count_exprs { () => (0); ($e:expr) => (1); // ^~~~~~~~~~~~~~~~~ 追加 } fn main() { const _0: usize = count_exprs!(); const _1: usize = count_exprs!(x); assert_eq!(_0, 0); assert_eq!(_1, 1); }
2 つなら?
macro_rules! count_exprs { () => (0); ($e:expr) => (1); ($e0:expr, $e1:expr) => (2); // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ 追加 } fn main() { const _0: usize = count_exprs!(); const _1: usize = count_exprs!(x); const _2: usize = count_exprs!(x, y); assert_eq!(_0, 0); assert_eq!(_1, 1); assert_eq!(_2, 2); }
2つの式の場合を再帰的に表現し直すことで、これを少し「単純化」できます。
macro_rules! count_exprs { () => (0); ($e:expr) => (1); ($e0:expr, $e1:expr) => (1 + count_exprs!($e1)); // ^~~~~~~~~~~~~~~~~~~~~ 変更 } fn main() { const _0: usize = count_exprs!(); const _1: usize = count_exprs!(x); const _2: usize = count_exprs!(x, y); assert_eq!(_0, 0); assert_eq!(_1, 1); assert_eq!(_2, 2); }
これは問題ありません。Rust は 1 + 1 を定数値に畳み込めるからです。では、式が3つある場合はどうでしょうか?
macro_rules! count_exprs { () => (0); ($e:expr) => (1); ($e0:expr, $e1:expr) => (1 + count_exprs!($e1)); ($e0:expr, $e1:expr, $e2:expr) => (1 + count_exprs!($e1, $e2)); // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 追加 } fn main() { const _0: usize = count_exprs!(); const _1: usize = count_exprs!(x); const _2: usize = count_exprs!(x, y); const _3: usize = count_exprs!(x, y, z); assert_eq!(_0, 0); assert_eq!(_1, 1); assert_eq!(_2, 2); assert_eq!(_3, 3); }
余談: これらのルールの順序を逆にできるのではないか、と思うかもしれません。この特定のケースでは、できます。しかし、マクロシステムは、何から回復しようとするか、また何からは回復しようとしないかについて、ときどき気難しいことがあります。複数ルールのマクロがあり、絶対に動くはずだと思うのに予期しないトークンに関するエラーが出る場合は、ルールの順序を変更してみてください。
ここでパターンが見えてきたことを願います。1つの式に続けて0個以上の式にマッチさせ、それを 1 + カウントへ展開することで、式のリストを常に短くできます。
macro_rules! count_exprs { () => (0); ($head:expr) => (1); ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 変更 } fn main() { const _0: usize = count_exprs!(); const _1: usize = count_exprs!(x); const _2: usize = count_exprs!(x, y); const _3: usize = count_exprs!(x, y, z); assert_eq!(_0, 0); assert_eq!(_1, 1); assert_eq!(_2, 2); assert_eq!(_3, 3); }
JFTE: これは、何かを数えるための唯一の方法でも、ましてや最良の方法でもありません。後でカウントセクションに目を通すとよいでしょう。
これで、recurrence を変更して mem に必要なサイズを決定できるようになりました。
// 追加: macro_rules! count_exprs { () => (0); ($head:expr) => (1); ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); } macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => { { use std::ops::Index; const MEM_SIZE: usize = count_exprs!($($inits),+); // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 追加 struct Recurrence { mem: [$sty; MEM_SIZE], // ^~~~~~~~ 変更 pos: usize, } struct IndexOffset<'a> { slice: &'a [$sty; MEM_SIZE], // ^~~~~~~~ 変更 offset: usize, } impl<'a> Index<usize> for IndexOffset<'a> { type Output = $sty; #[inline(always)] fn index<'b>(&'b self, index: usize) -> &'b $sty { use std::num::Wrapping; let index = Wrapping(index); let offset = Wrapping(self.offset); let window = Wrapping(MEM_SIZE); // ^~~~~~~~ 変更 let real_index = index - offset + window; &self.slice[real_index.0] } } impl Iterator for Recurrence { type Item = $sty; #[inline] fn next(&mut self) -> Option<$sty> { if self.pos < MEM_SIZE { // ^~~~~~~~ 変更 let next_val = self.mem[self.pos]; self.pos += 1; Some(next_val) } else { let next_val = { let n = self.pos; let a = IndexOffset { slice: &self.mem, offset: n }; (a[n-1] + a[n-2]) }; { use std::mem::swap; let mut swap_tmp = next_val; for i in (0..MEM_SIZE).rev() { // ^~~~~~~~ 変更 swap(&mut swap_tmp, &mut self.mem[i]); } } self.pos += 1; Some(next_val) } } } Recurrence { mem: [$($inits),+], pos: 0 } } }; } /* ... */ fn main() { let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]]; for e in fib.take(10) { println!("{}", e) } }
これができたので、最後のもの、つまり recur 式を置き換えられます。
```ignore macro_rules! count_exprs { () => (0); ($head:expr $(, $tail:expr)*) => (1 + count_exprs!($($tail),*)); } macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => { { const MEMORY: uint = count_exprs!($($inits),+); struct Recurrence { mem: [$sty; MEMORY], pos: uint, } struct IndexOffset<'a> { slice: &'a [$sty; MEMORY], offset: uint, } impl<'a> Index<uint, $sty> for IndexOffset<'a> { #[inline(always)] fn index<'b>(&'b self, index: &uint) -> &'b $sty { let real_index = *index - self.offset + MEMORY; &self.slice[real_index] } } impl Iterator<u64> for Recurrence { /* ... */ #[inline] fn next(&mut self) -> Option<u64> { if self.pos < MEMORY { let next_val = self.mem[self.pos]; self.pos += 1; Some(next_val) } else { let next_val = { let n = self.pos; let a = IndexOffset { slice: &self.mem, offset: n }; $recur // ^~~~~~ 変更 }; { use std::mem::swap; let mut swap_tmp = next_val; for i in range(0, MEMORY).rev() { swap(&mut swap_tmp, &mut self.mem[i]); } } self.pos += 1; Some(next_val) } } /* ... */ } Recurrence { mem: [$($inits),+], pos: 0 } } }; } fn main() { let fib = recurrence![a[n]: u64 = 1, 1 ... a[n-1] + a[n-2]]; for e in fib.take(10) { println!("{}", e) } }
そして、完成したマクロをコンパイルすると...
recurrence.rs:77:48: 77:49 error: unresolved name `a`
recurrence.rs:77 let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]];
^
recurrence.rs:7:1: 74:2 note: in expansion of recurrence!
recurrence.rs:77:15: 77:64 note: expansion site
recurrence.rs:77:50: 77:51 error: unresolved name `n`
recurrence.rs:77 let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]];
^
recurrence.rs:7:1: 74:2 note: in expansion of recurrence!
recurrence.rs:77:15: 77:64 note: expansion site
recurrence.rs:77:57: 77:58 error: unresolved name `a`
recurrence.rs:77 let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]];
^
recurrence.rs:7:1: 74:2 note: in expansion of recurrence!
recurrence.rs:77:15: 77:64 note: expansion site
recurrence.rs:77:59: 77:60 error: unresolved name `n`
recurrence.rs:77 let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]];
^
recurrence.rs:7:1: 74:2 note: in expansion of recurrence!
recurrence.rs:77:15: 77:64 note: expansion site
...待って、何だこれは? そんなはずはありません...マクロが何に展開されているのか確認してみましょう。
$ rustc -Z unstable-options --pretty expanded recurrence.rs
--pretty expanded 引数は、rustc にマクロ展開を実行させ、その結果の AST をソースコードへ戻させます。このオプションはまだ安定版とは見なされていないため、-Z unstable-options も必要です。出力(一部のフォーマットを整えた後)を以下に示します。特に、コード内で $recur が置換された箇所に注目してください:
#![feature(no_std)]
#![no_std]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std as std;
fn main() {
let fib = {
use std::ops::Index;
const MEM_SIZE: usize = 1 + 1;
struct Recurrence {
mem: [u64; MEM_SIZE],
pos: usize,
}
struct IndexOffset<'a> {
slice: &'a [u64; MEM_SIZE],
offset: usize,
}
impl <'a> Index<usize> for IndexOffset<'a> {
type Output = u64;
#[inline(always)]
fn index<'b>(&'b self, index: usize) -> &'b u64 {
use std::num::Wrapping;
let index = Wrapping(index);
let offset = Wrapping(self.offset);
let window = Wrapping(MEM_SIZE);
let real_index = index - offset + window;
&self.slice[real_index.0]
}
}
impl Iterator for Recurrence {
type Item = u64;
#[inline]
fn next(&mut self) -> Option<u64> {
if self.pos < MEM_SIZE {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
} else {
let next_val = {
let n = self.pos;
let a = IndexOffset{slice: &self.mem, offset: n,};
a[n - 1] + a[n - 2]
};
{
use std::mem::swap;
let mut swap_tmp = next_val;
{
let result =
match ::std::iter::IntoIterator::into_iter((0..MEM_SIZE).rev()) {
mut iter => loop {
match ::std::iter::Iterator::next(&mut iter) {
::std::option::Option::Some(i) => {
swap(&mut swap_tmp, &mut self.mem[i]);
}
::std::option::Option::None => break,
}
},
};
result
}
}
self.pos += 1;
Some(next_val)
}
}
}
Recurrence{mem: [0, 1], pos: 0,}
};
{
let result =
match ::std::iter::IntoIterator::into_iter(fib.take(10)) {
mut iter => loop {
match ::std::iter::Iterator::next(&mut iter) {
::std::option::Option::Some(e) => {
::std::io::_print(::std::fmt::Arguments::new_v1(
{
static __STATIC_FMTSTR: &'static [&'static str] = &["", "\n"];
__STATIC_FMTSTR
},
&match (&e,) {
(__arg0,) => [::std::fmt::ArgumentV1::new(__arg0, ::std::fmt::Display::fmt)],
}
))
}
::std::option::Option::None => break,
}
},
};
result
}
}
しかし、これは問題なさそうに見えます! 足りない #![feature(...)] 属性をいくつか追加して rustc の nightly ビルドに渡せば、実際にコンパイルさえできます! ……何だって?!
補足: 上記を
rustcの非 nightly ビルドでコンパイルすることはできません。これは、println!マクロの展開が、公開されて安定化されているわけではないコンパイラ内部の詳細に依存しているためです。
衛生的であること
ここでの問題は、Rust のマクロにおける識別子が衛生的であるという点です。つまり、2 つの異なるコンテキストから来た識別子は衝突できません。違いを示すために、より単純な例を見てみましょう。
/* macro_rules! using_a { ($e:expr) => { { let a = 42i; $e } } } let four = using_a!(a / 10); */ fn main() {}
このマクロは単に式を受け取り、それを変数 a が定義されたブロックで包みます。次に、これを 4 を計算する遠回りな方法として使います。この例には実際には2 つの構文コンテキストが関わっていますが、それらは目に見えません。そこで、これをわかりやすくするために、それぞれのコンテキストに異なる色を付けてみましょう。まず、コンテキストが 1 つだけ存在する、展開前のコードから始めます。
macro_rules! using_a {
($e:expr) => {
{
let a = 42;
$e
}
}
}
let four = using_a!(a / 10);
では、この呼び出しを展開してみましょう。
let four = { let a = 42; a / 10 };
ご覧のとおり、マクロによって定義された a は、呼び出しで提供した a とは異なるコンテキストにあります。そのため、コンパイラはそれらを完全に異なる識別子として扱います。字句上の見た目が同じであってもです。
これは、マクロを扱う際に本当に注意すべき点です。マクロは、コンパイルできない AST を生成することがありますが、それを手で書き出したり、--pretty expanded を使ってダンプしたりするとコンパイルできてしまうことがあります。
これに対する解決策は、適切な構文コンテキストを持つ識別子をキャプチャすることです。そのためには、再びマクロ構文を調整する必要があります。先ほどのより単純な例を続けると、次のようになります。
<pre class="rust rust-example-rendered"><span class="synctx-0"><span class="macro">macro_rules</span><span class="macro">!</span> <span class="ident">using_a</span> {
 (<span class="macro-nonterminal">$</span><span class="macro-nonterminal">a</span>:<span class="ident">ident</span>, <span class="macro-nonterminal">$</span><span class="macro-nonterminal">e</span>:<span class="ident">expr</span>) <span class="op">=></span> {
 {
 <span class="kw">let</span> <span class="macro-nonterminal">$</span><span class="macro-nonterminal">a</span> <span class="op">=</span> <span class="number">42</span>;
 <span class="macro-nonterminal">$</span><span class="macro-nonterminal">e</span>
 }
 }
}

<span class="kw">let</span> <span class="ident">four</span> <span class="op">=</span> <span class="macro">using_a</span><span class="macro">!</span>(<span class="ident">a</span>, <span class="ident">a</span> <span class="op">/</span> <span class="number">10</span>);</span></pre>
これは今度は次のように展開されます:
<pre class="rust rust-example-rendered"><span class="synctx-0"><span class="kw">let</span> <span class="ident">four</span> <span class="op">=</span> </span><span class="synctx-1">{
 <span class="kw">let</span> </span><span class="synctx-0"><span class="ident">a</span></span><span class="synctx-1"> <span class="op">=</span> <span class="number">42</span>;
 </span><span class="synctx-0"><span class="ident">a</span> <span class="op">/</span> <span class="number">10</span></span><span class="synctx-1">
}</span><span class="synctx-0">;</span></pre>
これでコンテキストが一致し、コードはコンパイルされます。 `a` と `n` を明示的にキャプチャすることで、この調整を `recurrence!` マクロに加えることができます。 必要な変更を行うと、次のようになります:
```rust
macro_rules! count_exprs {
() => (0);
($head:expr) => (1);
($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*));
}
macro_rules! recurrence {
( $seq:ident [ $ind:ident ]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => {
// ^~~~~~~~~~ ^~~~~~~~~~ 変更済み
{
use std::ops::Index;
const MEM_SIZE: usize = count_exprs!($($inits),+);
struct Recurrence {
mem: [$sty; MEM_SIZE],
pos: usize,
}
struct IndexOffset<'a> {
slice: &'a [$sty; MEM_SIZE],
offset: usize,
}
impl<'a> Index<usize> for IndexOffset<'a> {
type Output = $sty;
#[inline(always)]
fn index<'b>(&'b self, index: usize) -> &'b $sty {
use std::num::Wrapping;
let index = Wrapping(index);
let offset = Wrapping(self.offset);
let window = Wrapping(MEM_SIZE);
let real_index = index - offset + window;
&self.slice[real_index.0]
}
}
impl Iterator for Recurrence {
type Item = $sty;
#[inline]
fn next(&mut self) -> Option<$sty> {
if self.pos < MEM_SIZE {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
} else {
let next_val = {
let $ind = self.pos;
// ^~~~ 変更済み
let $seq = IndexOffset { slice: &self.mem, offset: $ind };
// ^~~~ 変更済み
$recur
};
{
use std::mem::swap;
let mut swap_tmp = next_val;
for i in (0..MEM_SIZE).rev() {
swap(&mut swap_tmp, &mut self.mem[i]);
}
}
self.pos += 1;
Some(next_val)
}
}
}
Recurrence { mem: [$($inits),+], pos: 0 }
}
};
}
fn main() {
let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]];
for e in fib.take(10) { println!("{}", e) }
}
そしてコンパイルできます! では、別の数列で試してみましょう。
macro_rules! count_exprs { () => (0); ($head:expr) => (1); ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); } macro_rules! recurrence { ( $seq:ident [ $ind:ident ]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => { { use std::ops::Index; const MEM_SIZE: usize = count_exprs!($($inits),+); struct Recurrence { mem: [$sty; MEM_SIZE], pos: usize, } struct IndexOffset<'a> { slice: &'a [$sty; MEM_SIZE], offset: usize, } impl<'a> Index<usize> for IndexOffset<'a> { type Output = $sty; #[inline(always)] fn index<'b>(&'b self, index: usize) -> &'b $sty { use std::num::Wrapping; let index = Wrapping(index); let offset = Wrapping(self.offset); let window = Wrapping(MEM_SIZE); let real_index = index - offset + window; &self.slice[real_index.0] } } impl Iterator for Recurrence { type Item = $sty; #[inline] fn next(&mut self) -> Option<$sty> { if self.pos < MEM_SIZE { let next_val = self.mem[self.pos]; self.pos += 1; Some(next_val) } else { let next_val = { let $ind = self.pos; let $seq = IndexOffset { slice: &self.mem, offset: $ind }; $recur }; { use std::mem::swap; let mut swap_tmp = next_val; for i in (0..MEM_SIZE).rev() { swap(&mut swap_tmp, &mut self.mem[i]); } } self.pos += 1; Some(next_val) } } } Recurrence { mem: [$($inits),+], pos: 0 } } }; } fn main() { for e in recurrence!(f[i]: f64 = 1.0 ... f[i-1] * i as f64).take(10) { println!("{}", e) } }
これにより、次の結果が得られます:
1
1
2
6
24
120
720
5040
40320
362880
成功しました!
% パターン
パースと展開のパターン。
% コールバック
macro_rules! call_with_larch { ($callback:ident) => { $callback!(larch) }; } macro_rules! expand_to_larch { () => { larch }; } macro_rules! recognise_tree { (larch) => { println!("#1, the Larch.") }; (redwood) => { println!("#2, the Mighty Redwood.") }; (fir) => { println!("#3, the Fir.") }; (chestnut) => { println!("#4, the Horse Chestnut.") }; (pine) => { println!("#5, the Scots Pine.") }; ($($other:tt)*) => { println!("I don't know; some kind of birch maybe?") }; } fn main() { recognise_tree!(expand_to_larch!()); call_with_larch!(recognise_tree); }
マクロが展開される順序のため、(Rust 1.2 時点では)別のマクロの展開から、あるマクロへ情報を渡すことは不可能です。これにより、マクロのモジュール化が非常に難しくなることがあります。
別の方法として、再帰を使用してコールバックを渡すことができます。これがどのように行われるかを示すために、上記の例のトレースを示します。
recognise_tree! { expand_to_larch ! ( ) }
println! { "I don't know; some kind of birch maybe?" }
// ...
call_with_larch! { recognise_tree }
recognise_tree! { larch }
println! { "#1, the Larch." }
// ...
tt の繰り返しを使用すると、任意の引数をコールバックへ転送することもできます。
macro_rules! callback { ($callback:ident($($args:tt)*)) => { $callback!($($args)*) }; } fn main() { callback!(callback(println("Yes, this *was* unnecessary."))); }
もちろん、必要に応じて引数に追加のトークンを挿入できます。
% インクリメンタル TT muncher
macro_rules! mixed_rules { () => {}; (trace $name:ident; $($tail:tt)*) => { { println!(concat!(stringify!($name), " = {:?}"), $name); mixed_rules!($($tail)*); } }; (trace $name:ident = $init:expr; $($tail:tt)*) => { { let $name = $init; println!(concat!(stringify!($name), " = {:?}"), $name); mixed_rules!($($tail)*); } }; } fn main() { let a = 42; let b = "Ho-dee-oh-di-oh-di-oh!"; let c = (false, 2, 'c'); mixed_rules!( trace a; trace b; trace c; trace b = "They took her where they put the crazies."; trace b; ); }
このパターンは、おそらく利用可能なマクロのパース手法の中で最も強力なもので、かなり複雑な文法をパースできます。
「TT muncher」とは、入力を1ステップずつインクリメンタルに処理することで動作する再帰的なマクロです。各ステップで、入力の先頭からトークン列の一部にマッチしてそれを削除(munch)し、中間出力を生成してから、入力の残りに対して再帰します。
名前に特に「TT」が含まれる理由は、入力の未処理部分が常に $($tail:tt)* としてキャプチャされるからです。これを行うのは、tt の繰り返しが、マクロ入力の一部を損失なくキャプチャする唯一の方法だからです。
TT muncher に対する厳格な制約は、マクロシステム全体に課されているものだけです。
- リテラル、および
macro_rules!でキャプチャ可能な文法構成要素に対してしかマッチできません。 - バランスの取れていないグループにはマッチできません。
ただし、マクロの再帰制限を念頭に置くことは重要です。macro_rules! には、末尾再帰除去や最適化の形式が一切ありません。TT muncher を書くときは、再帰をできるだけ抑えるために妥当な努力をすることが推奨されます。これは、(中間層へ再帰するのではなく)入力のバリエーションに対応する追加のルールを加えるか、標準の繰り返しをより扱いやすくするために入力構文について妥協することで実現できます。
% 内部ルール
#[macro_export] macro_rules! foo { (@as_expr $e:expr) => {$e}; ($($tts:tt)*) => { foo!(@as_expr $($tts)*) }; } fn main() { assert_eq!(foo!(42), 42); }
マクロは通常のアイテムのプライバシーやルックアップと相互作用しないため、どのようなパブリックマクロも、それが依存する他のすべてのマクロを伴っていなければなりません。これは、グローバルなマクロ名前空間の汚染や、他のクレートのマクロとの競合につながる可能性があります。また、マクロを選択的にインポートしようとするユーザーを混乱させる可能性もあります。ユーザーは、公開ドキュメントに記載されていない可能性があるものも含め、すべてのマクロを推移的にインポートしなければならないためです。
優れた解決策は、本来なら別のパブリックマクロになるものを、エクスポートされるマクロの内部に隠蔽することです。上の例は、一般的な as_expr! マクロを、それを使用している公開エクスポートマクロの中へ移動する方法を示しています。
@ を使う理由は、Rust 1.2 時点では、@ トークンが前置位置で使用されていないためです。そのため、これは何とも競合しません。必要に応じて他の記号や一意の接頭辞を使ってもかまいませんが、@ の使用は広まり始めているため、これを使うことで読者がコードを理解しやすくなる可能性があります。
注:
@トークンは以前、言語がポインター型を表すために印を使用していた頃、ガベージコレクションされるポインターを表すために前置位置で使用されていました。その唯一の現在の目的は、名前をパターンに束縛することです。ただし、この用途では中置演算子として使用されるため、ここでの使用とは競合しません。
さらに、内部ルールは多くの場合、「ベア」ルールよりも前に置かれます。これは、macro_rules! が内部呼び出しを、式などのように、到底そうではあり得ないものとして誤ってパースしようとする問題を避けるためです。
少なくとも 1 つの内部マクロをエクスポートすることが避けられない場合(たとえば、共通のユーティリティルール群に依存するマクロが多数ある場合)、このパターンを使用してすべての内部マクロを単一の巨大マクロにまとめることができます。
macro_rules! crate_name_util {
(@as_expr $e:expr) => {$e};
(@as_item $i:item) => {$i};
(@count_tts) => {0usize};
// ...
}
% プッシュダウン蓄積
#![allow(unused)] fn main() { macro_rules! init_array { (@accum (0, $_e:expr) -> ($($body:tt)*)) => {init_array!(@as_expr [$($body)*])}; (@accum (1, $e:expr) -> ($($body:tt)*)) => {init_array!(@accum (0, $e) -> ($($body)* $e,))}; (@accum (2, $e:expr) -> ($($body:tt)*)) => {init_array!(@accum (1, $e) -> ($($body)* $e,))}; (@accum (3, $e:expr) -> ($($body:tt)*)) => {init_array!(@accum (2, $e) -> ($($body)* $e,))}; (@as_expr $e:expr) => {$e}; [$e:expr; $n:tt] => { { let e = $e; init_array!(@accum ($n, e.clone()) -> ()) } }; } let strings: [String; 3] = init_array![String::from("hi!"); 3]; assert_eq!(format!("{:?}", strings), "[\"hi!\", \"hi!\", \"hi!\"]"); }
Rust のすべてのマクロは、式、項目、などの、サポートされている完全な構文要素にならなければなりません。これは、マクロを部分的な構造へ展開することは不可能である、ということを意味します。
上の例は、次のようにもっと直接的に表現できることを期待するかもしれません。
macro_rules! init_array {
(@accum 0, $_e:expr) => {/* 空 */};
(@accum 1, $e:expr) => {$e};
(@accum 2, $e:expr) => {$e, init_array!(@accum 1, $e)};
(@accum 3, $e:expr) => {$e, init_array!(@accum 2, $e)};
[$e:expr; $n:tt] => {
{
let e = $e;
[init_array!(@accum $n, e)]
}
};
}
期待されるのは、配列リテラルの展開が次のように進むことです。
[init_array!(@accum 3, e)]
[e, init_array!(@accum 2, e)]
[e, e, init_array!(@accum 1, e)]
[e, e, e]
しかし、これには各中間ステップが不完全な式へ展開されることが必要になります。中間結果がマクロコンテキストの外部で使われることが決してないとしても、それは依然として禁止されています。
しかし、プッシュダウンを使うと、完了前のどの時点でも実際に完全な構造を持つ必要なしに、トークン列を段階的に構築できます。冒頭に示した例では、マクロ呼び出しの列は次のように進みます。
init_array! { String:: from ( "hi!" ) ; 3 }
init_array! { @ accum ( 3 , e . clone ( ) ) -> ( ) }
init_array! { @ accum ( 2 , e.clone() ) -> ( e.clone() , ) }
init_array! { @ accum ( 1 , e.clone() ) -> ( e.clone() , e.clone() , ) }
init_array! { @ accum ( 0 , e.clone() ) -> ( e.clone() , e.clone() , e.clone() , ) }
init_array! { @ as_expr [ e.clone() , e.clone() , e.clone() , ] }
ご覧のとおり、各層は蓄積された出力に追加していき、最後に終了規則がそれを完全な構造として出力します。
上記の定式化で唯一重要な部分は、パースを引き起こさずに出力を保持するために $($body:tt)* を使っていることです。($input) -> ($output) の使用は、このようなマクロの振る舞いを明確にしやすくするために採用された単なる慣例です。
プッシュダウン蓄積は、任意に複雑な中間結果を構築できるようにするため、インクリメンタル TT マンチャー の一部として頻繁に使われます。
% 繰り返しの置換
macro_rules! replace_expr {
($_t:tt $sub:expr) => {$sub};
}
このパターンでは、マッチした繰り返しシーケンスを単に破棄し、その変数を代わりに使って、入力とは長さの点でのみ関係する何らかの繰り返しパターンを駆動します。
たとえば、12 個を超える要素を持つタプルのデフォルトインスタンスを構築する場合を考えてみましょう(Rust 1.2 時点での上限)。
#![allow(unused)] fn main() { macro_rules! tuple_default { ($($tup_tys:ty),*) => { ( $( replace_expr!( ($tup_tys) Default::default() ), )* ) }; } macro_rules! replace_expr { ($_t:tt $sub:expr) => {$sub}; } assert_eq!(tuple_default!(i32, bool, String), (0, false, String::new())); }
JFTE: 単に
$tup_tys::default()を使うこともできました。
ここでは、マッチした型を実際に使っているわけではありません。代わりに、それらを捨て、単一の繰り返される式で置き換えています。別の言い方をすると、型が何であるかは気にせず、いくつあるかだけを気にしています。
% 末尾のセパレーター
macro_rules! match_exprs {
($($exprs:expr),* $(,)*) => {...};
}
Rust の文法には、末尾のカンマが許可される箇所がさまざまあります。式のリストをマッチする一般的な 2 つの方法(たとえば、$($exprs:expr),* と $($exprs:expr,)*)は、末尾のカンマがない場合、または末尾のカンマがある場合のどちらか一方には対応できますが、両方には対応できません。
しかし、メインのリストの後に $(,)* の繰り返しを置くと、任意の数(0 個や 1 個を含む)の末尾のカンマ、または使用しているその他のセパレーターを捕捉できます。
これはすべてのコンテキストで使用できるわけではないことに注意してください。コンパイラーがこれを拒否する場合は、おそらく複数のアームやインクリメンタルマッチングを使用する必要があります。
% TT バンドル化
macro_rules! call_a_or_b_on_tail { ((a: $a:expr, b: $b:expr), call a: $($tail:tt)*) => { $a(stringify!($($tail)*)) }; ((a: $a:expr, b: $b:expr), call b: $($tail:tt)*) => { $b(stringify!($($tail)*)) }; ($ab:tt, $_skip:tt $($tail:tt)*) => { call_a_or_b_on_tail!($ab, $($tail)*) }; } fn compute_len(s: &str) -> Option<usize> { Some(s.len()) } fn show_tail(s: &str) -> Option<usize> { println!("tail: {:?}", s); None } fn main() { assert_eq!( call_a_or_b_on_tail!( (a: compute_len, b: show_tail), the recursive part that skips over all these tokens doesn't much care whether we will call a or call b: only the terminal rules care. ), None ); assert_eq!( call_a_or_b_on_tail!( (a: compute_len, b: show_tail), and now, to justify the existence of two paths we will also call a: its input should somehow be self-referential, so let's make it return some ninety one! ), Some(91) ); }
特に複雑な再帰マクロでは、識別子や式を後続の層へ運ぶために、多数の引数が必要になる場合があります。 しかし、実装によっては、これらの引数を転送する必要はあるものの、それらを使用する必要はない中間層が多数存在することがあります。
そのため、そのような引数をすべてグループに入れて単一の TT にまとめると、非常に有用な場合があります。 これにより、引数を使用する必要のない層は、引数グループ全体を正確にキャプチャして置換する代わりに、単一の tt をキャプチャして置換するだけで済みます。
上の例では、$a 式と $b 式をグループにまとめています。そのグループは、再帰ルールによって単一の tt として転送できます。 その後、このグループは終端ルールによって分解され、式にアクセスされます。
% 可視性
Rust にはいかなる種類の vis マッチャーも存在しないため、可視性をマッチして置換するのは厄介な場合があります。
マッチして無視する
コンテキストによっては、これは繰り返しを使用して行えます。
macro_rules! struct_name { ($(pub)* struct $name:ident $($rest:tt)*) => { stringify!($name) }; } fn main() { assert_eq!(struct_name!(pub struct Jim;), "Jim"); }
上の例は、private または public な struct アイテムにマッチします。あるいは pub pub(非常に public)、さらには pub pub pub pub(本当にとてもかなり public)にもマッチします。これに対する最善の防御策は、単にそのマクロを使う人々が過度に馬鹿げたことをしないよう願うことです。
マッチして置換する
繰り返しそのものを変数に束縛することはできないため、$(pub)* の内容を、置換できるように保存する方法はありません。その結果、複数のルールが必要になります。
macro_rules! newtype_new { (struct $name:ident($t:ty);) => { newtype_new! { () struct $name($t); } }; (pub struct $name:ident($t:ty);) => { newtype_new! { (pub) struct $name($t); } }; (($($vis:tt)*) struct $name:ident($t:ty);) => { as_item! { impl $name { $($vis)* fn new(value: $t) -> Self { $name(value) } } } }; } macro_rules! as_item { ($i:item) => {$i} } #[derive(Debug, Eq, PartialEq)] struct Dummy(i32); newtype_new! { struct Dummy(i32); } fn main() { assert_eq!(Dummy::new(42), Dummy(42)); }
関連項目: AST Coercion。
この場合、グループ内の任意のトークン列にマッチできる機能を使って、() または (pub) のいずれかにマッチし、その内容を出力に置換しています。パーサーはこの位置で tt の繰り返し展開を期待しないため、展開が正しくパースされるように AST Coercion を使用する必要があります。
% 暫定
このセクションは、価値が疑わしい、または収録するにはニッチすぎる可能性があるパターンや技法のためのものです。
そろばんカウンター
暫定: より説得力のある例が必要です。
Ook!マクロの重要な部分ではあるものの、Rust のグループで示されていないネストしたグループにマッチすることは十分に珍しく、収録に値しない可能性があります。
注記: このセクションでは、プッシュダウン蓄積とインクリメンタル TT muncherを理解していることを前提としています。
macro_rules! abacus { ((- $($moves:tt)*) -> (+ $($count:tt)*)) => { abacus!(($($moves)*) -> ($($count)*)) }; ((- $($moves:tt)*) -> ($($count:tt)*)) => { abacus!(($($moves)*) -> (- $($count)*)) }; ((+ $($moves:tt)*) -> (- $($count:tt)*)) => { abacus!(($($moves)*) -> ($($count)*)) }; ((+ $($moves:tt)*) -> ($($count:tt)*)) => { abacus!(($($moves)*) -> (+ $($count)*)) }; // 最終結果がゼロかどうかを確認する。 (() -> ()) => { true }; (() -> ($($count:tt)+)) => { false }; } fn main() { let equals_zero = abacus!((++-+-+++--++---++----+) -> ()); assert_eq!(equals_zero, true); }
この技法は、ゼロまたはゼロ付近から始まり、次の操作をサポートしなければならない可変のカウンターを追跡する必要がある場合に使用できます。
- 1 増やす。
- 1 減らす。
- ゼロ(または他の任意の固定された有限値)と比較する。
値 n は、グループ内に格納された特定のトークンの n 個のインスタンスで表されます。変更は再帰とプッシュダウン蓄積を使用して行います。使用するトークンが x であると仮定すると、上記の操作は次のように実装されます。
- 1 増やす:
($($count:tt)*)にマッチし、(x $($count)*)に置換する。 - 1 減らす:
(x $($count:tt)*)にマッチし、($($count)*)に置換する。 - ゼロと比較する:
()にマッチする。 - 1 と比較する:
(x)にマッチする。 - 2 と比較する:
(x x)にマッチする。 - (以下同様...)
このように、カウンターに対する操作は、そろばんのようにトークンを前後にはじくようなものです。1
負の値を表したい場合、-n は別のトークンの n 個のインスタンスとして表すことができます。上の例では、+n は n 個の + トークンとして格納され、-m は m 個の - トークンとして格納されます。
この場合、操作は少し複雑になります。カウンターが負の場合、インクリメントとデクリメントは実質的に通常の意味が逆になります。つまり、正と負のトークンとしてそれぞれ + と - を用いる場合、操作は次のように変わります。
- 1 増やす:
()にマッチし、(+)に置換する。(- $($count:tt)*)にマッチし、($($count)*)に置換する。($($count:tt)+)にマッチし、(+ $($count)+)に置換する。
- 1 減らす:
()にマッチし、(-)に置換する。(+ $($count:tt)*)にマッチし、($($count)*)に置換する。($($count:tt)+)にマッチし、(- $($count)+)に置換する。
- 0 と比較する:
()にマッチする。 - +1 と比較する:
(+)にマッチする。 - -1 と比較する:
(-)にマッチする。 - +2 と比較する:
(++)にマッチする。 - -2 と比較する:
(--)にマッチする。 - (以下同様...)
先頭の例では、いくつかのルールをまとめていることに注意してください(たとえば、() に対するインクリメントと ($($count:tt)+) に対するインクリメントを、($($count:tt)*) に対するインクリメントにまとめています)。
カウンターの実際の値を取り出したい場合は、通常のカウンターマクロを使用して行えます。上の例では、終端ルールを次のように置き換えることができます。
macro_rules! abacus {
// ...
// カウンターを整数式として抽出する。
(() -> ()) => {0};
(() -> (- $($count:tt)*)) => {
{(-1i32) $(- replace_expr!($count 1i32))*}
};
(() -> (+ $($count:tt)*)) => {
{(1i32) $(+ replace_expr!($count 1i32))*}
};
}
macro_rules! replace_expr {
($_t:tt $sub:expr) => {$sub};
}
JFTE: 厳密に言えば、上記の
abacus!の定式化は不必要に複雑です。マクロ内でカウンターの値に対してマッチする必要がないのであれば、繰り返しを使用してはるかに効率的に実装できます。macro_rules! abacus { (-) => {-1}; (+) => {1}; ($($moves:tt)*) => { 0 $(+ abacus!($moves))* } }
-
このかなり苦しい理屈は、この名前の本当の理由を覆い隠しています。つまり、名前に「token」が入るものをまたさらに増やすのを避けるためです。今すぐ意味飽和を避ける方法について執筆者に相談しましょう!
% ビルディングブロック
再利用可能なマクロコードのスニペット。
% AST 強制変換
Rust パーサーは、tt 置換に直面したとき、あまり堅牢ではありません。パーサーが特定の文法構造を期待しているときに、代わりに 置換された tt トークンの塊を見つけると、問題が発生することがあります。それらをパースしようとするのではなく、単に諦めてしまうことがよくあります。このような場合には、AST 強制変換を用いる必要があります。
#![allow(dead_code)] macro_rules! as_expr { ($e:expr) => {$e} } macro_rules! as_item { ($i:item) => {$i} } macro_rules! as_pat { ($p:pat) => {$p} } macro_rules! as_stmt { ($s:stmt) => {$s} } as_item!{struct Dummy;} fn main() { as_stmt!(let as_pat!(_) = as_expr!(42)); }
これらの強制変換は、パーサーに最終的な tt シーケンスを特定の種類の文法構造として扱わせるために、push-down accumulation マクロと併用されることがよくあります。
この特定のマクロ群は、マクロがキャプチャできるものではなく、マクロが何に展開することを許可されているかによって決まることに注意してください。つまり、マクロは型の位置に出現できないため1、as_ty! マクロを持つことはできません。
-
Issue #27245 を参照してください。 ↩
% カウント
置換を伴う反復
マクロ内で何かをカウントするのは、驚くほど厄介な作業です。最も単純な方法は、反復マッチで置換を使用することです。
macro_rules! replace_expr { ($_t:tt $sub:expr) => {$sub}; } macro_rules! count_tts { ($($tts:tt)*) => {0usize $(+ replace_expr!($tts 1usize))*}; } fn main() { assert_eq!(count_tts!(0 1 2), 3); }
これは小さめの数には問題のないアプローチですが、500 個程度のトークンを入力すると、おそらくコンパイラがクラッシュします。出力が次のようになることを考えてみてください。
0usize + 1usize + /* 約500個の `+ 1usize` */ + 1usize
コンパイラはこれを AST にパースする必要があり、その結果、実質的には 500 レベル以上の深さを持つ完全に不均衡な二分木が生成されます。
再帰
より古いアプローチは、再帰を使用することです。
macro_rules! count_tts { () => {0usize}; ($_head:tt $($tail:tt)*) => {1usize + count_tts!($($tail)*)}; } fn main() { assert_eq!(count_tts!(0 1 2), 3); }
注:
rustc1.2 の時点では、未知の型の大量の整数リテラルが推論される必要がある場合、コンパイラには深刻なパフォーマンス上の問題があります。ここではそれを避けるため、明示的にusize型のリテラルを使用しています。これが適切でない場合(型を置換可能にする必要がある場合など)は、
as(例:0 as $ty,1 as $tyなど)を使うことで状況を改善できます。
これは動作しますが、容易に再帰制限を超えてしまいます。反復アプローチとは異なり、一度に複数のトークンにマッチすることで入力サイズを拡張できます。
macro_rules! count_tts { ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt $_f:tt $_g:tt $_h:tt $_i:tt $_j:tt $_k:tt $_l:tt $_m:tt $_n:tt $_o:tt $_p:tt $_q:tt $_r:tt $_s:tt $_t:tt $($tail:tt)*) => {20usize + count_tts!($($tail)*)}; ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt $_f:tt $_g:tt $_h:tt $_i:tt $_j:tt $($tail:tt)*) => {10usize + count_tts!($($tail)*)}; ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt $($tail:tt)*) => {5usize + count_tts!($($tail)*)}; ($_a:tt $($tail:tt)*) => {1usize + count_tts!($($tail)*)}; () => {0usize}; } fn main() { assert_eq!(700, count_tts!( ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, // 反復はこのあたり以降で壊れる ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, )); }
この特定の形式は、約 1,200 トークンまで動作します。
スライス長
3 つ目のアプローチは、スタックオーバーフローにつながらない浅い AST をコンパイラが構築できるようにすることです。これは、配列リテラルを構築し、len メソッドを呼び出すことで実現できます。
macro_rules! replace_expr { ($_t:tt $sub:expr) => {$sub}; } macro_rules! count_tts { ($($tts:tt)*) => {<[()]>::len(&[$(replace_expr!($tts ())),*])}; } fn main() { assert_eq!(count_tts!(0 1 2), 3); }
これは 10,000 トークンまで動作することがテストされており、おそらくさらに大きくできます。欠点は、Rust 1.2 の時点では、これを定数式の生成に使用できないことです。結果は単純な定数に最適化できますが(デバッグビルドではメモリからのロードにコンパイルされます)、それでも定数位置(const の値や固定配列のサイズなど)では使用できません。
ただし、非定数のカウントで問題ない場合、これは非常に推奨される方法です。
Enum によるカウント
このアプローチは、相互に異なる識別子の集合をカウントする必要がある場合に使用できます。さらに、このアプローチの結果は定数として使用できます。
macro_rules! count_idents { ($($idents:ident),* $(,)*) => { { #[allow(dead_code, non_camel_case_types)] enum Idents { $($idents,)* __CountIdentsLast } const COUNT: u32 = Idents::__CountIdentsLast as u32; COUNT } }; } fn main() { const COUNT: u32 = count_idents!(A, B, C); assert_eq!(COUNT, 3); }
この方法には 2 つの欠点があります。第一に、上で示唆したように、有効な識別子(かつキーワードではないもの)しかカウントできず、それらの識別子の重複も許可されません。
第二に、このアプローチは hygienic ではありません。つまり、__CountIdentsLast の代わりに使用する何らかの識別子が入力として与えられると、enum 内のバリアントが重複するため、マクロは失敗します。
% 列挙型の解析
macro_rules! parse_unitary_variants { (@as_expr $e:expr) => {$e}; (@as_item $($i:item)+) => {$($i)+}; // 終了ルール。 ( @collect_unitary_variants ($callback:ident ( $($args:tt)* )), ($(,)*) -> ($($var_names:ident,)*) ) => { parse_unitary_variants! { @as_expr $callback!{ $($args)* ($($var_names),*) } } }; ( @collect_unitary_variants ($callback:ident { $($args:tt)* }), ($(,)*) -> ($($var_names:ident,)*) ) => { parse_unitary_variants! { @as_item $callback!{ $($args)* ($($var_names),*) } } }; // 属性を消費する。 ( @collect_unitary_variants $fixed:tt, (#[$_attr:meta] $($tail:tt)*) -> ($($var_names:tt)*) ) => { parse_unitary_variants! { @collect_unitary_variants $fixed, ($($tail)*) -> ($($var_names)*) } }; // 必要に応じて初期化子付きのバリアントを処理する。 ( @collect_unitary_variants $fixed:tt, ($var:ident $(= $_val:expr)*, $($tail:tt)*) -> ($($var_names:tt)*) ) => { parse_unitary_variants! { @collect_unitary_variants $fixed, ($($tail)*) -> ($($var_names)* $var,) } }; // ペイロードを持つバリアントで中止する。 ( @collect_unitary_variants $fixed:tt, ($var:ident $_struct:tt, $($tail:tt)*) -> ($($var_names:tt)*) ) => { const _error: () = "非単位バリアントを含む enum から単位バリアントを解析できません"; }; // エントリルール。 (enum $name:ident {$($body:tt)*} => $callback:ident $arg:tt) => { parse_unitary_variants! { @collect_unitary_variants ($callback $arg), ($($body)*,) -> () } }; } fn main() { assert_eq!( parse_unitary_variants!( enum Dummy { A, B, C } => stringify(variants:) ), "variants : ( A , B , C )" ); }
このマクロは、インクリメンタル tt muncher と プッシュダウン蓄積 を使用して、すべてのバリアントが単位バリアント(すなわち、ペイロードを持たない)である enum のバリアントを解析する方法を示しています。完了すると、parse_unitary_variants! はバリアントのリスト(および指定されたその他の任意の引数)を指定して コールバック マクロを呼び出します。
これは、struct フィールドの解析、バリアントのタグ値の計算、さらには任意の enum に含まれる すべての バリアント名の抽出も行えるように変更できます。
% 注釈付きの例
このセクションには、その設計と構成を説明するために注釈が付けられた現実世界の1マクロが含まれています。
-
大部分は。 ↩
% Ook!
このマクロは Ook! 難解プログラミング言語の実装であり、Brainfuck 難解プログラミング言語と同型です。
この言語の実行モデルは非常に単純です。メモリは、何らかの不定個数(通常は少なくとも30,000個)の「セル」(一般に少なくとも8ビット)の配列として表現されます。メモリへのポインターがあり、位置0から開始します。最後に、実行スタック(ループを実装するために使用)とプログラム内へのポインターがありますが、これら最後の2つは実行中のプログラムには公開されません。これらはランタイム自体のプロパティです。
この言語自体は、Ook.、Ook?、Ook! の3つのトークンだけで構成されています。これらはペアで組み合わされ、8つの異なる操作を形成します。
Ook. Ook?- ポインターをインクリメントする。Ook? Ook.- ポインターをデクリメントする。Ook. Ook.- ポインターの指すメモリセルをインクリメントする。Ook! Ook!- ポインターの指すメモリセルをデクリメントする。Ook! Ook.- ポインターの指すメモリセルを標準出力に書き込む。Ook. Ook!- 標準入力から読み取り、ポインターの指すメモリセルに格納する。Ook! Ook?- ループを開始する。Ook? Ook!- ポインターの指すメモリセルが0でない場合はループの先頭に戻る。それ以外の場合は続行する。
Ook! はチューリング完全であることが知られているため興味深いです。つまり、これを実装できるどのような環境も、それ自体もチューリング完全でなければならないということです。
実装
#![recursion_limit = "158"]
実はこれは、最後に示すサンプルプログラムが実際にコンパイルされる、可能な限り低い再帰制限です。何がこれほど途方もなく複雑で、デフォルト制限の5倍近い再帰制限を正当化するのか疑問に思っているなら……当ててみてください。
type CellType = u8;
const MEM_SIZE: usize = 30_000;
これらは、マクロ展開から見えるようにすることだけを目的としてここにあります。1
macro_rules! Ook {
名前は標準的な命名規約に合わせて ook! にするべきだったのかもしれませんが、この機会はあまりにも魅力的で見逃せませんでした。
このマクロのルールは、内部ルールパターンを使ってセクションに分割されています。
これらの最初は @start ルールで、残りの展開が行われるブロックのセットアップを担当します。ここには特に興味深いものはありません。いくつかの変数とヘルパー関数を定義し、その後、展開の大部分を行います。
いくつか小さな注意点:
- 主に、エラー処理を簡素化するために
try!を使えるよう、関数へ展開しています。 - アンダースコアで始まる名前を使っているのは、たとえばユーザーがI/Oを行わないOok!プログラムを書いた場合でも、未使用の関数や変数についてコンパイラーが警告しないようにするためです。
(@start $($Ooks:tt)*) => {
{
fn ook() -> ::std::io::Result<Vec<CellType>> {
use ::std::io;
use ::std::io::prelude::*;
fn _re() -> io::Error {
io::Error::new(
io::ErrorKind::Other,
String::from("ran out of input"))
}
fn _inc(a: &mut [u8], i: usize) {
let c = &mut a[i];
*c = c.wrapping_add(1);
}
fn _dec(a: &mut [u8], i: usize) {
let c = &mut a[i];
*c = c.wrapping_sub(1);
}
let _r = &mut io::stdin();
let _w = &mut io::stdout();
let mut _a: Vec<CellType> = Vec::with_capacity(MEM_SIZE);
_a.extend(::std::iter::repeat(0).take(MEM_SIZE));
let mut _i = 0;
{
let _a = &mut *_a;
Ook!(@e (_a, _i, _inc, _dec, _r, _w, _re); ($($Ooks)*));
}
Ok(_a)
}
ook()
}
};
オペコード解析
次は「execute」ルールで、入力からオペコードを解析するために使用されます。
これらのルールの一般形は (@e $syms; ($input)) です。@start ルールから分かるように、$syms はプログラムを実際に実装するために必要なシンボルの集合です: 入力、出力、メモリ配列、など。これらのシンボルを後続の中間ルールへ転送するのを簡単にするため、TTバンドリングを使用しています。
まず、再帰を終了させるルールです。入力がもうなくなったら停止します。
(@e $syms:tt; ()) => {};
次に、各オペコードのほぼそれぞれに対して1つのルールがあります。これらでは、オペコードを取り除き、対応するRustコードを出力し、その後、入力の残り部分に対して再帰します: 典型的なインクリメンタルTT muncherです。
// ポインターをインクリメントする。
(@e ($a:expr, $i:expr, $inc:expr, $dec:expr, $r:expr, $w:expr, $re:expr);
(Ook. Ook? $($tail:tt)*))
=> {
$i = ($i + 1) % MEM_SIZE;
Ook!(@e ($a, $i, $inc, $dec, $r, $w, $re); ($($tail)*));
};
// ポインターをデクリメントする。
(@e ($a:expr, $i:expr, $inc:expr, $dec:expr, $r:expr, $w:expr, $re:expr);
(Ook? Ook. $($tail:tt)*))
=> {
$i = if $i == 0 { MEM_SIZE } else { $i } - 1;
Ook!(@e ($a, $i, $inc, $dec, $r, $w, $re); ($($tail)*));
};
// ポインターの指すセルをインクリメントする。
(@e ($a:expr, $i:expr, $inc:expr, $dec:expr, $r:expr, $w:expr, $re:expr);
(Ook. Ook. $($tail:tt)*))
=> {
$inc($a, $i);
Ook!(@e ($a, $i, $inc, $dec, $r, $w, $re); ($($tail)*));
};
// ポインターの指すセルをデクリメントする。
(@e ($a:expr, $i:expr, $inc:expr, $dec:expr, $r:expr, $w:expr, $re:expr);
(Ook! Ook! $($tail:tt)*))
=> {
$dec($a, $i);
Ook!(@e ($a, $i, $inc, $dec, $r, $w, $re); ($($tail)*));
};
// stdout に書き込む。
(@e ($a:expr, $i:expr, $inc:expr, $dec:expr, $r:expr, $w:expr, $re:expr);
(Ook! Ook. $($tail:tt)*))
=> {
try!($w.write_all(&$a[$i .. $i+1]));
Ook!(@e ($a, $i, $inc, $dec, $r, $w, $re); ($($tail)*));
};
// stdin から読み取る。
(@e ($a:expr, $i:expr, $inc:expr, $dec:expr, $r:expr, $w:expr, $re:expr);
(Ook. Ook! $($tail:tt)*))
=> {
try!(
match $r.read(&mut $a[$i .. $i+1]) {
Ok(0) => Err($re()),
ok @ Ok(..) => ok,
err @ Err(..) => err
}
);
Ook!(@e ($a, $i, $inc, $dec, $r, $w, $re); ($($tail)*));
};
ここから物事はより複雑になります。このオペコード Ook! Ook? はループの開始を示します。Ook! のループは次のRustコードに変換されます:
> **注記**: これは、より大きなコードの一部では*ありません*。
>
> ```ignore
> while memory[ptr] != 0 {
> // ループの内容
> }
> ```
もちろん、実際には不完全なループを出力することはできません。これは [pushdown](../pat/README.html#push-down-accumulation) を使えば解決*できるかもしれません*が、より根本的な問題があります。つまり、`while memory[ptr] != {` を*どこにも*、まったく*書けない*のです。なぜなら、そうすると対応の取れていない波括弧が導入されてしまうためです。
これに対する解決策は、実際に入力を 2 つの部分、すなわちループの*内側*にあるすべてと、その*後*にあるすべてに分割することです。`@x` ルールが前者を処理し、`@s` が後者を処理します。
```ignore
(@e ($a:expr, $i:expr, $inc:expr, $dec:expr, $r:expr, $w:expr, $re:expr);
(Ook! Ook? $($tail:tt)*))
=> {
while $a[$i] != 0 {
Ook!(@x ($a, $i, $inc, $dec, $r, $w, $re); (); (); ($($tail)*));
}
Ook!(@s ($a, $i, $inc, $dec, $r, $w, $re); (); ($($tail)*));
};
ループ抽出
次は @x、つまり「抽出」ルールです。これらは入力 tail を受け取り、ループの内容を抽出する役割を持ちます。これらのルールの一般的な形式は (@x $sym; $depth; $buf; $tail) です。
$sym の目的は上記と同じです。$tail は解析対象の入力であり、$buf はループの内側にあるオペコードを収集するための push-down accumulation buffer です。では、$depth は何でしょうか。
ここで複雑なのは、ループがネストできるという点です。そのため、現在どれだけ深いレベルにいるのかを追跡する何らかの方法が必要です。解析を早く止めすぎることも、遅く止めすぎることもなく、レベルがちょうどよいときに止められる程度に、これを正確に追跡しなければなりません。2
マクロでは算術を行えず、明示的な整数マッチングルールを書き出すのも現実的ではありません(自明でない数の正の整数に対して、以下のルールをすべてコピー & ペーストすることを想像してください)。そのため、代わりに歴史上最も古く由緒ある数え方の 1 つ、つまり指で数える方法に戻ることにします。
しかし、マクロには指がありません。そこで代わりに token abacus counter を使います。具体的には @ を使い、それぞれの @ が深さの追加レベル 1 つを表します。これらの @ をグループ内に収めておけば、必要な 3 つの重要な操作を実装できます。
- インクリメント:
($($depth:tt)*)にマッチし、(@ $($depth)*)に置換する。 - デクリメント:
(@ $($depth:tt)*)にマッチし、($($depth)*)に置換する。 - ゼロとの比較:
()にマッチする。
最初は、解析中のループを閉じる、対応する Ook? Ook! シーケンスを見つけたことを検出するルールです。この場合、蓄積されたループ内容を、前に定義した @e ルールに渡します。
残りの入力 tail に対して何かを行う必要はないことに注意してください(それは @s ルールによって処理されます)。
(@x $syms:tt; (); ($($buf:tt)*);
(Ook? Ook! $($tail:tt)*))
=> {
// 最外側のループが閉じられた。バッファ済みのトークンを処理する。
Ook!(@e $syms; ($($buf)*));
};
次に、ネストしたループに入るためのルールと、そこから出るためのルールがあります。これらはカウンターを調整し、オペコードをバッファに追加します。
(@x $syms:tt; ($($depth:tt)*); ($($buf:tt)*);
(Ook! Ook? $($tail:tt)*))
=> {
// 1 レベル深くなる。
Ook!(@x $syms; (@ $($depth)*); ($($buf)* Ook! Ook?); ($($tail)*));
};
(@x $syms:tt; (@ $($depth:tt)*); ($($buf:tt)*);
(Ook? Ook! $($tail:tt)*))
=> {
// 1 レベル浅くなる。
Ook!(@x $syms; ($($depth)*); ($($buf)* Ook? Ook!); ($($tail)*));
};
最後に、「それ以外すべて」に対するルールがあります。$op0 と $op1 のキャプチャに注意してください。Rust から見れば、私たちの Ook! トークンは常に 2 つの Rust トークン、すなわち識別子 Ook と、もう 1 つのトークンです。そのため、!、?、. を tt としてマッチすることで、ループ以外のすべてのオペコードを一般化できます。
ここでは、$depth には手を加えず、単にオペコードをバッファに追加します。
(@x $syms:tt; $depth:tt; ($($buf:tt)*);
(Ook $op0:tt Ook $op1:tt $($tail:tt)*))
=> {
Ook!(@x $syms; $depth; ($($buf)* Ook $op0 Ook $op1); ($($tail)*));
};
ループスキップ
これは、ループ抽出とおおむね同じですが、ループの内容には関心がありません(したがって、蓄積バッファも不要です)。知る必要があるのは、いつループを通過したかだけです。その時点で、@e ルールを使って入力の処理を再開します。
したがって、これらのルールはこれ以上の説明なしで提示します。
// ループの終わり。
(@s $syms:tt; ();
(Ook? Ook! $($tail:tt)*))
=> {
Ook!(@e $syms; ($($tail)*));
};
// ネストしたループに入る。
(@s $syms:tt; ($($depth:tt)*);
(Ook! Ook? $($tail:tt)*))
=> {
Ook!(@s $syms; (@ $($depth)*); ($($tail)*));
};
// ネストしたループを抜ける。
(@s $syms:tt; (@ $($depth:tt)*);
(Ook? Ook! $($tail:tt)*))
=> {
Ook!(@s $syms; ($($depth)*); ($($tail)*));
};
// ループのオペコードではない。
(@s $syms:tt; ($($depth:tt)*);
(Ook $op0:tt Ook $op1:tt $($tail:tt)*))
=> {
Ook!(@s $syms; ($($depth)*); ($($tail)*));
};
エントリーポイント
これは唯一の非内部ルールです。
この定式化は、渡されたすべてのトークンに単純にマッチするため、極めて危険であることは注目に値します。何らかの間違いによって呼び出しが上記すべてのルールにマッチできなくなると、このルールまで落ちてきて、無限再帰を引き起こします。
このようなマクロを書いている、変更している、またはデバッグしているときは、このルールのようなものに一時的に @entry のような接頭辞を付けるのが賢明です。これにより無限再帰のケースを防ぎ、適切な場所でマッチャーエラーを得られる可能性が高くなります。
($($Ooks:tt)*) => {
Ook!(@start $($Ooks)*)
};
}
使用法
これが、最後に、私たちのテストプログラムです。
fn main() {
let _ = Ook!(
Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook! Ook? Ook? Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook? Ook! Ook! Ook? Ook! Ook? Ook.
Ook! Ook. Ook. Ook? Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook! Ook? Ook? Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook?
Ook! Ook! Ook? Ook! Ook? Ook. Ook. Ook.
Ook! Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook! Ook. Ook! Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook! Ook. Ook. Ook? Ook. Ook?
Ook. Ook? Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook! Ook? Ook? Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook?
Ook! Ook! Ook? Ook! Ook? Ook. Ook! Ook.
Ook. Ook? Ook. Ook? Ook. Ook? Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook! Ook? Ook? Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook? Ook! Ook! Ook? Ook! Ook? Ook.
Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook.
Ook? Ook. Ook? Ook. Ook? Ook. Ook? Ook.
Ook! Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook! Ook. Ook! Ook! Ook! Ook! Ook! Ook!
Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook.
Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook!
Ook! Ook! Ook! Ook! Ook! Ook! Ook! Ook!
Ook! Ook. Ook. Ook? Ook. Ook? Ook. Ook.
Ook! Ook. Ook! Ook? Ook! Ook! Ook? Ook!
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook. Ook. Ook. Ook.
Ook. Ook. Ook. Ook. Ook! Ook.
);
}
実行したときの出力(コンパイラが数百回もの再帰的なマクロ展開を行うため、かなり待たされた後)は次のとおりです。
Hello World!
これにより、macro_rules! がチューリング完全であるという恐ろしい真実を示したことになります!
余談
これは "Hodor!" という同型の言語を実装するマクロをベースにしています。その後、Manish Goregaokar が Hodor! マクロを使って Brainfuck インタープリターを実装しました。つまり、これは macro_rules! を使って実装された Hodor! で書かれた Brainfuck インタープリターです。
伝説によれば、再帰制限を 300万 まで引き上げ、4日間 実行し続けた後、ついに完了したそうです。
...スタックをオーバーフローさせて異常終了することで。今日に至るまで、マクロとしての esolang は Rust における開発手法として明らかに実用に耐えないままです。