Continuation-passing Style介绍及应用
Table of Contents1 什么是Continuation-passing Style:2 与 continuation 的区别:3 CPS在编译器中的应用4 CPS在异步编程中的应用:
Continuation-passing style(CPS)是指将控制流(Control flow)1显式的当做参数传递的编程风格。 函数的返回不在通过return语句,而是将返回值当做参数,调用控制流。
我们先来看一些例子,
普通的js函数我们这样定义:
function foo(x){ return x;}CPS风格的函数都会有一个额外的参数k,显式的表示了continuation(可以理解成控制流的流向,what comes next)。当CPS函数需要返回的时候,调用k,并将返回值作为k的参数。
function foo(x, k){ k(x); //注意:调用k(x)相当于“return”,所以这之后的语句都不会被执行了。 assert(false, "should not be here");}另一个嵌套的例子:
function goo(x){ function foo(y) { return y + 100; } return foo(x+100);}console.log(goo(100));CPS代码:
function goo(x, k){ function foo(y, ky) { y += 100; ky(y); } foo(x+100, function(z) { k(z); });}goo(100, function(x) { console.log(x); });CPS程序与普通程序相比有如下明显的不同:
CPS最初主要用于编译高级语言时一种中间代码表示,有了这种中间代码,编译器的复杂度大大降低。现在发现CPS在异步编程里,分布式编程里大显身手。
CPS风格代码也有自己的缺点:
CPS代码基本没法阅读和理解,看上面递归的例子就知道有多晦涩。CPS代码不断的嵌套调用,在不支持尾递归的语言中,可能出现栈溢出。continuaion 是对控制流2的一种抽象表示。此时控制流是一等公民,我们不用写CPS代码,但是在程序运行的时候,我们可以获取 控制流。
(call/cc (lambda (k) (displayln "before call k") (k "I'm back") ;调用k,控制流直接出去,后面的语句不会被调用掉。 (displayln "after call k")))(displayln "I'm the point of control flow k") ;调用k,控制流到了这里。 我们可以保存k,在以后任何时候调用,控制流还是会到这里。
continuation这个数据表示了程序执行过程中的某一个点。就好像程序断点一样,但是continuation这种”断点“是一个数据,我们可以保存他,可以在任意时刻恢复他。
将程序的控制流作为数据暴露给程序员,并且可以任意操作他们,此时控制流成为了first class value。通过对控制流的操作,我们可以方便的实现多线程,coroutine,generator等。
如果语言支持continuation,我们可以像写同步程序一样写异步程序。这一点可以看这篇文章racket web serve
CPS更多表示一种编程风格。当然使用了这种编程风格,我们可以实现continuation。
前面提到,CPS风格的代码作为编译的代码的一种中间表示,使得编译器实现复杂度大大降低。
这种受限制的style不在使用一些我们经常使用的控制结构,譬如while,break,continue等,这使得编译器可以更加简单的去分析程序的语法。
但与此同时,我们也可以非常简单的实现一些非常规的控制结构,譬如try-catch(下面有例子)或者其他非局部的控制流的跳转。
有很多方法可以将非CPS代码 自动 的转换成CPS代码,但是不同的方法翻译的质量可能不一样。翻译的方法描述起来非常繁琐,强烈建议有兴趣的同学去看这本书<Compiling with continuation>.
这里我列举一些翻译好的代码:
普通没有分支结构的function foo(x){ return x + 1;}function cps_foo(x, k){ (function(k1) { k1(x+1); })(function(x1) { k(x1); });}//有赋值语句的function foo(x){ var a = 200; return x + a;}function foo(x, k){ var a; (function(k1) { a = 200; k1(); })(function() { (function(k2) { k2(a + x); })(function(x2) { k(x2); }); });}有分支结构的function foo(x){ if (x > 100) { console.log("x>100"); } else { console.log("x<100"); } return x;}function cps_foo(x, k){ (function(k1) { k1(x > 100); })(function(b) { function k2() { k(x); } if (b) (function(k3) { console.log("x>100"); k3(); })(k2); else (function(k4) { console.log("x<100"); k4(); })(k2); });}CPS实现try-catchfunction goo(x){ function foo(a, b) { if (a > b) return a - b; else throw "a must gt b"; } try{ x = foo(x, 100); } catch(ex) { console.log("parameter error"); return; } return x;}function cps_goo(x, k){ (function(a, b, k, thro) { //为了说明方便,不在将if翻译成CPS了 if (a > b) k(a-b); else thro("a must gt b"); })(function(aminusb) { x = aminusb; k(x); }, function(ex) { console.log("parameter error"); k(); });}翻译成CPS的表达式,有一种inside-out的效果function foo(a, b){ return a * (a + b) * b;}function cps_foo(a, b, k){ (function(k) { k(a + b); //a * (a + b) * b 最先被计算的在最外层 --> inside-out })(function(c) { (function(k) { k(a * c); })(function(d) { (function(k1) { k1(d * b); })(function(ret) { k(ret); }); }); });}总结转换的一些策略:可以看到,每一条语句都被包装在一个函数内。原函数内剩下的语句被包装在continuation中。最终每个函数内只做一件不能在被分割的事情(譬如+,-, *, / 或者调用系统API等)可以更多的优化,譬如上面的例子每个con的参数名都不一样,其实是没必要的,因为每个函数实际上只关心传入自身的continuation参数。更多体会:异步编程一般是指调用者(caller)不去等待一个耗时操作(callee)执行结束。譬如:img=load(url); setTimeout; UI Event等。我们一般通过ballback来获悉异步操作执行情况。 一种实现方案是耗时的操作单独作为一个线程,当操作结束的时候,在原来的线程中回调callback。java的Swing UI framework采用这种方案。
浏览器端的js是单进程单线程的运行环境,如果我们同步请求网络数据,那么必然整个浏览器都暂停下载,等待网络数据,这显然不是很好的用户体现。所以一般我们会异步的请求网络数据。
loadAsync(url, callback)
这样,我们的控制流还是继续执行下去。当网络请求结束的时候,callback将被调用。
目前火热的Nodejs最大的优点就是non-blocking programming(当然我觉得最大的优势是前端工程师可以直接上手使用)。在NodeJs中,所有原本可能阻塞的操作全部都接受一个callback,当请求完成 的时候调用callback。
这种通过callback进行异步编程的风格是不完全的CPS(Partially CPS-converting),callback可以看成continuation。
异步编程相较于多线程编程有很多优势,可以参考这边文章:Async socket。采用这种不完全的CPS相对于select-base的实现更加优雅和简单。The world of select.
缺点: 通过callback进行异步编程是很困难的,程序的局部性被打碎了(no if while..),逻辑分割在各个callbcak里,如何组合它们,cancel,异常等都是难题。我们淹没在了callback(continuation)中。这种困难的本质原因:
解决之道:
2 控制流用英文一般是control flow或者flow of contro,用于表示独立的程序语句,指令,函数调用等。不要和if,while等结构化编程中对控制流的表达方式相混淆。
3 能让我们就像写同步代码一样写异步代码,coffee script会将他翻译成CPS代码。
4 js轻量级库,用于面向控制流编程,异步编程利器!
Author: ZhangPing
p.zhang.9.25@gmail.com