10

以下博客文章展示了如何在 F#foldBack中使用延续传递样式进行尾递归。

在 Scala 中,这意味着:

def foldBack[T,U](l: List[T], acc: U)(f: (T, U) => U): U = {
  l match {
    case x :: xs => f(x, foldBack(xs, acc)(f))
    case Nil => acc
  }
} 

可以通过这样做使尾递归:

def foldCont[T,U](list: List[T], acc: U)(f: (T, U) => U): U = {
  @annotation.tailrec
  def loop(l: List[T], k: (U) => U): U = {
    l match {
      case x :: xs => loop(xs, (racc => k(f(x, racc))))
      case Nil => k(acc)
    }
  }
  loop(list, u => u)
} 

不幸的是,对于长列表,我仍然会出现堆栈溢出。循环是尾递归和优化的,但我猜堆栈累积只是移动到继续调用中。

为什么这不是 F# 的问题?有没有办法用 Scala 解决这个问题?

编辑:这里有一些显示堆栈深度的代码:

def showDepth(s: Any) {
  println(s.toString + ": " + (new Exception).getStackTrace.size)
}

def foldCont[T,U](list: List[T], acc: U)(f: (T, U) => U): U = {
  @annotation.tailrec
  def loop(l: List[T], k: (U) => U): U = {
    showDepth("loop")
    l match {
      case x :: xs => loop(xs, (racc => { showDepth("k"); k(f(x, racc)) }))
      case Nil => k(acc)
    }
  }
  loop(list, u => u)
} 

foldCont(List.fill(10)(1), 0)(_ + _)

这打印:

loop: 50
loop: 50
loop: 50
loop: 50
loop: 50
loop: 50
loop: 50
loop: 50
loop: 50
loop: 50
loop: 50
k: 51
k: 52
k: 53
k: 54
k: 55
k: 56
k: 57
k: 58
k: 59
k: 60
res2: Int = 10
4

4 回答 4

6

乔恩,纳米,谢谢你的回答。根据您的评论,我想我会尝试使用蹦床。一些研究表明 Scala 在TailCalls. 这是我经过一番摆弄后得出的结论:

def foldContTC[T,U](list: List[T], acc: U)(f: (T, U) => U): U = {
  import scala.util.control.TailCalls._
  @annotation.tailrec
  def loop(l: List[T], k: (U) => TailRec[U]): TailRec[U] = {
    l match {
      case x :: xs => loop(xs, (racc => tailcall(k(f(x, racc)))))
      case Nil => k(acc)
    }
  }
  loop(list, u => done(u)).result
} 

我很想看看这与没有蹦床的解决方案以及默认foldLeftfoldRight实现相比如何。这是基准代码和一些结果:

val size = 1000
val list = List.fill(size)(1)
val warm = 10
val n = 1000
bench("foldContTC", warm, lots(n, foldContTC(list, 0)(_ + _)))
bench("foldCont", warm, lots(n, foldCont(list, 0)(_ + _)))
bench("foldRight", warm, lots(n, list.foldRight(0)(_ + _)))
bench("foldLeft", warm, lots(n, list.foldLeft(0)(_ + _)))
bench("foldLeft.reverse", warm, lots(n, list.reverse.foldLeft(0)(_ + _)))

时间安排是:

foldContTC: warming...
Elapsed: 0.094
foldCont: warming...
Elapsed: 0.060
foldRight: warming...
Elapsed: 0.160
foldLeft: warming...
Elapsed: 0.076
foldLeft.reverse: warming...
Elapsed: 0.155

基于此,蹦床似乎实际上产生了相当不错的性能。我怀疑在拳击/拆箱之上的惩罚相对来说还不错。

编辑:正如 Jon 的评论所建议的,这里是 1M 项目的时间,这些项目证实了更大的列表会降低性能。我还发现库 List.foldLeft 实现没有被覆盖,所以我用以下 foldLeft2 计时:

def foldLeft2[T,U](list: List[T], acc: U)(f: (T, U) => U): U = {
  list match {
    case x :: xs => foldLeft2(xs, f(x, acc))(f)
    case Nil => acc
  }
} 

