CPS(Continuation Passing Style)编程是一种编程风格,它的主要思想是将程序的控制流程显示地传递给下一个函数,而不是通过函数调用栈来控制。
在 CPS 编程中,每个函数都需要一个额外的参数,这个参数被称为 "continuation" (或者我们一般叫他 k
),它是一个函数,表示程序执行完当前函数后要继续执行的代码。
在函数执行完之后,它会将结果传递给 continuation 函数,从而控制程序的执行流程。这样,函数调用就变成了一个连续的函数调用链,每个函数都负责将结果传递给下一个函数,从而实现了控制流的显示传递。
我和主流作者不同的是我会用正常人看的编程语言来讲解 CPS,而不是用一些奇怪的语言,比如 Scheme
。
// 普通函数
function add(a, b) {
return a + b;
}
// CPS函数
function _add(a, b, k) {
return k(a + b);
}
有人会问他们有什么显著区别吗?
有!而且很大,举个例子
function whatever() {
const a = add(1, 2);
const b = add(3, 4);
return a + b;
}
正常人写出这种代码是非常显而易见的
function _whatever() {
return _add(1, 2,
(a) => _add(3, 4,
(b) =>
a + b));
}
这么写代码有什么优点呢?
我可以毫不客气的讲,如果你这么写代码我是你上司我第一个刀了你。
不过假如说你这么写的话就好看的多了
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));
}
你会发现这样你把一个连续的函数拆成了一个个的函数,这样你就可以在任何地方调用这个函数了,而不是只能在函数的最后调用。
这样做的好处就是你可以任意的传递函数的控制流,而不是只能在函数的最后调用。
所以这个函数可以被拆分成多个函数,这样就可以实现函数的复用。
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));
}
假如有个逻辑改变,你只需要改变一个函数就可以了,而不是整个函数
_whatever3(add12, add34);
_whatever3((k)=>k(0), add34);
// 思考一下下面的发生了什么?
_whatever3((k)=>0, add34);
很简单,k
作为这个函数的延续,假如你并没有调用 k
就返回了一个值,那么这个值就会被当做结果返回
这赋予了你更多的可组合性
那这玩意儿有什么用呢?我之前代码写的好好的为啥学这个?#
比如说有个错误处理的例子非常好的说明了这个问题
// 一会儿手动调节下看下效果
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) 可能会引入过多思考,请提前吃点核桃