我对续体传递风格CPS的理解

续体传递风格(Continuation-Passing Style)是一种编码风格,利用续体将硬编码的静态控制流转换为动态可编程控制流。

代码编译成的指令流,在执行时会修改系统状态(寄存器、栈、堆等)。因此代码可以看作定义在系统状态集合上的映射或运算:t = f(s)。假设有两个运算y = f(x)和m = g(n),我们构造一个复杂运算h(x):首先对参数x应用f运算,再对结果应用g运算。用代码来表达就是:

public int f(int x) {
  return x * x;
}

public int g(int x) {
  return 2 * x;
}

public int h(int x) {
  int y = f(x);
  return g(y);
}

我们可以从更抽象的层次理解h(x):首先对参数x应用f运算,再对结果应用另一个运算。在h(x)里,这“另一个函数”恰好是g(x)。按照这种理解,我们可以重新定义h(x)。

我们首先定义一个映射fcps(x, k) = k(f(x))。显然fcps表达的就是“先对x应用f运算,再对结果应用另一个运算”,这里的k就代表“另一个运算”。有了fcps,我们可以重新定义h(x)。我们将新定义记为h2(x)。h2(x) = fcps(x, g)。在新定义h2(x)里,g就是续体。续体就是一个复杂运算中的“后续运算”,也可以理解为程序中的“后续指令流”。

public int f_cps(int x, Function<Integer, Integer> k) {
  return k.apply(f(x));
}

public int h2(int x) {
  return f_cps(x, g);
}

观察h(x)和h2(x)的定义可以发现,h(x)的步骤是固定的、静态的。h2(x)则是动态的。通过调整fcps的第二个参数,可以很容易的构造出“先应用f运算,再应用另一个运算”的函数。这就是CPS的优势:通过续体将静态控制流转换为动态可编程控制流。有了CPS,可以方便的开发出“先调用HTTP接口,再根据数据渲染界面”的程序。

从形式上看,CPS类似回调函数。二者有一个重要的区别:CPS将后续控制权完全转移给续体,而回调函数返回之后,控制权仍然由主函数控制。因此回调函数往往用于通知事件,而非指令流转移。

另一个形式类似CPS的是尾递归优化TRO。在尾递归优化中,“续体”是主函数自身,并且仍然在主函数栈上运行。而CPS续体没有这样的限制,可以是任何函数,也不要求在主函数栈上执行。