はじめに
みなさんは「マクロ」と聞いてどのような印象をもちますか?
おそらく普段どのような言語でどのようなことをなさっているかによって変わってくるのではないでしょうか。私はもともと C/C++ からプログラミングに入門してほとんどそれ以外の経験がないため、マクロに対してあまりいい印象をもっていません。 C/C++ のマクロには様々な問題点があるからです。
しかし Rust のマクロは C/C++ のそれと比べればかなり安全にできています。ここでは何がどう安全なのかは他の記事に譲るとさせてください。うまくマクロを使うとずいぶんと楽ができます。
- 「あ、似たようなコードを書いてるな」と思ったときにさくっと簡単なマクロを定義すれば、コードが見た目上増えることを防げるので見やすい。変更があったときもマクロ内を変えれば全ての実装が切り替わるので、コピペに比べてコストも低い。
- 「なんかこの部分の記述、ごちゃごちゃしてるな」と思ったときにさくっとマクロを定義すれば、自分が見やすいと思う構文でシンプルに記述できる。
特にマクロはパターンマッチによって比較的自由に構文を決められます。単に可変長引数のようなマクロ1を定義することもできるし、 Rust の文法でないようなもの2をパースするようなものも書けます。これによって自分が見やすく使いやすいと感じるフォーマットで呼び出せるマクロを書くことができるのです。一種の構文拡張ですね。
さて、ここまで何も言わずにマクロと述べてきましたが、実は Rust のマクロには大きくわけて 2 種類あります。
-
macro_rules!
によるマクロ - 手続き的マクロ (procedural macro)
-
derive
マクロ - アトリビュートマクロ
- 関数形式のマクロ
-
macro_rules!
によるマクロは、 macro_rules!
という構文3を使って定義するマクロです。手続き的マクロは「トークン列をうけとってトークン列を返す」というような関数の形で定義するマクロです。手続き的マクロの方が普通の Rust プログラムと同様にトークンを好き放題加工できるため自由度はとても高いです。一方まだ一部が安定化されていないことと4、仕組み上新しいクレートを用意してゴリゴリと書く必要があって、さっと手軽には書けないことが欠点です。
今回は macro_rules!
によるマクロを扱っていきます。 macro_rules!
はどこにでも書けるため、ふと思い立ったときにさっとマクロを書く用途には最適です。
例としての都合上少々強引なのですが、どんな感じでマクロを使えるのかご紹介したいと思います。
BMI を計算する関数をつくりたい
突然ですが、健康管理のために BMI を計算したくなったとしましょう。 BMI は 体重 (kg)
を 身長 (m)
の二乗で割ることによって求められるもので、その人のおおよその肥満度を測る指数です。それと聞けば身長と体重を入力したら BMI を計算してくれる関数が欲しくなるのは自然なことです。それほど難しいことはなく次のような関数ですね。
/// BMI を計算する。 height はメートルで渡すこと!
fn bmi(height: f64, weight: f64) -> f64 {
weight / (height * height)
}
// 次のように使う
println!("BMI: {}", bmi(1.7, 60.0));
// => BMI: 20.761245674740486
しかし、この height
をうっかりセンチメートルで渡してしまうとどうなるでしょうか。
println!("BMI: {}", bmi(170.0, 60.0));
// => BMI: 0.0020761245674740486
当然エラーにはなりませんが、値は 1/10000 されてしまっています。これはバグです。そもそも引数の順番を間違えてしまったら?
println!("BMI: {}", bmi(60.0, 170.0));
// => BMI: 0.04722222222222222
これも再びエラーにはなりませんが、もはや値には何の意味もありません。
Newtype パターン
先の例の問題点は、メートル単位での身長 height
とキログラム単位での体重 width
をどちらも「単なる小数」としてしか扱っていないことにあります。せっかく静的型付け言語なのにもう少しなんとかならないのでしょうか?
なります。
Rust にはタプル構造体があり、 struct Meter(i64);
とすることによって無名の i64
型のメンバをフィールドとしてもつ新しい構造体を簡単に作ることができます。これを使えば数値としては同じ型の値に別々の意味を持たせることができます (newtype パターン) 。すると、意味の違う値を渡そうとしたときには型の不一致という形でエラーになってくれます。
struct Meter(f64);
struct Kilogram(f64);
#[derive(Debug)]
struct BMI(f64);
// しっかり型をつける。
fn bmi(height: Meter, weight: Kilogram) -> BMI {
BMI(weight.0 / (height.0 * height.0))
}
// 次のように使う。
// わざわざ Meter(...) として値を作るのでセンチメートル単位で渡す人はいないと思
// いたい。
println!("{:?}", bmi(Meter(1.7), Kilogram(60.0)));
// => BMI(20.761245674740486)
// 逆にすると通らない
println!("{:?}", bmi(Kilogram(60.0), Meter(1.7)));
// mismatched types ^^^^^^^^ expected `Meter`, found `Kilogram`
できました。しかし、なぜ println!()
で {:?}
(デバッグ出力) 指定をしなければいけないのでしょうか?表示も数字だけでなく BMI(...)
のように表示されてしまい、不自由です。 {}
でよいのでは?
println!("{}", bmi(Meter(1.7), Kilogram(60.0)));
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// `BMI` cannot be formatted with the default formatter
しかし、そうもいかないようです。これは BMI
は f64
とはもはや違う型なので、計算や表示を含めて f64
でできたことは一切できないのです。よって表示方法を別に定義してやる必要があります。表示をするためには Display
トレイトを実装してやればよいのでした。
impl fmt::Display for BMI {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, "{}", self.0)
}
}
println!("{}", bmi(Meter(1.7), Kilogram(60.0)));
// => 20.761245674740486
無事表示できたかと思います。この調子でメートルもキログラムも数値として表示できるようにしてしまいましょう。
impl fmt::Display for Meter {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, "{}", self.0)
}
}
impl fmt::Display for Kilogram {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, "{}", self.0)
}
}
impl fmt::Display for BMI {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, "{}", self.0)
}
}
...良いのですが、なんというか、大変ですね。実装対象の型が変わっただけで、それ以外の部分は全く同じトークン列です。これは根本的に別々の型についての話でたまたま実装が同じトークン列になっただけなのでジェネリクスなどにすることもできません。別に Display
ひとつくらいであれば何度でもコピペすれば済む話かもしれませんが、四則演算も欲しくなったらどうしましょうか。 Display
に加えて Add
, Sub
, Mul
, Div
を実装するとなると、これだけで トレイト 5 個 × 構造体 3 個 = 15 個の実装を用意しなければなりません。書き間違いやバグがあったとすれば、修正の際する際にも全て確実に直さなければなりません。別のトレイト、 Neg
の実装を増やそうと思ったらまた 3 個かかなければなりません。いよいよ本質的なコードよりこのボイラープレートの方が大きくなってきてしまうかもしれません。
面倒だなあ。そう思ったときにこそマクロを使いましょう5。
impl
ブロックをマクロでまとめる
先ほどもみたとおり、異なる 3 つの型への Display
の実装は実装先の型名以外は全く同じでした。ということは、実装先の型名を渡すと実装へ展開されるマクロを書けばよいのではないかと思い至ります。これは次のようにマクロを書くことで達成できます。
struct Meter(f64);
struct Kilogram(f64);
struct BMI(f64);
macro_rules! impl_display_for_newtype {
($target:ty) => {
impl fmt::Display for $target {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, "{}", self.0)
}
}
};
}
// 使うときはこれだけ
impl_display_for_newtype!(Meter);
impl_display_for_newtype!(Kilogram);
impl_display_for_newtype!(BMI);
マクロの書き方そのものは詳細には説明しませんが、おおよそ次のような意味です。
macro_rules! impl_display_for_newtype {
($target:ty) => {
// ------------ マッチャー。マクロの引数の形式を指定する。
// ここでは「型 (ty) が一つ、名前を $target とする」の意。
// これ以下は展開される内容がそのままくる。展開時、マッチャーで指定した
// 引数を使うことができる。
impl fmt::Display for $target {
// ------- 対象の型の部分を引数で受け取った型に。
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, "{}", self.0)
}
}
};
}
コードを眺めながら考えてみると、外で struct BMI(f64);
なんてしなくても、それすらもこのマクロの中で定義してやればよいのでは?と思うことでしょう。そうすれば構造体名を書く場所は一箇所で済むので、変更するときも楽です。
macro_rules! newtype_f64 {
($target:ident) => {
// ----- 「型」ではなく「識別子」を受け取ることにした。
struct $target(f64);
// -------------------- ここで構造体定義も生成する
impl fmt::Display for $target {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, "{}", self.0)
}
}
};
}
newtype_f64!(Meter);
newtype_f64!(Kilogram);
newtype_f64!(BMI);
println!("{}", bmi(Meter(1.7), Kilogram(60.0)));
// => 20.761245674740486
なお、$target
を型 (ty
) ではなく識別子 (ident
) であるというふうに訂正しました。これは struct $target(f64);
の $target
の部分には型ではなく識別子がくる必要があるからです。今の例のように実際に展開してみれば実は問題ない引数を与えていたとしてもコンパイルエラーになります6。
f64 以外の newtype も作りたい
さて、今は newtype の内部の型は f64
で固定です。しかし i32
や String
などを持ちたいこともあるでしょう。 newtype_i32!
や newtype_String!
も定義しましょうか?いえ、今のマクロをさらに改造して、内部の型を受け取るようにすればよいのです。
macro_rules! newtype {
($inner:ty => $target:ident) => {
struct $target($inner);
impl fmt::Display for $target {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, "{}", self.0)
}
}
};
}
newtype!(f64 => Meter);
newtype!(f64 => Kilogram);
newtype!(f64 => BMI);
newtype!(u32 => Age);
println!("{} years old", Age(123));
// => 123 years old
println!("{}", bmi(Meter(1.7), Kilogram(60.0)));
// => 20.761245674740486
どうでしょうか。 f64 => Meter
なんて、こんな構文は Rust にはありません。マクロを使えば自分の好きな構文を好きなように展開させられるようになります。段々面白くなってきたのではないでしょうか。
単位を表示させてみる
せっかく値に意味があるのだから、これを表示するときは単位を自動で付けてほしいなどと考えることもあるかもしれません。生の数字が必要なときは中身をとり出して数字として表示させればよいので。すると定義の時に Display
の表示内容に単位を含めればよいわけですね。もう我々は好きなように構文を増やせるので、自分が見やすいと思うような好きな形式で単位を与えることができます。
macro_rules! newtype {
($inner:ty => $target:ident [ $unit:literal ]) => {
// ----------------- [] で囲んで単位をリテラルで与える
struct $target($inner);
impl fmt::Display for $target {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, "{} {}", self.0, $unit)
}
}
};
}
newtype!(f64 => Meter ["m"]);
newtype!(f64 => Kilogram ["kg"]);
newtype!(u32 => Age ["years old"]);
// 勝手に単位がつく
println!("{}", Kilogram(2.0));
// => 2 kg
println!("{}", Age(123));
// => 123 years old
しかし BMI
には何と単位をつけたらよいでしょうか。単位を常に要求するというのでは困りそうですね。単位はつけてもつけなくてもよいと考えた方がよいかもしれません。そのためには write!
に与えるフォーマット文字列が変わってきますから、フォーマット文字列も与える必要があります。
macro_rules! newtype {
($inner:ty => $target:ident, $format:literal $( [ $unit:literal ] )?) => {
// --------------- ----------------------^
// | `?` は 0 個または 1 個を意味する。
// フォーマット文字列
struct $target($inner);
impl fmt::Display for $target {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, $format, self.0 $(, $unit)*)
}
}
};
}
newtype!(f64 => Meter, "{} {}" ["m"]);
newtype!(f64 => Kilogram, "{} {}" ["kg"]);
newtype!(f64 => BMI, "{}");
newtype!(u32 => Age, "{} {}" ["years old"]);
// 勝手に単位がつく
println!("{}", Kilogram(2.0));
// => 2 kg
println!("{}", Age(123));
// => 123 years old
println!("{}", bmi(Meter(1.7), Kilogram(60.0)));
// => 20.761245674740486
しかしフォーマット文字列はこのマクロの内部実装であり、単位があるかないかで必要となるフォーマット文字列は決まります。できればユーザーに入力させたくはないですね。
大丈夫です、解決策はあります。実はマクロのマッチャーは複数書くことができます。上から順に試していき、始めてマッチした部分のマクロが実行されます。これはちょうど match {}
式と同じような感じですね。そして、マクロも再帰できますので、次のように書くことができます。
macro_rules! newtype {
// 単位あり
($inner:ty => $target:ident [ $unit:literal ]) => {
// 中身で先程の形式を作る
newtype!($inner => $target, "{} {}" [$unit]);
};
// 単位なし
($inner:ty => $target:ident) => {
// 中身で先程の形式を作る
newtype!($inner => $target, "{}");
};
// 実装は両方こちらに移譲する
($inner:ty => $target:ident, $format:literal $( [ $unit:literal ] )?) => {
struct $target($inner);
impl fmt::Display for $target {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, $format, self.0 $(, $unit)*)
}
}
};
}
newtype!(f64 => Meter["m"]);
newtype!(f64 => Kilogram["kg"]);
newtype!(f64 => BMI);
newtype!(u32 => Age["years old"]);
// 勝手に単位がつく
println!("{}", Kilogram(2.0));
// => 2 kg
println!("{}", Age(123));
// => 123 years old
println!("{}", bmi(Meter(1.7), Kilogram(60.0)));
// => 20.761245674740486
少しだけ内部実装を整理しましょう。処理を移譲するマッチャーの部分はユーザーが直接入力することを意図していませんから、もっとマクロにとって扱いやすい形にしてもよいはずです。むしろ、変に似ているとユーザーが少し書き間違えたときに間違ってそちらに直接マッチされてしまうことがあります。完全に別の構文にしてしまいましょう。
macro_rules! newtype {
// 単位あり
($inner:ty => $target:ident [ $unit:literal ]) => {
newtype! {
@inner $inner;
@target $target;
@format "{} {}";
@unit $unit;
}
};
// 単位なし
($inner:ty => $target:ident) => {
newtype! {
@inner $inner;
@target $target;
@format "{}";
@unit;
}
};
// 実装は両方こちらに移譲する
(@inner $inner:ty;
@target $target:ident;
@format $format:literal;
@unit $($unit:literal)?;) => {
struct $target($inner);
impl fmt::Display for $target {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, $format, self.0 $(, $unit)*)
}
}
};
}
ちなみに Rust では、マクロを呼び出すときの括弧は macro!()
でも macro!{}
でもよいことになっています。今回は呼出が複数行に渡るので {}
を使ったほうが個人的に見やすいと思い、そうしました。
好きな単位から変換できるようにする
最後にもう少しだけ機能を追加しましょう。
今は身長をメートルで与える必要があることは明示されていますが、ではもともと身長がセンチメートルで与えられているときはどうしましょうか。 Meter(centimeter * 0.01)
で十分ですが、センチメートルからメートルの変換はいつ何時でも 1/100 すればよいですよね。書き間違いを防ぐためにも Meter::from_centi(centimeter)
みたいな感じで呼び出せる方がきっとよいはずです。
newtype!(f64 => Meter["m"]);
newtype!(f64 => Kilogram["kg"]);
newtype!(f64 => BMI);
impl Meter {
fn from_centi(centimeter: f64) -> Meter {
Meter(centimeter * 0.01)
}
}
println!("{}", bmi(Meter::from_centi(170.0), Kilogram(60.0)));
// => 20.761245674740486
とはいえ、変換元となるような長さはミリメートルやキロメートルなどたくさんありえます。これも全部書くのでしょうか?
impl Meter {
fn from_centi(value: f64) -> Meter {
Meter(value * 0.01)
}
fn from_milli(value: f64) -> Meter {
Meter(value * 0.001)
}
}
またほとんど同じコードです。うーん、さっきと同じにおいがしますね。もっとマクロで簡単にかけるようにできそうです。とりあえず関数名と掛ける数字を受け取ればよいはずです。目指すものは次のように書けるようにすることです7。
newtype! {
f64 => Meter["m"] multipliers {
new: 1.0;
from_centi: 0.01;
from_milli: 0.001;
from_kilo: 1000.0;
}
};
newtype! {
f64 => Kilogram["kg"] multipliers {
new: 1.0;
from_gram: 0.001;
from_ton: 1000.0;
}
};
newtype! {
f64 => BMI multipliers {
new: 1.0;
}
};
println!("{}", bmi(Meter::from_centi(170.0), Kilogram::new(60.0)));
println!("{}", bmi(Meter::from_milli(1700.0), Kilogram::new(60.0)));
// => 20.761245674740486
一気に複雑になったように見えますが、どのように実装したらよいでしょうか。実はそれほど難しいことはありません。この場合、 関数名: 乗数;
という決まった形式を繰り返すだけだからです。これはマクロの繰り返し構文で簡単にとれます。もしこの形式が行ごとに変わったりする場合はマクロの再帰を使ってごにょごにょする必要があり、途端に面倒になってきます。
macro_rules! newtype {
// 単位あり
($inner:ty => $target:ident [ $unit:literal ] multipliers { $($rest:tt)* }) => {
// ---------------------------- 追加
// パースはしないで単にトークンの
// 列として受け取り、実際の処理は
// 内部実装に投げる
newtype! {
@inner $inner;
@target $target;
@format "{} {}";
@unit $unit;
@multipliers { $($rest)* };
}
};
// 単位なし
($inner:ty => $target:ident multipliers { $($rest:tt)* }) => {
// ---------------------------- 追加
// パースはしないでトークンの列として内部に投げる
newtype! {
@inner $inner;
@target $target;
@format "{}";
@unit;
@multipliers { $($rest)* };
}
};
// 実装は両方こちらに移譲する
(@inner $inner:ty;
@target $target:ident;
@format $format:literal;
@unit $($unit:literal)?;
@multipliers {
$($ctor_name:ident: $mul:literal;)*
// -------------^--------------------- `関数名: 乗数;` の繰り返しをパースする
};) => {
struct $target($inner);
impl fmt::Display for $target {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, $format, self.0 $(, $unit)*)
}
}
impl $target {
// それぞれを関数定義にして、それを繰り返し展開する。
$(
#[allow(dead_code)]
fn $ctor_name(value: $inner) -> $target {
$target(value * $mul)
}
)*
}
};
}
最初のマッチでは multipliers
の中身はパースしません。ここでパースしてもこのマクロ内では使わないで内部実装に投げてしまいますし、入口が二箇所 (単位ありと単位なし) あるので二箇所に同じものを書く必要があって面倒だからです。少し前に「一度ある種類のトークン列としてマッチしたものは二度と別の種類のトークン列として再解釈することはできません」と述べましたが、実は単一のトークン木を表す tt
だけは特別です。 tt
は単なるトークンとしてしか扱われないので意味が確定しておらず、再びマクロのマッチャーによるマッチが可能です。ですから、とりあえず未処理の部分を tt
の列として受け取って ($($tt:tt)*
) 内部実装や自分自身に再帰的に投げるといったことはマクロではよく行われます。
まとめ
都合上 newtype にさらに Copy
を derive させましたが、最終的なコードは次のようになりました。これでいろいろなうっかりミスをできるだけ起こさせないような、快適な bmi()
関数ができました。
fn main() {
use std::fmt;
macro_rules! newtype {
// 単位あり
($inner:ty => $target:ident [ $unit:literal ] multipliers { $($rest:tt)* }) => {
newtype! {
@inner $inner;
@target $target;
@format "{} {}";
@unit $unit;
@multipliers { $($rest)* };
}
};
// 単位なし
($inner:ty => $target:ident multipliers { $($rest:tt)* }) => {
newtype! {
@inner $inner;
@target $target;
@format "{}";
@unit;
@multipliers { $($rest)* };
}
};
// 実装は両方こちらに移譲する
(@inner $inner:ty;
@target $target:ident;
@format $format:literal;
@unit $($unit:literal)?;
@multipliers {
$($ctor_name:ident: $mul:literal;)*
};) => {
#[derive(Clone, Copy)]
struct $target($inner);
impl fmt::Display for $target {
fn fmt(&self, b: &mut fmt::Formatter) -> fmt::Result {
write!(b, $format, self.0 $(, $unit)*)
}
}
impl $target {
$(
#[allow(dead_code)]
fn $ctor_name(value: $inner) -> $target {
$target(value * $mul)
}
)*
}
};
}
newtype! {
f64 => Meter["m"] multipliers {
new: 1.0;
from_centi: 0.01;
from_milli: 0.001;
from_kilo: 1000.0;
}
};
newtype! {
f64 => Kilogram["kg"] multipliers {
new: 1.0;
from_gram: 0.001;
from_ton: 1000.0;
}
};
newtype! {
f64 => BMI multipliers {
new: 1.0;
}
};
// しっかり型をつける。
fn bmi(height: Meter, weight: Kilogram) -> BMI {
BMI(weight.0 / (height.0 * height.0))
}
let height = Meter::from_centi(170.0);
let weight = Kilogram::new(60.0);
println!(
"身長 {} の人が体重 {} のとき、 BMI は {} です。",
height,
weight,
bmi(height, weight)
);
}
まあしかし、たかが BMI の計算のためにここまでする人はいないかもしれません。気づけばかなり遠くまできていましたが、でも必要がなければ何もここまでする必要はないのです。軽く振り返ってみると最初のマクロはこんなのでした。
impl_display_for_newtype!(Meter);
単に Meter
に中身を中身の型として表示する機能を実装しただけですが、 newtype の型が多いときにはそれだけでもかなり便利になると思いませんか。必要な機能が増えてきたら、また、それに応じてマクロを作り直すなりオプションをつけたりすればよいのです。
また、マクロのマッチャーの機能は非常に強力で、 Rust に本来そなわっていないような構文を生み出すことが容易にできます。フルに生かせば Rust の構造体全体をほとんどパースしてしまうこともできます。「こんなふうに書ければ便利なのに、見やすいのに」と思うことがあれば、それはマクロで解決できるかもしれません。マクロで構文を拡張することもとても楽しいものです。
-
vec![a, b, c];
などです。 ↩ -
lazy_static! { static ref X: Type = init; }
など。しかし Rust にはstatic ref
という構文はありません。 ↩ -
これ自体も組み込みのマクロという扱いだったはずです。 ↩
-
関数形式のマクロを式位置で使えません。まだ健全性を保証する仕組みがないからです。 ↩
-
実は
Display
やAdd
などの標準ライブラリに存在するようなトレイトの実装だけでよいなら、newtype_derive
などのクレートを利用する手もあります。これを使うと#[derive(NewtypeDisplay)]
のように書くだけで簡単にそれらのトレイトを実装させることができます。 ↩ -
型というのは
&'a mut Type
やsome::path::for::Type
なども含まれますが、struct &'a mut Type(f64)
のようなものはまったく意味をなしません。よって、そもそも構文定義の際にstruct XXX(...)
のXXX
に現れることができるのは型 (ty
) ではなく識別子 (ident
) であるとされています (現時点でのパーサーのコードより。マクロで生成されたものもここを通っているのかどうかは私には分かりませんが、とりあえずstruct
定義では識別子を欲していることは分かります) 。また、マクロの引数で一度ある種類のトークン列としてマッチしたものは二度と別の種類のトークン列として再解釈することはできません。なので今の例のように実際には単一のトークンが与えられていたとしても、ty
として受け取ったからには型が入れる場所にしか配置することができません。 ↩ -
せっかく型を自由にしたのに、このような定義では
String
の newtype にnew()
を作らせることができません。まあしかしそれは必要になってからctor { new: String::new() }
みたいなものを受け取れるように追加するか、もっとよい形に書き直してやればよいのです。楽をすることが目的ですからね。 ↩