Skip to content
Published on

Rustのマクロ:宣言的 vs 手続き的(derive)

Authors

はじめに — マクロは「コードを書くコード」

Rustを少し使っていると、こうしたものに毎日出会います。出力のためのprintln!、ベクタを作るvec!、そして何より構造体の上に付く#[derive(Debug)]。これらは関数ではなくマクロです。名前の後の感嘆符や#[derive(...)]という構文がその合図です。

マクロが関数と根本的に違うのは、値を扱うのではなくコードそのものを扱うという点です。マクロはコンパイル時に実行され、あなたが少なく書いたコードをより多くのコードへと展開します。これをメタプログラミングと呼びます。そしてRustには、性格がはっきり異なる二種類のマクロがあります。この記事はその二つを分けて理解することを目標にします。

構文を実際に動かしながら身につけたいなら、読みながらこのサイトのRust学習ラボで例を転がしてみてください。

二種類のマクロの概観

Rustのマクロは大きく二つに分かれます。

  • 宣言的マクロ(declarative macro)macro_rules!で定義します。入力コードのパターンをマッチし、マッチした断片からあらかじめ決めておいたコードを打ち出します。正規表現が文字列のパターンを扱うように、宣言的マクロはコード断片のパターンを扱います。
  • 手続き的マクロ(procedural macro) — 入力コードをトークンストリーム(TokenStream)として受け取り、任意のRustコードでそれを解析して新しいトークンストリームを作り出します。さらに三つに分かれます。#[derive(...)]が呼び出すderiveマクロ#[route(...)]のような属性マクロ(attribute macro)、そしてsql!(...)のように関数のように呼ばれる**関数風マクロ(function-like macro)**です。

一行でまとめると、宣言的マクロは「パターンを見て決められたコードを打つ」道具、手続き的マクロは「コードをプログラムとして読み、プログラムでコードを書く」道具です。まずは易しいほうから見ます。

宣言的マクロ — macro_rules!

最も馴染みのあるマクロの一つvec!を真似てみると、宣言的マクロの感覚がつかめます。以下は要素を並べてベクタを作る単純化した版です。

macro_rules! my_vec {
    // カンマ区切りの任意個数の式を受け取る
    ( $( $x:expr ),* ) => {
        {
            let mut temp = Vec::new();
            $(
                temp.push($x);
            )*
            temp
        }
    };
}

fn main() {
    let v = my_vec![1, 2, 3];
    println!("{:?}", v); // [1, 2, 3]
}

この短いコードに宣言的マクロの核心がすべて入っています。矢印の左はマッチするパターン、右は展開するコードです。

パターンに使われた記号をほどくとこうです。ドル記号を前に付けたマクロ変数(上の例のx)は、マッチしたコード断片を入れます。後ろに付いたexprフラグメント指定子で、「ここには一つの式が来る」という意味です。式のほかにもident(識別子)、ty(型)、stmt(文)、block(ブロック)、tt(トークンツリー)など何種類もあります。そしてパターンを囲む繰り返し表記は「この部分が0回以上繰り返される」という意味です。繰り返しの中のカンマは区切りで、アスタリスクは「0回以上」を意味します。アスタリスクの代わりにプラスを使えば「1回以上」になります。

展開する側でも同じ繰り返し表記を使います。マッチ時に複数を捕まえたそのマクロ変数を、展開時に再び繰り返して、それぞれについて push 呼び出しを一行ずつ打ち出します。つまりmy_vec![1, 2, 3]push(1); push(2); push(3);に展開されます。

複数のルールを並べることもできます。マクロは上から最初に合うルールを選びます。

macro_rules! greet {
    // 引数がないとき
    () => {
        println!("こんにちは!");
    };
    // 名前を一つ受け取るとき
    ($name:expr) => {
        println!("こんにちは、{}さん!", $name);
    };
}

fn main() {
    greet!();          // こんにちは!
    greet!("ラスト");   // こんにちは、ラストさん!
}

衛生性 — なぜマクロが変数を汚さないのか

