【问题标题】:Functional processing of Scala streams without OutOfMemory errors没有 OutOfMemory 错误的 Scala 流的功能处理
【发布时间】:2011-05-07 04:25:20
【问题描述】:

是否可以将函数式编程应用于 Scala 流,以便按顺序处理流,但可以对流中已处理的部分进行垃圾回收?

例如,我定义了一个Stream,其中包含从startend 的数字:

def fromToStream(start: Int, end: Int) : Stream[Int] = {
  if (end < start) Stream.empty
  else start #:: fromToStream(start+1, end)
}

如果我以函数式的方式总结价值:

println(fromToStream(1,10000000).reduceLeft(_+_))

我得到一个OutOfMemoryError - 可能是因为调用reduceLeft 的堆栈帧包含对流头部的引用。但如果我以迭代方式执行此操作,它会起作用:

var sum = 0
for (i <- fromToStream(1,10000000)) {
  sum += i
}

有没有一种方法可以在不获取OutOfMemory 的情况下以功能样式执行此操作?

更新:这是a bug in scala,现已修复。所以现在这或多或少已经过时了。

【问题讨论】:

  • 虽然这绝不会回答您的问题,但我发现流的#:: 语法比Stream.cons 更具可读性

标签: scala functional-programming


【解决方案1】:

当我开始了解Stream 时,我觉得这很酷。然后我意识到Iterator 几乎是我一直想要使用的。

如果您确实需要 Stream 但想让 reduceLeft 工作:

fromToStream(1,10000000).toIterator.reduceLeft(_ + _)

如果您尝试上面的行,它会很好地收集垃圾。我发现使用 Stream 很棘手,因为它很容易在没有意识到的情况下抓住头部。有时标准库会以非常微妙的方式为您保留它。

