【问题标题】:Why is this tail-call optimized method not recognized as such by the Scala compiler?为什么 Scala 编译器无法识别这种尾调用优化方法?
【发布时间】:2021-05-05 18:37:56
【问题描述】:

这个简单的正则表达式实现 (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, rs@_*), Seq(p, '*', xs@_*)) => (r == p && aliasMatches(rs, remainingPattern)) ||
                matchesRemainder(remainder, xs)

            case (Seq(_, rs@_*), Seq('.', ps@_*)) => matchesRemainder(rs, ps)
            case (Seq(r, rs@_*), Seq(p, ps@_*)) => r == p && matchesRemainder(rs, ps)
        }

    def aliasMatches(remainder: Seq[Char], p: Seq[Char]): Boolean =
        matchesRemainder(remainder, p)

    matchesRemainder(input, pattern)
}

【问题讨论】:

  • Scala 1.13.5 不存在
  • 最后一个表达式不是递归调用而是对||的调用
  • @LuisMiguelMejíaSuárez 那么为什么在使用别名时它会编译?如果第一个参数失败,|| 不会评估它的最后一个参数,使最后一个参数成为尾调用?
  • @cchantep darn,哎呀,你说得对,我显然忽略了我的错字(两次)。抱歉,我会改正的。
  • @JoostPapendorp 嗯,你是对的,但我不确定编译器是否足够聪明,可以解决这个问题;它可能是(它至少比我聪明)。 - 无论如何,如果你在同一个块中有两个递归调用,即使最后一个在尾部位置,第一个不是,这意味着你的代码确实会炸毁堆栈。

标签: scala tail-call-optimization


【解决方案1】:

让我们稍微简化一下,以便更清楚地看到问题。

    @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的第一次调用实际上并没有发生在外部foo内部。

【讨论】:

  • 谢谢,这真的很丰富。编译器只接受完全尾递归,即使它可以优化部分。我知道使用别名是作弊,但现在我也知道为什么了。
  • 我不认为“部分尾递归”是一回事。尾递归意味着 O(1) 堆栈。所以,要么是,要么不是,没有“部分”。
猜你喜欢
  • 2021-11-23
  • 1970-01-01
  • 2011-06-14
  • 2023-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多