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

Posted

技术标签:

【中文标题】如何在 Scala 中使用 Stream.cons 编写不泄漏的尾递归函数?【英文标题】:How to write non-leaking tail-recursive function using Stream.cons in Scala? 【发布时间】:2012-09-13 19:51:03 【问题描述】:

在编写一个在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 函数的堆栈帧包含对蒸汽头的引用,并且在 @987654331 之前不会让流被垃圾收集@call 返回,根据对B) 的调用次数,可能会很长(就递归步骤而言)。

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

【问题讨论】:

另见相关***.com/questions/12486762/… 有可能有一段工作代码吗? IE。一个OOM? 克里斯:当然,比较两者:gist.github.com/3769565(2.10.0-M7 不适合 ok.scala) 谢谢,我终于有时间看它了,根据您的回答,您只需要保留一个参考,整个流都保存在内存中。 你应该尝试像final def recHelp[A](as: => Stream[A]): Stream[B]那样通过引用助手来传递流。 【参考方案1】:

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

最好的答案是简单地避免这种令人惊讶的行为,使用 Scalaz EphemeralStream 并看到它不会 oom 并且运行得更快,因为它对 gc 更好。使用它并不总是那么简单,例如head 是一个 () => 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,否则那里的惊喜要少得多。

【讨论】:

ES 是个好点。我需要 Stream 因为底层源不是计算,而是可变迭代器。我知道我最好使用 Iteratees(或类似的东西,请参阅 ***.com/questions/12496654/…)。【参考方案2】:

一种可能的解决方法是使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 也负责同步。

【讨论】:

以上是关于如何在 Scala 中使用 Stream.cons 编写不泄漏的尾递归函数?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Pyspark 中使用 Scala 函数?

SORM:如何在 Scala 2.11.6 中使用 Sorm

如何在 Spark/Scala 中使用 countDistinct?

关于序列化:把某个对象序列化成字节流

如何在 Java 中使用 Scala 隐式类

如何在 Java 中使用 Scala 隐式类