LemonHX

LemonHX

CEO of Limit-LAB 喜欢鼓捣底层的代码,意图改变世界
twitter
tg_channel

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

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) 可能会引入过多思考,请提前吃点核桃

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。