Help us understand the problem. What is going on with this article?

ためしておぼえる Rust のマクロ

More than 1 year has passed since last update.

はじめに

  • Rust のマクロをためしながらおぼえてみたので Playground で実行可能な形でまとめました。

コード

  • Playground のリンクはこちらです
  • 項目ごとにマクロの定義とマクロのテストという形で記述していったので 「TEST」 ボタン押下で実行してみてください。
#![allow(unused_macros)]

/*
    基本的なマクロの宣言

    マクロは「macro_rules! {マクロ名} {マクロ定義}」という形式で宣言する
*/
macro_rules! basic {
    /*
        マクロ定義部分

        「()」はマクロへの入力部分で「パターン」と呼ぶ
        「=>」は区切り文字
        「{ "basic" }」はマクロ展開時に置き換えられる部分で「テンプレート」と呼ぶ

        このマクロは入力を受けず、展開されると "basic" という文字列に置き換えられる。
    */
    () => { "basic" };
}

#[test]
fn test_basic() {
    assert_eq!(basic!(), "basic");
}

/*
    複数パターンをもつマクロの宣言

    マクロ定義部分に複数のパターンを記述すると、パターンマッチのように一致するパターンのテンプレートが展開される
*/
macro_rules! patterns {
    /*
        パターンに任意の文字列を記載すると、マクロ呼び出し時に同じ文字列が指定された場合にマッチしてテンプレートが展開される
    */
    (cat) => { "cat" };
    /*
        パターンとテンプレートのカッコは閉じと開きが揃っていれば「()」でも「[]」でも「{}」でも使える
    */
    {dog} => [ "dog" ];
}
#[test]
fn test_patterns() {
    assert_eq!(patterns!(cat), "cat");
    /* マクロ呼び出し時のカッコも閉じと開きが揃っていれば「()」でも「[]」でも「{}」でも使える */
    assert_eq!(patterns![ dog ], "dog");
}

/*
    入力に変数を含むパターンをもつマクロの宣言

    関数名・返り値の型・返り値の式、を入力で受け取って関数を定義するマクロを宣言する
*/
macro_rules! define_fn {
    /*
        パターンに指定する変数は地のコードの変数と区別するため「メタ変数」と呼ばれる

        メタ変数は「{変数名}: {フラグメント指定子}」という形式で指定する
        メタ変数の変数名は地のコードの変数と区別するため「$」記号から始まる文字列になる

        フラグメント指定子とはメタ変数の文法的な特徴を表す文字列
        フラグメント指定子には以下のようなものがある

        ident => 識別子(例:「std」「Json」)
        expr => 式(例:「1 + 2」「"foo".to_string()」 )
        ty => 型名(例:「i32」「String」 )
        stmt => 式もしくは宣言(例:「let x = 10」)
        path => パス(例:「::std::vec」「ferns」)
        pat => パターン(例:「Some(x)」「_」)
        item => アイテム(例:「struct User」「use std::io」)(参考リンク https://doc.rust-lang.org/reference/items.html)
        block => ブロック(例:「{ 10 }」)
        meta => 属性のボディ部分(例:「derive(Debug)」「inline」)
        tt => トークンツリー(詳細は後述するので省略)
    */
    ($f: ident, $ret: ty, $val: expr) => {
        fn $f() -> $ret { $val }
    };
}

#[test]
fn test_define_fn() {
    define_fn!(six, i32, 1 + 2 + 3);
    assert_eq!(six(), 6);

    define_fn!(hello, String, "hello".to_string());
    assert_eq!(hello(), "hello");
}

