% マクロ、実践的な入門
この章では、比較的単純で実践的な例を使って、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
成功しました!