val size = 1000000
val list = List.fill(size)(1)
val warm = 10
val n = 2
bench("foldContTC", warm, lots(n, foldContTC(list, 0)(_ + _)))
bench("foldLeft", warm, lots(n, list.foldLeft(0)(_ + _)))
bench("foldLeft2", warm, lots(n, foldLeft2(list, 0)(_ + _)))
bench("foldLeft.reverse", warm, lots(n, list.reverse.foldLeft(0)(_ + _)))
bench("foldLeft2.reverse", warm, lots(n, foldLeft2(list.reverse, 0)(_ + _)))

产量:

foldContTC: warming...
Elapsed: 0.801
foldLeft: warming...
Elapsed: 0.156
foldLeft2: warming...
Elapsed: 0.054
foldLeft.reverse: warming...
Elapsed: 0.808
foldLeft2.reverse: warming...
Elapsed: 0.221

所以 foldLeft2.reverse 是赢家...

于 2011-12-18T20:36:34.427 回答
4

问题在于延续函数(racc => k(f(x, racc)))本身。它应该针对整个业务进行优化,但不是。

Scala 不能对任意尾调用进行尾调用优化,只能对可以转换为循环的那些进行尾调用优化(即,当函数调用自身时,而不是其他函数时)。

于 2011-12-18T04:34:06.187 回答
4

为什么这不是 F# 的问题?

F# 优化了所有尾调用。

有没有办法用 Scala 解决这个问题?

您可以使用蹦床等其他技术进行 TCO,但您会失去互操作性,因为它改变了调用约定并且速度慢了约 10 倍。这是我不使用 Scala 的三个原因之一。

编辑

您的基准测试结果表明 Scala 的蹦床我上次测试它们时快得多。此外,使用 F# 添加等效的基准测试和更大的列表也很有趣(因为在小列表上执行 CPS 没有意义!)。

对于使用 1.67GHz N570 Intel Atom 的上网本上的 1,000 元素列表中的 1,000 倍,我得到:

List.fold     0.022s
List.rev+fold 0.116s
List.foldBack 0.047s
foldContTC    0.334s

对于 1x 1,000,000 元素列表,我得到:

List.fold     0.024s
List.rev+fold 0.188s
List.foldBack 0.054s
foldContTC    0.570s

在用优化的尾递归函数替换 OCaml 的非尾递归列表函数的上下文中,您可能还对 caml-list 上关于此的旧讨论感兴趣。

于 2011-12-18T15:14:34.403 回答
3

我迟到了这个问题,但我想展示如何在不使用完整蹦床的情况下编写尾递归 FoldRight;通过累积一个延续列表(而不是让它们在完成时相互调用,这会导致堆栈溢出)并在最后折叠它们,有点像保留一个堆栈,但在堆上:

object FoldRight {

  def apply[A, B](list: Seq[A])(init: B)(f: (A, B) => B): B = {
    @scala.annotation.tailrec
    def step(current: Seq[A], conts: List[B => B]): B = current match {
      case Seq(last) => conts.foldLeft(f(last, init)) { (acc, next) => next(acc) }
      case Seq(x, xs @ _*) => step(xs, { acc: B => f(x, acc) } +: conts)
      case Nil => init
    }
    step(list, Nil)
  }

}

最后发生的折叠本身就是尾递归的。在 ScalaFiddle 中尝试一下

在性能方面,它的表现略逊于尾调用版本。

[info] Benchmark            (length)  Mode  Cnt   Score    Error  Units
[info] FoldRight.conts           100  avgt   30   0.003 ±  0.001  ms/op
[info] FoldRight.conts         10000  avgt   30   0.197 ±  0.004  ms/op
[info] FoldRight.conts       1000000  avgt   30  77.292 ±  9.327  ms/op
[info] FoldRight.standard        100  avgt   30   0.002 ±  0.001  ms/op
[info] FoldRight.standard      10000  avgt   30   0.154 ±  0.036  ms/op
[info] FoldRight.standard    1000000  avgt   30  18.796 ±  0.551  ms/op
[info] FoldRight.tailCalls       100  avgt   30   0.002 ±  0.001  ms/op
[info] FoldRight.tailCalls     10000  avgt   30   0.176 ±  0.004  ms/op
[info] FoldRight.tailCalls   1000000  avgt   30  33.525 ±  1.041  ms/op
于 2016-09-22T07:34:15.973 回答