/*
    フラグメント指定子で tt(トークンツリー)を使ったマクロの宣言

    tt とは「任意の字句で構成されるツリー構造を持つデータ」のこと
    Rust の文法とは独立したデータ形式で以下のような性質を持つ

    - 空白区切りでトークンを判別する
    - `()` や `[]` や `{}` というカッコで囲まれたトークンがあればカッコの開きから閉じまでをひとつのトークンとする

    tt がどのように入力にマッチするかを確認するために 1 〜 3 個の tt にマッチするパターンを持つマクロをつくって試してみる
*/
macro_rules! parse_tt {
    ($t1: tt) => { format!("1. {}", stringify!($t1)) };
    ($t1: tt $t2: tt) => { format!("1. {}, 2. {}", stringify!($t1), stringify!($t2)) };
    ($t1: tt $t2: tt $t3: tt) => { format!("1. {}, 2. {}, 3. {}", stringify!($t1), stringify!($t2), stringify!($t3)) };
}
#[test]
fn test_parse_tt() {
    assert_eq!(parse_tt!(100), "1. 100");
    assert_eq!(parse_tt!(aaa bbb), "1. aaa, 2. bbb");
    assert_eq!(parse_tt!(true + false), "1. true, 2. +, 3. false");
    assert_eq!(parse_tt!([1, 2, 3]), "1. [ 1 , 2 , 3 ]");
    assert_eq!(parse_tt!{
        struct User {
            name: String,
            age: i32,
        }
    }, "1. struct, 2. User, 3. { name : String , age : i32 , }");
}

/*
    繰り返しを含むパターンをもつマクロの宣言

    パターンでは正規表現の量指定子のような形式で繰り返しを指定できる
*/
macro_rules! repeat {
    /*
        パターンのなかで「$(...)*」とすると「...」の繰り返しを捕捉することができる

        「$(...)*」の「*」は正規表現の量指定子と同様に「0 回以上の繰り返し」の意味
        「$(...)+」とすると「1 回以上の繰り返し」を意味する

        「$(...),*」というように閉じカッコの後に記載された文字は繰り返し文字列同士のセパレータとなる
        セパレータとしては「,」と「;」が指定できる
    */
    ( $($e: expr),* ) => {
        /*
            テンプレートでも繰り返しを扱うために「$(...)*」を使う
            パターンの繰り返しで指定したメタ変数が「$(...)*」の中にあればセパレータの文字列区切りで展開される
        */
        &[ $( $e ),* ]
    };
    /*
        上のパターンのセパレータ違いバージョン
        「,」区切りでも「;」区切りでもマッチするように定義する
    */
    ( $($e: expr);* ) => { repeat!( $( $e ),* ) };
}

#[test]
fn test_repeat() {
    let v: &[i32] = repeat!(10, 20, 30);
    assert_eq!(format!("{:?}", v), "[10, 20, 30]");

    let v: &[i32] = repeat!(100; 200; 300);
    assert_eq!(format!("{:?}", v), "[100, 200, 300]");
}

/*
    複雑な繰り返しパターンを含むマクロの宣言
*/
macro_rules! complex_repeat {
    /*
        「$(...)*」による繰り返しはネストすることができる
    */
    (
        $(
            $x: expr => [
                $( $y: expr ),*
            ]
        )*
    ) => {
        /*
            ネストした繰り返しはテンプレートでも同じレベルでネストして記述される必要がある
        */
        &[
            $(
                $( $x + $y ),*
            ),*
        ]
    };
    /*
        同じレベルで複数の繰り返しを定義することができる
    */
    (
        $(
            $x: expr;
            $( -> $y: expr )*
            $( => $z: expr )*
        )*
    ) => {
        /*
            パターンで複数の繰り返しが定義されている場合
            テンプレートでは「$(...)*」中で指定されているメタ変数に合わせて繰り返しが展開される
        */
        &[
            $(
                $x
                $( - $z )*
                $( + $y )*
            ),*
        ]
    };
}

#[test]
fn test_complex_repeat() {
    let v: &[i32] = complex_repeat!{
        100 => [1, 2, 3]
        200 => [4, 5, 6]
    };
    assert_eq!(format!("{:?}", v), "[101, 102, 103, 204, 205, 206]");

    let v: &[i32] = complex_repeat!{
        100;
        -> 1
        => 10

        200;
        -> 2
        -> 3
        => 10
        => 20

        300;

        400;
        => 30
    };
    assert_eq!(format!("{:?}", v), "[91, 175, 300, 370]");
}

/*
    マクロ実装パターンその1: 末尾カンマを無視する
*/
macro_rules! match_exprs {
    /* 本来の処理 */
    ($($exprs:expr),*) => {
        ( $($exprs),* )
    };
    /* 末尾カンマありのパターン */
    ($($exprs:expr),* $(,)*) => {
        /*
            末尾カンマなしで再帰的に自身のマクロを呼び出す
            これにより末尾カンマの有無にかかわらず利用できる便利なマクロが定義できる
        */
        match_exprs!( $($exprs),* )
    };
}

