1
0

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マクロ冬期講習Advent Calendar 2024

Day 6

Rust 宣言マクロ( macro_rules! )の勘所

Last updated at Posted at 2024-12-23

こちらの記事は Rustマクロ冬期講習アドベントカレンダー 6日目の記事です!

今回は宣言マクロの必要事項をまとめました!

宣言マクロ 要約

宣言マクロ ( macro_rules! によるマクロ)、これだけ!

基本形
macro_rules! マクロ名 {
    ( $メタ変数:フラグメント指定子 ) => {
        // $メタ変数を利用した置き換え内容
    };
}

fn main() {
    // マクロ呼び出し
    マクロ名!( マクロに与える引数 );
}

気をつけなければならない細部を無視すると、知っておけば良い勘所は2つのみです!

  • 勘所1: メタ変数の記述方法
    • 引数部分
      • $メタ変数:フラグメント指定子 : キャプチャしたい構文要素を指定
      • $(...) 区切り文字 繰り返し種類 : 繰り返し。例: $($v:literal),* なら 1, 2, 3 などにマッチ
        • 区切り文字: ,; が一般的
        • 繰り返し種類
          • ?: 0回か1回
          • *: 0回以上の繰り返し
          • +: 1回以上の繰り返し
    • 展開される部分
      • $メタ変数 でキャプチャした変数へ展開
      • $(...)* で繰り返して展開
  • 勘所2: フラグメント指定子
    • ident , expr , literal など。
    • 最初は勘でおk!詳細は別記事にまとめています。

色々モリモリにした宣言マクロ。展開後例を添えて

宣言マクロ詳細全部解説
macro_rules! マクロ名 {
    // 呼び出しパターン
    // $メタ変数:フラグメント指定子 
    ($メタ変数1:ident, $メタ変数2:expr) => { // マクロ名!( a, b ) という呼び出しにマッチ
        // 置き換え内容をここに書く
        // 例↓
        let $メタ変数1 = $メタ変数2;
        println!("{} = {}", stringify!($メタ変数1), $メタ変数1);
    }; // <- セミコロンで終わる

    // 複数のパターンからなることもある
    // $(...) 区切り文字 * で繰り返しにマッチできる
    (# $($s:stmt);* ) => { // マクロ名!(# let hoge = "..."; fn fuga() {} ) のような呼び出しにマッチ
        $( // $(...)* で繰り返しを展開
            println!("{}", stringify!($s));
            $s
        )*
    };
    
    // ヘンテコなパターンも書ける
    // ただしマッチ順に注意
    (@~ $t:tt ~%) => {
        println!("Hey, {}!", stringify!($t));
    };

    // 上からパターンマッチされるので、これは全てにマッチしなかった時
    // ただし定義順が悪いのでマッチさせられる表現はなさそう
    ( $(els:tt)* ) => {
        println!("マッチせず");
    };
}

fn main() {
    マクロ名!(hoge, 10);
    // ↑↓ カッコの種類 (), [], {} は呼び出しに無関係
    マクロ名![hoge, 10];

    // 共に以下に展開される
    // let hoge = 10;
    // println!("{} = {}", stringify!(hoge), hoge);

    // 2つ目にマッチ
    マクロ名!{# fn fuga() { println!("beep"); }; fuga(); fuga() };

    // 3つ目にマッチ
    マクロ名!(@~ + ~%);
}

マクロの分類 で話したマクロ定義方法のうち、宣言マクロについて取り組んでみたいと思います!

宣言マクロは、ソースコード内に直接書くことができ、手軽に作成できます。一方で、置き換え前のパターンマッチ内の書き方に制約がある影響で高度なことは手続きマクロと比べればしにくくなっています。特に複雑な繰り返し構造(回数指定など)は再帰を使うことで書けることもある一方、複雑になったり処理が重くなったりしがちです。

置換結果を例示することでマクロを作るという特徴より、宣言マクロ (Declarative Macro) や、例示マクロ (Macros By Example, MBE) と呼ばれます。

宣言マクロの定義方法

先に掲載した通り、Rustの宣言マクロは、次のように書きます。

Rust
macro_rules! マクロ名 {
    ( $メタ変数:フラグメント指定子 ) => {
        // $メタ変数を利用した置き換え内容
    };
}

...と言われてもどう書けば何ができるのかよくわからないと思うので、知っている人には退屈な内容かもしれないですが、一応軽いハンズオンで丁寧に解説したいと思います。

自分が以前書いた記事にちょうど「マクロを使う方法がありますが」として放置していた良い題材があるので、これで行きましょう!

ユーザーサイドで定義した IsEven トレイトをプリミティブ型に定義したいというシーンです。 u32 型と i32 型にはすでに定義されている状況です。

Rust
trait IsEven {
    fn is_even(&self) -> bool;
}

impl IsEven for u32 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}

