マクロに入門したので概要を書く (Rust)

はじめに

Rustのマクロは難しそうなので、必要な時に理解すればいいやと後回しにしていました。結局理解できていませんが徐々に攻めていこうと思います。

マクロの文法の説明はしません。また、マクロの展開に関わる抽象構文木の話は別の記事で書きます。

マクロに入門する人が概要をわかることを目指して書きます。

マクロ

マクロの種類は以下の2つです。

  • 宣言的マクロ
  • 手続き的マクロ

ドキュメントを見るとすごい複雑そうで参りました。ただ、誤解を恐れずにいうと、結局やっている基本的なことは変換です。今回はこのうち宣言的マクロの触りを説明します。

マクロはイメージで言うと下図のようにテキストを入力して、何かしらの変換をしてテキストを出力します。

f:id:gkuga:20200207220201p:plain

「変換する仕組み」がマクロというコンパイラの機能です。テキストと書いているのは、プログラムとして意味のあるものではなく単純な文字の並びだからです。

例えばプログラムだと下記のように書かないといけないところを、

let x: Vec<u32> = {
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
};

マクロにより、

let x: Vec<u32> = vec![1, 2, 3];

これだけの記述で済みます。このことはRustで書くと冗長な書き方をしないといけないところを、マクロによってそれをカバーしている、柔軟性をもたせているということを表しています。

vec![1, 2, 3];

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
};

に変換しています。プログラムを評価しているのではなく単純にテキストの変換をしています。その時にRustが用意している変換の仕組みを使うのですが、例えば上記の変換だと下記のようなルールを書く必要があります。

macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

このルールを理解するのがハードルがあります。これは基本的には下記のような構造になっています。

macro_rules! vec {
    ( テキストにマッチするパターンを書く ) => {
        {
            マッチしたやつをうまく使って変換後のテキストを作る
        }
    };
}

この変換の仕組みを理解してうまく変換後のテキストにするのに頭を使うのと、それをメンテナンスしていくのがマクロの大変なところでしょうか。この時に変換の仕組みのルールに従わないと下図のようなイメージでエラーになります。

f:id:gkuga:20200207222307p:plain

うまく変換はできたとして、変換後のテキストはRustのプログラムとして評価されますので、コンパイラに普通にチェックされます。

f:id:gkuga:20200207222402p:plain

これらをエラーなく通すだけではなく、実運用する時はメンテナビリティのあるマクロかどうかなども考えないといけないでしょう。これはなかなか大変そうです。ただ、何か面白いですね。パズルみたいな楽しさがありそうです。再帰呼び出しもできるようです。

累乗を計算するマクロ pow!()

ここで示す例は、Rustで書くと冗長になってしまうところをマクロで書くことで簡潔になるという例ではありません。単純にマクロを使えばこういうこともできるという例です*1

試しに3^2みたいな感じで3の2乗を計算するようなマクロ pow!()を作ってみました。テキストの変換なので、3^2なら3を2回かけるようなRustのプログラムに変換すればいいなと思いました。マクロを作るといっても、プログラムを書く感覚に近いですね。ただ、繰り返しがちょっとややこしくて今回は妥協して、関数のpowを呼ぶだけにしましたw

main.rs

macro_rules! pow {
    ($x:literal ^ $n:literal) => {
        $x.pow($n)
    };
}

fn main() {
    let num = pow!(3_i32 ^ 2);
    println!("{}", num);
}

これを動かすと以下のようになりました。

$ cargo run
   Compiling macros v0.1.0 (/Users/s06913/dev/src/github.com/gkuga/rust-examples/macros)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/macros`
9

できました。

おわりに

本当は階乗を計算するfactを作ろうとしたのですがうまくいかずでした。ググると、下記のような記事を見つけました。

マクロクラブ Rust支部 | κeenのHappy Hacκing Blog

外から展開されるからうまくマッチしない、CKマシンを使えばいいんですね。なるほど(全然わからない)。マクロクラブに入会するので教えてほしいです。

参考

*1:本来はマクロの効果がよく出ている例を出したいのですが、時間の都合上簡潔な例を示すだけにします。