LemonHX

LemonHX

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

CPS Series 1: What is the use of CPS (Continuation-Passing Style) for normal people?

CPS (Continuation Passing Style) programming is a programming style that involves explicitly passing the control flow of a program to the next function, rather than controlling it through a function call stack.

In CPS programming, each function requires an additional parameter called a "continuation" (or commonly referred to as k), which is a function that represents the code to be executed after the current function completes. After the function execution, it passes the result to the continuation function, thereby controlling the program's execution flow. This way, function calls become a chain of continuous function calls, with each function responsible for passing the result to the next function, thus achieving explicit control flow passing.

What sets me apart from mainstream authors is that I explain CPS using programming languages that normal people can understand, instead of using some strange languages like Scheme.

// Normal function
function add(a, b) {
  return a + b;
}

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

Some people may ask, what are the significant differences between them?
There are! And they are quite significant. Let me give you an example.

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

It is very obvious how a normal person would write this code.

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

What are the advantages of writing code like this?
I can be blunt and say that if you write code like this and I'm your boss, I'll fire you immediately.

However, if you write it like this, it looks much better.

function _whatever2() {
    // Manually curry it
    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));
}

You'll notice that by breaking down a continuous function into individual functions, you can call this function anywhere, not just at the end of the function.

The benefit of doing this is that you can pass the control flow of the function anywhere, rather than just at the end of the function. This allows the function to be split into multiple functions, enabling function reuse.

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));
}

If there is a logic change, you only need to modify one function instead of the entire function.

_whatever3(add12, add34);
_whatever3((k)=>k(0), add34);
// What happens below?
_whatever3((k)=>0, add34);

It's simple. k, as the continuation of this function, if you don't call k and return a value, that value will be treated as the result.

This gives you more composability.

So, what's the use of this? Why should I learn this when my code was fine before?#

For example, an error handling example illustrates this problem very well.

// Adjust manually later to see the effect
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");
    })));
}

You'll notice, hey! Wow, how come the logic for error handling looks the same as normal logic???

I can now tell you that this is the implementation principle of the ? operator in the Rust language.

At the compiler level, it breaks down this function into individual functions, combines these functions, and then inlines them, thus achieving the functionality of the ? operator.

In the next chapter, I will explain what delimited continuation is, which may require more thinking, so please have some walnuts in advance.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.