【问题标题】:Scala newbie: recursion and stackoverflow errorScala新手:递归和stackoverflow错误
【发布时间】:2012-07-26 00:56:46
【问题描述】:

作为 Scala 新手,我正在阅读书籍、文档并尝试解决在 http://aperiodic.net/phil/scala/s-99/ 上发现的问题。似乎正确的 Scala 代码基于不可变值 (val) 和递归而不是循环和变量,以使并行性更安全并避免使用锁。

例如,练习 P22 (http://aperiodic.net/phil/scala/s-99/p22.scala) 的可能解决方案是:

// Recursive.
def rangeRecursive(start: Int, end: Int): List[Int] =
if (end < start) Nil
else start :: rangeRecursive(start + 1, end)

当然,这段代码很紧凑,看起来很聪明,但是当然,如​​果递归次数很高,您将面临 StackOverflow 错误(例如 rangeRecusrsive(1,10000) 没有 JVM 调整)。如果您查看内置 List.range 的来源(https://github.com/scala/scala/blob/v2.9.2/src/library/scala/collection/immutable/List.scala#L1),您'会看到使用了循环和变量。

我的问题是如何管理促进 vals 和递归的 Scala 学习内容的影响,因为知道此类代码可能会因递归次数而中断?

【问题讨论】:

  • Scala 编译器足够聪明,可以在trampoolined 尾递归中编译尾递归调用(JVM 不支持 TCE),这不会导致 stackoveflow。如果您想确定您的代码是尾递归的,请在方法签名中添加 @tailrec 注释

标签: scala recursion stack-overflow


【解决方案1】:

Scala 的好处在于您可以轻松进入它。一开始,您可以编写循环,并随着您对语言的熟悉程度越来越高,使用递归做更多事情。您不能使用更“纯”的函数式语言(例如 Clojure 或 Haskell)来做到这一点。换句话说,您可以对不可变性和val 感到满意,然后继续使用递归。

当您开始使用递归时,您应该查找尾调用递归。如果递归调用是函数中的最后一次调用,Scala 编译器会将其优化为字节码中的循环。这样,您将不会收到StackOverflowErrors。此外,如果您将@tailrec 注解添加到您的递归函数中,如果您的函数不是尾调用递归,编译器会警告您。

例如,您问题中的函数不是尾调用递归。看起来对rangeRecursive 的调用是函数中的最后一个,但是当这个调用返回时,它仍然必须将start 附加到调用的结果中。因此,它不能是尾调用递归:调用返回时它仍然需要工作。

【讨论】:

  • 谢谢,所以最终目标是做尾递归函数,而不仅仅是递归函数,对吧?
  • @Brice 尽可能,是的。有时这是不可能的,但通常是。在这些情况下,您可以获得性能提升,并且您不必担心堆栈溢出。
【解决方案2】:

这是使该方法尾递归的示例。 @tailrec 注释不是必需的,编译器将在没有它的情况下进行优化。但是拥有它会使编译器在无法进行优化时标记错误。

scala> def rangeRecursive(start: Int, end: Int): List[Int] = {
    |   @scala.annotation.tailrec
    |   def inner(accum : List[Int], start : Int) : List[Int] = {
    |       if (end < start) accum.reverse
    |       else inner(start :: accum, start + 1)
    |   }
    |   
    |   inner(Nil, start)
    | }
rangeRecursive: (start: Int,end: Int)List[Int]

scala> rangeRecursive(1,10000)
res1: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,...

它使用一种称为“累加器传递方式”的常用技术,其中中间结果被累加并传递到递归的下一步。最底层的步骤负责返回累积的结果。在这种情况下,累加器恰好反向构建其结果,因此基本情况必须反转它。

【讨论】:

  • 还有另一种方法可以做你在那里所做的事情,inner(start :: accum, start+1) 不使用相反的方法。可能是这样的:inner(accum ::: List(start), start + 1) 我只是不知道编译器会更贵。
  • 我自己找到了答案。我的其他解决方案在性能方面真的很糟糕。没关系。
【解决方案3】:

如果你将上面的代码重写为尾递归,编译器会将代码优化为一个while循环。此外,您可以使用@tailrec 注释在它注释的方法不是尾递归时获取错误。从而让您知道“什么时候做对了”。

【讨论】:

    【解决方案4】:

    这是 James Iry 的答案的替代方案,具有相同的行为:

    def rangeRecursive(start: Int, end: Int): List[Int] = {
      def inner(start : Int) : Stream[Int] = {
          if (end < start) Stream.empty
          else start #:: inner(start + 1)
      }
    
      inner(start).toList
    }
    
    scala> rangeRecursive(1,10000)
    res1: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,...
    

    这不会抛出StackOverflowError,因为Stream.cons-operator (#::) 通过引用存储尾部。换句话说,直到调用 stream.toList 才会计算 Stream 元素。

    在我看来,这比累加器模式更具可读性,因为它最类似于朴素的初始算法(只需将 :: 替换为 #:: 并将 Nil 替换为 Stream.empty)。另外,也不需要accum.reverse,这很容易被遗忘。

    【讨论】:

    • 我在我的代码中使用了这种模式,但我很好奇为什么'inner(start).toList'没有给出stackOverflowError。 => 因为它不是递归构造?
    • #:: 运算符将右侧作为函数而不是值。 inner(start).toList 将使用循环遍历所有值。在该循环的每次迭代中都会计算下一个值。
    猜你喜欢
    • 2011-12-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-31
    • 1970-01-01
    相关资源
    最近更新 更多