コンテンツにスキップ

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つの引数を扱う本物のコードに展開されます。呼び出し方が違えば、展開されるコードも違います。

macro_rules! を使うと、自分でマクロを定義できます。数を2乗するマクロを作ってみましょう。

macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
println!("{}", square!(5));
}

実行結果:

25
▶ 実際に試してみよう

($x:expr) は「式を1つ受け取って、それを $x と呼ぶ」という意味です。=> { ... } の部分が、マクロが展開されてできるコードです。square!(5) と書いたところはどこでも、実際のコンパイルが始まる前に、コンパイラが 5 * 5 に置き換えます。

マクロは、もっと複雑なパターンにもマッチできます。引数の繰り返しのリストもその1つです。これは、ふつうの関数にはまったくできないことです。

macro_rules! sum {
($($x:expr),*) => {
{
let mut total = 0;
$(total += $x;)*
total
}
};
}
fn main() {
println!("{}", sum!(1, 2, 3));
println!("{}", sum!(10, 20));
}

実行結果:

6
30
▶ 実際に試してみよう

$($x:expr),* は「カンマで区切られた、0個以上の式」という意味です。vec![1, 2, 3] も vec![1, 2, 3, 4, 5] も1つのマクロ定義で動くのは、これと同じパターンのおかげです。

マクロには、セミコロンで区切って複数のルールを書けます。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));
}

実行結果:

7
9
2.5
▶ 実際に試してみよう

max!(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"

マクロ(println!、vec!、format!、#[derive(Debug)])は、自分で書かなくても毎日のように使います。独自のマクロを書くのは、少し特殊なスキルです。主に、大きなコードベースで本当に繰り返しの多い定型コードをなくしたいライブラリの作者が使います。マクロがあること、そしてだいたいどう動くのかを知っておけば十分で、最初から手を出すものではありません。

カンマで区切った値をいくつでも受け取り、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);
}

実行結果:

apple
42
3.5
▶ 実際に試してみよう

このマクロは、引数1つにつき1つずつ、合わせて3つの println! の呼び出しに展開されます。値ごとに型が違っていてもかまいません。これはふつうの関数にはできないことです。

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 は上から順に試し、ルールの中からそのマクロ自身をもう一度呼ぶこともできます。
  • マクロを書くのは特殊なスキルです。標準ライブラリにすでにあるマクロを使うだけで、必要なことのほとんどはまかなえます。

理解度チェック

1. println! をふつうの関数として書けないのはなぜですか?

2. macro_rules! のパターンの中の $x:expr はどういう意味ですか?

3. マクロが本物のコードに展開されるのはいつですか?

スコア: 0 / 3