是否可以使用continuation来使foldRight尾递归?

时间:2011-12-18 02:33:00

标签: scala f# fold tail-call-optimization

以下blog article显示了如何使用延续传递样式对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 个答案:

答案 0 :(得分: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就是赢家......

答案 1 :(得分:4)

问题是延续函数(racc => k(f(x, racc)))本身。应该对整个业务进行尾随优化,但事实并非如此。

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

答案 2 :(得分:4)

  

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

F#优化了所有尾调用。

  

有没有办法解决这个与Scala?

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

修改

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

对于带有1.67GHz N570 Intel Atom的上网本上1,000个元素列表中的1,000x,我得到:

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中关于此的旧讨论感兴趣。

答案 3 :(得分: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)
  }

}

最后发生的折叠本身是尾递归的。试一试in 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