Continuation-passing style
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的更多相关文章
- 控制结构(11): Continuation passing style(CPS)
// 上一篇:控制结构(10)指令序列(opcode) [注释]: 这个笔记系列需要告一个段落了,收尾部分整理下几个时髦(The New Old Things)结构. 后面打算开一个算法方面的,重新学 ...
- 尾递归(Tail Recursion)和Continuation
递归: 就是函数调用自己. func() { foo(); func(); bar(); } 尾调用:就是在函数的最后,调用函数(包括自己). foo(){ return bar(); } 尾递归:就 ...
- scheme Continuation
Continuation Pass Style在函数式编程(FP)中有一种被称为Continuation Passing Style(CPS)的风格.在这种风格的背后所蕴含的思想就是将处理中可变的一部 ...
- 简单易懂的程序语言入门小册子(6):基于文本替换的解释器,引入continuation
当我写到这里的时候,我自己都吃了一惊. 环境.存储这些比较让人耳熟的还没讲到,continuation先出来了. 维基百科里对continuation的翻译是“延续性”. 这翻译看着总有些违和感而且那 ...
- 尾递归与Continuation(转载)
递归与尾递归 关于递归操作,相信大家都已经不陌生.简单地说,一个函数直接或间接地调用自身,是为直接或间接递归.例如,我们可以使用递归来计算一个单向链表的长度: public class Node { ...
- 尾递归与Continuation
怎样在不消除递归的情况下防止栈溢出?(无论如何都要使用递归) 这几天恰好和朋友谈起了递归,忽然发现不少朋友对于“尾递归”的概念比较模糊,网上搜索一番也没有发现讲解地完整详细的资料,于是写了这么一篇文章 ...
- 栈编程和函数控制流: 从 continuation 与 CPS 讲到 call/cc 与协程
原标题:尾递归优化 快速排序优化 CPS 变换 call/cc setjmp/longjmp coroutine 协程 栈编程和控制流 讲解 本文为部分函数式编程的扩展及最近接触编程语言控制流的学习和 ...
- 如何设计一门语言(七)——闭包、lambda和interface
人们都很喜欢讨论闭包这个概念.其实这个概念对于写代码来讲一点用都没有,写代码只需要掌握好lambda表达式和class+interface的语义就行了.基本上只有在写编译器和虚拟机的时候才需要管什么是 ...
- 探索c#之递归APS和CPS
接上篇探索c#之尾递归编译器优化 累加器传递模式(APS) CPS函数 CPS变换 CPS尾递归 总结 累加器传递模式(Accumulator passing style) 尾递归优化在于使堆栈可以不 ...
随机推荐
- php删除文件夹和其下的内容
原文地址:http://www.cnblogs.com/xiaochaohuashengmi/archive/2011/05/13/2045158.html <?php function del ...
- Linux系统扫描技术及安全防范
1.概述 一.主机扫描 二.路由扫描 三.批量服务扫描 四.linux防范恶意扫描安全策略 一个典型的网络安全事件 ·案例:通过网络扫描方式获取某运营商核心设备管理权限 step01:通过tracer ...
- Oracle中Error while performing database login with the XXXdriver; Listener refused the connection with the following error; ORA-12505,TNS:listener does not currently know of SID given inconnect descrip
一次连接数据库怎么也连接不上,查了多方面资料,终于找到答案,总结 首先应该保证数据库的服务启动 在myeclipse的数据库视图中点 右键->new 弹出database driver的窗口, ...
- 【H5】移动端页面根font-size设置
h5rem.js (配置写法①) (function (doc, win) { var docEl = doc.documentElement, resizeEvt = 'orientationc ...
- Android WebView漏洞(转)
一.漏洞描述 近期,微信等多款安卓流行应用曝出高危挂马漏洞:只要点击好友消息或朋友圈中的一条网址,手机就会自动执行黑客指令,出现被安装恶意扣费软件.向好友 发送欺诈短信.通讯录和短信被窃取等严重后果. ...
- Luogu P4322 [JSOI2016]最佳团体
JZdalao昨天上课讲的题目,话说JSOI的题目是真的不难,ZJOI的题目真的是虐啊! 题意很简单,抽象一下就是:有一棵树,一次只能选从根到某个节点上的链上的所有点,问从中取出k个节点所得到的总价值 ...
- mac终端将本地代码push到github总结
1.创建一个github账号 2.在本地目录下创建一个本地仓库,用来存放代码 mkdir prepass_repository (/Users/gejuncheng/文件/prepass_reposi ...
- L017-linux系统定时任务crond入门小节
L017-linux系统定时任务crond入门小节 oh my god!how old are you? 怎么老是你?没错,我又来了,哈哈哈,今天是我的生日呢,在这么重要的日子里,必须要更一篇学习小节 ...
- ECMAScript6——Set数据结构
/** * 数据结构 Set */ // ----------------------------------------------------- /** * 集合的基本概念:集合是由一组无序且唯一 ...
- C#_添加xml文件
引用:System.Xml; XmlDocument doc = new XmlDocument(); XmlElement Root = doc.CreateElement("Root&q ...