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) 尾递归优化在于使堆栈可以不 ...
随机推荐
- Mapnik初学笔记
前言:夏天总是感觉想要睡觉,一心想颓废的我却要一周六天都要处于工作状态,但有些事虽然麻烦,但还是要去做,不由得想起火影忍者里面鹿丸这一个角色,有时候真能理解他的心理状态,或许我应该向他学习:善于思考的 ...
- jQueryMobile的按钮样式
好吧,已经学了jQueryMobile一年了,今天心血来潮,想要写一篇关于jQueryMobile的博客文章,记得去年暑假在公司实习jQueryMobile,想一想真是怀念当时还是菜鸟的自己,年轻就是 ...
- GAN初步——本质上就是在做优化,对于生成器传给辨别器的生成图片,生成器希望辨别器打上标签 1,体现在loss上!
from:https://www.sohu.com/a/159976204_717210 GAN 从 2014 年诞生以来发展的是相当火热,比较著名的 GAN 的应用有 Pix2Pix.CycleGA ...
- 巧用ios朗读kindle图书
想必大家都有想过kindle出中文的有声阅读刊物吧? 今天突发奇想想到一招能够让我们听自己拿kindle买的中文图书.当然这是有条件的. 前提是你得有一个ios设备,不管是iphone还是ipad,i ...
- 一、Django前后端交互之Ajax和跨域问题
一.Ajax介绍 1.概述 AJAX 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术.AJAX(Asynchronous Javascript And XML)翻译成中文就是“异步Jav ...
- UWP简单示例(一):快速合成音乐MV
说明 本文发布时间较早,内容可能已过时.最新动态请关注 TypeScript 版本.(2019 年 3 月 注) 在线演示: 音频可视化(TypeScript) 准备 IDE:Visual Studi ...
- Asp.Net_Ajax调用WebService返回Json前台获取循环解析
利用JQuery的$.ajax()可以很方便的调用 asp.net的后台方法.但往往从后台返回的json字符串不能够正确解析,究其原因,是因为没有对返回的json数据做进一步的加工.其实,这里只需 要 ...
- Oracle中Clob类型处理解析 (转)
转:原文:http://blog.csdn.net/pojianbing/article/details/2789426 最近利用NHibernate映射类型为Clob字段在插入数据时发现当 ...
- 金蝶K3 11.0 WISE版本盘点机PDA条码数据采集器仓库条码管理
- Vigenere加密
Vigenere加密法原理很简单,实现起来也不难.与普通的单码加密法不同,明文经过加密之后,每个字母出现的频率就不会有高峰和低峰. 密钥中字母代表行和明文中的字母代表行.在vigenere表中找到对应 ...