「用 macro! 实现逆波兰表达式」运算符计算

511 阅读2分钟

「这是我参与11月更文挑战的第 21 天,活动详情查看:2021最后一次更文挑战


不过我们仍然缺少对操作员的支持。我们如何匹配运算符?

如果我们的RPN是一连串的标记,我们希望以完全相同的方式来处理,我们可以简单地使用$($token:tt)* 这样的列表。不幸的是,这不能让我们通过列表,根据每个标记推送一个操作数或应用一个运算符。

书中说 "宏系统完全不处理解析的模糊性",这对单个宏分支来说是正确的 —— 我们不能匹配一个数字序列后面的运算符,如 $($num:tt)*+,因为 + 也是一个有效的标记,可以被tt组匹配,但这又是递归宏存在的原因。

如果你的宏定义中有不同的分支,Rust会逐一进行尝试,所以我们可以把我们的操作符分支放在数字分支之前,这样就可以避免任何冲突:

macro_rules! rpn {
  ([ $($stack:expr),* ] + $($rest:tt)*) => {
    // TODO
  };

  ([ $($stack:expr),* ] - $($rest:tt)*) => {
    // TODO
  };

  ([ $($stack:expr),* ] * $($rest:tt)*) => {
    // TODO
  };

  ([ $($stack:expr),* ] / $($rest:tt)*) => {
    // TODO
  };

  ([ $($stack:expr),* ] $num:tt $($rest:tt)*) => {
    rpn!([ $num $(, $stack)* ] $($rest)*)
  };
}

正如我前面所说,运算符被应用于堆栈上的最后两个数字,所以我们需要分别匹配它们,"计算" 结果(构建一个正则的infix表达式)并将其放回。

macro_rules! rpn {
  ([ $b:expr, $a:expr $(, $stack:expr)* ] + $($rest:tt)*) => {
    rpn!([ $a + $b $(, $stack)* ] $($rest)*)
  };

  ([ $b:expr, $a:expr $(, $stack:expr)* ] - $($rest:tt)*) => {
    rpn!([ $a - $b $(, $stack)* ] $($rest)*)
  };

  ([ $b:expr, $a:expr $(, $stack:expr)* ] * $($rest:tt)*) => {
    rpn!([ $a * $b $(,$stack)* ] $($rest)*)
  };

  ([ $b:expr, $a:expr $(, $stack:expr)* ] / $($rest:tt)*) => {
    rpn!([ $a / $b $(,$stack)* ] $($rest)*)
  };

  ([ $($stack:expr),* ] $num:tt $($rest:tt)*) => {
    rpn!([ $num $(, $stack)* ] $($rest)*)
  };
}

不过我不是很喜欢这种明显的重复,但是就像字面意思一样,没有特殊的标记类型来匹配运算符。

然而,我们可以做的是,添加一个负责计算的辅助函数,并将任何显式运算符分支委托给它。

在宏中,你不能真正使用外部辅助函数,但你唯一能确定的是你的宏已经在范围内了,所以通常的技巧是在同一个宏中用一些独特的标记序列 "标记" 一个分支,然后像我们在常规分支中那样递归地调用它。

让我们使用 @op 作为这样的标记,并通过它里面的 tt 接受任何运算符(tt 在这种情况下是明确的,因为我们将只向这个助手传递运算符)。

而且,栈不再需要在每个单独的分支中展开 —— 因为我们在前面将其包裹在[]括号中,它可以作为另一个标记树(tt)进行匹配,然后传递到我们的辅助函数。

macro_rules! rpn {
  (@op [ $b:expr, $a:expr $(, $stack:expr)* ] $op:tt $($rest:tt)*) => {
    rpn!([ $a $op $b $(, $stack)* ] $($rest)*)
  };

  ($stack:tt + $($rest:tt)*) => {
    rpn!(@op $stack + $($rest)*)
  };

  ($stack:tt - $($rest:tt)*) => {
    rpn!(@op $stack - $($rest)*)
  };

  ($stack:tt * $($rest:tt)*) => {
    rpn!(@op $stack * $($rest)*)
  };

  ($stack:tt / $($rest:tt)*) => {
    rpn!(@op $stack / $($rest)*)
  };

  ([ $($stack:expr),* ] $num:tt $($rest:tt)*) => {
    rpn!([ $num $(, $stack)* ] $($rest)*)
  };
}