この記事で伝えたいこと
Rustマクロは呼び出し側の前後の文脈に影響しては・されてはいけない!
- 変数参照はメタ変数を通そう!
- マナーレベルの話なので利用者を混乱させなければおk
- 特にパスに気をつけましょう
こちらの記事は Rustマクロ冬期講習アドベントカレンダー 4日目の記事です!
2日目・3日目の記事では、RustのマクロはASTに基づいて置換が行われるため、脈略のない置換は不可能であることを書いてきました。つまり、「No Context Macros 1」などというミームがX上で流行ることはRustに関してはないわけです。
...本当でしょうかね?置換のルールとして正しくても、可読性が低い、というよりは、思いもしない挙動をする突拍子のないマクロなら書けそうな気がします。突然関係なさそうな変数の中身を変えてみましょう!
macro_rules! damn {
() => {
{
value = "No-context str is bound!!!";
}
};
}
fn main() {
let mut value: &'static str = "init value";
damn!();
println!("{}", value);
}
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=d20fa592a8e1c9c3acee3355c9f911a9
このコード2は cargo expand
の出力結果を見た限り正しいRustコードですが、コンパイルエラーになります 。
エラー内容
Compiling playground v0.0.1 (/playground)
error[E0425]: cannot find value `value` in this scope
--> src/main.rs:4:13
|
4 | value = "No-context str is bound!!!";
| ^^^^^ not found in this scope
...
12 | damn!();
| ------- in this macro invocation
|
= note: this error originates in the macro `damn` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider importing one of these functions
|
1 + use nom::combinator::value;
|
1 + use toml_edit::value;
|
For more information about this error, try `rustc --explain E0425`.
error: could not compile `playground` (bin "playground") due to 1 previous error
fn main() {
let mut value: &'static str = "init value";
{
value = "No-context str is bound!!!";
};
{
println!("{}", value);
};
}
※ 結果に影響がない範囲で println
マクロを展開せず残しました。
...せっかく、「 value
の中身が脈略ない damn
マクロのせいで書き換えられてしまいプログラマを混乱させる」というミームを実現したかったのに、Rustはそれすらさせてくれないのです!
これはマクロの 衛生性 (Hygiene) あるいは健全性と呼ばれる考えに基づいてエラーになっています。衛生性は、一言で言うと「存在が定かでない変数など(識別子)を参照するようなのは不健全、タヒ刑!」という考え方です。
例えば次のようなマクロが「衛生的(健全)なマクロ」とされています。
- マクロが生成した識別子(変数や関数名)を呼び出し側が利用しない・利用できない
- 呼び出し側が生成した識別子を前提としてマクロが利用することがない
今回はマクロが知り得ないはずの外にある value
変数にアクセスしてしまっていることで、不健全な関係になっています。結果的にコンパイルエラーになってしまいました。
マクロの衛生性を考える時、別物ではあるものの「純粋関数」や「グローバル変数撲滅」を考える時と同じ思考回路が使われがちな気がします。 マクロのいわゆる参照透過性を担保するため 、極力マクロが前後の文脈に依存しないようにするのです!関数名(マクロ名)や関数のシグネチャ(マクロの呼び出し方)からは想像がつかない副作用は、なんであれ混乱を招く、というわけです。
なんでもかんでもエラーになるわけではないし、あえて不健全っぽい感じのマクロもほしい
先ほど見せた通り変数の場合はマクロから外を参照したりするとエラーになりましたが、実はマクロで定義した「関数」は呼び出してもエラーにならなかったりします。
macro_rules! def_fn {
() => {
fn hoge() {}
};
}
fn main() {
def_fn!();
hoge();
}
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=a441fba6cdf55dc3db1a93bbeb3c275d
不健全そうなマクロでもコンパイルエラーになるとは限らないということです。それに、関数を定義するマクロについてはその使用を許容してほしい場合はしばしばあります。
macro_rules! impl_greet {
($strct:ident) => {
impl $strct {
fn greet(&self) {
println!("{}", stringify!($strct));
}
}
};
}
fn main() {
struct Hoge;
impl_greet!(Hoge);
Hoge.greet();
}
ここまでの話を振り返ればこれは greet
という脈略ないメソッドが定義されていることより不健全そうですが、「構造体に効率よくメソッドを定義する」ことはまさしく積極的にマクロを活用していきたいシーンでもあります(こういう時はderiveマクロのほうが適切そうではありますが、どちらにせよ健全性に関する見方は変わりません)。
つまり「不健全だけどマクロとしては許容してほしい」という場合が多々あるわけです。この例のように例えばマクロ名で明示すれば脈略のなさはある程度改善できますし、グローバル変数をなるべく避けるのと似た感じで、 マナー感覚ぐらいで健全性と向き合う のが程よい距離感なんじゃないかと思います。
不健全性で怒られたら
というわけで、少なくとも変数名や関数名等に関する不健全性については、コンパイラに怒られるまでは"努力義務"です!そんなに神経質になる必要はないと思います(というか読みやすいように普通に書いていれば不健全なマクロは避けられると思います)。とはいえ「じゃあ変数が不健全で怒られたらどうすればいいのか」は気になるでしょう。
答えはシンプルで、「利用側にメタ変数として与えてもらう」ようなマクロにします。
macro_rules! modify_str {
($str_value:ident) => {
{
$str_value = "No-context str is bound!!!";
}
};
}
fn main() {
let mut value: &'static str = "init value";
modify_str!(value);
println!("{}", value);
}
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=de6001778c9c140f9ebc3880a567758e
マクロの名前と渡している変数名から、このマクロが value
の値を変えるようなものであるということが明確です。健全なマクロ呼び出しになりましたね!
パスの健全性
上記で、「少なくとも変数名や関数名等に関する不健全性については」と書いたのは、もう一つ不健全になる、すなわち「外の文脈に依存してマクロが出力する結果が変わる」例があるためです。
それはパスです。
macro_rules! use_some_in_this_macro {
($val:ident) => {
let val = Some($val);
println!("{:?}", val);
};
}
fn main() {
let hoge = "hoge";
use_some_in_this_macro!(hoge);
// 出力: Some("hoge")
{
use std::fmt;
// オレオレSome構造体!!!!
struct Some<T>(T);
impl<T: fmt::Display> fmt::Debug for Some<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "No Context Some value: {}", self.0)
}
}
let hoge = "hoge";
// さっきとおなじ結果になる...?
use_some_in_this_macro!(hoge);
// 出力: No Context Some value: hoge
}
}
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=181e3369d3f1a33134d18f38a6fe8d9f
マクロ作成者はきっと std::option::Option::Some
の意味で Some
と書いたに違いありません。
しかし、マクロ利用者は勝手に「オレオレ Some
構造体」を作ってしまい、意図しない結果が出力されてしまいました...パスは文脈の影響を強く受けるので、マクロを健全にする場合なるべく フルパスで 記述しましょう!
macro_rules! use_some_in_this_macro {
($val:ident) => {
// なるべく全てフルパスで書く
let val = ::std::option::Option::Some($val);
// 理想論で言えばこういう呼び出しも。
::std::println!("{:?}", val);
};
}
fn main() {
let hoge = "hoge";
use_some_in_this_macro!(hoge);
{
use std::fmt;
// オレオレSome構造体!!!!
struct Some<T>(T);
impl<T: fmt::Display> fmt::Debug for Some<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "No Context Some value: {}", self.0)
}
}
let hoge = "hoge";
// 無駄無駄無駄ぁ!!!
use_some_in_this_macro!(hoge);
}
}
Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=fbd7d53f5242535dc54431df3fb55e50
なんとなくですが、「マクロの衛生性・健全性」は、変数や関数の定義よりは、ここで示したようなパスの問題のほうがより気をつけたほうが良さそうですね。
まとめ
本記事は(というかRustマクロ冬期講習シリーズは)The Little Book of Rust Macrosを参考にしているのですが、特に今回の話は元にした次のページのほうがわかりやすく詳細に書かれているかもしれません。というわけでリンクを載せておきます。
とりあえずパス以外に関しては、文脈を守った丁寧にマクロを書けば問題にならないでしょう。衛生性に配慮して、可読性の高いマクロを書きましょう!
-
わざわざ
{}
でスコープを作ったのは、「マクロが出力するASTによりスコープが区切られたからコンパイルエラーになったのでは...?」という予想や考え方を否定するためです。ちなみに、普通に書いた場合マクロの出力結果はスコープを深めません。 ↩