impl IsEven for i32 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}

// u8, i8, u16, i16, u64, i64, u128, i128 にも実装したい...!
// え?ここに全部書くの...?ご冗談でしょ?

プリミティブ型それぞれについて、以下に示す記述を全部書くことになります。

Rust
impl IsEven for 型名 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}

このようなボイラーテンプレートの記述量を減らせるのがマクロ、というわけです!

Rust
impl_is_even!(u32);

// ↑ 上のように書けば ↓ 下のように書いたことにしたい!

impl IsEven for u32 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}

では書いてみましょう。impl_is_even という名前のマクロにしたいので次のようにします。

Rust
macro_rules! impl_is_even {}

次にマクロの引数部分を考えます。 u32 の部分ですね。引数として与えた u32 を先ほど 型名 としていた部分に当てはめたいです。というわけで、メタ変数で u32 をキャプチャします!

メタ変数名はなんでも良いです。数値型が入りそうなので $num としておきます。

Rust
macro_rules! impl_is_even {
    ( $num:フラグメント指定子 ) => {}
}

フラグメント指定子 とした部分には、キャプチャするメタ変数がどんな構文要素かを指定します。フラグメント指定子を以下の表にまとめました!

フラグメント指定子 説明
ident hoge, self 変数名や関数名などの識別子全般
expr 10 + 20, funcall(), if a { b } else { c } 式。いわゆる評価後に値を返す何か
literal 10, "hoge", true リテラル。言い換えると、ソースコード内で直接表現される固有値
stmt let a = 10;, struct Hoge;, fn foo() {} ステートメント。文。何かしら意味を持つものの評価値とはならないもの
ty i32, &'a str, fn() -> ()
path std::option::Option, Turbo::<Fish>, ::std::println パス。一見型と同じに見えるが、 :: で接続されたパスを表す意味が強い。例えば ::std::thread::spawn は関数であり型ではなく、これ自体はパスである
block {} スコープを作り出す中括弧を含めた全体
item pub fn foo() {}, use std::fs;, impl Hoge {} use文や関数宣言など。一部は文でもある。アイテム...という名前だとわかりにくいので、「ファイル内で最も外側に(あるいはトークン木の最上位に)配置できる要素」とおぼえておくと良さそう
pat / pat_param Some(ref hoge), None, 0..10 パターンマッチとして使用できる構造。簡単に言えば match 式のアーム部分になり得るもの。 pat_param は古いエディションとの互換性を保つためなので忘れて良い
lifetime 'a, 'static, '_ ライフタイム
meta derive(Debug), path = "hoge", serde 属性風マクロ等のアトリビュート(属性)、すなわち #[] の内側に書くことができる要素。めったに出てこないので忘れてよさそう
vis pub, pub(crate) Visibility、すなわち可視性の略。これのみをマッチさせる必要もないためめったに出てこない
tt トークン木 (Token Tree) のことで、 任意の Rustコード片の塊にマッチするワイルドカード。つまりジョーカー

詳細は次の記事にまとめました!↓

Rustマクロ フラグメント指定子 #Rust - Qiita

「フラグメント指定子がどうしても覚えられない!!!!」という筆者のような方向けに、フラグメント指定子判別器を作ったので良かったら使ってみてください!

Rustマクロ フラグメント指定子(ident, expr, item, stmt...)なんもわからん!となったので判別器作った #Rust - Qiita

今回、 u32 は型名なのでフラグメント指定子は ty が適切そうです!

Rust
macro_rules! impl_is_even {
    ( $num:ty ) => {}
}

最後に、 {} の中にマクロの置換結果となるコードを記述します!

Rust
macro_rules! impl_is_even {
    ( $num:ty ) => {
        impl IsEven for $num {
            fn is_even(&self) -> bool {
                *self % 2 == 0
            }
        }
    }
}

...おっと、忘れ物です!() => {} の後にはセミコロン ; を入れましょう! これ忘れがちなのですが、マクロでマッチパターンが増えた時にセミコロンを忘れると要らぬエラーと戦うことになるので注意です!

Rust
macro_rules! impl_is_even {
    ( $num:ty ) => {
        impl IsEven for $num {
            fn is_even(&self) -> bool {
                *self % 2 == 0
            }
        }
    }; // <- 終わりのセミコロン、忘れない!
}

これで、次のように記述することでプリミティブ型へ簡単に IsEven を実装できるようになりました!

Rust (src/main.rs)
trait IsEven {
    fn is_even(&self) -> bool;
}

macro_rules! impl_is_even {
    ( $num:ty ) => {
        impl IsEven for $num {
            fn is_even(&self) -> bool {
                *self % 2 == 0
            }
        }
    };
}

impl_is_even!(u8);
impl_is_even!(i8);
impl_is_even!(u16);
impl_is_even!(i16);
impl_is_even!(u32);
impl_is_even!(i32);
impl_is_even!(u64);
impl_is_even!(i64);
impl_is_even!(u128);
impl_is_even!(i128);