Cのテキスト置換マクロを使ったことがある人は、マクロが外の変数を静かに壊す悪夢を知っています。Rustの宣言的マクロは**衛生的(hygienic)**です。マクロの中で作った変数は、マクロの外の同名の変数と衝突しません。

macro_rules! make_temp {
    () => {
        let temp = 42;
    };
}

fn main() {
    let temp = 1;
    make_temp!(); // マクロ内の temp は独立した別の変数
    println!("{}", temp); // 1 (汚染されない)
}

マクロが内部的にtempという名前を使っても、その名前はマクロの文脈に属し、外のtempと混ざりません。衛生性のおかげで、宣言的マクロは名前の衝突を心配せずに安全に使えます。これは単純なテキスト置換と決定的に違う点です。

手続き的マクロ — コードをデータとして扱う

宣言的マクロが「パターンマッチ」なら、手続き的マクロは本格的な「コード変換プログラム」です。手続き的マクロは入力コードをトークンストリームとして受け取ります。トークンストリームはソースコードを細かく刻んだトークンの流れです。たとえばlet x = 1;はおおよそletx=1;というトークンの並びです。

手続き的マクロ関数のシグネチャはこういう形です。トークンストリームを受け取り、トークンストリームを返します。

use proc_macro::TokenStream;

#[proc_macro_derive(MyTrait)]
pub fn my_derive(input: TokenStream) -> TokenStream {
    // input: マクロが付いた構造体/列挙型のトークン
    // 戻り値: 生成する新しいコードのトークン
    // ...
}

問題は、トークンストリームを手でパースして組み立てる作業が非常に面倒だということです。そこでエコシステムは事実上二つのクレートに依存します。

  • syn — トークンストリームをパースして扱いやすい**構文木(syntax tree)**に変えます。構造体名、フィールド一覧、型といった情報を楽に取り出せます。
  • quote — 逆に、作りたいコードをほぼそのまま書き下すと、それをトークンストリームに変えてくれます。quote!マクロの中に目標のコードを書き、変数の場所に値を差し込みます。