【讨论】:

    【解决方案2】:

    是的,你可以。诀窍是使用尾递归方法,以便本地堆栈帧包含对Stream 实例的唯一引用。由于该方法是尾递归的,因此一旦递归调用自身,对前一个 Stream 头的本地引用将被删除,从而使 GC 能够在您执行时收集 Stream 的开头。

    Welcome to Scala version 2.9.0.r23459-b20101108091606 (Java HotSpot(TM) Server VM, Java 1.6.0_20).
    Type in expressions to have them evaluated.
    Type :help for more information.
    
    scala> import collection.immutable.Stream
    import collection.immutable.Stream
    
    scala> import annotation.tailrec
    import annotation.tailrec
    
    scala> @tailrec def last(s: Stream[Int]): Int = if (s.tail.isEmpty) s.head else last(s.tail)
    last: (s: scala.collection.immutable.Stream[Int])Int
    
    scala> last(Stream.range(0, 100000000))                                                                             
    res2: Int = 99999999
    

    此外,您必须确保您传递给上述方法last 的东西在堆栈上只有一个引用。如果将Stream 存储到局部变量或值中,则在调用last 方法时它不会被垃圾回收,因为它的参数不是留给Stream 的唯一引用。下面的代码内存不足。

    scala> val s = Stream.range(0, 100000000)                                                                           
    s: scala.collection.immutable.Stream[Int] = Stream(0, ?)                                                            
    
    scala> last(s)                                                                                                      
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space                                              
            at sun.net.www.ParseUtil.encodePath(ParseUtil.java:84)                                                      
            at sun.misc.URLClassPath$JarLoader.checkResource(URLClassPath.java:674)                                     
            at sun.misc.URLClassPath$JarLoader.getResource(URLClassPath.java:759)                                       
            at sun.misc.URLClassPath.getResource(URLClassPath.java:169)                                                 
            at java.net.URLClassLoader$1.run(URLClassLoader.java:194)                                                   
            at java.security.AccessController.doPrivileged(Native Method)                                               
            at java.net.URLClassLoader.findClass(URLClassLoader.java:190)                                               
            at java.lang.ClassLoader.loadClass(ClassLoader.java:307)                                                    
            at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)                                            
            at java.lang.ClassLoader.loadClass(ClassLoader.java:248)                                                    
            at scala.tools.nsc.Interpreter$Request$$anonfun$onErr$1$1.apply(Interpreter.scala:978)                      
            at scala.tools.nsc.Interpreter$Request$$anonfun$onErr$1$1.apply(Interpreter.scala:976)                      
            at scala.util.control.Exception$Catch.apply(Exception.scala:80)
            at scala.tools.nsc.Interpreter$Request.loadAndRun(Interpreter.scala:984)                                    
            at scala.tools.nsc.Interpreter.loadAndRunReq$1(Interpreter.scala:579)                                       
            at scala.tools.nsc.Interpreter.interpret(Interpreter.scala:599)                                             
            at scala.tools.nsc.Interpreter.interpret(Interpreter.scala:576)
            at scala.tools.nsc.InterpreterLoop.reallyInterpret$1(InterpreterLoop.scala:472)                             
            at scala.tools.nsc.InterpreterLoop.interpretStartingWith(InterpreterLoop.scala:515)                         
            at scala.tools.nsc.InterpreterLoop.command(InterpreterLoop.scala:362)
            at scala.tools.nsc.InterpreterLoop.processLine$1(InterpreterLoop.scala:243)
            at scala.tools.nsc.InterpreterLoop.repl(InterpreterLoop.scala:249)
            at scala.tools.nsc.InterpreterLoop.main(InterpreterLoop.scala:559)
            at scala.tools.nsc.MainGenericRunner$.process(MainGenericRunner.scala:75)
            at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:31)
            at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)
    

    总结一下:

    1. 使用尾递归方法
    2. 将它们注释为尾递归
    3. 当您调用它们时,确保它们的参数是对Stream 的唯一引用

    编辑:

    请注意,这也有效,不会导致内存不足错误:

    scala> def s = Stream.range(0, 100000000)                                                   
    s: scala.collection.immutable.Stream[Int]
    
    scala> last(s)                                                                              
    res1: Int = 99999999
    

    EDIT2:

    对于您需要的reduceLeft,您必须为结果定义一个带有累加器参数的辅助方法。

    对于 reduceLeft,您需要一个累加器参数,您可以使用默认参数将其设置为某个值。一个简化的例子:

    scala> @tailrec def rcl(s: Stream[Int], acc: Int = 0): Int = if (s.isEmpty) acc else rcl(s.tail, acc + s.head)
    rcl: (s: scala.collection.immutable.Stream[Int],acc: Int)Int
    
    scala> rcl(Stream.range(0, 10000000))
    res6: Int = -2014260032
    

    【讨论】:

    • 你会在哪里定义辅助方法?如果在 reduceLeft 的内部方法中,辅助方法的调用者不会冒险抓住流的头部吗?
    • 嗯。好点子——他确实会的。而尾调用优化只能应用于递归方法。你说得对。但是可以使用默认参数。请参阅我的第二次编辑。
    • 我有同样的 OutOfMemory 问题,但使用 stream.foreach - 我该如何解决?
    【解决方案3】:

    你可能想看看 Scalaz 的ephemeral streams

    【讨论】:

    • 一个 sn-p 会很高兴看到临时流如何应用于这个特定问题。您提供的链接指向一个没有注释的源文件。
    【解决方案4】:

    事实证明,这是 reduceLeft 当前实现中的a bug。问题是reduceLeft调用了foldLeft,因此reduceLeft的stackframe在整个调用过程中都持有对流头部的引用。 foldLeft 使用尾递归来避免这个问题。比较:

    (1 to 10000000).toStream.foldLeft(0)(_+_)
    (1 to 10000000).toStream.reduceLeft(_+_)
    

    这些在语义上是等价的。在 Scala 版本 2.8.0 中,对 foldLeft 的调用有效,但对 reduceLeft 的调用会引发 OutOfMemory。如果 reduceLeft 自己做,就不会出现这个问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-04-19
      • 2011-10-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-12
      相关资源
      最近更新 更多