CPS,控制上下文与四十行代码的传说
光剑系列的第七作! 前六作: 第一篇:Let’s build our mathematics by using lambda calculus && church encoding! 第二篇:惰性求值、无穷流与发生的魔法 第三篇:协程、生成器与 call/cc 的控制流 第四篇:动态作用域、词法作用域与表达式求值的环境模型 第五篇:基于环境模型的解释器 第六篇:调用栈、de bruijn 索引与堆栈的内存模型 朱约(juyo)/瓦帕德(vaapad)是光剑七式的最后一技。很高兴我们的“光剑”终于抵达了这里!不过也许会有续集。 [此处应有温杜的图片] 一点说明 我更新了工具,本文使用 racket(scheme 的一种实现和变体)完成。这些代码大多可以直接挪用在 scheme 中,但是 match 除外,请使用自己的模式匹配库。我也转载了一个简单的模式匹配库:https://www.luogu.com.cn/article/4kw6oewn 注意使用 #lang racket。 发现有不少 racket 的在线环境,懒得下 racket 的话可以直接用。给一个:https://onecompiler.com/racket 引子 在前作中,我们提到过“call/cc”也即“call-with-current-continuation”的存在。它可以捕获当前的“续延”(coutinuation)并将它作为一个一等公民值。 续延代表程序的“计算上下文”,也即“我们接下来要做什么?”调用一个续延,可以让我们瞬间跳回续延被捕获的那个时间点,还原环境(包括堆和栈,不过那是具体的内存模型。环境是更抽象的“数据上下文”。)以及更重要的,“计算”。 看上去真是神秘又强大(事实上,从 call/cc 和条件判断,我们可以造出其余的所有控制流。)。我们的第五作实现了一个 scheme 子集的解释器,但是也没有实现这个功能。 那么,这个操作是如何实现的呢?“剩下的计算”是如何被表示的呢?这就是我们今天要探讨的话题。 CPS 变换 一个例子 先看下面这段代码: 1 2 3 4 5 6 (define (fact-cps n k) (if (= n 0) (k 1) (fact-cps (- n 1) (lambda (v) (k (* v n)))))) 从名字就可以看出,这是一个计算阶乘的函数。不过它看上去非常的不同寻常。 那个叫做“k”的参数是什么东西?它在干什么? 事实上,这个“k”就是一个外部传入的续延。它代表“当前函数执行完毕后,还要进行哪些操作”。 现在让我们来看看代码。条件判断很寻常。 ...