fn main() {
    println!("{}", 1u8.is_even());
    println!("{}", 2i32.is_even());
    println!("{}", 3u128.is_even());
}

これで望んだ動作をします。しかし本当に意図通りに展開されているでしょうか? cargo expand で調べてみましょう!

Rust
$ cargo expand
    Checking project v0.1.0 (/path/to/project)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
trait IsEven {
    fn is_even(&self) -> bool;
}
impl IsEven for u8 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}
impl IsEven for i8 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}
impl IsEven for u16 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}
impl IsEven for i16 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}
impl IsEven for u32 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}
impl IsEven for i32 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}
impl IsEven for u64 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}
impl IsEven for i64 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}
impl IsEven for u128 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}
impl IsEven for i128 {
    fn is_even(&self) -> bool {
        *self % 2 == 0
    }
}
fn main() {
    {
        ::std::io::_print(format_args!("{0}\n", 1u8.is_even()));
    };
    {
        ::std::io::_print(format_args!("{0}\n", 2i32.is_even()));
    };
    {
        ::std::io::_print(format_args!("{0}\n", 3u128.is_even()));
    };
}

問題なさそうです!

繰り返しの記述

...前節までで確かに問題ないですが、まだ何か冗長な感じがします!

繰り返し構文 $(...),* を使うことで impl_is_even! の記述回数も減らしましょう!

Rust
macro_rules! impl_is_even {
    ( $($num:ty),* ) => {
        $(
            impl IsEven for $num {
                fn is_even(&self) -> bool {
                    *self % 2 == 0
                }
            }
        )*
    };
}

impl_is_even!(
    u8,   i8,
    u16,  i16,
    u32,  i32,
    u64,  i64,
    u128, i128
);

メタ変数側も展開側も $(...) 区切り文字 繰り返し種類 と書くことで繰り返し表現が可能です!この場合繰り返される回数は、キャプチャした型の個数分になります。

  • 区切り文字には ,; を指定しておくのが無難でしょう。
    • 筆者も使える文字を正確には把握していません :sweat_smile:
  • 繰り返し種類は以下3つありますがほぼ * しか使うことがないです。意味合いは正規表現と同一ですね
    • ?: 0回か1回。(区切り文字は指定できない)
    • *: 0回以上
    • +: 1回以上

繰り返しの中身は結構柔軟なので、こんな感じに書くことも可能です。

Rust
macro_rules! print_is_even {
    ($(($num:expr, $numty:ty)),*) => {
        $(
            println!("{}", ($num as $numty).is_even());
        )*
    };
}

fn main() {
    print_is_even! {
        (1, u8),
        (2, i32),
        (3, u64),
        (4, i128)
    };
}

全体のPlayground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=49c3b3f5db390e327c7a3d16719de02d

マッチ(引数部分)についてもう少し

先程挙げたマクロの引数部分についてです。

Rust
macro_rules! print_is_even {
    ($(($num:expr, $numty:ty)),*) => {
        $(
            println!("{}", ($num as $numty).is_even());
        )*
    };
}

$(($num:expr, $numty:ty)),* ってなんだよ!って感じですよね。スペース多めにして解説します。

$( ( $num:expr , $numty:ty ) ),*

外側の $(),* は繰り返し構文なので一旦外して考えてみます。

( $num:expr , $numty:ty )

これはつまり、 (式, 型) みたいな引数にヒットしますよ!ということを表しています。

このように、宣言マクロの引数部分にはメタ変数の他特定の構造を要求することができます。マッチさえすればヘンテコなパターンでもある程度は大丈夫です!

Rust
macro_rules! strange {
    (@strange # $e:expr) => { // @strange # 何某 にマッチ
        println!("You are Strange!: {}", stringify!($e));
    };
    () => { // 空だった時にマッチ
        println!("Empty pattarn.");
    };
    ($($_t:tt)*) => { // それ以外にマッチ
        println!("Another pattern.");
    };
}

fn main() {
    strange!(@strange # if true { () } else { () });
    
    strange![];
    
    strange!{# $ % &};
}

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=cc74fe07df5b185e96ee8a16438f0254

また残りの基礎事項についても上のマクロから確認しましょう!

  • パターンは () => {}; で区切ることにより複数定義できます。
  • マクロを呼び出す時のカッコは () , [] , {} のどれでも構わないです!マクロによって慣例的に決まっていることが多そう (vec! なら vec![] など)

まとめ・所感

細かい部分で詰まることはあるかもしれませんが、基本は以上です!つまるところ、フラグメント指定子さえ迷わず書ければ宣言マクロはスラスラと書けるようになるかと思います。

というわけで、次回はフラグメント指定子の詳細をまとめたいと思います!

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?