【问题标题】:@tailrec error - "recursive call targeting a supertype"@tailrec 错误 - “针对超类型的递归调用”
【发布时间】:2016-06-01 10:01:30
【问题描述】:

我有以下问题。

class NonEmpty(elem: Tweet, left: TweetSet, right: TweetSet) extends TweetSet {

  @tailrec final def filterAcc(p: Tweet => Boolean, acc: TweetSet): TweetSet =
    if(p(elem))
      (this remove elem).filterAcc(p, acc incl elem)
    else
      (this remove elem).filterAcc(p, acc)

}

Scala 告诉我,它无法优化@tairec,因为我的方法包含一个针对超类型的递归调用。 (NonEmpty 的超类是 TweetSet 类,其中定义了方法 filterAcc)。如何处理这样的错误?

【问题讨论】:

    标签: scala recursion functional-programming


    【解决方案1】:

    这是预期的行为。由于@tailrec 的工作方式导致错误发生:本质上,编译器尝试将递归调用转换为本地循环。尽管您的方法是递归的,但它通过在另一个实例上调用自身来递归,即使不是相同类型,而是超类型 - 由于TweetSet 的其他子级可能提供完全不同的filterAcc 实现,编译器无法确保此方法可以安全地展开为循环。

    尾递归函数的经验法则是确保它始终准确地调用 自身 作为最后一条语句 - 在您的情况下,而不是调用

    (this remove elem).filterAcc(...)
    

    您必须尝试以模拟 this remove elem 的方式转换 acc 并调用

    filterAcc(p, <transformed acc>)
    

    在涉及递归的函数式编程中,最好在超类中定义整个方法(在您的情况下为 TweetSet)并依赖 ADT 成员的模式匹配而不是多态性 - 这样优化就不会有问题递归。

    更新:blog post 很好地解释了尾递归,正如this question 的回答中所指定的那样

    【讨论】:

    • 恐怕这是不可能的,因为 filterAcc 返回这个所有元素的集合,女巫满足 p 条件(p 是一个逻辑公式),所以我需要每次调用 filterAcc 来获得更小的集合。
    • 我只是想指出这不是尾递归的一般限制。 OP 的方法尾递归的,但是它不是直接尾递归。 Scala 只优化直接尾递归。 Scala 语言的语义中也没有任何内容可以阻止优化间接尾递归甚至一般的尾调用。相反,这个限制是一个务实的选择:Scala 开发人员希望在 JVM 上实现高性能、高度可互操作的 Scala 实现,而在保持这两个属性的同时在 JVM 上实现高级非本地控制流是不可能的
    • 我在接受 Clojure 的创建者 Rich Hickey 的一次采访中称其为“Hickey 定律”(它做出了非常相似的选择)关于为什么 Clojure 与 Scheme 不同,不支持 Proper尾调用:“高级控制流、互操作、性能。选择两​​个。”并不是 Scala 不能进行适当的尾调用(它可以,例如,Scala-Native 很快就会这样做)。并不是说您不能在 JVM 上实现正确的尾调用(JVM 上的方案实现就是这样做的)。但是您必须实现自己的堆栈才能做到这一点(或类似的东西),这需要您与 JVM 堆栈进行互操作。
    【解决方案2】:

    在这种情况下,将filterAcc 移动到伴随对象中可能是有意义的:

    trait TweetSet {
      final def filterAcc(p: Tweet => Boolean) = TweetSet.filterAcc(p, this, Empty)
    }
    
    object TweetSet {
      @tailrec 
      private def filterAcc(p: Tweet => Boolean, self: TweetSet, acc: TweetSet) = self match {
        case ne: NonEmpty =>
          // the original body with `this` replaced by `self` and made an argument for recursive calls
          val elem = ne.elem
          if(p(elem))
            filterAcc(p, self remove elem, acc incl elem)
          else
            filterAcc(p, self remove elem, acc)
        // other cases
      }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-06-18
      • 1970-01-01
      • 2016-12-29
      • 1970-01-01
      • 2020-06-25
      • 1970-01-01
      • 1970-01-01
      • 2021-10-05
      相关资源
      最近更新 更多