为什么 Scala 编译器无法识别这种尾调用优化方法?

问题描述

这个简单的正则表达式实现 (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} }.

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...