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 《vmware下克隆的centos无法配置固定ip》

    1.用vmware克隆一个centos 2.进入centos,打开命令行输入ifconfig,运行后发现没有eth0 3.运行网卡启动命令ifconfig eth0 up,再运行ifconfig wa ...

  2. 2.3《想成为黑客,不知道这些命令行可不行》(Learn Enough Command Line to Be Dangerous)——重命名,复制,删除

    最常用的文件操作除了将文件列出来外,就应该是重命名,复制,删除了.正如将文件列出来一样,大多数现代操作系统为这些任务提供了用户图形界面,但是在许多场景中,用命令行还是会更方便. 使用mv命令重命名一个 ...

  3. 笔记:载入viewcontroller的几种方式

    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil]; ...

  4. iOS Swift WisdomScanKit图片浏览器功能SDK

    iOS Swift WisdomScanKit图片浏览器功能SDK使用 一:简介      WisdomScanKit 由 Swift4.2版编写,完全兼容OC项目调用. WisdomScanKit的 ...

  5. 大数据入门第二十天——scala入门(一)入门与配置

    一.概述 1.什么是scala  Scala是一种多范式的编程语言,其设计的初衷是要集成面向对象编程和函数式编程的各种特性.Scala运行于Java平台(Java虚拟机),并兼容现有的Java程序. ...

  6. PySide图形界面开发(一)

    一.为什么要使用PySide? PySide由Qt的官方团队--Nokia Qt进行维护,集成了Qt和Python的优势.一个PySide程序员只需要使用简单的Python语言就能够发挥Qt的所有功能 ...

  7. Hadoop日记Day13---使用hadoop自定义类型处理手机上网日志

    测试数据的下载地址为:http://pan.baidu.com/s/1gdgSn6r 一.文件分析 首先可以用文本编辑器打开一个HTTP_20130313143750.dat的二进制文件,这个文件的内 ...

  8. springmvc 结合 自动封装异常信息输出为json 报错 500内部服务器错误的原因

    补充:还有一个原因是因为spring的对象没有被成功注入,例如 mapper没有被成功注入,抛出异常时在这种封装场景下将会抛出 500 服务器内部错误, 这种情况下要排查还是靠debug然后看看到底是 ...

  9. linux中wget的使用方法介绍

    wget是在Linux下开发的开放源代码的软件,作者是Hrvoje Niksic,后来被移植到包括Windows在内的各个平台上.它有以下功能和特点:(1)支持断点下传功能:这一点,也是网络蚂蚁和Fl ...

  10. 【Android UI设计与开发】第01期:引导界面(一)ViewPager介绍和使用详解

    做Android开发加起来差不多也有一年多的时间了,总是想写点自己在开发中的心得体会与大家一起交流分享.共同进步,刚开始写也不知该如何下手,仔细想了一下,既然是刚开始写,那就从一个软件给人最直观的感受 ...