【问题标题】:Scala's Stream and StackOverflowErrorScala 的 Stream 和 StackOverflowError
【发布时间】:2012-11-11 17:52:53
【问题描述】:

考虑一下这段代码(取自 Martin Odersky 的“Functional programming principles in Scala”课程):

def sieve(s: Stream[Int]): Stream[Int] = {
  s.head #:: sieve(s.tail.filter(_ % s.head != 0))
}

val primes = sieve(Stream.from(2))
primes.take(1000).toList

它工作得很好。请注意,sieve 实际上不是尾递归(或者是吗?),即使 Stream 的尾是惰性的。

但是这段代码:

def sieve(n: Int): Stream[Int] = {
  n #:: sieve(n + 1).filter(_ % n != 0)
}

val primes = sieve(2)
primes.take(1000).toList

抛出StackOverflowError

第二个例子有什么问题?我猜filter 搞砸了,但我不明白为什么。它返回一个Stream,所以它不会使评估变得急切(对吗?)

【问题讨论】:

    标签: scala stream stack-overflow lazy-evaluation


    【解决方案1】:

    您可以使用一些跟踪代码突出显示问题:

    var counter1, counter2 = 0
    
    def sieve1(s: Stream[Int]): Stream[Int] = {
      counter1 += 1
      s.head #:: sieve1(s.tail.filter(_ % s.head != 0))
    }
    
    def sieve2(n: Int): Stream[Int] = {
      counter2 += 1
      n #:: sieve2(n + 1).filter(_ % n != 0)
    }
    
    sieve1(Stream.from(2)).take(100).toList
    sieve2(2).take(100).toList
    

    我们可以运行它并检查计数器:

    scala> counter1
    res2: Int = 100
    
    scala> counter2
    res3: Int = 540
    

    所以在第一种情况下,调用堆栈的深度是素数的数量,而在第二种情况下,它是最大的素数本身(嗯,减一)。

    【讨论】:

    • 谢谢!所以,这两个例子都会溢出堆栈,但第二个只是让它更快。让我感到困惑的是为什么第一个示例不这样做,当它不是尾递归时。现在我明白了。
    • 其实不是这样的。第一个不会炸毁堆栈。 #:: 是故意懒惰的。
    • 问题几乎可以肯定是过滤器在尾调用之外的位置,这可能会构建大量的流以从中提取。 (计数只是计数调用,not 堆栈深度)。另外,我也不能让它溢出 scala 2.10,所以也许 Stream 上的过滤方法从那时起已经改进了。
    【解决方案2】:

    这些都不是尾递归的。

    使用tailrec注解会告诉你一个函数是否是尾递归的。

    将@tailrec 添加到上面的两个函数中:

    import scala.annotation.tailrec
    
    @tailrec
    def sieve(s: Stream[Int]): Stream[Int] = {
      s.head #:: sieve(s.tail.filter(_ % s.head != 0))
    }
    
    @tailrec
    def sieve(n: Int): Stream[Int] = {
      n #:: sieve(n + 1).filter(_ % n != 0)
    }
    

    加载这个表明两个定义都不是尾递归的:

    <console>:10: error: could not optimize @tailrec annotated method sieve: it contains a recursive call not in tail position
             s.head #:: sieve(s.tail.filter(_ % s.head != 0))
                    ^
    <console>:10: error: could not optimize @tailrec annotated method sieve: it contains a recursive call not in tail position
             n #:: sieve(n + 1).filter(_ % n != 0)
    

    【讨论】:

    • 是的,我知道这两个例子都不是尾递归的,我只是想知道为什么第一个在第二个失败时可以正常工作。谢谢!
    • 简短的回答是它们都会炸毁堆栈。第二个只是比第一个更快。尝试sieve1 逐渐增大数字。
    • 这不是真的。 #:: 方法是惰性的,因此递归调用的评估被延迟。
    • @ChrisLeishman 我也这么认为。但是,tailrec 确实抱怨并且使用 take(10000) 而不是 take(100) 会导致我的 OutOfMemoryError。
    • 不是tailrec,所以tailrec注解应该报错。实际上,它在技术上根本不是递归的,因为递归部分是延迟的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-08-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-24
    相关资源
    最近更新 更多