文 / 李博(光宇广贞)
CPS,Continuation-Passing Style,后继传递格式。
顾名思义,该格式就是向后接力传递一个东西,传什么呢?引述维基百科对 CPS 的定义如下:
In functional programming, continuation-passing style is a style of programming in which control is passed explicitly in the form of a continuation.
CPS 实现 Fibonacci 一例
在展开讨论之前,先举一个函数式语言(FP)的小例子:
左右两张图示代码是一个意思,只不过左边使用 FP 风格的“模式匹配”,右边使用 IP 内格的 if-else。函数的功能是计算指定位置的 Fibonacci 数值,计算风格采用 CPS。
直观上计算 Fibonacci 数列想到的方法是二叉递归,也即按 F(n)=F(n-1)+F(n-2) 公式向深处递归。与 CPS 有何区别?就相当于普通递归与尾递归的区别。其实,尾递归(Tail Recursion)就是一种 CPS。
何为 CPS?为何 C++ / C# 引入 Lambda 意义重大
何为 CPS?我理解:
CPS 就是泛函闭包;闭包就像车间,一组指定模型的函数组成的流水线对数据完成加工处理。
它与一般一组函数次第调用过程的区别在于:
void Fck ( int n )
{
Sck ( n ); // call next step processing data n.
n++; // extra sequence and should be ignored.
} // 作者按:为何我要用英文注释呢?
那句 n++ 不应该出现。为何?CPS 要求控制权的完美交接,Fck 把控制权给 Sck 后,怎么能再收回来继续处理呢?流水线是不能返工的。
CPS 传递的是数据处理的控制权。在早期的 IP 语言中(也不很早,C# 3.0 以前,C++ 09 以前),数据处理的控制权是通过返回值和参数表在函数之间来回抛接秀球实现的。这是纯粹的 IP 思维,在高阶应用上显得很吃力,比如我曾在《C++ 0x 之 Lambda:贤妻与娇娃,你娶谁当老婆?听 FP 如何点化 C++》文中提到的“泛函模板”的实现,C++ 98/03 就办得很悲壮。高阶复杂应用需要 IP 语言提炼出“闭包(closure)”概念,这就为 IP 语言引入 Lambda 提供了市场。
其实 CPS 并非 FP 的专利,在设计“泛函模板”时,我使用 C++ 98/03 也给出了一种实现,只不过其实现成本过高。有个成语叫“有价无市”,CPS 这东西再好,在 IP 里实现成本过高,那就没人敢用,也就等于没有。为何说 IP 语言从 FP 语言学习并引入 Lambda 机制意义重大呢?因为 Lambda 机制可低成本实现 CPS 思想,使得 IP 语言普及 CPS 思想成为可能。辩证唯物主义教导我们:“科学技术就是生产力”。Lambda 便是支撑 CPS 思想的关键技术。
IP 语言中使用 CPS
先举一个简单的例子,目标是向控制台打印一行字:“使用 CPS 输出 FCK”。如下:
例子简单明了。数据的处理控制权只经过一步传递,交给了处理函数,将字串常量输出。这个例子太简单了,以至于使用 CS(C-Style)的“回调函数(Callback)”机制就能做,何必要用 CPS 呢?好,那就再给一个回调函数干不了的。就去实现 Fibonacci 吧!
话说,引进技术,不仅仅着眼于技术本身,解决类似“泛函模板”的问题并不是 Lambda 值得 C++ 程序员依赖的地方。引进技术更应着眼于这项技术所支撑的思想体系。Lambda 对于 CPS 是重要的。还未出炉的 C++ 新标准在这方面表现得已经不如人意。
CPS 的重要意义和重要应用
流水线工艺之于当今人类工业文明之意义有多大,CPS 之于计算机科技之意义就有多大——此乃吾一家之言,哈哈……
要说应用,首先能想到的便是密集型计算。闭包是密集性计算最佳载体(比起 C++ 和 C# 我更看好 F# 的原因之一)。
尾递归是 CPS 的拿手好戏,FP 语言对递归都有着良好的优化(比起 C++ 和 C# 我更看好 F# 的原因之二)。
多线程异步调用更是 CPS 一展身手的地方。闭包封闭处理过程,却对线程开放。闭包安排好线程间数据处理的顺序,于是线程间便不用轮询(Polling)等待,分步按序完成一系列操作。
更多传统上的应用,引用 MSDN 博客上的话如是说:
Compilers use a more thorough CPS transformation to produce an intermediate form amenable to many analyses. UI frameworks use CPS to keep the UI responsive while allowing nonlinear program interaction. Web servers use CPS to allow computation to flow asynchronously across pages.
C++、c# 的缺憾
感觉 IP 语言对 FP 技术和思想的引入只得其表,不得其里。
比如说,虽然 C#、C++ 习得了 Lambda,但是使用尾递归仍然会栈溢出。分别使用图一或图二的 F#、和图六的 C# 代码去计算 Fibonacci 30,F# 成功,而 C# 会栈溢出而失败。在 C# 下使用闭包密集型计算是灾难。
再比如说,闭包对线程开放安全的前提是函数的无状态。FP 语言先天函数就是无状态的,而不幸的是 C++、C# 是有状态的,会有副作用(side-effect),副作用一直是多线程处理中剪不断理还乱的痼疾。
计算大型不平衡树结点数的问题
Fibonacci 的计算其实就是计算大型不平衡树结点树的一个变种问题。使用二叉搜索法会造成栈溢出的问题,而使用 CPS 方法可以轻松胜任这一任务。
所属分类:C++、C#、F#
参考:
C++ 0x 之 Lambda 受 VS 2010 支持
维基百科之 CPS
MSDN 之 CPS
Recursing on recursion continuation passing