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

Rust宣言マクロ 再帰テクニック

Posted at

本記事で伝えたいこと

  • 宣言マクロ内でマクロを呼ぶことが可能で、 再帰的な呼び出しも可能
  • 再帰を使うことで、条件分岐のような複雑な繰り返しも可能
  • しかしただでさえ遅いマクロがより低速になるデメリットがあるのでできるなら他の手段が吉
Rust
macro_rules! list {
    ( $v:expr ) => { // 停止するルール
        Rc::new(Cons($v, Rc::new( Nil )))
    };
    ( $v:expr, $($rest:expr),* ) => { // 再帰呼び出しするルール
        Rc::new(Cons($v, list!($($rest),*) ))
    };
}

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

宣言マクロを使えるようになるための詳細については前々回の記事で大体網羅しました。特に複雑なものはありません。

今回は知らなくても宣言マクロは使えるけど覚えておくと良さげな応用テクニック「宣言マクロの再帰呼び出し」を軽く紹介したいと思います!

今回の題材

TRPLのこの章ではスマートポインタの一つとして std::rc::Rc が紹介され、リスト構造を実現しています。

Rust
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

ちょっとしたリストを作るたびに一々このように書いていたのでは面倒です。こういう時こそ宣言マクロの出番です!

Rust
macro_rules! list {
    // todo!
}

macro_rules! cons {
    // todo!
}

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = list![5, 10]; // Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))) に展開
    let b = cons!(3; a); // Rc::new(Cons(3, Rc::clone(&a))) に展開
    // ↓ ちょっと改変
    let c = cons!(4, 2; a); // Rc::new(Cons(4, Rc::new(Cons(2, Rc::clone(&a))))) に展開
}

今回のような入れ子構造に対しては繰り返し $(...)* で記述することが難しいです。逆に再帰だととても書きやすいので、再帰で書いてみます!

再帰でマクロ実装

まず list マクロから作ってみます。

[1, 2, 3] というリストを list![1, list![2, list![3]]] のような感じに見て考えます。一回のマクロ呼び出しで、list![1, 2, 3, ...]Rc::new(Cons(1, list![2, 3, ...])) という形に変形したいので、そのように書きます!

Rust
macro_rules! list {
    ( $v:expr, $($rest:expr),* ) => { // 再帰呼び出しするルール
        Rc::new(Cons($v, list!($($rest),*) )) // 残りの部分をlistマクロに渡す
    };
}

ただこれだと list![3] みたいに最後の一つになった時の条件がないので、そのルールを始めに持っていきます! Nil を入れると良さそうですね

Rust
macro_rules! list {
    ( $v:expr ) => { // 停止するルール
        Rc::new(Cons($v, Rc::new( Nil )))
    };
    ( $v:expr, $($rest:expr),* ) => { // 再帰呼び出しするルール
        Rc::new(Cons($v, list!($($rest),*) ))
    };
}

次に cons マクロを作ってみます。

cons!(a, b ; 追加先リスト) という風に書くと、 Rc::new(Cons(a, Rc::new(Cons(b, Rc::clone(&追加先リスト))))) になるように書きたいです。先ほどと似た感じで、追加する値が一つになるまで再帰呼び出しするように書けばおkです!

Rust
macro_rules! cons {
    ($v:expr; $list:expr) => {
        Rc::new(Cons($v, Rc::clone(&$list)))
    };
    ($v:expr, $($rest_v:expr),*; $list:expr) => {
        Rc::new(Cons($v, cons!($($rest_v),*; $list)))
    };
}

全体はこんな感じになりました

Rust
macro_rules! list {
    ( $v:expr ) => {
        Rc::new(Cons($v, Rc::new( Nil )))
    };
    ( $v:expr, $($rest:expr),* ) => {
        Rc::new(Cons($v, list!($($rest),*) ))
    };
}

macro_rules! cons {
    ($v:expr; $list:expr) => {
        Rc::new(Cons($v, Rc::clone(&$list)))
    };
    ($v:expr, $($rest_v:expr),*; $list:expr) => {
        Rc::new(Cons($v, cons!($($rest_v),*; $list)))
    };
}

#[derive(Debug)]
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use std::rc::Rc;
use List::{Cons, Nil};

fn main() {
    let a = list![5, 10];
    let b = cons!(3; a);
    let c = cons!(4, 2; a);

    dbg!(a, b, c);
}

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

パターンをまとめる

別れたままでも良いのですが、大して複雑でもないので list マクロと cons マクロを統合してしまいたいと思います!

Rust
macro_rules! list {
    ( $v:expr ) => {
        Rc::new(Cons($v, Rc::new( Nil )))
    };
    ( $v:expr, $($rest:expr),* ) => {
        Rc::new(Cons($v, list!($($rest),*) ))
    };
    // consマクロからコピペ
    ($v:expr; $list:expr) => {
        Rc::new(Cons($v, Rc::clone(&$list)))
    };
    ($v:expr, $($rest_v:expr),*; $list:expr) => {
        Rc::new(Cons($v, list!($($rest_v),*; $list))) // consマクロではなくlistマクロを呼ぶようにしただけ
    };
}

#[derive(Debug)]
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use std::rc::Rc;
use List::{Cons, Nil};

fn main() {
    let a = list![5, 10];
    let b = list![3; a];
    let c = list![4, 2; a];

    dbg!(a, b, c);
}

; の有無で場合分けされるのでコピペするだけで難なく動きました!

まとめ・所感

リスト構造をマクロで楽に作るという例でマクロの再帰呼び出しテクニックを紹介しましたが、そもそも題材が再帰的な構造を持つものだったので恣意的な例だったかもしれません。

ともかく、 $(...)* による繰り返し記述方法の他に、再帰的に書くことでも繰り返し表現が可能なことを伝えられたかなと思います。

ただ、再帰パターンは下手に書くと(例えば2変数で再帰するなど)マクロの展開をかなり遅くしてしまう可能性があるので、あまり乱用しない方が良いでしょう。

参考: https://veykril.github.io/tlborm/decl-macros/patterns/tt-muncher.html#performance

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