我意识到这与通常的SO问题相反,但是即使我认为它不起作用,下面的代码也能正常工作。下面是一个小型Scala程序,它使用while循环的continuation。根据我对连续传递样式的理解,这段代码应该通过在while循环的每次迭代中向堆栈添加一个帧来产生堆栈溢出错误。但是,它的工作正常。
import util.continuations.{shift, reset}
class InfiniteCounter extends Iterator[Int] {
var count = 0
var callback: Unit=>Unit = null
reset {
while (true) {
shift {f: (Unit=>Unit) =>
callback = f
}
count += 1
}
}
def hasNext: Boolean = true
def next(): Int = {
callback()
count
}
}
object Experiment3 {
def main(args: Array[String]) {
val counter = new InfiniteCounter()
println(counter.next())
println("Hello")
println(counter.next())
for (i <- 0 until 100000000) {
counter.next()
}
println(counter.next())
}
}
输出结果为:
1
Hello
2
100000003
我的问题是:为什么没有堆栈溢出? Scala编译器是否正在进行尾部调用优化(我认为它不能用延续)或是否还有其他事情发生?
(此实验在github上以及运行它所需的sbt配置:https://github.com/jcrudy/scala-continuation-experiments。请参阅commit 7cec9befcf58820b925bb222bc25f2a48cbec4a6)
答案 0 :(得分:7)
由于您使用shift
和callback()
的方式就像 trampoline ,因此您没有在此处获得堆栈溢出的原因。
每次执行线程到达shift
构造时,它将callback
设置为等于当前的continuation(闭包),然后立即将Unit
返回到调用上下文。当您调用next()
并调用callback()
时,执行延续闭包,它只执行count += 1
,然后跳回循环的开头并再次执行shift
。
CPS转换的一个主要好处是它在延续中捕获控制流而不是使用堆栈。当你在每个“迭代”上设置callback = f
时,你将覆盖对函数的前一个继续/状态的唯一引用,并允许它被垃圾收集。
此处的堆栈只能达到几帧的深度(由于所有嵌套的闭包,它可能大约为10帧)。每次执行shift
时,它都会捕获闭包中的当前状态(在堆中),然后堆栈将展开回到for
表达式。
我觉得图表会让这个更清晰 - 但是使用调试器逐步执行代码可能会同样有用。我认为这里的关键点是,因为你基本上建造了一个蹦床,所以你永远不会砸到堆叠。