マクロ: 実践的説明
本章では、Rustの宣言的なMacro-By-Exampleのシステムについて、比較的シンプルで実践的な例を通して説明していきます。
高水準な視点からの説明としては、他にもThe Rust Bookのマクロの章があります。 また本書の形式的説明の章では、このマクロシステムについて詳細に説明しています。
背景を少し
Note: 落ち着いて! これに続くのはマクロの説明に関係するちょっとした数学の話です。 早くこの章の本題に入りたいのであれば、この節を飛ばして読んでも大丈夫です。
詳しくない方向けに説明すると、漸化式とは、各値が1つ以上前の値に基づいて定まる数列で、全ての始まりである1つ以上の初期値を伴います。 例えば、フィボナッチ数列1は次の漸化式により定義されます:
\[F_{n} = 0, 1, ..., F_{n-2} + F_{n-1}\]
訳注: 日本語版はこちら。
したがって、数列の最初の2つの数は 0 と 1、3番めは \( F_{0} + F_{1} = 0 + 1 = 1\)、 4番めは \( F_{1} + F_{2} = 1 + 1 = 2\)、という具合に無限に続きます。
さて、このような数列は無限に続くため、fibonacci
関数を定義するのは少しややこしい作業になります。というのも、明らかに完全なベクタを返すべきではないからです。
ここですべきことは、必要に応じて数列の要素を遅延的に計算する何かを返すことです。
Rustにおいて、これはIterator
を生成せよ、ということです。
これは特別難しいことではありませんが、かなりの量のボイラープレートを必要とします。独自の型を定義し、その型に保存すべき状態を考え出し、Iterator
トレイトを実装する必要があります。
ですが、小さなmacro_rules!
マクロに基づくコード生成だけでこれらの詳細のほとんどを括りだすことができるくらい、漸化式はシンプルです。
それでは、以上のことを踏まえて、早速始めていきましょう。
構成要素
たいてい、新しい macro_rules!
マクロの実装に取りかかるとき、私が初めにするのはその呼び出し方を決めることです。
今回のケースでは、最初の試行は次のようなものになりました:
let fib = recurrence![a[n] = 0, 1, ..., a[n-2] + a[n-1]];
for e in fib.take(10) { println!("{}", e) }
これをもとに、実際の展開形について確信は持てなくとも、macro_rules!
マクロがどのように定義されるべきかを考えてみることはできます。
入力の構文をパースする方法を思いつけないのであれば、構文を変更する必要があるかもしれないということなので、これは有用な考え方です。
macro_rules! recurrence {
( a[n] = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ };
}
fn main() {}
この構文は見慣れないものだと思いますので、少し説明させてください。
これは recurrence!
という名前の、macro_rules!
のシステムを使った構文拡張の定義になります。
この macro_rules!
マクロはただ一つの構文ルールを持っています。
そのルールは、呼び出しの入力が次のものに一致しなければならないというものです:
- リテラルトークンの列
a
[
n
]
=
,
を区切りとする、1回以上 (+
) の妥当な式の繰り返し ($( ... )
)。この式はメタ変数inits
に捕捉される ($inits:expr
)- リテラルトークンの列
,
...
,
- 妥当な式。この式はメタ変数
recur
に捕捉される ($recur:expr
)
結局、このルールは、もし入力がこのルールに一致したら、マクロの呼び出しを /* ... */
というトークンの列で置き換えよ、ということを表しています。
inits
は、その名前が示唆するように、最初や最後だけではなく、その位置にあるすべての式を含むことに注意してください。
さらにいえば、inits
は、それらの式を不可逆的にまとめてペーストするような形ではなく、列として捕捉します。
また、+
の代わりに *
を使えば「0回以上」の繰り返しを、?
を使えば「任意」、つまり「0回か1回」の繰り返しを表せます。
練習として、先に挙げた入力をこのルールに与えてみて、どのように処理されるか見てみましょう。
「位置」欄では、次に構文パターンのどの部分がマッチングされるかを「 ⌂ 」で示しています。
ある状況では、マッチング対象となる「次」の要素の候補が複数存在することがあるのに注意してください。
「入力」欄は、まだ消費されていないトークンです。
inits
・recur
欄はそれらに捕捉されている内容です。
位置 | 入力 | inits |
recur |
---|---|---|---|
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
a[n] = 0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
[n] = 0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
n] = 0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
] = 0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
= 0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
0, 1, ..., a[n-2] + a[n-1] |
||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂ ⌂
|
, 1, ..., a[n-2] + a[n-1] |
0 |
|
Note: ここには2つの ⌂ がある。これは次の入力トークンが、繰り返しの要素間のコンマ区切りか、繰り返しの後のコンマのどちらかにマッチしうるため。 マクロシステムは、どちらに従うべきかが確定するまでの間、両方の可能性を追跡する。 | |||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂ ⌂
|
1, ..., a[n-2] + a[n-1] |
0 |
|
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂ ⌂
|
, ..., a[n-2] + a[n-1] |
0 , 1 |
|
Note: 3つめの取り消し線つきのマーカーは、最後のトークンの消費の結果、マクロシステムがありうる選択肢の1つをふるい落としたことを表す。 | |||
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂ ⌂
|
..., a[n-2] + a[n-1] |
0 , 1 |
|
a[n] = $($inits:expr),+ , ... , $recur:expr
|
, a[n-2] + a[n-1] |
0 , 1 |
|
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
a[n-2] + a[n-1] |
0 , 1 |
|
a[n] = $($inits:expr),+ , ... , $recur:expr
⌂
|
0 , 1 |
a[n-2] + a[n-1] |
|
Note: このステップは、コンパイラが持つ「妥当な式の構成要素」に関する知識を用いて、$recur:exprのような束縛が式全体を消費することを明確にする。 後述するように、他の言語要素に対してもこれを行うことができる。 |
ここで重要なのは、マクロシステムが、入力として与えられたトークンたちを所与のルールに対してインクリメンタルにマッチングを試みるということです。 「試みる」という部分については後で補足します。
さて、最後の、完全に展開された形を書きはじめましょう。 この展開に対しては、次のようなものが求められています:
let fib = {
struct Recurrence {
mem: [u64; 2],
pos: usize,
}
これは実際のイテレータ型になるべきものです。
mem
は漸化式を計算するのに必要となる、直近の数個の値を保持するメモバッファになります。
pos
は n
の値を追跡するための変数です。
余談:
u64
は、数列の要素を表すのに「十分大きな」型として選びました。 これが他の数列に対して上手くいくかを心配する必要はありません。きっと上手くいきますよ。
impl Iterator for Recurrence {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
if self.pos < 2 {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
数列の初期値を生成する分岐が必要です。難しいところはないでしょう。
} else {
let a = /* something */;
let n = self.pos;
let next_val = a[n-2] + a[n-1];
self.mem.TODO_shuffle_down_and_append(next_val);
self.pos += 1;
Some(next_val)
}
}
}
こちらはちょっと難しいです。a
を厳密にどう定義するかについては、あとで見ていきます。
TODO_shuffle_down_and_append
も仮実装になっています。
ここには、next_val
を配列の末尾に配置し、残りの要素を1つずつずらし、最初の要素を削除するものが必要です。
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;
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 = /* something */;
let n = self.pos;
let next_val = (a[n-2] + a[n-1]);
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
構造体とその実装を定義することを意味します。 ほとんどの部分は最終的なバイナリ上では最適化されるでしょう。
展開形を書きながら、それを見直すのも有用です。
展開形の中に、呼び出しのたびに異なるべき何かがあって、それがマクロが実際に受け入れる構文の中にないのであれば、それをどこに導入するか考える必要があります。
今回の例では、u64
を追加しましたが、それはユーザにとってもマクロ構文中にも必ずしも必要なものではありません。修正しましょう。
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ }; } /* let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-2] + a[n-1]]; for e in fib.take(10) { println!("{}", e) } */ fn main() {}
新たにメタ変数 sty
を追加しました。これは型にマッチします。
余談: メタ変数のコロン以降の部分は、マッチする構文の種類を表します。 よく使われるのは
item
,expr
, そしてty
です。 詳しい説明は 「マクロ: 形式的説明」 の章の 「メタ変数」の項目をご覧ください。もう一つ知っておくべきことがあります。言語の将来の変化に備える意味で、コンパイラはマッチパターンの種類に応じて、そのあとに続けられるトークンの種類に制限を設けています。 概して、これは式や文にマッチングさせようとしたときに問題になります。 これらのあとに続けられるのは
=>
,,
,;
のみとなります。完全なリストは「枝葉末節」の章の「メタ変数と展開・再考」の節にあります。
添字付け (indexing) と入れ替え
マクロの話からそれることになるので、ここはさらっと流そうと思います。
a
に添字にアクセス機能をつけることで、ユーザが数列の前のほうの値にアクセスできるようにしたいです。
これは、数列の直近の数個(今回の例では2個)の要素を保持するスライディングウィンドウのように動きます。
ラッパー型によって、いとも簡単にこれを実現できます:
struct IndexOffset<'a> {
slice: &'a [u64; 2],
offset: usize,
}
impl<'a> Index<usize> for IndexOffset<'a> {
type Output = u64;
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
を用いてIndexOffset
がその参照をいつまで保持できるかを追跡する必要があります。
'b
が用いられているのは、Index::index
関数 (添字記法 (subscript syntax) の実装本体) もまた、借用された参照を返すためにライフタイムによってパラメータ化されているためです。'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-2] + a[n-1]]; 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-2] + a[n-1] }; { use std::mem::swap; let mut swap_tmp = next_val; for i in [1,0] { 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
の宣言の順序が入れ替わっており、さらにそれらが(漸化式の計算式と一緒に)ブロックで囲まれていることに注意してください。
前者の理由は明白でしょう(n
を a
の初期化で使うため)。
後者の理由は、参照の借用 &self.mem
が、その後の入れ替え処理の実行を妨げてしまうためです(別の場所で借用された値を変更することはできません)。
このブロックにより、&self.mem
の借用は入れ替え処理よりも前に失効するようになります。
ちなみに、mem
swap を実行するコードをブロックで囲んでいるのは、コードの整頓の目的で、std::mem::swap
が使えるスコープを限定するためでしかありません。
このコードを実行すると、次の結果が得られます:
0
1
1
2
3
5
8
13
21
34
成功です! さて、これをマクロの展開形の部分にコピー & ペーストして、展開結果のコードをマクロの呼び出しに置き換えてみましょう。 次のようになります:
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { { /* What follows here is *literally* the code from before, cut and pasted into a new position. No other changes have been made. */ 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; 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; 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-2] + a[n-1]) }; { 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-2] + a[n-1]]; for e in fib.take(10) { println!("{}", e) } }
明らかに、まだメタ変数を使っていませんが、メタ変数を使う形に変更するのはとても簡単です。
しかし、これをコンパイルしようとすると、rustc
は次のような文句を言って中断します:
error: local ambiguity: multiple parsing options: built-in NTs expr ('inits') or 1 other option.
--> src/main.rs:75:45
|
75 | let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-2] + a[n-1]];
|
ここで、我々は macro_rules!
システムの限界に達してしまいました。
問題となるのは2つめのコンマです。
展開中にそのコンマを見た段階で、macro_rules!
は次に inits
のためのもう一つの式と、...
のどちらをパースすべきかを決めることができないのです。
悲しいことに、...
が妥当な式ではないと気づけるほど macro_rules!
システムは賢くないので、諦めてしまいます。
理論的には、これは思ったとおりに動作すべきですが、現時点ではそうなっていません。
余談: 我々のルールがマクロシステムによってどのように解釈されるかについて、私は少し嘘をつきました。 一般に、それは書いたように動くべきですが、今回のような場合は動きません。 現状、
macro_rules
の機構には弱点があり、うまく動くようにするためには形を少し歪める必要がある、ということを折に触れて思い出すとよいでしょう。今回の例においては、2つの問題があります。 1つめは、マクロシステムが、多種多様な文法要素(例: 式)について、何が構成要素となり、何が構成要素となりえないのかに関する知識を持たないということです。これはパーサの仕事なのです。 そのため、マクロシステムは
...
が式になりえないことを知りません。 2つめは、(式のような)複合的な文法要素を捕捉しようとするには、それに100%身を捧げるしかないということです。言い換えると、マクロシステムはパーサに何らかの入力を式としてパースするよう依頼することができますが、パーサは任意の問題に対して「中断」という形で応える、ということです。 現状、マクロシステムがこれに対処するための唯一の方法は、それが問題になるような状況を禁じることだけです。
明るい面を挙げるとすれば、誰もこの事態について躍起にはなっていないということです。 より綿密に定義された、未来のマクロシステムのために、
macro
というキーワードがすでに予約されています。 これが使えるようになるまでは、やりたくなくてもそうするしかありません。
ありがたいことに、修正は比較的シンプルに済みます。構文からコンマを取り除くのです。
バランスを取るために、...
の両側のコンマを取り除きましょう:
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => { // ^~~ changed /* ... */ // Cheat :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-2] + a[n-1]]; // ^~~ changed for e in fib.take(10) { println!("{}", e) } }
やったか!?と思いきや…
以前は問題なかったにもかかわらず、これはコンパイラによって拒否されてしまいます。
理由は、コンパイラは今 ...
をトークンとして認識するようになり、ご存知のように式フラグメントの後ろでは =>
, ,
または ;
しか使えないためです。
よって、残念ながら我々が夢見た構文は動作しません。運は尽きました。代わりに使える中で、最も相応しいものを選びましょう。,
を ;
に書き換えます。
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { // ^~~~~~^ changed /* ... */ // Cheat :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-2] + a[n-1]]; // ^~~~~^ changed for e in fib.take(10) { println!("{}", e) } }
やりました!今回は本当に成功です。
置換
マクロによって捕捉したものを使って置換を行うのはとても簡単です。メタ変数 $sty:ty
の中身を $ty
を使って挿入できます。
では、u64
を直していきましょう:
macro_rules! recurrence { ( a[n]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { { use std::ops::Index; struct Recurrence { mem: [$sty; 2], // ^~~~ changed pos: usize, } struct IndexOffset<'a> { slice: &'a [$sty; 2], // ^~~~ changed offset: usize, } impl<'a> Index<usize> for IndexOffset<'a> { type Output = $sty; // ^~~~ changed #[inline(always)] fn index<'b>(&'b self, index: usize) -> &'b $sty { // ^~~~ changed 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; // ^~~~ changed #[inline] fn next(&mut self) -> Option<$sty> { // ^~~~ changed /* ... */ 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-2] + a[n-1]) }; { 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-2] + a[n-1]]; for e in fib.take(10) { println!("{}", e) } }
もっと難しいことに挑戦してみましょう。inits
を配列リテラル [0, 1]
と 配列の型 [$sty; 2]
の両方に変換するにはどうすればよいでしょう。
最初にできるのはこんな感じのことです:
Recurrence { mem: [$($inits),+], pos: 0 }
// ^~~~~~~~~~~ changed
これは実質的にキャプチャと逆のことをしています。コンマで区切りつつ、inits
を1回以上繰り返すのです。
これは期待されているトークン列 0, 1
に展開されます。
inits
を リテラル 2
にするのは少し大変そうです。
結局のところこれを直接行う方法はないのですが、もう一つの macro_rules!
マクロを使えば可能です。
一歩ずつ進んでいきましょう。
macro_rules! count_exprs { /* ??? */ () => {} } fn main() {}
自明なケースである、0個の式が与えられたときは、 count_exprs
は リテラル 0
に展開されるべきです。
macro_rules! count_exprs { () => (0); // ^~~~~~~~~~ added } 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); // ^~~~~~~~~~~~~~~~~ added } 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); // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ added } 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)); // ^~~~~~~~~~~~~~~~~~~~~ changed } 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)); // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ added } 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); }
余談: もしルールの順序を逆にしたとしても問題ないのだろうか、と思った方がいるかもしれません。 今回の例に限っていえば、問題ありません。しかし、マクロシステムは時にルールの順序にうるさくなり、なかなか言うことを聞かなくなることがあります。 間違いなく動くはずだと思っていた複数のルールを持つマクロが、「予期しないトークン(unexpected tokens)」というエラーを出すようであれば、ルールの順序を変えてみましょう。
パターンが見えてきたのではないでしょうか。 1つの式とそれに続く0個以上の式にマッチングさせ、1 + (残りのカウント) の形に展開することで、式のリストを畳み込む(reduce)ことができます。
macro_rules! count_exprs { () => (0); ($head:expr) => (1); ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ changed } 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
に必要なサイズを割り出すようにできます。
// added: 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),+); // ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ added struct Recurrence { mem: [$sty; MEM_SIZE], // ^~~~~~~~ changed pos: usize, } struct IndexOffset<'a> { slice: &'a [$sty; MEM_SIZE], // ^~~~~~~~ changed 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); // ^~~~~~~~ changed 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 { // ^~~~~~~~ changed 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-2] + a[n-1]) }; { use std::mem::swap; let mut swap_tmp = next_val; for i in (0..MEM_SIZE).rev() { // ^~~~~~~~ changed 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-2] + a[n-1]]; for e in fib.take(10) { println!("{}", e) } }
inits
の置換はこれで完成したので、ついに最後の recur
式の置換に移れます。
macro_rules! count_exprs { () => (0); ($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<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 }; $recur // ^~~~~~ changed }; { 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 = 1, 1; ...; a[n-2] + a[n-1]]; for e in fib.take(10) { println!("{}", e) } }
そして、完成した macro_rules!
マクロをコンパイルすると...
error[E0425]: cannot find value `a` in this scope
--> src/main.rs:68:50
|
68 | let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-2] + a[n-1]];
| ^ not found in this scope
error[E0425]: cannot find value `n` in this scope
--> src/main.rs:68:52
|
68 | let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-2] + a[n-1]];
| ^ not found in this scope
error[E0425]: cannot find value `a` in this scope
--> src/main.rs:68:59
|
68 | let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-2] + a[n-1]];
| ^ not found in this scope
error[E0425]: cannot find value `n` in this scope
--> src/main.rs:68:61
|
68 | let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-2] + a[n-1]];
| ^ not found in this scope
... 待て、何だって? そんなはずは... マクロがどう展開されているか確かめてみましょう。
$ rustc +nightly -Zunpretty=expanded recurrence.rs
-Zunpretty=expanded
引数は rustc
にマクロの展開を行うように伝え、それから結果の抽象構文木をソースコードに戻します。
出力(フォーマット整理済)を以下に示します。
特に、$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ビルドに食わせると、なんとコンパイルが通ります! ... 何だって?!
余談: 上のコードはnightlyビルドでない
rustc
ではコンパイルできません。 これはprintln!
マクロの展開形が、公に標準化されていないコンパイラの内部詳細に依存しているためです。
衛生的に行こう
ここでの問題は、Rustの構文拡張において識別子が衛生的(hygienic)であるということです。 つまり、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
とは別のコンテキストにあります。
そのため、字句的な見た目が同じにもかかわらず、コンパイラはそれらを完全に別の識別子として扱います。
これは macro_rules!
マクロを扱う際、さらには一般の構文拡張を扱う際に、本当に注意すべきことです。
同じ見た目の抽象構文木であっても、マクロが出力したものはコンパイルに失敗し、手書きや -Zunpretty=expanded
を用いてダンプした結果であればコンパイルに成功する、ということがありうるのです。
これに対する解決策は、識別子を適切な構文コンテキストにおいて捕捉することです。 そのためには、再度マクロの構文を調整する必要があります。 引き続き単純な例で考えます:
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
と n
を明示的に捕捉することで、recurrence!
マクロに対してもこの調整を行うことができます。
必要な変更を加えると、次のようになります:
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 ) => { // ^~~~~~~~~~ ^~~~~~~~~~ changed { 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; // ^~~~ changed let $seq = IndexOffset { slice: &self.mem, offset: $ind }; // ^~~~ changed $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-2] + a[n-1]]; 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
成功です!