ハイサイ!オースティンやいびーん!
概要
Rustのmacro_rules!を紹介し、structのimplメソッドで使うselfをmacroの中でどうやって参照できるか、二つの方法を紹介します!
macro_rules!とは?
macro_rules!はRustの標準機能で、「コードを書いてくれるコード」を簡易的に書かせてくれる機能です。
コードを書くコード、つまりメタプログラミングなのです。
Rustにはいくつかのメタプログラミング手法がありますが、macro_rules!は最も広く使われている 宣言的 メタプログラミングのツールです。
いくつかの制限があったりしますが、難しいと言われているメタプログラミングの中でも比較的手を出しやすいようになっています。
メタプログラミング
メタプログラミングは無論、Rust特有の概念ではありません。実は、メタプログラミングは80年代から早くも流行した技法です。Lisp言語が流行らせた概念です。
簡単にいうと、つまりWikipedia通りにいうと、メタプログラミングの定義は、プログラムは他のプログラムを自分のデータとして扱えることです。とあるプログラムは、自分の中身を見て、新たに作ったり、変えたり、消したりできることです。
コードを作るコード、それがメタプログラミング。
簡単な例
では、簡単なmacroを書いてみれば以下のようになります。
macro_rules! print_ten_lns {
($s: expr) => {
for _ in 0..10 {
println!("{}", $s);
}
};
}
print_ten_lns!("Hello world!");
macro_rules!の後にmacro名になるprint_ten_lnsがあり、その次に複数のパターンと展開後のコードを定義するところがあります。
($hoge: …)とはmacroの入力となるパターンのことです。複数の変数を定義することができます。$を頭にしてMacroの変数名を定義して、コロン:の後にはどのようなMacro変数なのかを定義します。いくつかのMacro変数の種類がありますが、本記事では以下の二つの使います。
-
- expr: エクスプレッション。Rustだとほとんどのものがこれに該当する。let hoge = …の…の部分です。
- ident: アイデンティファイヤー。let hoge = “Hello hoge!”;のhogeに該当します。
太い矢印=>とカーリブレースに入っているのは、展開後のコードです。展開後とは、このMacroを使った時に、実際にどのようなRustコードになるか記載するところです。
さらに、任意の数の変数を入力して展開させることもできます。例えば、上記の例のように10回プリントするのではなく、任意の変数の数をプリントしたいとなったら以下のように書けます。
macro_rules! printlns {
($($s:expr),+ $(,)?) => {
$(println!("{}", $s);)+
};
}
printlns!("Hello world!", "Hello Japan!", "こんにちは", "你好");
パターンにキーワードは少し正規表現に似ています。
上記の繰り返しパターンが以下のようなコードに展開されます。
println!("{}", "Hello world!");
println!("{}", "Hello Japan!");
println!("{}", "こんにちは");
println!("{}", "你好");
Macroを使うことでさびた鉄でも柔軟性を出して、DRYを徹底できます。
macro_rules!についてきちんとした説明が以下の資料にあります!
macro_rules!でselfを参照する
macro_rules!に制限があると上述しましたが、その一つはselfの扱いです。その制限の中でメソッド内のmacroでselfをどうやって使えるのか見ていきましょう。
selfをmacroの引数として渡す
最初の方法は、selfをメソッド内でmacroの引数として渡してあげるやり方です。ident型のmacro変数を使うと渡せます。
以下の例では、Option<Vec>のプロパティをたくさん持っているパラメータのstructに、メソッドでOption内の配列に何かをプッシュするものです。ただし、Option<Vec>がNoneの場合は、空の配列を入れた上で、プッシュするのです。
macro_rules! add_to_vec {
($self: ident, $prop: ident, $v_key: ident) => {{
let vec = $self.$prop.get_or_insert(Vec::new());
vec.push($v_key);
$self
}};
}
これを以下のようにメソッド内で使えます。
impl<'a> ParamBuilder<'a> {
// ...
pub fn category_name(mut self, s: &'a str) -> Self {
add_to_vec!(self, term_slug_and, s)
}
// ...
}
これは以下のようなコードに展開されます。
pub fn category_name(mut self, s: &'a str) -> Self {
let vec = self.term_slug_and.get_or_insert(Vec::new());
vec.push(s);
self
}
メソッド内でmacro_rules!を使ってmacroを定義する
一つのメソッドで複数回、似たようなロジックをコピペーして変数名、プロパティ名だけを変更するという経験はよくありますが、読者は如何でしょうか?
メソッド内でmacro_rules!を使って定義すると、selfを直接macroの展開文の中で使えます。
impl<'a> QueryBuilder<'a> {
// ...
pub fn query(mut self) -> QueryAndValues {
let params = self.params;
macro_rules! add_if_some_id {
($prop: ident, $query: expr) => {
if let Some(id) = params.$prop {
self.query.push_str($query);
self.values.push(Value::UInt(id));
}
};
}
add_if_some_id!(author, " AND post_author = ?");
add_if_some_id!(p, " AND wp_post.ID = ?");
// ...
}
}
このやり方だと、selfも使えるし、selfから抜いたparamsの変数も見えますので、わざわざ$selfを渡す必要はないです!
まとめ
macro_rules!を紹介して、selfをmacroの中でどう扱うかも説明して参りましたが、如何でしょうか?
DRY(同じコードを繰り返すな)を徹底する上では非常に有用なmacro_rules!です。筆者はwp_query_rsで紹介した二つのmacroで200行ほど短縮できました。
興味ある方はソースコードも公開しているので、ぜひご覧ください。
筆者は、The Bookの説明だけ読んで、Rustのmacro_rules!が上級者向けで自分はなかなか使えないだろうと思っていましたが、Jon GjengsetさんのCrust of RustシリーズのCrust of Rust: Declarative Macrosを見たら、案外使えるなと改めて思いました。
他の日本のRustaceanたちもいかがでしょうか?macro_rules!を現場で使えていますでしょうか?