CPS系列 1: CPS(延续传递) 对正常人来说有什么用？

CPS（Continuation Passing Style）编程是一种编程风格，它的主要思想是将程序的控制流程显示地传递给下一个函数，而不是通过函数调用栈来控制。

在 CPS 编程中，每个函数都需要一个额外的参数，这个参数被称为 "continuation" (或者我们一般叫他 k )，它是一个函数，表示程序执行完当前函数后要继续执行的代码。
在函数执行完之后，它会将结果传递给 continuation 函数，从而控制程序的执行流程。这样，函数调用就变成了一个连续的函数调用链，每个函数都负责将结果传递给下一个函数，从而实现了控制流的显示传递。

我和主流作者不同的是我会用正常人看的编程语言来讲解 CPS，而不是用一些奇怪的语言，比如 Scheme 。

Copy
// 普通函数
function add(a, b) {
return a + b;
}

// CPS函数
function _add(a, b, k) {
return k(a + b);
}

有人会问他们有什么显著区别吗？
有！而且很大，举个例子

Copy
function whatever() {
const a = add(1, 2);
const b = add(3, 4);
return a + b;
}

正常人写出这种代码是非常显而易见的

Copy
function _whatever() {
return _add(1, 2,
(a) => _add(3, 4,
(b) =>
a + b));
}

这么写代码有什么优点呢？
我可以毫不客气的讲，如果你这么写代码我是你上司我第一个刀了你。

不过假如说你这么写的话就好看的多了

Copy
function _whatever2() {
// 手动柯里化一下
const call = (f) => (...args) => (k) => f(...args, k);
const add12 = call(_add)(1, 2);
const add34 = call(_add)(3, 4);
return add12((a) => add34((b) => a + b));
}

你会发现这样你把一个连续的函数拆成了一个个的函数，这样你就可以在任何地方调用这个函数了，而不是只能在函数的最后调用。

这样做的好处就是你可以任意的传递函数的控制流，而不是只能在函数的最后调用。
所以这个函数可以被拆分成多个函数，这样就可以实现函数的复用。

Copy
const call = (f) => (...args) => (k) => f(...args, k);
const add12 = call(_add)(1, 2);
const add34 = call(_add)(3, 4);

function _whatever3(add12, add34) {
return add12((a) => add34((b) => a + b));
}

假如有个逻辑改变，你只需要改变一个函数就可以了，而不是整个函数

Copy
_whatever3(add12, add34);
_whatever3((k)=>k(0), add34);
// 思考一下下面的发生了什么？
_whatever3((k)=>0, add34);

很简单，k 作为这个函数的延续，假如你并没有调用 k 就返回了一个值，那么这个值就会被当做结果返回

这赋予了你更多的可组合性

那这玩意儿有什么用呢？我之前代码写的好好的为啥学这个？

比如说有个错误处理的例子非常好的说明了这个问题

Copy
// 一会儿手动调节下看下效果
var fail = false;

function maybe_error_function_(ok_do, error_do, k) {
if (!fail) {
ok_do(k);
} else {
error_do(()=>{});
}
}

function main() {
const ok_do = (k) => {
console.log("ok");
k();
};
const error_do = (k) => {
console.log("error");
k();
};
const before = call(_add)(1, 2);
const maybe_err = call(maybe_error_function_)(ok_do, error_do);
const after = call(_add)(3, 4);
return before((befor_result) => maybe_err((maybe_err_result) => after((after_result) => {
console.log(befor_result + after_result);
console.log("done");
})));
}

你会发现，诶！卧槽，怎么错误处理的逻辑写起来跟正常的逻辑一样了？？？

我现在可以告诉你，这个就是 Rust 语言的 ? 运算符的实现原理

他就是在编译器层面把这个函数拆成了一个个的函数，然后把这些函数组合起来，然后再内联掉，这样就实现了 ? 运算符的功能

下一章我会讲什么叫 限界延续 (Delimited Continuation) 可能会引入过多思考，请提前吃点核桃