前言#
昨天我们了解了声明宏的一些细节,今天我们来学习声明宏中的模式。
模式#
接下来我们将了解到模式的解析和展开。
回调#
由于宏展开的顺序问题,在1.2之前是没办法从一个宏的展开产物中传递信息给另一个宏的。
来看个例子:
macro_rules! recognize_tree {
(larch) => { println!("#1, the Larch.") };
(redwood) => { println!("#2, the Mighty Redwood.") };
(fir) => { println!("#3, the Fir.") };
(chestnut) => { println!("#4, the Horse Chestnut.") };
(pine) => { println!("#5, the Scots Pine.") };
($($other:tt)*) => { println!("I don't know; some kind of birch maybe?") };
}
macro_rules! expand_to_larch {
() => { larch };
}
fn main() {
recognize_tree!(expand_to_larch!());
// first expands to: recognize_tree! { expand_to_larch ! ( ) }
// and then: println! { "I don't know; some kind of birch maybe?" }
}先执行expand_to_larch!,然后将展开产物作为recognize_tree!的输入,但是这个时候拿到的宏产物是什么呢?实际上是一个AST tree节点,所以这个时候并不会走第一个规则,而是走了最后一个tt的规则。
这就使得宏的模块化变得非常困难。
一种方案是基于宏递归 + 回调的方案,比如:
// ...
macro_rules! call_with_larch {
($callback:ident) => { $callback!(larch) };
}
fn main() {
call_with_larch!(recognize_tree);
// first expands to: call_with_larch! { recognize_tree }
// then: recognize_tree! { larch }
// and finally: println! { "#1, the Larch." }
}另一种方案是基于tt重复内容,也是递归+回调:
macro_rules! callback {
($callback:ident( $($args:tt)* )) => {
$callback!( $($args)* )
};
}
fn main() {
callback!(callback(println("Yes, this *was* unnecessary.")));
}Incremental TT Munchers#
官方提供的中文译本将这个标题意译为:增量式TT撕咬机,翻译的好好!。
直接看个例子:
macro_rules! mixed_rules {
() => {};
(trace $name:ident; $($tail:tt)*) => {
{
println!(concat!(stringify!($name), " = {:?}"), $name);
mixed_rules!($($tail)*);
}
};
(trace $name:ident = $init:expr; $($tail:tt)*) => {
{
let $name = $init;
println!(concat!(stringify!($name), " = {:?}"), $name);
mixed_rules!($($tail)*);
}
};
}
这个模式大概是当前可使用的最强大的宏解析技术,可以解析非常复杂的场景。不过过度使用它会增加编译的时间,所以使用时需要考虑清楚。
TT撕咬机是一个递归macro_rules!宏,它会对输入的内容按次、逐步的处理,每一次都会从头开始移除输入的部分被处理的token序列,并且生成一些中间体(intermediate)的输出内容,而这些输出内容将作为递归的输入内容。
名称中含有“TT”(token tree),是因为输入中尚未被处理的部分总是被捕获在 $($tail:tt)* 的形式中。之所以如此,是因为只有通过使用反复匹配 tt 才能做到 无损地 (losslessly) 捕获住提供给宏的输入部分。
对于TT的严格限制只来自于macro_rules!宏系统:
- 你只能匹配字面量或者语法结构(
grammar constructs),这俩都是可以被macro_rules!捕获的; - 你无法匹配不成对的标记组 (
unbalanced groups) 。
不过需要注意的一点是递归的限制,我们前面了解到一个宏的递归次数默认是128,也可以手动声明调整,但是尽量保持在这个数以下,再往上容易导致编译过久。一般可以搭配一些别的规则来减少递归的次数。
性能#
TT天然就是二次复杂度的(inherently quadratic),因为它要递归。比如如果传递给TT 100个token trees,那么会发生如下事情:
- 第一次调用时会匹配到
100个token trees, - 第一次递归则是99个,
- 第二次递归则是98个
也就是说有100个token就递归98次,直到token变成1,包括第一次处理就是99次调用。
所以说尽量避免过多的使用TT,尤其是token序列很多的情况下。
如果可选的话,用一个简单的宏调用多次也不要用TT来处理。
比如:
f! {
fn f_u8(x: u32) -> u8;
fn f_u16(x: u32) -> u16;
fn f_u32(x: u32) -> u32;
fn f_u64(x: u64) -> u64;
fn f_u128(x: u128) -> u128;
}可以换成这样:
f! { fn f_u8(x: u32) -> u8; }
f! { fn f_u16(x: u32) -> u16; }
f! { fn f_u32(x: u32) -> u32; }
f! { fn f_u64(x: u64) -> u64; }
f! { fn f_u128(x: u128) -> u128; }另外,如果TT之外还有别的规则,那么尽量让这些规则排在TT前面,避免不必要的匹配失败。
最后,如果可以直接使用可选重复操作符*或者+来实现重复内容,那就尽量这么写,而不是用TT去递归。TT很强,但是也很危险,使用需要付出一定的性能代价。
我们后面会接触到一个叫quote的,这货也是处理的token tree,类似TT都可以处理token,但是不会有TT的性能代价。
内部规则#
内部规则可以统一多个macro_rules!宏为一个,或者让它变得容易读和写TT,通过明确你想在宏里面调用的规则的名字。
这一特性挺重要的,因为2015版是没有macro_rules!宏的命名空间的(namespace),这就使得我们需要重新导出所有的内部macro_rules!宏,导致污染全局,导致和其它的crate里的宏起冲突。
好在1.30开始就没有这个问题了,具体可看: Import and Export chapter
扯远了,我们来了解下如何统一宏以及这种特性的原理。
先来看个例子:
#[macro_export]
macro_rules! as_expr { ($e:expr) => {$e} }
#[macro_export]
macro_rules! foo {
($($tts:tt)*) => {
as_expr!($($tts)*)
};
}这里我们有俩宏,一个是常见的as_expr!,另一个是foo,foo调用了as_expr!。
这种写法不好,因为两个宏都导出到全局去了,而as_expr!只会在foo里面调用,所以把as_expr!和foo!包在一起是很有必要的。
先来看下写法:
#[macro_export]
macro_rules! foo {
(@as_expr $e:expr) => {$e};
($($tts:tt)*) => {
foo!(@as_expr $($tts)*)
};
}as_expr变成foo的一条”rule“,但实际上匹配不会走这条规则,@as_expr是你这条“rule”的名字,也就是被合并进来宏的名字,仅在内部调用中可用。
@是必要的,在1.2之后@没有地方会将它作为前缀,所以不会有冲突。当然你也可以使用#或者!,不过目前约定俗成是@,所以记住用@即可。
注意:@ 符号很早之前曾作为前缀被用于表示被垃圾回收了的指针, 那时 Rust 还在采用各种记号代表指针类型。
而现在的 @ 只有一种用法: 将名称绑定至模式中(譬如 match 的模式匹配中)。 在这种用法中它是中缀运算符,与我们的上述用例并不冲突。
另外还有一点,合并进来的宏(后面叫内部规则了)进来排在真正的规则之前,这么做可以避免macro_rules!宏把内部规则解析成别的东西,比如表达式。
性能#
这货也有性能问题。尽管只有真正的规则会被匹配到,但是编译器还是会按顺序尝试匹配所有的规则,这会增长规则的匹配失败数量。
另外,@xxx也使得规则编写变得过长,可读性较差,也会加大编译器的工作量。
所以,能不写内部规则也尽量不要写。
Push-down Accumulation
下推式累积(啥玩意儿),来看个例子:
macro_rules! init_array {
[$e:expr; $n:tt] => {
{
let e = $e;
accum!([$n, e.clone()] -> [])
}
};
}
macro_rules! accum {
([3, $e:expr] -> [$($body:tt)*]) => { accum!([2, $e] -> [$($body)* $e,]) };
([2, $e:expr] -> [$($body:tt)*]) => { accum!([1, $e] -> [$($body)* $e,]) };
([1, $e:expr] -> [$($body:tt)*]) => { accum!([0, $e] -> [$($body)* $e,]) };
([0, $_:expr] -> [$($body:tt)*]) => { [$($body)*] };
}
let strings: [String; 3] = init_array![String::from("hi!"); 3];
自己套自己,但是是递推式的,也就是想去到特定的某一个rule继续处理。
在rust中宏展开必须是完全的(complete),有效的语法元素,比如表达式、项等。这意味着在rust中展开为一个”半成品“(partial construct)是不可能的。
你可能会想,上面的代码能不能优化成下面这种写法:
macro_rules! init_array {
[$e:expr; $n:tt] => {
{
let e = $e;
[accum!($n, e.clone())]
}
};
}
macro_rules! accum {
(3, $e:expr) => { $e, accum!(2, $e) };
(2, $e:expr) => { $e, accum!(1, $e) };
(1, $e:expr) => { $e };
}预期的展开应该如下:
[accum!(3, e.clone())]
[e.clone(), accum!(2, e.clone())]
[e.clone(), e.clone(), accum!(1, e.clone())]
[e.clone(), e.clone(), e.clone()]然而并不能,因为这需要中间的几次展开是展开的半成品,这是不可能的,即使只是在这个宏里面会用到,也是禁止的。
而我们这里使用的下推式累积这种方法就可以不用考虑展开的完整性,可以直接使用半成品。
前面下推式累积代码的展开过程如下:
init_array!(String::from("hi!"); 3)
accum!([3, e.clone()] -> [])
accum!([2, e.clone()] -> [e.clone(),])
accum!([1, e.clone()] -> [e.clone(), e.clone(),])
accum!([0, e.clone()] -> [e.clone(), e.clone(), e.clone(),])
[e.clone(), e.clone(), e.clone(),]可以看到中间产物都是半成品,它会一直下推累积知道完整展开。
这种方案的关键点就在于使用到了$($body:tt)*,它保证了中间过程输出可以保留而不是被解析。($input) -> ($output)只是一种约定俗成的习惯,用来解释清楚这些宏的行为。
这里也涉及到了tt,所以你应该联想到了TT撕咬机。是的,这种方案实际上也是一种递归,一般就是用于TT,因为它能保留任意复杂度的半成品。另外内部规则在这里也很好用,因为它简化了宏的创建。
性能#
那么代价是什么呢?又是性能问题,既然涉及到了tt,那么下推式累积也是二次复杂度(inherently quadratic)。举个例子,如果一个下推式累积规则输入是100个token,那么它的执行过程如下:
- 第一次调用,累积量为0;
- 第二次调用则是第一次递归,累积量为1;
- 第三次调用即第二次递归,累积量是2;
以此类推,和TT撕咬机差不多,不过是反着来,是典型的二次(auadratic)模式,过长的输入会增加编译的时间。这还不是最恐怖的,实际上如果真用在TT撕咬机中,会导致双倍的二次!
前面对TT的建议同样适用于下推式累积。简单地说,尽量不要过分的使用。
最后,确保你的累积规则是放在最后的而不是放在前面,因为如果规则失败了,编译器也不用花大把时间帮你下推。
重复替换#
这种模式是废弃匹配到的重复内容,变量被用来驱动重复,它们的存在仅和输入的长度有关(还有类型)。比如:
#![allow(unused)]
fn main() {
macro_rules! tuple_default {
($($tup_tys:ty),*) => {
(
$(
replace_expr!(
($tup_tys)
Default::default()
),
)*
)
};
}
macro_rules! replace_expr {
($_t:tt $sub:expr) => {$sub};
}
assert_eq!(tuple_default!(i32, bool, String), (i32::default(), bool::default(), String::default()));
}这里的tup_tys类型片段限定符就是用来推动重复的,不过这里还有另外一层作用:类型。
不过这里也不是非得这么写,实际上replace_expr没要存在,可以直接$tup_tys::default()。
TT捆#
直接来看下例子:
macro_rules! call_a_or_b_on_tail {
((a: $a:ident, b: $b:ident), call a: $($tail:tt)*) => {
$a(stringify!($($tail)*))
};
((a: $a:ident, b: $b:ident), call b: $($tail:tt)*) => {
$b(stringify!($($tail)*))
};
($ab:tt, $_skip:tt $($tail:tt)*) => {
call_a_or_b_on_tail!($ab, $($tail)*)
};
}
fn compute_len(s: &str) -> Option<usize> {
Some(s.len())
}
fn show_tail(s: &str) -> Option<usize> {
println!("tail: {:?}", s);
None
}
fn main() {
assert_eq!(
call_a_or_b_on_tail!(
(a: compute_len, b: show_tail),
the recursive part that skips over all these
tokens does not much care whether we will call a
or call b: only the terminal rules care.
),
None
);
assert_eq!(
call_a_or_b_on_tail!(
(a: compute_len, b: show_tail),
and now, to justify the existence of two paths
we will also call a: its input should somehow
be self-referential, so let us make it return
some ninety-one!
),
Some(91)
);
}这个例子稍微有些复杂,但也不复杂,简单的分析下:
前面两条规则必须在ab括号之后匹配到call b/a:,所以一开始前面两条规则都是不生效的,所以走最后一条tt,其中$ab:tt后面接的是,分隔符,所以匹配的是((a: compute_len, b: show_tail)),然后$skip:tt分隔符是空格space,所以它表示$ab之后的第一个单词,第一次调用时是the,第二次则是recursive,以此类推。而$($tail:tt)*这个重复内容则是第一个单词后面的一大段,每次调用都少一个单词。
直到$ab之后接入call a/b:,那么就会匹配到第一个或者第二个规则,然后不再走TT。
我们来看下过程:
expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), the recursive part that skips over all these
tokens does not much care whether we will call a or call b: only the terminal
rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), recursive part that skips over all these
tokens does not much care whether we will call a or call b: only the terminal
rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), recursive part that skips over all these
tokens does not much care whether we will call a or call b: only the terminal
rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), part that skips over all these tokens does
not much care whether we will call a or call b: only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), part that skips over all these tokens does not
much care whether we will call a or call b: only the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), that skips over all these tokens does not
much care whether we will call a or call b: only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), that skips over all these tokens does not much
care whether we will call a or call b: only the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), skips over all these tokens does not much
care whether we will call a or call b: only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), skips over all these tokens does not much care
whether we will call a or call b: only the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), over all these tokens does not much care
whether we will call a or call b: only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), over all these tokens does not much care
whether we will call a or call b: only the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), all these tokens does not much care whether
we will call a or call b: only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), all these tokens does not much care whether we
will call a or call b: only the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), these tokens does not much care whether we
will call a or call b: only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), these tokens does not much care whether we
will call a or call b: only the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), tokens does not much care whether we will
call a or call b: only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), tokens does not much care whether we will call
a or call b: only the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), does not much care whether we will call a or
call b: only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), does not much care whether we will call a or
call b: only the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), not much care whether we will call a or call
b: only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), not much care whether we will call a or call
b: only the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), much care whether we will call a or call b:
only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), much care whether we will call a or call b:
only the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), care whether we will call a or call b: only
the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), care whether we will call a or call b: only
the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), whether we will call a or call b: only the
terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), whether we will call a or call b: only the
terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), we will call a or call b: only the terminal
rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), we will call a or call b: only the terminal
rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), will call a or call b: only the terminal
rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), will call a or call b: only the terminal rules
care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), call a or call b: only the terminal rules
care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), call a or call b: only the terminal rules
care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), a or call b: only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), a or call b: only the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), or call b: only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), or call b: only the terminal rules care. }`
= note: to `call_a_or_b_on_tail!
((a: compute_len, b: show_tail), call b: only the terminal rules care.)`
= note: expanding `call_a_or_b_on_tail! { (a: compute_len, b: show_tail), call b: only the terminal rules care. }`
= note: to `show_tail (stringify! (only the terminal rules care.))`可读性有些差,不过耐心点看可以看出和我们的推测是一样的。
接着来看下打印的结果:

可以看到符合预期。
举着个例子的目的是什么呢?实际上你应该注意到了$ab:tt,他俩被捆在一起了,是的,这就是本章节想表达的。我们可以把多个必要的tt当做是一个tt,这样可以在中间传递的过程中省下来很多事。
总结#
今天我们学习了模式,学习到了一些编写规则的方法,当然也有代价,使用需要谨慎。
