将Scala continuations与while循环一起使用
我意识到这与通常意义上的SO问题背道而驰,但是下面的代码可以工作,尽管我认为它不应该工作。下面是一个小型Scala程序,它使用带while循环的continuations。根据我对延续传递样式的理解,这段代码应该通过为while循环的每次迭代向堆栈添加一个帧来产生堆栈溢出错误。然而,它工作得很好将Scala continuations与while循环一起使用,scala,continuations,delimited-continuations,Scala,Continuations,Delimited Continuations,我意识到这与通常意义上的SO问题背道而驰,但是下面的代码可以工作,尽管我认为它不应该工作。下面是一个小型Scala程序,它使用带while循环的continuations。根据我对延续传递样式的理解,这段代码应该通过为while循环的每次迭代向堆栈添加一个帧来产生堆栈溢出错误。然而,它工作得很好 import util.continuations.{shift, reset} class InfiniteCounter extends Iterator[Int] { var coun
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())
}
}
我的问题是:为什么没有堆栈溢出?Scala编译器是在做尾部调用优化(我认为它不能用continuations)还是在做其他事情
(这个实验是在github上进行的,同时还有运行它所需的sbt配置:。请参阅提交7CEC9BEFC58820B925BB22BC25F2A48CBEC4A6)这里没有出现堆栈溢出的原因,因为您使用的
shift
和回调()
的方式就像一个函数
每次执行线程到达shift
构造时,它都将回调设置为等于当前延续(闭包),然后立即将单元
返回到调用上下文。当调用next()
并调用callback()
时,执行continuation闭包,它只执行count+=1
,然后跳回到循环的开头并再次执行shift
CPS转换的一个关键好处是它在延续中捕获控制流,而不是使用堆栈。当您在每次“迭代”中设置callback=f
时,您将覆盖对函数上一个继续/状态的唯一引用,这将允许对其进行垃圾收集
这里的堆栈深度只有几帧(由于所有嵌套闭包的缘故,它可能在10帧左右)。每次执行shift
时,它都会捕获闭包(在堆中)中的当前状态,然后堆栈会展开回您的for
表达式
我觉得一个图表可以让这一点更清楚,但是使用调试器单步执行代码可能也同样有用。我认为这里的关键点是,既然你基本上已经建造了一个蹦床,你就永远不会把事情搞砸。我认为这是一个很好的解释,可以解释一些非常令人困惑的事情。我可能会尝试用视觉的方式来描绘这个,如果我创建了一些说明性的东西,我会在这里发布一个链接。澄清一下,这意味着这种构造不仅不会破坏堆栈,而且总内存需求也是适度的,并不取决于“迭代次数”@jcrudy-是的,内存需求独立于for
表达式中的迭代次数。作为一个简单的测试,我运行了如下代码:JAVA_OPTS=“-Xmx2M-verbose:gc”scala-Experiment3
(这在我的笔记本电脑上运行)。这证明了100M迭代只需要2MB的堆空间。添加详细垃圾收集选项还可以让您看到,在整个执行过程中,实际内存使用量几乎保持不变
1
Hello
2
100000003