はじめに
いきなりですが私は Rust が好きです。好きな機能の一つにマクロがあります。
Rust のマクロはコードを生成するための機能です。
マクロは非常に強力で、魔法のようにコードを書くことができます。
マクロには宣言的マクロと手続きマクロがあり、手続きマクロは非常に難しいと言われています。
私は宣言的マクロを使うことはあるのですが、手続きマクロは使ったことがありませんでした。
そこで、今回は手続きマクロに挑戦してみました。
最近競技プログラミングを始めてみたので、競技プログラミングの標準入力を受け取るマクロを作ることにしました。
すでに他の素晴らしいマクロが存在するのですが、今回は勉強目的でオリジナルを作成してみました。
今回は手続きマクロを作成するにあたって、感じたことなどを共有したいと思います。
作成したマクロ
まずは、作成したマクロを紹介します。1
GitHub にもコードを公開していますので、興味があればご覧ください。
use pte::pte;
#[pte]
fn solve(a:isize,b:iszie) -> isize {
a + b
}
上のコードは以下のように展開されます。
(空行やコメントなどを入れて実際のコードよりも見やすくしています)
// 文字列を読み込む独自の構造体
use pte::Lines;
fn solve(a:isize,b:isize) -> isize {
a + b
}
// main関数を作成
fn main() {
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
// 標準入力のデータを実行する関数の引数に変換する
let mut lines = Lines::new(&input);
let a = lines.consume::<isize>().unwrap();
let b = lines.consume::<isize>().unwrap();
// 関数を実行
let result = solve(a,b);
// 結果を出力
println!("{}", result);
}
工夫点は標準入力から読み取る行数を数値による指定や標準入力の 1 行目のある数値から指定できるようにしたことです。
(指定しない場合は 1 行のみの標準入力を受け取るようになっています。)
下の例では、標準入力の 1 行目の 1 要素目(0 を最初とする)の数値を行数として読み取ります。
#[pte(row = in1)]
fn solve(v:Vec<Vec<isize>>) -> isize {
v.iter().flatten().sum::<isize>()
}
cargo run
2 3 # 3行2列を表現している入力。row = in1で3を読み取れる。
1 2 # solveのvに[1,2]を追加
3 4 # solveのvに[3,4]を追加
5 6 # solveのvに[5,6]を追加
21 # 1+2+3+4+5+6=21
ちなみに、今回は手続きマクロにて実装しましたが、以下のような宣言的マクロを実装することも可能です。2
今回の記事は手続きマクロについてですので、宣言的マクロは割愛します。
#[macro_export]
macro_rules! pte {
(fn $fn_name:ident($($args:ident:$ty:ty),*) -> $ret_ty:ty $body:block) => {
fn $fn_name($($args:$ty),*) -> $ret_ty $body
use pte::Lines;
fn main() {
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
let mut lines = Lines::new(&input);
$(
let $args = lines.consume::<$ty>().unwrap();
)*
let result = $fn_name($($args),*);
println!("{}", result);
}
};
}
pte!{
fn solve(a:isize,b:isize) -> isize {
a + b
}
}
やってみて
手続きマクロを作成してみて、以下のような感想を持ちました。
案外できた
噂に聞いたより書くこと自体は難しくありませんでした。(読むのは難しいですが)
まだ最適化する余地やベストな書き方があると思いますが、とりあえず動くマクロを作成することはできました。
syn クレート、quote クレート, proc_macro2 クレートが相当優秀で使いやすかったです。
そして何より、ChatGPT-4o さんが優秀でした。
完全に正確なコードを生成することは難しかったですが、
8 割ぐらいは完成しているコードが出てきたので、それを調べつつ修正することで完成させることができましたし、スムーズに手続きマクロを習得することができたのではないかと思います。
最後に私個人の感想ですが、宣言的マクロよりも手続きマクロの方が普通のプログラムに近い気がしました。
その分記述も多くなりますし、泥臭い部分も多いですが、わけがわからん!ということまでにはならなかった印象です。
手続きマクロについて学べた
以下のようなことがわかり、他の手続きマクロを書く際も役立ちそうです。
-
手続きマクロは TokenStream を受け取って TokenStream を返す関数を定義する
- 受け取った TokenStream をいじって新しい TokenStream を返すイメージ
- 基本は syn クレートで TokenStream を解析し、quote クレートで新しい TokenStream を生成する
- quote クレートは proc_macro2 クレートの TokenStream を簡単に生成することができる
-
手続きマクロを作成したクレートは手続きマクロ以外を公開することはできない
- もし手続きマクロで展開されるコードで独自のものを利用したい場合は他のクレートに分ける必要がある
- 今回の私の例は、macro というクレートに 手続きマクロを作成、helper というクレートに Lines 構造体を作成し、pte クレートで二つのクレートを利用し、そのまま公開しています。
-
proc_macro2 クレートを利用することで、proc_macro ではできないテストなどが可能になる
デバッグが難しい → エラーは独自のエラーに変換する
マクロが展開された姿を思い描きながらデバッグする必要があると感じました。
エラーメッセージは展開後のコードに対するものなどが多く、どこでエラーが発生しているのかが分かりにくいです。
できるだけ syn クレートなどのエラーは独自のエラーメッセージに変換することが重要だと感じました。
そうすることで、どこの行でエラーが発生しているのかが分かりやすくなります。
少し話がそれますが、こちらの本でもコンポーネントの境界で発生したエラーを独自のエラーメッセージに変換しないとそれはエラーではなく、バグである、といったことが書かれていました。
この本自体は Go の並行性に関わる話ですが、まさか Rust の手続きマクロを作成して実感するとは思いませんでした笑
終わりに
今回は手続きマクロを作成してみました。
手続きマクロは難しいと言われていますが、実際に作成してみると案外できるものだと感じました。
今までびびって触っていなかったのがもったいなかったですが、今回であらかた経験できてよかったです。
Rust は他にも面白い機能がたくさんありますので、これからも楽しみながら学んでいきたいと思います。
もし、間違っている点や改善点があれば教えていただけると幸いです。
ここまで読んでいただき、ありがとうございました。