构建过程

通常来说,在构建新宏时,我所做的第一件事,是决定宏调用的形式。在我们当前所讨论的情况下,我的初次尝试是这样:

  1. let fib = recurrence![a[n] = 0, 1, ..., a[n-1] + a[n-2]];
  2. for e in fib.take(10) { println!("{}", e) }

以此为基点,我们可以向宏的定义迈出第一步,即便在此时我们尚不了解该宏的展开部分究竟是什么样子。此步骤的用处在于,如果在此处无法明确如何解析输入语法,那就可能意味着,整个宏的构思需要改变。

  1. macro_rules! recurrence {
  2. ( a[n] = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ };
  3. }
  4. # fn main() {}

假装你并不熟悉相应的语法,让我来解释。上述代码块使用macro_rules!系统定义了一个宏,称为recurrence!。此宏仅包含一条解析规则,它规定,此宏必须依次匹配下列项目:

  • 一段字面标记序列,a [ n ] =
  • 一段重复 ($( ... ))序列,由,分隔,允许重复一或多次(+);重复的内容允许:
    • 一个有效的表达式,它将被捕获至变量inits ($inits:expr)
  • 又一段字面标记序列, ... ,
  • 一个有效的表达式,将被捕获至变量recur ($recur:expr)。

最后,规则声明,如果输入被成功匹配,则对该宏的调用将被标记序列/* ... */替换。

值得注意的是,inits,如它命名采用的复数形式所暗示的,实际上包含所有成功匹配进此重复的表达式,而不仅是第一或最后一个。不仅如此,它们将被捕获成一个序列,而不是——举例说——把它们不可逆地粘贴在一起。还注意到,可用*替换+来表示允许“0或多个”重复。宏系统并不支持“0或1个”或任何其它更加具体的重复形式。

作为练习,我们将采用上面提及的输入,并研究它被处理的过程。“位置”列将揭示下一个需要被匹配的句法模式,由“⌂”标出。注意在某些情况下下一个可用元素可能存在多个。“输入”将包括所有尚未被消耗的标记。initsrecur将分别包含其对应绑定的内容。

位置 输入 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的绑定将消耗一个完整的表达式。此处,究竟什么算是一个完整的表达式,将由编译器决定。稍后我们会谈到语言其它部分的类似行为。

从此表中得到的最关键收获在于,宏系统会依次尝试将提供给它的每个标记当作输入,与提供给它的每条规则进行匹配。我们稍后还将谈回到这一“尝试”。

接下来我们首先将写出宏调用完全展开后的形态。我们想要的结构类似:

  1. let fib = {
  2. struct Recurrence {
  3. mem: [u64; 2],
  4. pos: usize,
  5. }

它就是我们实际使用的迭代器类型。其中,mem负责存储最近算得的两个斐波那契值,保证递推计算能够顺利进行;pos则负责记录当前的n值。

附注:此处选用u64是因为,对斐波那契数列来说,它已经“足够”了。先不必担心它是否适用于其它数列,我们会提到这一点的。

  1. impl Iterator for Recurrence {
  2. type Item = u64;
  3. #[inline]
  4. fn next(&mut self) -> Option<u64> {
  5. if self.pos < 2 {
  6. let next_val = self.mem[self.pos];
  7. self.pos += 1;
  8. Some(next_val)

我们需要这个if分支来返回序列的初始值,没什么花哨。

  1. } else {
  2. let a = /* something */;
  3. let n = self.pos;
  4. let next_val = (a[n-1] + a[n-2]);
  5. self.mem.TODO_shuffle_down_and_append(next_val);
  6. self.pos += 1;
  7. Some(next_val)
  8. }
  9. }
  10. }

这段稍微难办一点。对于具体如何定义a,我们稍后再提。TODO_shuffle_down_and_append的真面目也将留到稍后揭晓;我们想让它做到:将next_val放至数组末尾,并将数组中剩下的元素依次前移一格,最后丢掉原先的首元素。

  1. Recurrence { mem: [0, 1], pos: 0 }
  2. };
  3. for e in fib.take(10) { println!("{}", e) }

最后,我们返回一个该结构的实例。在随后的代码中,我们将用它来进行迭代。综上所述,完整的展开应该如下:

  1. let fib = {
  2. struct Recurrence {
  3. mem: [u64; 2],
  4. pos: usize,
  5. }
  6. impl Iterator for Recurrence {
  7. type Item = u64;
  8. #[inline]
  9. fn next(&mut self) -> Option<u64> {
  10. if self.pos < 2 {
  11. let next_val = self.mem[self.pos];
  12. self.pos += 1;
  13. Some(next_val)
  14. } else {
  15. let a = /* something */;
  16. let n = self.pos;
  17. let next_val = (a[n-1] + a[n-2]);
  18. self.mem.TODO_shuffle_down_and_append(next_val.clone());
  19. self.pos += 1;
  20. Some(next_val)
  21. }
  22. }
  23. }
  24. Recurrence { mem: [0, 1], pos: 0 }
  25. };
  26. for e in fib.take(10) { println!("{}", e) }

附注:是的,这样做的确意味着每次调用该宏时,我们都会重新定义并实现一个Recurrence结构。如果#[inline]属性应用得当,在最终编译出的二进制文件中,大部分冗余都将被优化掉。

在写展开部分的过程中时常检查,也是一个有效的技巧。如果在过程中发现,展开中的某些内容需要根据调用的不同发生改变,但这些内容并未被我们的宏语法定义囊括;那就要去考虑,应该怎样去引入它们。在此示例中,我们先前用过一次u64,但调用端想要的类型不一定是它;然而我们的宏语法并没有提供其它选择。因此,我们可以做一些修改。

  1. macro_rules! recurrence {
  2. ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ };
  3. }
  4. /*
  5. let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]];
  6. for e in fib.take(10) { println!("{}", e) }
  7. */
  8. # fn main() {}

我们加入了一个新的捕获sty,它应是一个类型(type)。

附注:如果你不清楚的话,在捕获冒号之后的部分,可是几种语法匹配候选项之一。最常用的包括itemexprty。完整的解释可在宏,彻底解析-macro_rules!-捕获部分找到。

还有一点值得注意:为方便语言的未来发展,对于跟在某些特定的匹配之后的标记,编译器施加了一些限制。这种情况常在试图匹配至表达式(expression)或语句(statement)时出现:它们后面仅允许跟进=>,;这些标记之一。

完整清单可在宏,彻底解析-细枝末节-再探捕获与展开找到。