13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Rustのマクロ、便利そうだから使ってみたんですよ。

vec![] とか println!() とか、標準ライブラリでよく見るし、自分でも作れたらかっこいいじゃないですか。

3時間後、自分で書いたコードが読めなくなりました。

この記事では、私がマクロ沼にハマった過程と、そこから得た知見を共有します。

目次

  1. マクロとは何か
  2. 宣言的マクロ (macro_rules!)
  3. パターンマッチの基本
  4. 繰り返しパターン
  5. 実用的なマクロ例
  6. 手続き的マクロ
  7. マクロのデバッグ
  8. まとめ

マクロとは何か

マクロは コードを生成するコード です。

関数との違い:

項目 関数 マクロ
実行タイミング 実行時 コンパイル時
引数の数 固定 可変
生成するもの コード
// 関数:値を返す
fn add(a: i32, b: i32) -> i32 {
    a + b
}

// マクロ:コードを生成する
macro_rules! add_macro {
    ($a:expr, $b:expr) => {
        $a + $b
    };
}

fn main() {
    let x = add(1, 2);        // 関数呼び出し
    let y = add_macro!(1, 2); // マクロ展開 → 1 + 2 に置き換わる
}

宣言的マクロ (macro_rules!)

Rustのマクロには2種類ありますが、まずは macro_rules! から。

基本構文

macro_rules! マクロ名 {
    (パターン) => {
        展開されるコード
    };
}

最小のマクロ

macro_rules! say_hello {
    () => {
        println!("Hello!");
    };
}

fn main() {
    say_hello!();  // Hello!
}

パターンマッチの基本

メタ変数

$名前:種類 でパターンを定義します。

種類 説明
expr 1 + 2, foo()
ident 識別子 foo, bar
ty i32, String
pat パターン Some(x), _
stmt let x = 1;
block ブロック { ... }
item アイテム fn foo() {}
literal リテラル 42, "hello"
tt トークンツリー なんでも

使用例

macro_rules! create_function {
    ($func_name:ident) => {
        fn $func_name() {
            println!("Called: {}", stringify!($func_name));
        }
    };
}

create_function!(foo);  // fn foo() { ... } を生成
create_function!(bar);  // fn bar() { ... } を生成

fn main() {
    foo();  // Called: foo
    bar();  // Called: bar
}

複数のパターン

macro_rules! test {
    // パターン1:引数なし
    () => {
        println!("No arguments")
    };
    // パターン2:1つの式
    ($e:expr) => {
        println!("One expression: {}", $e)
    };
    // パターン3:2つの式
    ($a:expr, $b:expr) => {
        println!("Two expressions: {} and {}", $a, $b)
    };
}

fn main() {
    test!();           // No arguments
    test!(42);         // One expression: 42
    test!(1, 2);       // Two expressions: 1 and 2
}

繰り返しパターン

ここからが本番。vec![] みたいな可変長引数を受け取るマクロを作ります。

基本構文

$( パターン ),*   // 0回以上、カンマ区切り
$( パターン ),+   // 1回以上、カンマ区切り
$( パターン )?    // 0回か1回

vec! を自作してみる

macro_rules! my_vec {
    // 空のベクタ
    () => {
        Vec::new()
    };
    // 要素を持つベクタ
    ($($elem:expr),+ $(,)?) => {
        {
            let mut v = Vec::new();
            $(
                v.push($elem);
            )+
            v
        }
    };
}

fn main() {
    let v1 = my_vec![];           // []
    let v2 = my_vec![1, 2, 3];    // [1, 2, 3]
    let v3 = my_vec![1, 2, 3,];   // 末尾カンマOK
}

解説

  • $($elem:expr),+ : 1つ以上の式をカンマ区切りでマッチ
  • $(,)? : 末尾のカンマはオプション
  • $( v.push($elem); )+ : マッチした各要素に対してコード生成

実用的なマクロ例

HashMap初期化マクロ

macro_rules! hashmap {
    ($($key:expr => $value:expr),* $(,)?) => {
        {
            let mut map = std::collections::HashMap::new();
            $(
                map.insert($key, $value);
            )*
            map
        }
    };
}

fn main() {
    let scores = hashmap! {
        "Alice" => 100,
        "Bob" => 85,
        "Charlie" => 92,
    };
}

デバッグプリントマクロ

macro_rules! debug_print {
    ($($arg:tt)*) => {
        #[cfg(debug_assertions)]
        {
            println!("[DEBUG] {}", format!($($arg)*));
        }
    };
}

fn main() {
    debug_print!("x = {}", 42);  // デバッグビルドでのみ出力
}

変数名と値を同時に表示

