Rustには、宣言的マクロ(Declarative macro)と手続き的マクロ(Procedural macro)があるのですが、今回は比較的書く機会の多い宣言的マクロについて記載します。
宣言的マクロとはなにか?
Rustでは、メタプログラミングの機能としてマクロが存在しています。
宣言的マクロは、コードの置き換えをした結果のコードを生成するものです(メタですね)。
実際の例を見たほうが早いと思います(コードは println!
のパクリです)。
use std::concat; macro_rules! format_ln { // 指定なしの場合は、改行のみを返す () => ("\n"); // 式のみの場合は、それに改行を付けて返す ($fmt:expr) => (concat!($fmt, "\n")); // 式 + 複数のsingle token tree の場合は、最初の引数をフォーマットとして扱い、最後に改行を付けて返す ($fmt:expr, $($arg:tt)*) => (format!(concat!($fmt, "\n"), $($arg)*).as_str()); } fn main() { print!(format_ln!()); print!(format_ln!("str")); print!("{}", format_ln!("formatted: {}!", "foo")); }
コメントで補足していますが、よくわからない部分が多いかと思います。
ただなんとなく、パターンマッチのように渡されるデータの違いによって処理を分けているというのがわかると思います。
宣言的マクロは、 match
のようなパターンマッチをします。ただし、パターンとしてみなすものは、マクロに渡されたRustのソースコード(!)に対してマッチするコードに置き換えます。これは実行時ではなく、コンパイル時に解決されます。
非常にパワフルな機能であることは間違いありませんが、一方で通常の関数などに比べると、マクロ自体が読みにくく、わかりにくく、また実行時のエラーも置き換えられたコードで起きるため、理解が難しくなります。
宣言的マクロの呼び出し
呼び出しの際、関数のように ()
をつけて先程は呼び出していましたが、 {}
や []
でも大丈夫です。 {}
についてはセミコロンがなくても良いという違いがあります。
一般的に println!
のような関数的な使い方をする場合は ()
、 vec!
のような配列データを渡す場合は []
、 try!
のようなブロックを渡すような場合は {}
を使うのが一般的です。
// どれも同じ format_ln!(); format_ln![]; format_ln!{}
宣言的マクロの定義
例として記載したように、 macro_rules!
を使って定義します。
// 何も受け取らず、何もしないコードを生成するマクロ macro_rules! foo { () => {} } foo!();
宣言的マクロのメタ変数
マクロに渡される部分に対するマッチャーの書き方は $
name :
fragment-specifier のようになっています。nameは変数名なので、変数名として使える文字列が使えます。fragment-specifierはどのようなRustの構文の部品をマッチさせるかを指定する部分です。
種類 | マッチするもの |
---|---|
item | 関数、構造体、列挙子、定数、モジュール、トレイト、use宣言など |
block | ブロック( {} で囲まれているもの) |
stmt | 文(セミコロンは除く) |
pat_param | matchのパターン指定できるもの。ただし、 | でつないだもの、if がついているものはマッチしない |
pat | pat_paramと同じ。ただしこちらのほうがよく使われる |
expr | 式 |
ty | 型。未定義でも文法上許されるものであれば良いが、予約語はNG |
ident | 識別子。予約語でもマッチする |
path | 型のパス( std::collections::HashMap のようなもの ) |
tt | single token tree。()、[]、{} に囲まれたもの |
meta | アトリビュートの中身 |
lifetime | ライフタイム指定する部分 |
vis | 可視性( pub 、 pub(crate) など) |
literal | リテラル |
この中で一番わかりにくいものとしては、 tt
でしょう。
fn foo () { println!("foo"); }
上記はトークンツリーとしては以下のように分けられます。
fn foo () └ ∅ { ... } └ println └ ! └ ( ... ) └ "foo" └ ;
この文は4つのツリーからなっています。そのため、 tt
にはマッチしません。
逆に { println!("foo"); }
などであれば、 tt
にマッチするということになります。
このあたりはStack overflowにも質問として挙がっていました。
繰り返しの指定
宣言的マクロは、繰り返しでメタ変数を受け取ったり、それを展開したりすることもできます。
繰り返しの指定 | 繰り返し回数 |
---|---|
* | 0以上 |
+ | 1以上 |
? | 0か1 |
この繰り返しの機能を使って、標準の vec!
の簡易版を実装してみます。
macro_rules! vec { // , 区切りで0個以上受け取る ($( $x:expr ),*) => { { let mut v = Vec::new(); // 受け取った分、pushを繰り返し呼び出す $(v.push($x);)* v } }; } fn main() { let v = vec![1, 2, 3]; println!("{}", v[0]); println!("{}", v[1]); println!("{}", v[2]); }
まとめ
Rustの宣言的マクロは、ある程度覚えてしまえば、想定したコードだけを受け付ける制約を入れつつ、そこそこ簡単に書けるのではないかと思います。ただし、あまり乱用するとわかりにくいコードになってしまう点には注意が必要ですね。
今回は簡単にまとめたので、詳細はリファレンスを参考してみてください。