はじめに
本記事はRustのproc_macro
を用いて、自由自在にマクロを操れるようになろうというものです。つい1ヶ月前まで私も「マクロ?なにそれ美味しいの?」状態だったので全く難しいものでもありません。この記事を通してマクロを書けるようになると皆様のRustコード全体の見通しが良くなり、眩しいプログラミングライフを手に入れられることでしょう!!
読者対象
- Rustに興味があるけどなかなか手を出せてない方
- ある程度の手続き的なプログラミングの経験がある方
- そもそもマクロってなんなのか分からない方
- etc...
そもそもマクロとは
簡単にいうと「プログラムを生成するプログラム」です。
「???」となる方も少なくないと思います。簡単な例を見てみましょう。
新入社員のヒヨコのピヨ子さん(🐣)は上司(🐔)から10000個の要素を持つEnumを明日までに定義してねと頼まれました。
enum Sample {
Value1,
Value2,
...
}
5時間経ちましたがピヨ子さんは300個を超えたあたりで腱鞘炎になり退勤...
ピヨ子さんはどうすればよかったのでしょうか。1から10000までのEnumを手作業で生成していたら日が暮れてしまうのは明白でしょう。
そこで出てくるのが「マクロ」です!!
bakadeka_10000_enum!();
fn main() {
let value1 = Sample::Value1;
let value9567 = Sample::Value9567;
}
雰囲気だけ掴んで頂きたいのでここでは内部実装を割愛しますが、こんな感じにEnumを定義できちゃいます。
「バカデカEnumなんて需要ねぇんだよ!」と思ってる方もいらっしゃるかもしれませんが、マクロでできる0.01%を紹介したまでです。怒らないでください。
事前学習
注意
proc_macro
はライブラリクレート内でのみ定義可能です。
それ以外の場所で定義するとコンパイラに怒られるので新しくプロジェクトファイルを作成して下さい。
必要になるcrate
と設定
proc_macro
を定義するクレートのcargo.toml
ではproc-macro = true
とする必要があります。
また構文木を扱いやすくするための便利なクレート(syn
, proc-macro2
, quote
)があるのでdependencies
に追加して下さい。
anyhow
は好みで構いません。
[package]
name = "macros"
version = "0.1.0"
edition = "2021"
resolver = "2"
[dependencies]
proc-macro2 = "1.0.69"
syn = "2.0.38"
quote = "1.0.33"
anyhow = "1.0.75"
[lib]
proc-macro = true
実際になにか作ってみよう
百聞は一見にしかず。作りながら一緒に覚えていきましょう。
作るもの
競技プログラミングの標準入力を簡単にできるようにするマクロを作ってみましょう。
次の様な入力があったとします。
hello
1
1 2 3 4 5
1 2 3
4 5 6
7 8 9
1 Hello
2 Hi
1 Hello 2 3
4 Wow 5 6
1 2
普通に書くのであればfor
文をぶん回したりして面倒臭いですが、次のように受け取れたら楽ですよね。
fn main() {
include_input! {
hello: String,
one: i128,
array: [u8; 5],
array_array: [[i32; 3]; 3],
array_tuple1: [(u16, String); 2],
array_tuple2: [(u16, String, usize, usize); 2],
tuple: (u8, u8),
};
println!(
"\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}",
hello, one, array, array_array, array_tuple1, array_tuple2, tuple
);
}
今回はこれを作ってみましょう。
入力の受け取り
include_input! {
hello: String,
one: i128,
array: [u8; 5],
array_array: [[i32; 3]; 3],
array_tuple1: [(u16, String); 2],
array_tuple2: [(u16, String, usize, usize); 2],
tuple: (u8, u8),
};
フィールド名: 型
と,
の形が連続していることが分かります。まずはこれを受け取れるようにしましょう。
「面倒な書き方で定義するのかな?」って思ったでしょ。めっちゃ簡単に定義できます。
まずフィールド名: 型
から定義してみます。
use proc_macro2::Ident;
use syn::parse::{Parse, ParseStream};
use syn::{Token, Type};
pub struct NameAndType {
name: Ident,
_colon: Token![:],
ty: Type,
}
impl Parse for NameAndType {
fn parse(input: ParseStream) -> syn::Result<Self> {
let name = input.parse()?;
let _colon = input.parse()?;
let ty = input.parse()?;
Ok(NameAndType { name, _colon, ty })
}
}
あれ、めちゃめちゃ簡単ですね。
-
name: Ident
- ここはフィールド名など一意なデータを指すものにマッチします。
Ident
は他にも変数名や構造体名などにもマッチするのでderive
マクロを書く時にもお世話になります。
- ここはフィールド名など一意なデータを指すものにマッチします。
-
_colon: Token![:]
- ここは見ての通り
:
にマッチします。実際には_colon
を使ってなにかをするわけではないのですが、マッチ条件の可読性を上げるために公式が書くことを推奨しています。意地でも書きたくなければスキップしても良いでしょう。
- ここは見ての通り
-
ty: Type
- ここは型名にマッチします。
u8
,String
,[u16; 5]
など、型であればマッチします。
- ここは型名にマッチします。
これで入力の第1ステップは完了です。
続いてNameAndType
と,
の繰り返しにマッチするようにしましょう。
繰り返しに関してはsyn::punctuated::Punctuated<T, P>
が役に立ちます。
use crate::name_and_type::NameAndType;
use std::ops::Deref;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::Token;
pub struct MyPunctuated(Punctuated<NameAndType, Token![,]>);
impl Parse for MyPunctuated {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(MyPunctuated(Punctuated::parse_terminated(input)?))
}
}
impl Deref for MyPunctuated {
type Target = Punctuated<NameAndType, Token![,]>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
このように簡単に定義できてしまいました。 Deref
はお好みで追加して下さい。
あとはマクロ本体を定義するだけで入力までは動くようになります。
use input_punctuated::MyPunctuated;
use proc_macro::TokenStream;
use syn::parse_macro_input;
#[proc_macro]
pub fn include_input(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as MyPunctuated);
TokenStream::new()
}
マクロを定義するときは#[proc_macro]
と関数の頭に書くだけでinclude_input!(...)
のように呼び出すことが可能になります。
引数と戻り値にTokenStream
とありますがこれは構文木と言って、プログラムを木構造として保持しているものになります。
input
を先ほど定義したMyPunctuated
に変換して、戻り値は一旦空のTokenStream
を返していざ実行してみましょう!
fn main() {
include_input! {
hello: String,
one: i128,
array: [u8; 5],
array_array: [[i32; 3]; 3],
array_tuple1: [(u16, String); 2],
array_tuple2: [(u16, String, usize, usize); 2],
tuple: (u8, u8)
}
}
おそらく何も起こらずに終了したかと思います。しかし内部では全てのフィールド名: 型,
はしっかりMyPunctuated
に変換されています。
これで晴れて入力は完全にマスターしましたね!
プログラムを自動生成してみる
use proc_macro2::TokenStream;
use quote::quote;
pub fn expand_input(input: MyPunctuated) -> TokenStream {
quote! {
println!("Hello, World!");
}
}
新しくファイルを作ってMyPunctuated
からTokenStream
を生成する関数を定義しましょう。
ここで注意が必要なのが、ここで使うTokenStream
はproc_macro2::TokenStream
です。lib.rs
のTokenStream
はproc_macro::TokenStream
なので気をつけましょう。
ではlib.rs
にexpand_input
をimportして書き直してみましょう。
use crate::input::expand_input;
use input_punctuated::MyPunctuated;
use proc_macro::TokenStream;
use syn::parse_macro_input;
#[proc_macro]
pub fn include_input(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as MyPunctuated);
expand_input(input).into()
}
proc_macro2::TokenStream
からproc_macro::TokenStream
に変換するには、Into traitが実装されているので末尾に.into()
を書くだけで良いです。
実際に動かしてみてください!なにもないところからHello, World!
が現れたと思います。これは何を意味しているかというと、入力の変換に成功して尚且つプログラムの自動生成に成功したことを意味しています。
MyPunctuated
からプログラムを生成する
MyPunctuated
は,
で区切られたNameAndType
をもとに生成しているので直感的にMyPunctuated
はiteratorだと勘付くかもしれません。
実はその通りで、Deref
を実装していればMyPunctuated
からiter()
を呼べて、個々のNameAndType
にアクセスできます。これをもとにプログラムを自動生成するのです。
pub fn expand_input(input: MyPunctuated) -> anyhow::Result<TokenStream> {
let token_streams = input
.iter()
.map(|field| (field.name(), field.ty()))
.map(|(ident, ty)| expand_several_type(ident, ty, 0))
.collect::<Vec<_>>();
// Vec<Option<TokenStream>> => Vec<TokenStream> に変換する
let mut result = Vec::new();
for token_stream_result in token_streams {
result.push(token_stream_result?);
}
Ok(quote! {
#(#result)*
})
}
fn expand_several_type(ident: &Ident, ty: &Type) -> anyhow::Result<TokenStream> {
todo!()
}
Ident
とType
からプログラムを生成できるようにexpand_several_type(ident: &Ident, ty: &Type)
を定義しましょう!expand_several_type
は入力に依存する部分なのでanyhow
を用いて失敗可能性を表現しておきましょう。
(panicでも別に良いですが、Resultからcompile_errorに変換することが可能なのでpanicで逃げる必要性は薄いと言って良いと思ってます)
ここで以下のような表現にびっくりしているかもしれません。
quote! {
#(#result)*
}
実はquoteマクロの中では#
を用いて変数を展開することが可能なのです!
(厳密にはToTokens
traitを実装している型であれば可能)
文字列型や数字型はもちろんのことながら、Vec<TokenStream>
にも対応していて表現の幅が広いです。
Ident
も展開可能なのでIdent
を生成してやれば任意の文字を展開できます。
Vec<TokenStream>
に対しては#(#token_streams),*
のようにすると?,?,?,...
のように展開させることが可能です。
let number = 1;
let string = String::from("hello");
let ident = Ident::new(&format!("{}_{}", string, number), Span::call_site());
let token_streams = vec![quote!(10), quote!("Wow"), quote!("Yay")];
quote! {
let hoge = #number; // let hoge = 1; として展開される
let fuga = #string; // let fuga = "hello"; として展開される
let #ident = 1u8; // let hello_1 = 1u8; として展開される
let tuple = (#(#token_streams),*); // let tuple = (10, "Wow", "Yay"); として展開される
}
コレが分かれば、あとはもうパズルのように組み立てるだけですね!
実際に組み立ててみたのが以下の通りです。
/// Typeに合わせて展開する
fn expand_several_type(ident: &Ident, ty: &Type, depth: i8) -> anyhow::Result<TokenStream> {
// サポートするのは2次元までにする
if depth >= 3 {
bail!("Maximum depth reached");
}
match ty {
Type::Array(type_array) => expand_array(ident, type_array.clone(), depth),
Type::Tuple(type_tuple) => Ok(expand_tuple(ident, type_tuple.clone(), depth)),
Type::Path(_) => Ok(quote! {
let mut #ident = String::new();
::std::io::stdin().read_line(&mut #ident).expect("failed to read");
let #ident = #ident.trim().to_string().parse::<#ty>().unwrap();
}),
_ => bail!("Unsupported type"),
}
}
/// タプルとして展開する
fn expand_tuple(ident: &Ident, type_tuple: TypeTuple, depth: i8) -> TokenStream {
let token_streams = type_tuple
.elems
.iter()
.enumerate()
.map(|(i, ty)| {
quote! {
split_input[#i].parse::<#ty>().unwrap()
}
})
.collect::<Vec<_>>();
let token_stream = quote! {
let mut input = String::new();
::std::io::stdin().read_line(&mut input).expect("failed to read");
let trimed_string = input.trim().to_string();
let split_input = trimed_string.split(' ').collect::<Vec<_>>();
};
if depth == 1 {
quote! {
#token_stream
let #ident = (#(#token_streams),*);
}
} else {
quote! {
#token_stream
#ident.push((#(#token_streams),*));
}
}
}
/// 配列として展開する
fn expand_array(ident: &Ident, type_array: TypeArray, depth: i8) -> anyhow::Result<TokenStream> {
let (array_element_type, array_length) = (type_array.elem, type_array.len);
match *array_element_type {
Type::Array(_) | Type::Tuple(_) => {
let token_stream = expand_several_type(ident, &array_element_type, depth + 1)?;
Ok(quote! {
let mut #ident = Vec::new();
for _ in 0..#array_length {
#token_stream
}
let #ident: [#array_element_type; #array_length] = #ident.try_into().expect("Failed to cast to an array from Vec");
})
}
Type::Path(_) => {
let token_stream = quote! {
.trim()
.split(" ")
.map(|n| n.parse::<#array_element_type>().expect("Failed to cast"))
.collect::<Vec<_>>()
.try_into()
.expect("Failed to cast to an array from Vec");
};
let result = if depth == 1 {
quote! {
let mut #ident = String::new();
::std::io::stdin().read_line(&mut #ident).expect("Failed to read");
let #ident: [#array_element_type; #array_length] = #ident #token_stream
}
} else {
quote! {
let mut input = String::new();
::std::io::stdin().read_line(&mut input).expect("Failed to read");
let input: [#array_element_type; #array_length] = input #token_stream
#ident.push(input);
}
};
Ok(result)
}
_ => {
bail!("Unsupported type");
}
}
}
複雑に見えますが、ゴールを想像するところから始めればとても簡単です。
おそらく最初は知らない型が多くあると思いますが、tuple
やstruct
が何を持っているか(識別子・Visility・型など)を頭の片隅に置いておくと大体何を指しているのか分かるはずです。
仕上げはResult
型にしたのでErr
の場合はコンパイルエラーにしてあげましょう。
#[proc_macro]
pub fn include_input(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as MyPunctuated);
expand_input(input)
.map_err(|e| syn::Error::new(proc_macro2::Span::call_site(), e.to_string()))
.unwrap_or_else(|e| e.into_compile_error())
.into()
}
map_err
を用いてsyn
のError
に変換してあげるだけでOKです。
完成しましたね! お疲れ様でした。
実行してみよう🎉
入力
-> cargo run
hello
1
1 2 3 4 5
1 2 3
4 5 6
7 8 9
1 Hello
2 Hi
1 Hello 2 3
4 Wow 5 6
1 2
実行結果
"hello"
1
[1, 2, 3, 4, 5]
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[(1, "Hello"), (2, "Hi")]
[(1, "Hello", 2, 3), (4, "Wow", 5, 6)]
(1, 2)
しっかり型変換もできていますね!
タプルや配列もしっかり生成されています。
まとめ
quote
マクロを使えば簡単にさまざま表現ができることがわかりました。
マクロと聞いて抵抗感があった人でも少しは心理的ハードルが下がったのではないでしょうか。
まだまだ他にも紹介しきれていないものもありますが、今回は入門記事ということでここらへんでやめておこうと思います。