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. linux系统下php通过php_oci8扩展连接oracle数据库 Nginx

    相关版本信息: PHP Version 5.6.30 nginx version: nginx/1.10.3 Linux version 2.6.32-358.el6.x86_64 (mockbuil ...

  2. jquery操作checked

    jquery操作checkbox,如何获取勾选状态?如何使得勾选?如何取消勾选? 来段代码就知道了: <html> <head> <meta charset=" ...

  3. Regionserver启动后又关闭

    今天启动hbase shell,输入hbase命令时报错: ERROR [regionserver/regionserver1/172.18.0.61:16020] reggionserver.HRe ...

  4. 大数据入门第二十天——scala入门(二)scala基础01

    一.基础语法 1.变量类型 // 上表中列出的数据类型都是对象,也就是说scala没有java中的原生类型.在scala是可以对数字等基础类型调用方法的. 2.变量声明——能用val的尽量使用val! ...

  5. EZ 2018 05 13 NOIP2018 模拟赛(十三)

    这次的比赛真心水,考时估分240,然后各种悠闲乱逛 然后测完T1数组开小了炸成40,T2,T3都没开long long,T2炸成20,T3爆0 掉回1600+的深渊,但是还有CJJ dalao比我更惨 ...

  6. 重新解读DDD领域驱动设计(一)

    回顾 十年前,还未踏入某校时,便听闻某学长一毕业就入职北京某公司,月薪过万.对于一个名不见经传的小学院,一毕业能拿到这个薪水还是非常厉害的.听闻他学生期间参与开发了一款股票软件,股票那时正迎来一波疯涨 ...

  7. Flume的简单理解

    由于没具体研究过画图,以前在公司每天都用Excel,所以很多图画都是画在了Excel上再剪切的,看着可能不太舒服. 先来看一下数据走向: 这样我们就大致了解了flume是干嘛的,在什么位置了. Flu ...

  8. java Script复习总结

    一:基础知识 1.JavaScript语言的历史 l  早期名称:livescript l  开发公司:网景公司(netscape) 2.JavaScript语言的基本特点 l  基于对象 l  事件 ...

  9. 自动化部署-Jenkins+SVN+MSBuild

    这篇文章主要介绍下使用Jenkins实现自动化部署 下载 https://jenkins.io/download/ 安装 按步骤安装即可,下载的是windows版本,安装完成后,会看到这样一个正在运行 ...

  10. Web项目开发流程 PC端

      一.了解.明确需求. 这个应该是第一步了,不了解需求你就不知道为什么要做,要怎么去做这个项目的工作. (1)明确需求是相当重要的,很有必要去和产品经理.设计人员去沟通,需要明白每一个按钮,每一个开 ...