Continuation-passing style

参考书籍:

EOPL (  Essentials of Programming Languages, 3rd Edition )

作者:知乎用户
链接:https://www.zhihu.com/question/20259086/answer/141162748
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

要理解CPS,首先要理解 Continuation 是什么。

计算是有先后顺序的,比如要在 Racket 里计算1+2+3:

(+ 1 (+ 2 3))

显然需要首先计算 (+ 2 3),再计算 (+ 1 5),在得到 (+ 2 3) 的结果之前是无法计算 (+ 1 ...) 的。

我们把 (+ 1 ...) 这个表达式中缺少的部分叫做 hole,一个带有 hole 的表达式就是 continuation,它需要另一个东西来填补这个 hole 才能进行进一步的计算(这个其实就是 Evaluation Contexts 的概念)。

Continuation 是链式的,一个 continuation 还链接着另一个 continuation。可以把 continuation 表示为 <e_w_hole, cont> 构成的二元组,在完成当前的 e_w_hole 的求值后,其结果会被填补给下一个 cont 的 hole,直到 cont 是 halt 停机为止。比如

(+ 1 (+ 2 (+ 3 4)))

(+ 3 4)的 continuation 是 <(+ 2 ...), cont1>,而 cont1 则是 <(+ 1 ...), halt>。我们也可以把所有的 continuation 内联在一起得到完整的 continuation(或叫 evaluation context):<(+ 2 ...), <(+ 1 ...), halt>>。

那么 CPS 作为一种编程方法就是人为地把 continuation 作为一个高阶函数显式地暴露出来,这个函数的参数就是hole,当我们apply这个 continuation(函数)就是在填补这个hole,并进行后续的计算。

上面的代码很容易转换为CPS(虽然加法操作是 primitive 的,但我们仍然可以自己编写一个CPS版本的 k+ 来模拟,同时我们还有一个 identity continuation 作为halt):

(define k+ (lambda (x y k) (k (+ x y))))
(define id (lambda (x) x))
(k+ 2 3 (lambda (five) (k+ 1 five id)))

至于 call/cc 其实并不属于 CPS 的一部分,call/cc 是语言提供给程序员用以获得当前 continuation 的机制。在语言实现层面为了支持 call/cc 操作可以首先将程序进行CPS变换;或将解释器写为 CPS 形式。

上面的 (+ 1 (+ 2 3)) 例子也可以用 call/cc 来实现:

(+ 1 (call/cc (lambda (k) (k (+ 2 3)))))

这时候 k 便代表 (+ 2 3) 的 continuation,也就是 (+ 1 ...)。通过获取当前的 continuation,实际上获得了『此刻』以后所有的计算过程,于是便可以做一些有意思的事情,比如实现 non-deterministic 的 amb 操作符、线程和 coroutine 等。

因为最少形式的纯 CPS 的程序只需要有 lambda 和 function application,因此 CPS 程序中所有的递归函数调用都是尾递归。

比如正常 map 函数可以这样实现:

(define (map f xs)
(if (empty? xs) '()
(cons (f (first xs)) (map f (rest xs)))))
(map (λ (x) (+ x 1)) '(1 2 3))

但这个实现并不是尾递归,因为第3行在 (rest xs) 上调用完 map 之后我们仍然有continuation 要做,也就是将 (f (first xs)) 的结果 cons 起来。

而 CPS 版的 map 则是:

(define (map-k f xs k)
(if (empty? xs) (k '())
(f (first xs) (λ (v) (map-k f (rest xs)
(λ (rest-v) (k (cons v rest-v))))))))
(map-k (λ (x k) (k (+ x 1))) '(1 2 3) (λ (x) x))

可以看到,在 map-k 中,所有的函数调用都是尾递归,也就不存在由递归引起的 stack 空间的消耗(在支持tail-call 优化的语言中)。

Continuation 作为程序语言研究中的一个基础概念,历史上被很多人以不同的形式反复发现,例如 SECD Machine 的 J Operator、goto、escape、Monad 等等。John Reynolds 的 The Discoveries of Continuations 和 Olivier Danvy 为 Peter Landin 写的纪念文都是非常好的阅读材料。

在 CPS 中,continuation 被表示为一个高阶函数,那么这个函数本身也可以有其 continuation(被称作 meta continuation)。泛化这个想法,我们便得到了一个可以有 n 级 continuations 的 CPS Hierachy。这种风格也被称作Extended Continuation-Passing Style,ECPS 可以用于方便地实现delimited control operators 比如 shift 和 reset。请参考 Abstracting Control。

(E)CPS 同 Monad 是“等价”的,理论上任何 Monad 都可以通过等价的 CPS 形式表达(或 shift/reset)出来,这部分可以看 The Essence of Functional Programming 和 Representing Monads。

CPS 在在过去是函数式语言编译器中常用的IR,在编译和程序分析中有很多应用。当程序被转换为 CPS 的时候,Continuation 是直接在 lexical scope 中暴露出来的,而全部的 control flow 转移都是通过调用 continuation 来实现,这样可以直接进行control flow analysis。在进行partial evaluation 的时候 CPS 变换后也可以获得更好的特化效果。

但是 CPS 的可读性太差了,后来 direct style 的 A-Normal Form 在编译和程序分析中流行起来。而 ANF 和 CPS 是等价的,A-Normalize 的过程等价于 CPS convert->Beta normalize->un-CPS convert,请参考 The Essence of Compiling with Continuation。

CPS 同 Static Single Assignment 也是同构的,在 CPS 中每个变量都通过 lambda 来引入,变量的 mutation 也是通过新的 continuation 来引入;正对应于SSA中每个变量只被赋值一次,并 dominate 接下来的 use,而 Phi node 则在 CPS 中通过对于同一个 cont 传入不同的值来实现。可参考 A Correspondence between Continuation Passing Style and Static Single Assignment Form。

==================== End

Continuation-passing style的更多相关文章

  1. 控制结构(11): Continuation passing style(CPS)

    // 上一篇:控制结构(10)指令序列(opcode) [注释]: 这个笔记系列需要告一个段落了,收尾部分整理下几个时髦(The New Old Things)结构. 后面打算开一个算法方面的,重新学 ...

  2. 尾递归(Tail Recursion)和Continuation

    递归: 就是函数调用自己. func() { foo(); func(); bar(); } 尾调用:就是在函数的最后,调用函数(包括自己). foo(){ return bar(); } 尾递归:就 ...

  3. scheme Continuation

    Continuation Pass Style在函数式编程(FP)中有一种被称为Continuation Passing Style(CPS)的风格.在这种风格的背后所蕴含的思想就是将处理中可变的一部 ...

  4. 简单易懂的程序语言入门小册子(6):基于文本替换的解释器,引入continuation

    当我写到这里的时候,我自己都吃了一惊. 环境.存储这些比较让人耳熟的还没讲到,continuation先出来了. 维基百科里对continuation的翻译是“延续性”. 这翻译看着总有些违和感而且那 ...

  5. 尾递归与Continuation(转载)

    递归与尾递归 关于递归操作,相信大家都已经不陌生.简单地说,一个函数直接或间接地调用自身,是为直接或间接递归.例如,我们可以使用递归来计算一个单向链表的长度: public class Node { ...

  6. 尾递归与Continuation

    怎样在不消除递归的情况下防止栈溢出?(无论如何都要使用递归) 这几天恰好和朋友谈起了递归,忽然发现不少朋友对于“尾递归”的概念比较模糊,网上搜索一番也没有发现讲解地完整详细的资料,于是写了这么一篇文章 ...

  7. 栈编程和函数控制流: 从 continuation 与 CPS 讲到 call/cc 与协程

    原标题:尾递归优化 快速排序优化 CPS 变换 call/cc setjmp/longjmp coroutine 协程 栈编程和控制流 讲解 本文为部分函数式编程的扩展及最近接触编程语言控制流的学习和 ...

  8. 如何设计一门语言(七)——闭包、lambda和interface

    人们都很喜欢讨论闭包这个概念.其实这个概念对于写代码来讲一点用都没有,写代码只需要掌握好lambda表达式和class+interface的语义就行了.基本上只有在写编译器和虚拟机的时候才需要管什么是 ...

  9. 探索c#之递归APS和CPS

    接上篇探索c#之尾递归编译器优化 累加器传递模式(APS) CPS函数 CPS变换 CPS尾递归 总结 累加器传递模式(Accumulator passing style) 尾递归优化在于使堆栈可以不 ...

随机推荐

  1. day75

    昨日回顾:  1 inclusion_tag   -干什么用的?生成html的片段(动态,传参数,传数据)   -app下新建一个模块,templatetags   -创建一个py文件(mytag.p ...

  2. jqgrid 选中行触发编辑,切换下一行时验证和异步保存上一行数据

    有时,我们需要批量修改或填写一些相似的数据.可以以jqgrid表来显示,可能的效果如下: 选中触发行编辑参考:jqgrid 单击行启用行编辑,切换行保存原编辑行 本文主要说说验证和异步保存上一条数据的 ...

  3. handsontable 拖动末尾列至前面列位置,被拖动列消失的问题

    问题描述:将最后一列在往前面列位置进行拖动后,被拖动的最后列消失掉了. 解决办法:在handsontabel绑定中去设置data值,取消通过 loadData 绑定data $("#topF ...

  4. 保存网格(mesh)到磁盘上

    Unity提供了很方便的工具来保存mesh之类的,下面的代码挂在GameObject上后,按下F键能把mesh(该GameObject必须有mesh组件)保存到磁盘的Assets目录下.在磁盘上是.a ...

  5. hung task机制

    最近在修改内核源码的时候一直出现格式化磁盘的时候,进程会出现状态D,看内核日志会看到如下信息: INFO: task filebench: blocked seconds. Oct :: localh ...

  6. springboot 定制错误页面

    项目中经常遇到的异常情况 400-Bad Request 401-Unauthorized If the request already included Authorization credenti ...

  7. ABAP error:CONVT_NO_NUMBER

    今天写了个接口,传入数据到SAP,结果接收后在报表展示时直接报错. 检查后发现数据转换出错,接收到的数据格式混乱. 最后检查了所有地方发现,源系统传入的数据长度为9个字节,但是自己的接收程序,定义数据 ...

  8. 2017-2018 Exp4 恶意代码分析 20155214曾士轩

    目录 Exp4 恶意代码分析 实验内容 使用schtasks指令监控系统运行 主要思路 知识点 启发 Exp4 恶意代码分析 本次实验操使用Windows下的schtasks,sysmon,Proce ...

  9. 20155302《网络对抗》Exp7 网络欺诈防范

    20155302<网络对抗>Exp7 网络欺诈防范 实验内容 (1)简单应用SET工具建立冒名网站 (1分) (2)ettercap DNS spoof (1分) (3)结合应用两种技术, ...

  10. NetWork——描述一次完整的网络请求过程

    台根DNS,根DNS服务器收到请求后会返回负责这个域名(.net)的服务器的一个IP,本地DNS服务器使用该IP信息联系负责.net域的这台服务器.这台负责.net域的服务器收到请求后,如果自己无法解 ...