#[test]
fn test_match_exprs() {
    let t = match_exprs!( 1, "hello", true );
    assert_eq!(format!("{:?}", t), r#"(1, "hello", true)"#);

    let t = match_exprs!{
        true,
        false,
    };
    assert_eq!(format!("{:?}", t), r#"(true, false)"#);
}

/*
    マクロ実装パターンその2: 繰り返しのメタ変数を無視する

    テンプレートで繰り返しはしたいが繰り返し用のメタ変数の値は使わない、という場合にメタ変数の値を捨てるパターン
*/
/*
    $_t と $sub を受け取り $sub だけ返すことで $_t を捨てる
*/
macro_rules! replace_expr {
    ($_t:tt $sub:expr) => {$sub};
}
/*
    指定した型のデフォルト値が入ったタプルを定義するマクロ
*/
macro_rules! tuple_default {
    ($name:ident, $($tup_tys:ty),*) => {
        let $name: ( $( $tup_tys ),* ) = (
            /*
                指定された型の分だけ繰り返し値を作る必要があるが、
                メタ変数の値自体は利用しないので replace_expr を使って値を捨てる
            */
            $(
                replace_expr!(
                    ($tup_tys)
                    Default::default()
                )
            ),*
        );
    };
}
#[test]
fn test_tuple_default() {
    tuple_default!(t, i32, String, bool);
    assert_eq!(format!("{:?}", t), r#"(0, "", false)"#);
}

/*
    マクロ実装パターンその3: tt のカッコを展開する

    tt を利用したマクロの応用編として括弧に囲まれた字句を展開して再帰的に tt の解釈を行うマクロを宣言してみる

    sum_tt は入力をすべて tt の繰り返しとして扱い、すべての tt 同士を足し合わせる式に展開される
    括弧に囲まれた tt は flatten に展開される
*/
macro_rules! sum_tt {
    /*
        2つ以上の tt にマッチするパターン
        先頭が $t でそれ以降が $u に補足される
        それぞれ再帰的に sum_tt に渡されて葉として処理される
    */
    ( $t: tt $( $u: tt )+ ) => {
        sum_tt!($t) $( + sum_tt!( $u ) )*
    };
    /*
        () に囲まれた tt にマッチするパターン
        () の中の要素がそのまま sum_tt にわたされる
    */
    ( ( $( $t: tt )+ ) ) => {
        sum_tt!( $( $t )* )
    };
    /*
        [] に囲まれた tt にマッチするパターン
        () の場合と同様
    */
    ( [ $( $t: tt )+ ] ) => {
        sum_tt!( $( $t )* )
    };
    /*
        {} に囲まれた tt にマッチするパターン
        () の場合と同様
    */
    ( { $( $t: tt )+ } ) => {
        sum_tt!( $( $t )* )
    };
    /*
        ツリーの葉にマッチするパターン
        tt を数字としてパースできたら数字に、できなければ 0 にする
    */
    ( $t: tt ) => { stringify!($t).parse::<i32>().unwrap_or_default() };
}
#[test]
fn test_sum_tt() {
    assert_eq!(sum_tt!( 1 2 3 ), 6);
    assert_eq!(sum_tt!( 4 (5 6) ), 15);
    assert_eq!(sum_tt!( [7] 8 {9} (10 11) ), 45);
    assert_eq!(sum_tt!( 1 true 2 foo bar 3 + pi ), 6);
}

おわりに

  • 今回はマクロを使いまくりなクレートのコードを読めるようになるためにマクロを勉強してみたのですが、マクロを読みたいだけで IntelliJ を使っていたら、マクロの呼び出し箇所で右クリックして Show recursibely expanded macro で展開後のマクロのコードを見るのが一番手っ取り早いということが途中で判明しました。

参考サイト

https://doc.rust-jp.rs/the-rust-programming-language-ja/1.6/book/macros.html
https://danielkeep.github.io/tlborm/book/README.html
https://keens.github.io/blog/2018/02/17/makurokurabu_rustshibu/
https://qiita.com/k5n/items/758111b12740600cc58f

nirasan
フリーで開発者をしています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away