构建过程
通常来说,在构建新宏时,我所做的第一件事,是决定宏调用的形式。在我们当前所讨论的情况下,我的初次尝试是这样:
let fib = recurrence![a[n] = 0, 1, ..., a[n-1] + a[n-2]];
for e in fib.take(10) { println!("{}", e) }
以此为基点,我们可以向宏的定义迈出第一步,即便在此时我们尚不了解该宏的展开部分究竟是什么样子。此步骤的用处在于,如果在此处无法明确如何解析输入语法,那就可能意味着,整个宏的构思需要改变。
macro_rules! recurrence {
( a[n] = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ };
}
# fn main() {}
假装你并不熟悉相应的语法,让我来解释。上述代码块使用macro_rules!
系统定义了一个宏,称为recurrence!
。此宏仅包含一条解析规则,它规定,此宏必须依次匹配下列项目:
- 一段字面标记序列,
a
[
n
]
=
; - 一段重复 (
$( ... )
)序列,由,
分隔,允许重复一或多次(+
);重复的内容允许:- 一个有效的表达式,它将被捕获至变量
inits
($inits:expr
)
- 一个有效的表达式,它将被捕获至变量
- 又一段字面标记序列,
...
,
; - 一个有效的表达式,将被捕获至变量
recur
($recur:expr
)。
最后,规则声明,如果输入被成功匹配,则对该宏的调用将被标记序列/* ... */
替换。
值得注意的是,inits
,如它命名采用的复数形式所暗示的,实际上包含所有成功匹配进此重复的表达式,而不仅是第一或最后一个。不仅如此,它们将被捕获成一个序列,而不是——举例说——把它们不可逆地粘贴在一起。还注意到,可用*
替换+
来表示允许“0或多个”重复。宏系统并不支持“0或1个”或任何其它更加具体的重复形式。
作为练习,我们将采用上面提及的输入,并研究它被处理的过程。“位置”列将揭示下一个需要被匹配的句法模式,由“⌂”标出。注意在某些情况下下一个可用元素可能存在多个。“输入”将包括所有尚未被消耗的标记。inits
和recur
将分别包含其对应绑定的内容。
位置 | 输入 | inits | recur |
---|---|---|---|
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ | a[n] = 0, 1, …, a[n-1] + a[n-2] | ||
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ | [n] = 0, 1, …, a[n-1] + a[n-2] | ||
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ | n] = 0, 1, …, a[n-1] + a[n-2] | ||
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ | ] = 0, 1, …, a[n-1] + a[n-2] | ||
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ | = 0, 1, …, a[n-1] + a[n-2] | ||
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ | 0, 1, …, a[n-1] + a[n-2] | ||
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ | 0, 1, …, a[n-1] + a[n-2] | ||
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ ⌂ | , 1, …, a[n-1] + a[n-2] | 0 | |
注意:这有两个 ⌂,因为下个输入标记既能匹配 重复元素间的分隔符逗号,也能匹配 标志重复结束的逗号。宏系统将同时追踪这两种可能,直到决定具体选择为止。 | |||
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ ⌂ | 1, …, a[n-1] + a[n-2] | 0 | |
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ ⌂ | , …, a[n-1] + a[n-2] | 0 , 1 | |
注意:第三个被划掉的记号表明,基于上个被消耗的标记,宏系统排除了一项先前存在的可能。 | |||
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ ⌂ | …, a[n-1] + a[n-2] | 0 , 1 | |
a[n] = $($inits:expr),+ , … , $recur:expr | , a[n-1] + a[n-2] | 0 , 1 | |
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ | a[n-1] + a[n-2] | 0 , 1 | |
a[n] = $($inits:expr),+ , … , $recur:expr ⌂ | 0 , 1 | a[n-1] + a[n-2] | |
注意:这一步表明,类似$recur:expr的绑定将消耗一个完整的表达式。此处,究竟什么算是一个完整的表达式,将由编译器决定。稍后我们会谈到语言其它部分的类似行为。 |
从此表中得到的最关键收获在于,宏系统会依次尝试将提供给它的每个标记当作输入,与提供给它的每条规则进行匹配。我们稍后还将谈回到这一“尝试”。
接下来我们首先将写出宏调用完全展开后的形态。我们想要的结构类似:
let fib = {
struct Recurrence {
mem: [u64; 2],
pos: usize,
}
它就是我们实际使用的迭代器类型。其中,mem
负责存储最近算得的两个斐波那契值,保证递推计算能够顺利进行;pos
则负责记录当前的n
值。
附注:此处选用
u64
是因为,对斐波那契数列来说,它已经“足够”了。先不必担心它是否适用于其它数列,我们会提到这一点的。
impl Iterator for Recurrence {
type Item = u64;
#[inline]
fn next(&mut self) -> Option<u64> {
if self.pos < 2 {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
我们需要这个if
分支来返回序列的初始值,没什么花哨。
} else {
let a = /* something */;
let n = self.pos;
let next_val = (a[n-1] + a[n-2]);
self.mem.TODO_shuffle_down_and_append(next_val);
self.pos += 1;
Some(next_val)
}
}
}
这段稍微难办一点。对于具体如何定义a
,我们稍后再提。TODO_shuffle_down_and_append
的真面目也将留到稍后揭晓;我们想让它做到:将next_val
放至数组末尾,并将数组中剩下的元素依次前移一格,最后丢掉原先的首元素。
Recurrence { mem: [0, 1], pos: 0 }
};
for e in fib.take(10) { println!("{}", e) }
最后,我们返回一个该结构的实例。在随后的代码中,我们将用它来进行迭代。综上所述,完整的展开应该如下:
let fib = {
struct Recurrence {
mem: [u64; 2],
pos: usize,
}
impl Iterator for Recurrence {
type Item = u64;
#[inline]
fn next(&mut self) -> Option<u64> {
if self.pos < 2 {
let next_val = self.mem[self.pos];
self.pos += 1;
Some(next_val)
} else {
let a = /* something */;
let n = self.pos;
let next_val = (a[n-1] + a[n-2]);
self.mem.TODO_shuffle_down_and_append(next_val.clone());
self.pos += 1;
Some(next_val)
}
}
}
Recurrence { mem: [0, 1], pos: 0 }
};
for e in fib.take(10) { println!("{}", e) }
附注:是的,这样做的确意味着每次调用该宏时,我们都会重新定义并实现一个
Recurrence
结构。如果#[inline]
属性应用得当,在最终编译出的二进制文件中,大部分冗余都将被优化掉。
在写展开部分的过程中时常检查,也是一个有效的技巧。如果在过程中发现,展开中的某些内容需要根据调用的不同发生改变,但这些内容并未被我们的宏语法定义囊括;那就要去考虑,应该怎样去引入它们。在此示例中,我们先前用过一次u64
,但调用端想要的类型不一定是它;然而我们的宏语法并没有提供其它选择。因此,我们可以做一些修改。
macro_rules! recurrence {
( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ };
}
/*
let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]];
for e in fib.take(10) { println!("{}", e) }
*/
# fn main() {}
我们加入了一个新的捕获sty
,它应是一个类型(type)。
附注:如果你不清楚的话,在捕获冒号之后的部分,可是几种语法匹配候选项之一。最常用的包括
item
,expr
和ty
。完整的解释可在宏,彻底解析-macro_rules!
-捕获部分找到。还有一点值得注意:为方便语言的未来发展,对于跟在某些特定的匹配之后的标记,编译器施加了一些限制。这种情况常在试图匹配至表达式(expression)或语句(statement)时出现:它们后面仅允许跟进
=>
,,
和;
这些标记之一。完整清单可在宏,彻底解析-细枝末节-再探捕获与展开找到。