如何在Scala中使用Stream.cons编写非泄漏尾递归函数?

时间:2012-09-21 11:32:35

标签: scala recursion memory-leaks stream

当编写在Stream(s)上运行的函数时,会有不同的递归概念。第一个简单的意义在编译器级别上不是递归的,因为如果不立即计算尾部,那么函数会立即返回,但返回的流是递归的:

final def simpleRec[A](as: Stream[A]): Stream[B] = 
  if (a.isEmpty) Stream.empty              
  else someB(a.head) #:: simpleRec(a.tail) 

上述递归概念不会引起任何问题。第二个是在编译器级别上真正的尾递归:

@tailrec
final def rec[A](as: Stream[A]): Stream[B] = 
  if (a.isEmpty) Stream.empty              // A) degenerated
  else if (someCond) rec(a.tail)           // B) tail recursion
  else someB(a.head) #:: rec(a.tail)       // C) degenerated

这里的问题是,C)情况被编译器检测为非tailrec调用,即使没有执行实际调用。这可以通过将流尾部分解为辅助函数来避免:

@tailrec
final def rec[A](as: Stream[A]): Stream[B] = 
  if (a.isEmpty) Stream.empty              
  else if (someCond) rec(a.tail)          // B)
  else someB(a.head) #:: recHelp(a.tail)  

@tailrec
final def recHelp[A](as: Stream[A]): Stream[B] = 
  rec(as)

在编译时,这种方法最终会导致内存泄漏。由于最终从rec函数调用尾递归recHelp,因此recHelp函数的堆栈帧保存对蒸汽头的引用,并且不允许流为收集垃圾直到rec调用返回,这可能会很长(就递归步骤而言),具体取决于对B)的调用次数。

请注意,即使在无助手的情况下,如果编译器允许@tailrec,内存泄漏可能仍然存在,因为惰性流尾部实际上会创建一个匿名对象,持有对流头的引用。

2 个答案:

答案 0 :(得分:3)

可能的解决方法是使recHelp方法不保留对流头的引用。这可以通过将包装好的流传递给它,并改变包装器以从中删除引用来实现:

@tailrec
final def rec[A](as: Stream[A]): Stream[B] = 
  if (a.isEmpty) Stream.empty              
  else if (someCond) rec(a.tail)          
  else {
    // don't inline and don't define as def,
    // or anonymous lazy wrapper object would hold reference
    val tailRef = new AtomicReference(a.tail)
    someB(a.head) #:: recHelp(tailRef)  
  }

@tailrec
final def recHelp[A](asRef: AtomicReference[Stream[A]]): Stream[B] = 
  // Note: don't put the content of the holder into a local variable
  rec(asRef.getAndSet(null))

AtomicReference只是方便,在这种情况下不需要原子性,任何简单的持有者对象都可以。

另请注意,由于recHelp包含在流Cons尾部,因此它只会被评估一次,Cons也会处理同步。

答案 1 :(得分:2)

正如您所暗示的那样,问题在于您粘贴的代码中filterHelp函数保留了头部(因此您的解决方案将其删除)。

最佳答案是简单地避免这种令人惊讶的行为,使用Scalaz EphemeralStream并且看到它既不是oom而且运行得更快,因为它对gc更好。使用它并不总是那么简单。 head是a()=> A不是A,没有提取器等,但它都适用于一个目标,可靠的流使用。

您的filterHelper函数通常不必关心它是否保留了引用:

import scalaz.EphemeralStream

@scala.annotation.tailrec
def filter[A](s: EphemeralStream[A], f: A => Boolean): EphemeralStream[A] = 
  if (s.isEmpty) 
    s
  else
    if (f(s.head())) 
      EphemeralStream.cons(s.head(), filterHelp(s.tail() , f) )
    else
      filter(s.tail(), f)

def filterHelp[A](s: EphemeralStream[A], f: A => Boolean) =
  filter(s, f)

def s1 = EphemeralStream.range(1, big)

我甚至可以说,除非你有令人信服的理由使用Stream(其他库依赖等),然后坚持使用EphemeralStream,那里的惊喜要少得多。