この二つを使うと、手続き的マクロの典型的な流れは三段階になります。

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloName)]
pub fn hello_name(input: TokenStream) -> TokenStream {
    // 1) syn で入力をパースする
    let ast = parse_macro_input!(input as DeriveInput);
    let name = ast.ident; // 構造体(または列挙型)の名前

    // 2) quote で生成するコードを組み立てる
    let expanded = quote! {
        impl #name {
            fn hello() {
                println!("やあ、私は {}!", stringify!(#name));
            }
        }
    };

    // 3) トークンストリームとして返す
    expanded.into()
}

ここでquote!の中の#nameは、パースして得た構造体名をその場所に差し込む構文です。使う側でこのマクロをこう付けると:

#[derive(HelloName)]
struct Robot;

fn main() {
    Robot::hello(); // やあ、私は Robot!
}

コンパイラはRobotという名前でhelloメソッドを自動的に生成します。手でimplブロックを書いていないのにです。なお手続き的マクロは別のクレートに分けて定義しなければならず、Cargo.tomlproc-macro = trueと表示する必要があります。

serdeの#[derive(Serialize)]は何をするのか

この原理の最も有名で強力な実用例がserdeです。RustでデータをJSONのような形式にシリアライズするとき、私たちはたいていこう書きます。

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct User {
    name: String,
    age: u32,
    active: bool,
}

fn main() {
    let user = User {
        name: "あよん".to_string(),
        age: 30,
        active: true,
    };
    // serde_json でシリアライズ
    let json = serde_json::to_string(&user).unwrap();
    println!("{}", json);
    // {"name":"あよん","age":30,"active":true}
}

ここで#[derive(Serialize)]がすることは、まさに前の節で見た手続き的deriveマクロです。コンパイル時に、serdeのderiveマクロがUser構造体の定義をトークンストリームとして受け取り、各フィールド(nameageactive)を巡回して、それぞれをどうシリアライズするか知っているSerializeトレイトの実装を丸ごと生成します。

肝心なのは、もしこのコードがなければフィールドごとに手で書かねばならなかった退屈でミスの多いシリアライズコードを、マクロがフィールド定義を読んで正確に打ち出すという点です。フィールドを一つ追加すれば、次のコンパイル時にマクロがそのフィールドまで処理するコードを再生成します。自分で保守するコードがないのです。これが手続き的マクロの与える力であり、「コードをデータとして読み、コードを書く」メタプログラミングの実質的な価値です。

serdeはここに属性マクロの味も混ぜます。フィールドの上に付けるヒントでシリアライズの動作を調整できます。

use serde::Serialize;

#[derive(Serialize)]
struct Config {
    // JSON ではこのフィールド名を別にする
    #[serde(rename = "maxRetries")]
    max_retries: u32,

    // 値がなければ出力から丸ごと外す
    #[serde(skip_serializing_if = "Option::is_none")]
    nickname: Option<String>,
}

こうした#[serde(...)]属性は、deriveマクロがコードを生成するときに参照する指示です。マクロがフィールドを巡回しながらこのヒントを読み、それに合わせて生成コードを変えます。

宣言的 vs 手続き的 — いつどちらを

二つのマクロの性格を表にまとめると選択が明確になります。

観点宣言的マクロ手続き的マクロ
定義方法macro_rules!専用クレート + proc_macro
動作原理パターンマッチ後にコード置換トークンストリームをプログラムで変換
表現力決まったパターンの中で任意のRustロジック
作成の難しさ低い高い (syn/quote が必要)
代表例vec!, println!#[derive(Serialize)]

実用的な指針はこうです。単純なコードの繰り返しや便利構文なら宣言的マクロで十分です。 反復的な初期化や短い補助構文くらいならmacro_rules!が軽くて安全です。一方、構造体や列挙型の定義を読んでトレイト実装を自動生成するように、入力の構造を実際に解析する必要があるなら手続き的マクロが答えです。 serde、clapのderive、その他多くのライブラリがこの道を選びました。

マクロを使うべきでないとき

マクロは強力ですが無料ではありません。乱用するとコードベースはかえって理解しにくくなります。次の兆候が見えたら一度立ち止まって考えてください。

  • 関数で足りるなら関数を使う。 マクロが必要な本当の理由はたいてい二つです。可変個の引数を受け取らねばならないか(例: println!)、コードそのものを生成せねばならないときです。値を扱うだけでよいなら、普通の関数やジェネリクスがほぼ常に優れています。関数は型チェック、デバッグ、ドキュメント化のすべてが容易です。
  • デバッグのコストを覚えておく。 マクロが作り出したコードは目に見えないため、エラーメッセージが見慣れない位置を指したり理解しにくかったりします。cargo expandのような道具で展開されたコードを確認できますが、それ自体が追加の負担です。
  • コンパイル時間を意識する。 特に手続き的マクロはsynのような重い依存を引き込み、コンパイル時に実行されるので、ビルド時間を増やします。小さな便利さのために大きなコンパイルコストを払っていないか見てください。
  • 可読性を優先する。 マクロがチームメンバーには読みにくい「魔法」になりつつあるなら、その魔法が本当に見合うかを問うべきです。明示的なコード数行のほうが賢いマクロ一つより良いことが多いのです。

まとめると、マクロは「ほかの方法では表現できないもの」のために取っておく道具です。可変引数、コンパイル時のコード生成、新しい構文 — こうした本当の必要があるときに輝き、単なる便利さのために乱発すると負担になります。

おわりに

Rustのマクロはコンパイル時にコードを生成するメタプログラミングの道具であり、性格の異なる二つの枝に分かれます。宣言的マクロはmacro_rules!でパターンをマッチしてコードを打ち出す軽くて衛生的な道具、手続き的マクロは入力をトークンストリームとして受け取りsynとquoteで解析・生成する強力な道具です。serdeの#[derive(Serialize)]は後者の代表的な成果で、手で書けば退屈だったコードをフィールド定義から自動的に作り出します。

二つの道具の境界を理解すれば、いつmacro_rules!で足り、いつ手続き的マクロが必要で、そしていつ単に関数を使うのが正しいかを判断できます。マクロは控えめに使うときこそ最も強力です。その節度が、あなたのコードを魔法ではなく道具のまま残します。

参考資料