Rustのマクロ
構文のページで、「! が付いていたら、ふつうの関数ではなくマクロ」という目安を学びました。println!、vec!、format! などです。このページでは、それが実際にはどういう意味なのかを、いよいよ説明します。
println! がふつうの関数ではいけない理由
Section titled “println! がふつうの関数ではいけない理由”fn add(a: i32, b: i32) -> i32 のようなふつうの関数は、いつも決まった数の、決まった型の引数を受け取ります。ところが println! は、引数の数も型もまったく違う呼び出し方ができます。
fn main() { println!("引数なし"); println!("{}", 42); println!("{}と{}", "文字列", 2);}実行結果:
引数なし42文字列と2こうした呼び出しをすべて受け付けられる関数のシグネチャは、ふつうの関数では書けません。マクロ(macro)は仕組みが違います。マクロは、コードが完全にコンパイルされる前に動き、呼び出され方に合わせて本物の Rust のコードを生成するのです。println!("{}と{}", "文字列", 2) は、ちょうどその2つの引数を扱う本物のコードに展開されます。呼び出し方が違えば、展開されるコードも違います。
かんたんなマクロを書く
Section titled “かんたんなマクロを書く”macro_rules! を使うと、自分でマクロを定義できます。数を2乗するマクロを作ってみましょう。
macro_rules! square { ($x:expr) => { $x * $x };}
fn main() { println!("{}", square!(5));}実行結果:
25($x:expr) は「式を1つ受け取って、それを $x と呼ぶ」という意味です。=> { ... } の部分が、マクロが展開されてできるコードです。square!(5) と書いたところはどこでも、実際のコンパイルが始まる前に、コンパイラが 5 * 5 に置き換えます。
複数の引数を受け取るマクロ
Section titled “複数の引数を受け取るマクロ”マクロは、もっと複雑なパターンにもマッチできます。引数の繰り返しのリストもその1つです。これは、ふつうの関数にはまったくできないことです。
macro_rules! sum { ($($x:expr),*) => { { let mut total = 0; $(total += $x;)* total } };}
fn main() { println!("{}", sum!(1, 2, 3)); println!("{}", sum!(10, 20));}実行結果:
630$($x:expr),* は「カンマで区切られた、0個以上の式」という意味です。vec![1, 2, 3] も vec![1, 2, 3, 4, 5] も1つのマクロ定義で動くのは、これと同じパターンのおかげです。
複数のルール
Section titled “複数のルール”マクロには、セミコロンで区切って複数のルールを書けます。Rust はルールを上から順に試し、最初にマッチしたものを使います。ルールの中から、そのマクロ自身をもう一度呼ぶこともできます。引数がいくつあっても動く max! を作ってみましょう。
macro_rules! max { // ルール1:値が1つだけ。いちばん大きいのはその値そのもの。 ($x:expr) => { $x }; // ルール2:値が1つと、そのあとに1つ以上の値。 ($x:expr, $($rest:expr),+) => { { let first = $x; let biggest_of_rest = max!($($rest),+); if first > biggest_of_rest { first } else { biggest_of_rest } } };}
fn main() { println!("{}", max!(7)); println!("{}", max!(3, 9, 4)); println!("{}", max!(2.5, 1.0));}実行結果:
792.5max!(3, 9, 4) の場合、値が2つ以上あるのでルール1にはマッチせず、ルール2にマッチします。ルール2は 3 を取っておいて max!(9, 4) を展開し、それがさらに max!(4) を展開します。最後の max!(4) はルール1にマッチするので、そこで止まります。順番が大事なのはこのためです。Rust はいつもルール1から試します。
ここでは、新しい書き方が2つ出てきました。
+は1個以上、sum!で使った*は0個以上という意味です。ルール2ではカンマのあとに少なくとも1つの値が必要なので、内側のmax!(...)の呼び出しには必ず何かが渡されます。max!は整数にも浮動小数点数にも使えます。マクロはfirst > biggest_of_restというコードをそのまま貼り込むだけだからです。関数で同じことをするにはジェネリクスが必要です。
マクロが受け取れるのは expr だけではありません。実際のマクロでよく見かけるものをいくつか紹介します。
| フラグメント | マッチするもの | 例 |
|---|---|---|
expr |
任意の式 | 2 + 3、name.len() |
ident |
名前(識別子) | count、Point |
ty |
型 | i32、Vec<String> |
literal |
リテラル値 | 42、"hi" |
マクロに実際に出会う場面
Section titled “マクロに実際に出会う場面”マクロ(println!、vec!、format!、#[derive(Debug)])は、自分で書かなくても毎日のように使います。独自のマクロを書くのは、少し特殊なスキルです。主に、大きなコードベースで本当に繰り返しの多い定型コードをなくしたいライブラリの作者が使います。マクロがあること、そしてだいたいどう動くのかを知っておけば十分で、最初から手を出すものではありません。
1. 全部表示する
Section titled “1. 全部表示する”カンマで区切った値をいくつでも受け取り、1つずつ別の行に表示するマクロ print_all! を書いてください。print_all!("apple", 42, 3.5) なら3行が表示されます。ヒント:sum! と同じように $($x:expr),* にマッチさせ、$( ... )* で値ごとに println! を繰り返します。
解答を見る
macro_rules! print_all { ($($x:expr),*) => { $(println!("{}", $x);)* };}
fn main() { print_all!("apple", 42, 3.5);}実行結果:
apple423.5このマクロは、引数1つにつき1つずつ、合わせて3つの println! の呼び出しに展開されます。値ごとに型が違っていてもかまいません。これはふつうの関数にはできないことです。
2. 自分だけの vec!
Section titled “2. 自分だけの vec!”vec! と同じように動く my_vec! を書いてください。my_vec![] は空の Vec を、my_vec![1, 2, 3] はその要素が入った Vec を返します。ヒント:ルールを2つ使います。1つ目は何もない () にマッチします。2つ目は $($x:expr),+ にマッチし、Vec::new() を作って、それぞれの $x を push します。
解答を見る
macro_rules! my_vec { () => { Vec::new() }; ($($x:expr),+) => { { let mut v = Vec::new(); $(v.push($x);)+ v } };}
fn main() { let empty: Vec<i32> = my_vec![]; let numbers = my_vec![1, 2, 3]; println!("{:?} {:?}", empty, numbers);}実行結果:
[] [1, 2, 3]どのマクロも ()、[]、{} のどれでも呼び出せます。vec! のようなリストっぽいマクロには角かっこを使うのが慣習ですが、my_vec!(1, 2, 3) でも動きます。空の Vec に : Vec<i32> という型が必要なのは、要素が1つもないと、何を入れるベクタなのか Rust にはわからないからです。
- マクロはコンパイルの前に本物のコードへ展開されます。だから
println!は、呼び出すたびに違う数・違う型の引数を受け取れるのです。 - 名前のあとに
!が付いていたら、それは関数ではなくマクロを呼び出しているしるしです。 macro_rules!を使うと、1つの式($x:expr)や繰り返しのリスト($($x:expr),*)のようなパターンにマッチする、自分だけのマクロを定義できます。- マクロには複数のルールを書けます。Rust は上から順に試し、ルールの中からそのマクロ自身をもう一度呼ぶこともできます。
- マクロを書くのは特殊なスキルです。標準ライブラリにすでにあるマクロを使うだけで、必要なことのほとんどはまかなえます。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. println! をふつうの関数として書けないのはなぜですか?
2. macro_rules! のパターンの中の $x:expr はどういう意味ですか?
3. マクロが本物のコードに展開されるのはいつですか?
スコア: 0 / 3
さらに詳しく
Section titled “さらに詳しく”- マクロ — The Rust Programming Language 日本語版:
vec!がどう定義されているかと、#[derive]のような手続き的マクロの入門。 - macro_rules! — Rust By Example 日本語版:指定子、オーバーロード、繰り返しを、短くて実行できる例で紹介しています。
- Macros By Example — The Rust Reference(英語):
expr、ident、ttなどのフラグメントの全一覧と、マッチングの正確な仕組み。 - The Little Book of Rust Macros(英語):大きなマクロを書きたくなったときのための、無料でわかりやすい詳しい解説。