こんにちは。
Rust始めました。
オライリーのThe Rust Programming Language(蟹の本)を2週間位で読破したところ、実践Rust入門の作者様からTwitterで捕捉され、販促用の本を一冊頂いてしまった3^5です。
ちょっと前にThe Little Book of Rust Macrosという文献を見つけたのでちょっとずつ読んでいます。 英語分からんですが。
この本です。(https://danielkeep.github.io/tlborm/book/README.html)

3章まで読んだのですが、 ちょっとしんどいので2章の途中までの内容を僕の知識と交えながら簡単にまとめます。
主にマクロの話しかしません。 英語怪しいので間違ってるところがあれば優しく指摘してください。
マクロとは
マクロを知らない、聞いたことがない人向けの雑な簡単な説明です。
マクロは、基本的にはコードの量を減らすためのプログラミング言語が搭載している機能です。言語によってはないものもありますが、C言語やこのRust言語では使うことができます。
マクロは、コンパイラがソースコードを機械語に翻訳する直前に「展開」されます。
例えば、C言語では#defineでマクロを定義できます。
#define NUMBER 42
こう書くと、そのファイル中のNUMBERという文字列(stringじゃなくてソースコード全部をテキストとしてみたときにNUMBERとして認識できる部分全て)がコンパイル前に42に展開されるわけです。 プログラム中のマジックナンバーを解消するために使うことが多いですね。
また、マクロは引数を取ることもできて、 うまく使えばコードをさらに簡潔にできます。 例えば競プロerがよく使ってるマクロでrepがあります。
#define rep(i,n) for(int (i)=0;(i)<(n);++(i))
どうせfor文は0から回すことがほとんどなので簡略化して文字数を減らしたいわけですね。
Rustのマクロ
おまたせしました。こっから本文です。
マクロ展開のタイミング
コンパイラはコンパイルの最初にRustのコードテキストをトークンに分け、それを用いてASTを構築します。 RustのマクロはこのASTの構築後に展開されます。 マクロ展開後にマクロが含まれていた場合も全部展開します。 ただし、デフォルトの設定では再帰的な展開は32回まででそれ以上はコンパイルエラーになるらしいです。
マクロ定義のやり方
macro_rules!マクロでマクロの定義ができます。 構文はこんな感じ。
macro_rules! マクロ名{
ルール1 ;
ルール2 ;
//...
ルールN
}
ルールの項は最低一つは書く必要があって、最後の項のセミコロンは省略できます。 ルールとは展開の規則のことで、こんな式が来たらこう展開するよ、みたいなことを書きます。こんな感じで。
(パターン) => { 展開後の式 }
必要なのはこの2つの知識だけです。万人にマクロの扉は開かれています。
macro_rules! four {
() => { 1 + 3 }
}
これは呼び出すと1+3
という式に置換されるマクロです。
受け取る式が()
のときのルールしか書いてないし結果はいつでも4なので何の面白みもありませんが、なんとfour!()
でもfour!{}
でもfour![]
でも呼び出すことができます。
すごいね!
変数の捕捉
流石に受け取る式が()のときのルールしか書けないのはあんまりなので、 受け取った式をprintln!するマクロを書いてみましょう。 ルールの左辺の()の中に$+引数名、そしてその引数のASTにおける属性(言葉選びが怪しいけどスルーしてください)を書くことで変数を捕捉できます。ここの引数のことをメタ変数といいます。
macro_rules! my_println {
($e:expr) => { println!("{}",$e); }
}
$e(変数名)の後のexpr
によって、eという名前で束縛できる対象が式であることを示しています。
つまり、my_println!("hello");
とかやると展開後の$e
のところが"hello"
という式に置換されるわけです。
カンマで区切ることで複数個の式を束縛できます。
macro_rules! multiply_add {
($a:expr, $b:expr, $c:expr) => {$a * ($b + $c)}
}
ここまで書くとこんな感じのことができます。
macro_rules! my_println {
($e:expr) => {
println!("{}",$e)
}
}
macro_rules! multiply_add {
($a:expr, $b:expr, $c:expr) => {$a * ($b + $c)}
}
fn main() {
my_println!(multiply_add!(1,2,3)); //=>5
}
繰り返し
捕捉パターンで繰り返し記法が使えます。
$( ... ) sep rep
という構文で、括弧の中にはそれぞれの繰り返しの中で束縛する変数、sepは引数のセパレータ記号、repは繰り返す回数を*(0回以上)、もしくは+(1回以上)で指定します。
これに関しては見たほうが早そうですね。
macro_rules! my_vec {
( $( $element:expr ) , * )
=>
{
{ let mut v = Vec::new();
$( v.push($element); )*
v }
};
}
これでvectorのコンストラクタとしてmy_vec!
が使えます。
いつも使うvec!と同じ使い方ができます。(効率と再利用性のために実際の実装はちょっと違うらしいです。)
マッチング
覚えてますか?ルールは複数書けます。
macro_rules! double_rule {
($e:expr) => { "single exp" };
($e:expr, $f:expr) => { "double exps" };
}
複数のルールがあるマクロを展開するとき、
コンパイラはルールを上から順にマッチングしていきます。
例えばdouble_rules!(2,3)
のような呼び出しがあったら、
まず1つ目の($e:expr)
にマッチするか調べ、失敗して2つ目でマッチしているわけです。
このマッチングで一つ注意点があります。 メタ変数の捕捉が始まるとそれ以降のルールには一切マッチしないということです。 次のような例を考えてみましょう。
macro_rules! dead_rule {
($e:expr) => { ... };
($i:ident +) => { ... };
}
dead_rules(x +);//=>compile error!
コメントに書いているように、dead_rules(x +);
は展開に失敗します。
x +
は式として有効であるために1つ目の例にマッチしてしまい、その後で右項が存在していないのでパーサがpanicを返すわけです。
この問題を避けるため、ルールは具体的なものから書いていくのが良いでしょう。
健全性
Rustのマクロは非常に健全なので
意図しない変数捕捉が起こりません!めでたい!
そこのLisper、アナフォリックマクロとかつぶやくのをやめなさい。
意図しない変数捕捉ってなんでしょう?例えばC言語ならこんなことが起こります。
#define incv(v) { int a=5; (v)++; }
int main(void){
int a=10;
incv(a);
printf("%d\n",a);//=>11?
}
ここでは11が出力されてほしいんですが、
incvが展開された結果{int a=5; a++;}
になるため、
++演算子でインクリメントされるのは5が入ってる内側のaであり、
外側のスコープのaは10のまま変わらないわけです。
この挙動はincvの引数がaのときだけ起こります。 aという変数名がマクロ内部のものと干渉してしまうからですね。 これを意図しない変数捕捉と呼びます。詳しくはOn Lispとかマクロの健全性の話とかを読むといいでしょう。
意図しない変数捕捉を解決する最も簡単な方法は衝突しにくい名前をつけることでしょう。aだから干渉してしまうのであって変な名前なら大丈夫、という論理です。
#define incv(v) { int INNER_a=5; (v)++; }
INNER_aという変数が作られない限りはまあ大丈夫です。 しかしINNER_aという明らかなバグを潜ませたままにするのは怖いので、 いくつかの言語のマクロシステムではこれを回避する方法があります。 例えばCommon Lispではgensym関数を使ってあらゆるシンボルに対して等しいと判定されないようなシンボルを生成できます。
(defmacro incv (v)
(let ((a (gensym)))
`(let ((,a 0))
(setf ,v (+ ,v 1)))))
Rustは、識別子に構文コンテキスト値が付加されていて、変数名が一致しても文脈が異なると干渉しないようになっています。素晴らしい!
macro_rules! using_a {
($e:expr) => {
{
let a = 42;
$e
}
}
}
fn main(){
let a = 100;
let four = using_a!(a / 10);
println!("{}",a);//=> 10
}
終わりに
長くなってきて疲れたので終わります。 続きは2月以降にアップロードされるメタプログラミング班の後期活動報告書に書いときます。 お疲れ様でした。