f# - CPS转换时如何避免堆栈溢出?
问题描述
我正在编写从 Scheme 子集到 CPS 语言的转换。它在 F# 中实现。在大输入程序中,转换因堆栈溢出而失败。
我正在使用论文Compiling with Continuations中描述的某种算法。我试图将工作线程的最大堆栈大小增加到 50 MB,然后它就可以工作了。
也许有一些方法可以修改算法,这样我就不需要调整堆栈大小?
例如,算法变换
(foo (bar 1) (bar 2))
到
(let ((c1 (cont (r1)
(let ((c2 (cont (r2)
(foo halt r1 r2))))
(bar c2 2)))))
(bar c1 1))
wherehalt
是完成程序的最终延续。
解决方案
也许您的实际问题有简单的解决方案来避免大量堆栈消耗,所以请不要介意添加细节。但是,如果没有更多关于您的特定代码的知识,这里有一个通用的方法来减少递归程序中的堆栈消耗,基于蹦床和延续。
沃克
这是一个典型的递归函数,它不是普通的尾递归,用 Common Lisp 编写,因为我不知道 F#:
(defun walk (form transform join)
(typecase form
(cons (funcall join
(walk (car form) transform join)
(walk (cdr form) transform join)))
(t (funcall transform form))))
然而,代码非常简单,希望它会遍历由 cons 单元组成的树:
- 如果表单是一个 cons-cell,递归地走上汽车(resp. cdr)并加入结果
- 否则,对值应用变换
例如:
(walk '(a (b c d) 3 2 (a 2 1) 0)
(lambda (u) (and (numberp u) u))
(lambda (a b) (if a (cons a b) (or a b))))
=> (3 2 (2 1) 0)
代码遍历表单,仅保留数字,但保留(非空)嵌套。
使用上面的示例调用trace
显示walk
了 8 个嵌套调用的最大深度。
延续和蹦床
这是一个改编版本,称为
walk/then
,它像以前一样遍历表单,当结果可用时,调用then
它。这then
是一个延续。
该函数还返回一个thunk,即无参数闭包。发生的情况是,当我们返回闭包时,堆栈被展开,当我们应用thunk时,它将从一个新的堆栈开始,但在计算中已经推进(我通常想象有人走上一个下降的自动扶梯)。我们返回一个 thunk 以减少堆栈帧数的事实是蹦床的一部分。
该then
函数取一个值,即当前walk最终将返回的结果。结果因此被向下传递到堆栈中,并且在每一步返回的是一个 thunk 函数。
嵌套延续允许通过在嵌套延续中推动计算的剩余部分来捕获transform
/的复杂行为。join
(defun walk/then (form transform join then)
(typecase form
(cons (lambda ()
(walk/then (car form) transform join
(lambda (v)
(walk/then (cdr form) transform join
(lambda (w)
(funcall then (funcall join v w))))))))
(t (funcall then (funcall transform form)))))
例如,(walk/then (car form) transform join (lambda (v) ...))
读作如下:带着参数和走形式的汽车,并最终调用结果;即,沿着 cdr 走,然后加入两个结果;最终,在连接的结果上transform
join
(lambda (v) ...)
then
调用输入。
缺少的是一种不断调用返回的 thunk 直到用尽的方法;这是一个循环,但这很容易成为一个尾递归函数:
(loop for res =
(walk/then '(a (b c d) 3 2 (a 2 1) 0)
(lambda (u) (and (numberp u) u))
(lambda (a b) (if a (cons a b) (or a b)))
#'identity)
then (typecase res (function (funcall res)) (t res))
while (functionp res)
finally (return res))
以上返回(3 2 (2 1) 0)
,并且在追踪时追踪的深度永远不会超过2 walk/then
。
请参阅Eli Bendersky 的文章以了解 Python 中的另一篇文章。
推荐阅读
- vue.js - OKta + Vue :当从按钮调用或从 vue 页面创建事件调用时,Okta 注销代码的行为不同
- flutter - 未定义命名参数“形状”和“颜色”
- freeradius - 使用 NAS-Port 属性执行认证
- html - 使用 Vue 3 的 Vue 路由器引发 17 条警告,并且应用程序文件的内容不显示
- math - 如何为我的 CNN 计算数学公式?
- java - 遍历一个 Json 对象并将一些值存储在一个数组中
- postgresql - 当我只使用一个客户端时,为什么 Postgres 会抱怨“FATAL #53300 sorry, too many clients already”?
- java - Bean 只是 Last monoids 的产物还是我遗漏了什么?
- delphi - 如何自定义 Hint Font.Size?
- spring-kafka - Spring Cloud Stream + Kafka enable.auto.commit