macro_rules! dbg_vars {
    ($($var:ident),+ $(,)?) => {
        $(
            println!("{} = {:?}", stringify!($var), $var);
        )+
    };
}

fn main() {
    let x = 10;
    let y = "hello";
    let z = vec![1, 2, 3];
    
    dbg_vars!(x, y, z);
    // x = 10
    // y = "hello"
    // z = [1, 2, 3]
}

newtype パターン

macro_rules! newtype {
    ($name:ident, $inner:ty) => {
        #[derive(Debug, Clone, PartialEq, Eq)]
        pub struct $name($inner);
        
        impl $name {
            pub fn new(value: $inner) -> Self {
                Self(value)
            }
            
            pub fn value(&self) -> &$inner {
                &self.0
            }
        }
    };
}

newtype!(UserId, u64);
newtype!(Email, String);

fn main() {
    let user_id = UserId::new(12345);
    let email = Email::new("test@example.com".to_string());
}

手続き的マクロ

macro_rules! より強力だけど、複雑。別クレートが必要。

3種類

  1. derive マクロ: #[derive(MyTrait)]
  2. 属性マクロ: #[my_attribute]
  3. 関数マクロ: my_macro!()

derive マクロの例(概要)

// proc-macro クレートで定義
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(MyDebug)]
pub fn my_debug_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;
    
    let expanded = quote! {
        impl std::fmt::Debug for #name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, stringify!(#name))
            }
        }
    };
    
    TokenStream::from(expanded)
}

使用側:

#[derive(MyDebug)]
struct Point {
    x: i32,
    y: i32,
}

手続き的マクロは深いので、また別の記事で...

マクロのデバッグ

cargo expand

マクロが何に展開されるか見るのに必須:

cargo install cargo-expand
cargo expand
// 展開前
fn main() {
    let v = vec![1, 2, 3];
}

// 展開後
fn main() {
    let v = <[_]>::into_vec(
        #[rustc_box]
        ::alloc::boxed::Box::new([1, 2, 3])
    );
}

trace_macros!(nightly)

#![feature(trace_macros)]

trace_macros!(true);

fn main() {
    let v = vec![1, 2, 3];
}

trace_macros!(false);

println! デバッグ

マクロの中で compile_error! を使う:

macro_rules! debug_macro {
    ($($arg:tt)*) => {
        compile_error!(stringify!($($arg)*));
    };
}

よくあるハマりポイント

ハマり1:セミコロンの位置

// ❌ 間違い
macro_rules! bad {
    ($e:expr) => {
        $e  // セミコロンがない
    }
}

let x = bad!(1 + 2);  // エラーになる場合がある

// ⭕ 正しい
macro_rules! good {
    ($e:expr) => {
        $e
    };  // ここにセミコロン
}

ハマり2:式とブロックの違い

macro_rules! returns_value {
    () => {
        {
            let x = 42;
            x  // 最後にセミコロンなし → 式として評価
        }
    };
}

let v = returns_value!();  // 42

ハマり3:衛生性(Hygiene)

macro_rules! declare_x {
    () => {
        let x = 42;
    };
}

fn main() {
    declare_x!();
    // println!("{}", x);  // エラー!マクロ内のxは外から見えない
}

マクロ内で定義した変数は外からアクセスできない。これを「衛生的マクロ」と呼びます。

ハマり4:再帰マクロ

macro_rules! count {
    () => { 0 };
    ($head:tt $($tail:tt)*) => {
        1 + count!($($tail)*)
    };
}

fn main() {
    let n = count!(a b c d e);  // 5
}

再帰は便利だけど、深すぎると recursion limit に引っかかる。

まとめ

マクロを使うべき場面

  • 可変長引数が必要
  • ボイラープレートコードの削減
  • DSL(ドメイン特化言語)の作成
  • コンパイル時のコード生成

マクロを避けるべき場面

  • 関数で十分な場合
  • 可読性が大きく損なわれる場合
  • デバッグが困難になる場合

チェックリスト

  • cargo expand でマクロの展開結果を確認
  • パターンマッチの順序に注意(上から順にマッチ)
  • 繰り返しパターンの区切り文字を正しく指定
  • 衛生性を理解して変数スコープに注意

今すぐできるアクション

  1. cargo install cargo-expand でツールを入れる
  2. 標準ライブラリのマクロを cargo expand で見てみる
  3. 簡単なマクロから書いてみる

マクロ、最初は本当に意味がわからなかったけど、cargo expand でしか展開結果を見るようにしたら少しわかるようになりました。

でも複雑なマクロは今でも読めません。こわいですねぇ。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

13
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?