What is it, naokirin?

Rustの宣言的マクロを書いてみる

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 可視性( pubpub(crate) など)
literal リテラル

この中で一番わかりにくいものとしては、 tt でしょう。

fn foo () { println!("foo"); }

上記はトークンツリーとしては以下のように分けられます。

fn

foo

()
└ ∅

{ ... }
└ println
└ !
└ ( ... )
  └ "foo"
└ ;

この文は4つのツリーからなっています。そのため、 tt にはマッチしません。

逆に { println!("foo"); } などであれば、 tt にマッチするということになります。

このあたりはStack overflowにも質問として挙がっていました。

stackoverflow.com

繰り返しの指定

宣言的マクロは、繰り返しでメタ変数を受け取ったり、それを展開したりすることもできます。

繰り返しの指定 繰り返し回数
* 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の宣言的マクロは、ある程度覚えてしまえば、想定したコードだけを受け付ける制約を入れつつ、そこそこ簡単に書けるのではないかと思います。ただし、あまり乱用するとわかりにくいコードになってしまう点には注意が必要ですね。

今回は簡単にまとめたので、詳細はリファレンスを参考してみてください。

doc.rust-lang.org