以下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
答案 0 :(得分:6)
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
}
我很有兴趣看到这与没有蹦床的解决方案以及默认的foldLeft
和foldRight
实现相比如何。以下是基准代码和一些结果:
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