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

huy*_*hjl 10 f# scala fold tail-call-optimization

以下博客文章展示了如何foldBack使用延续传递样式使F#尾部递归.

在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
  }
} 
Run Code Online (Sandbox Code Playgroud)

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

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)
} 
Run Code Online (Sandbox Code Playgroud)

不幸的是,我仍然为长列表获得堆栈溢出.循环是尾递归和优化但我想堆栈累积只是移动到延续调用.

为什么这不是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)(_ + _)
Run Code Online (Sandbox Code Playgroud)

这打印:

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
Run Code Online (Sandbox Code Playgroud)

huy*_*hjl 6

Jon,nm,谢谢你的回答.根据你的评论,我想我会尝试使用蹦床.一些研究表明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
} 
Run Code Online (Sandbox Code Playgroud)

我有兴趣看到这与没有蹦床的解决方案以及默认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)(_ + _)))
Run Code Online (Sandbox Code Playgroud)

时间是:

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
Run Code Online (Sandbox Code Playgroud)

基于此,似乎蹦床实际上产生了相当不错的表现.我怀疑拳击/拆箱之上的惩罚相对并不那么糟糕.

编辑:根据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)(_ + _)))
Run Code Online (Sandbox Code Playgroud)

收益率:

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
Run Code Online (Sandbox Code Playgroud)

所以foldLeft2.reverse就是赢家......


n. *_* m. 4

问题在于连续函数(racc => k(f(x, racc)))本身。它应该针对整个业务的工作进行尾调用优化,但事实并非如此。

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