示例:JavaScript中的后续传递风格(1)
现在,CPS作为非阻塞式(通常是分布式的)系统的编程风格而被再次发掘出来。
我对CPS很有好感,因为它是我获取博士学位的一个秘密武器。它十有八九帮我消减掉了一两年的时间和一些难以估量的痛苦。
本文介绍了CPS所扮演的两种角色——作为JavaScript中的一种非阻塞编程风格,以及作为一种功能性语言的中间形式(简要介绍)。
内容包括:
◆JavaScript中的CPS
◆CPS用于Ajax编程
◆用在非阻塞式编程(node.js)中的CPS
◆CPS用于分布式编程
◆如何使用CPS来实现异常
◆极简Lisp的一个CPS转换器
◆如何用Lisp实现call/cc
◆如何用JavaScript实现call/cc
请往下阅读以了解更多内容。
什么是持续传送风格?
如果一种语言支持后续(continuation)的话,编程者就可以添加诸如异常、回溯、线程以及构造函数一类的控制构造。
可惜的是,许多关于后续的解释(我的也包括在内)给人的感觉是含糊不清,令人难以满意。
后续传递风格是那么的基础。
后续传递风格赋予了后续在代码方面的意义。
更妙的是,编程者可以自我发掘出后续传递风格来,如果其受限于下面这样的一个约束的话:
没有过程被允许返回到它的调用者中——永远如此。
存在的一个启示使得以这种风格编程成为可能:
过程可以在它们返回值时调用一个回调方法。
当一个过程(procedure)准备要“返回”到它的调用者中时,它在返回值时调用“当前后续(current continuation)”这一回调方法(由它的调用者提供)
一个后续是一个初始类型(first-class)返回点。
例子:标识函数
考虑这个正常写法的标识函数:
- function id(x) {
- return x ;
- }
然后是后续传递风格的:
- function id(x,cc) {
- cc(x) ;
- }
有时候,把当前后续参数命名为ret会使得其目的更为明显一些:
- function id(x,ret) {
- ret(x) ;
- }
例子:朴素阶乘
下面是标准的朴素阶乘:
- function fact(n) {
- if (n == 0)
- return 1 ;
- else
- return n * fact(n-1) ;
- }
下面是CPS风格实现的:
- function fact(n,ret) {
- if (n == 0)
- ret(1) ;
- else
- fact(n-1, function (t0) {
- ret(n * t0) }) ;
- }
接下来,为了“使用”这一函数,我们把一个回调方法传给它:
- fact (5, function (n) {
- console.log(n) ; // 在Firebug中输出120
- })
例子:尾递归阶乘
下面是尾递归阶乘:
- function fact(n) {
- return tail_fact(n,1) ;
- }
- function tail_fact(n,a) {
- if (n == 0)
- return a ;
- else
- return tail_fact(n-1,n*a) ;
- }
然后,是CPS实现方式的:
- function fact(n,ret) {
- tail_fact(n,1,ret) ;
- }
- function tail_fact(n,a,ret) {
- if (n == 0)
- ret(a) ;
- else
- tail_fact(n-1,n*a,ret) ;
- }
CPS和Ajax
Ajax是一种web编程技术,其使用JavaScript中的一个XMLHttpRequest对象来从服务器端(异步地)提取数据。(提取的数据不必是XML格式的。)CPS提供了一种优雅地实现Ajax编程的方式。使用XMLHttpRequest,我们可以写出一个阻塞式的过程fetch(url),该过程抓取某个url上的内容,然后把内容作为串返回。这一方法的问题是,JavaScript是一种单线程语言,当JavaScript阻塞时,浏览器就被暂时冻结,不能动弹了。这会造成不愉快的用户体验。一种更好的做法是这样的一个过程fetch(url, callback),其允许执行(或是浏览器呈现工作)的继续,并且一旦请求完成就调用所提供的回调方法。在这种做法中,部分CPS转换变成了一种自然的编码方式。
实现fetch
实现fetch过程并不难,至于其以非阻塞模式或是阻塞模式操作则取决于编程者是否提供回调方法:
- /*
- 对于客户端—>服务器端的请求来说,
- fetch是一个可选阻塞的过程。
- 只有在给出url的情况下,过程才会阻塞并返回该url上的内容。
- 如果提供了onSuccess回调方法,
- 则过程是非阻塞的,并使用文件的
- 内容来调用回调方法。
- 如果onFail回调方法也提供了的话,
- 则过程在失败事件出现时调用onFail。
- */
- function fetch (url, onSuccess, onFail) {
- // 只有在定义回调方法的情况下才是异步的
- var async = onSuccess ? true : false ; // (别抱怨此行代码的效率低下,
- // 否则你就是不明白关键所在。)
- var req ; // XMLHttpRequest对象.
- // XMLHttpRequest的回调方法:
- function processReqChange() {
- if (req.readyState == 4) {
- if (req.status == 200) {
- if (onSuccess)
- onSuccess(req.responseText, url, req) ;
- } else {
- if (onFail)
- onFail(url, req) ;
- }
- }
- }
- // 创建XMLHttpRequest对象:
- if (window.XMLHttpRequest)
- req = new XMLHttpRequest();
- else if (window.ActiveXObject)
- req = new ActiveXObject("Microsoft.XMLHTTP");
- // 如果是异步的话,设定回调方法:
- if (async)
- req.onreadystatechange = processReqChange;
- // 发起请求:
- req.open("GET", url, async);
- req.send(null);
- // 如果是异步的话,
- // 返回请求对象,否则
- // 返回响应.
- if (async)
- return req ;
- else
- return req.responseText ;
- }
例子:提取数据
考虑一个程序,该程序需要从UID中抓取一个名字
下面的两种做法都要用到fetch:
- // 阻塞直到请求完成:
- var someName = fetch("./1031/name") ;
- document.write ("someName: " + someName + "
- ") ;
- //不做阻塞的:
- fetch("./1030/name", function (name) {
- document.getElementById("name").innerHTML = name ;
- }) ;
CPS和非阻塞式编程
node.js是一个高性能的JavaScript服务器端平台,在该平台上阻塞式过程是不允许的。
巧妙的是,通常会阻塞的过程(比如网络或是文件I/O)利用了通过结果来调用的回调方法。
对程序做部分CPS转换促成了自然而然的node.js编程。