本記事で伝えたいこと
- 宣言マクロ内でマクロを呼ぶことが可能で、 再帰的な呼び出しも可能
- 再帰を使うことで、条件分岐のような複雑な繰り返しも可能
- しかしただでさえ遅いマクロがより低速になるデメリットがあるのでできるなら他の手段が吉
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
が紹介され、リスト構造を実現しています。
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));
}
ちょっとしたリストを作るたびに一々このように書いていたのでは面倒です。こういう時こそ宣言マクロの出番です!
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, ...]))
という形に変形したいので、そのように書きます!
macro_rules! list {
( $v:expr, $($rest:expr),* ) => { // 再帰呼び出しするルール
Rc::new(Cons($v, list!($($rest),*) )) // 残りの部分をlistマクロに渡す
};
}
ただこれだと list![3]
みたいに最後の一つになった時の条件がないので、そのルールを始めに持っていきます! Nil
を入れると良さそうですね
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です!
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)))
};
}
全体はこんな感じになりました
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
マクロを統合してしまいたいと思います!
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