问题描述
这个简单的正则表达式实现 (scastie here) 无法编译,正如我预期的那样。错误出现在第 14 行,其中一个中间递归调用被解释为破坏 @tailrec
要求。虽然这个中间递归调用确实不在尾调用位置,但表达式的实际最后调用是,使得整个表达式尾调用得到优化。
可以通过将中间递归调用别名化,将其转换为“任意”调用来说明这一思路。当 this 调用别名为 aliasMatches
时,代码按预期编译和执行。
为什么编译器不接受非别名实现?这是实际限制,还是上述推理有问题?有没有办法让编译器接受我缺少的第一个版本(除了完整的累加器重写)?
(使用 Scala 2.13.5)
import scala.annotation.tailrec
def matches(input: String,pattern: String): Boolean = {
@tailrec
def matchesRemainder(remainder: Seq[Char],remainingPattern: Seq[Char]): Boolean =
(remainder,remainingPattern) match {
case (Seq(),Seq()) => true
case (Seq(_@_*),Seq()) => false
case (Seq(),Seq(_,'*',_@_*)) => true
case (Seq(),Seq(_@_*)) => false
\\ vvvv error vvvv
case (Seq(_,rs@_*),Seq('.',xs@_*)) => matchesRemainder(rs,remainingPattern) ||
matchesRemainder(remainder,xs)
case (Seq(r,Seq(p,xs@_*)) => (r == p && aliasMatches(rs,remainingPattern)) ||
matchesRemainder(remainder,xs)
case (Seq(_,ps@_*)) => matchesRemainder(rs,ps)
case (Seq(r,ps@_*)) => r == p && matchesRemainder(rs,ps)
}
def aliasMatches(remainder: Seq[Char],p: Seq[Char]): Boolean =
matchesRemainder(remainder,p)
matchesRemainder(input,pattern)
}
解决方法
让我们稍微简化一下,以便更清楚地看到问题。
@tailrec
def foo(x: Int): Boolean = x == 0 || foo(x-1) || foo(x-2)
这不会编译,因为它不能完全消除递归:foo(x-1)
必须将控制权返回给调用者,因此 in 可以评估结果并返回它,或者调用 foo(x-2)
。
尾递归只有在递归调用的结果立即直接返回时才会发生,控制权不返回给调用者。
现在为什么要编译?
def bar(x: Int) = foo(x)
@tailrec
def foo(x: Int): Boolean = x == 0 || bar(x-1) || foo(x-2)
好吧,那只是作弊 :) 编译器不希望你那么狡猾,它无法知道 bar
只会调用 foo
,所以它必须信任你在那个。
基本上 @tailrec
只能检测到立即递归,如果你试图屏蔽它......好吧,你会成功:)
从上面删除 || foo(x-2)
- 它会停止编译,并告诉您没有递归。即使它现在是一个完美的尾递归函数,它也无法对其进行优化。
这不是金子弹。这是另一个怪癖:
@tailrec
def foo(x: => Future[Int]): Future[Int] = {
x.recoverWith { case _ => foo(x) }
foo(Future(1))
}
这个是尾递归的,但它会拒绝编译,因为它没有意识到对 foo
的第一次调用实际上并没有发生在外部 {{1} }.