RustマクロでFizz Buzz
RustのマクロでFizz Buzzを書いてみた。
このFizz Buzzは、ループと倍数判定をマクロで処理している。10進数として表示する部分はマクロではなくRust本体に任せていりう。
動作の説明
Rustの macro_rules!
でメタプログラミングを行う場合、トークン単位での処理が基本となる。そのため使えるマッチャーは基本的に tt
マッチャーだけである。 tt
マッチャーは、トークンツリー(括弧以外のトークンか、括弧で囲まれた任意のトークン列)1個にマッチする。
他のマッチャーを使うと、その部分は構文解析されてASTに変換される。ASTに変換された部分は単一のトークンとみなされてしまい、 macro_rules!
からは非透過的にしか扱えない。
macro_rules! fizzbuzz_cond {
...
}
このマクロは、特定の2進数に対するFizzBuzz判定を呼ぶ。引数は、
- 入力した2進数の残り (0,1のトークン列で表現する。大きい桁から順番に消費する)
- 消費した部分のmod 3 (1の個数で表現する)
- 消費した部分のmod 5 (1の個数で表現する)
- 消費した部分をあらわす整数型の式 (printするために使う)
である。このマクロは次のように動作する。
- mod 3とmod 5をあらわす部分が大きすぎる場合は、削ってやり直す。
- まだ入力が残っていれば、これを処理する。処理中の数を2倍するか、2倍して1を足す。
- 入力が残っていなければ、printをするための文を出力する。このときmod 3とmod 5に応じて分岐する。
macro_rules! fizzbuzz_rec {
...
}
このマクロは、指定された範囲のFizzBuzzを実行する。引数は、
- 開始位置と終了位置をあらわす、同じ桁数の2つの2進数。
(0 1 1 0), (1 0 1 1)
のような別々の形ではなく、桁ごとにzipした((0, 1) (1, 0) (1, 1) (0, 1))
のような形で持つ。これは桁数の不一致によるミスを防ぐほかに重要な意味がある。この方法だと、全ての桁を0で置きかえる操作がしやすい。 - 確定した部分の2進数。
である。 fizzbuzz_rec!
は、2進数の桁の構造にもとづいて再帰的に fizzbuzz_rec!
を呼ぶ。このような仕組みにしているのは、こうすると再帰深度をlogに抑えることができるからである。
macro_rules! fizzbuzz {
...
}
このマクロは、単に fizzbuzz_rec!
を呼び出すドライバである。
Rustにおけるマクロメタプログラミングの特徴
実用的には、Rustの宣言的マクロは、boilerplate的なコードを大量に作る必要があり、型多相性を駆使するよりも構文的に生成したほうがうまくいく場合に、強力な効果を発揮する。むろん、このFizz Buzzのような例は遊び以上の意味はない。
マクロメタプログラミングをしてみてわかった強い制約は、マクロ呼び出しの結果の再利用の難しさである。マクロの入力がトークン列であるのに対して、マクロの出力は抽象構文木である。抽象構文木はトークン列として再解釈できるものの、各抽象構文木は単一のトークンでラップされてしまい、宣言的マクロからは透過的にしか取り扱えない。これは実用上はほぼ問題ないが、マクロメタプログラミングにとっては重大な障壁となる。関数によるプログラムの構造化を妨げているからである。これにより、マクロメタプログラミングでは実質的に、末尾再帰的な方法でしかプログラムを構造化できないことになる。
それに加えて、Rustのマクロは厳しい展開深度制限がある(crateの属性で拡張できる)。展開深度制限は通常、今回実装したように再帰深度をlogに抑えるテクニックを用いて回避することが多い。しかし、Rustマクロメタプログラミングでは末尾再帰的な書き方しかできないから、このテクニックが一般的には使えないと考えられる。
Rustのマクロメタプログラミングは、Cのプリプロセッサメタプログラミングと比べても、決して自由度が高いとはいえないだろう。これはもちろん悪いことではない。メタプログラミングの容易さは、コンパイラやソースコード解析ツールなどにとっての扱いやすさとトレードオフの関係にあるからである。Rustのマクロメタプログラミングがこのように難しいのもまたこのトレードオフの結果だと考えられる。またそもそも、遊びとしてのメタプログラミングは、このように少し難しいくらいが面白い。