{
"title": "CPS系列 1: CPS(延续传递) 对正常人来说有什么用?",
"tags": [
"post",
"CPS",
"plt",
"编程语言"
],
"summary": "CPS(Continuation Passing Style)编程是一种编程风格,它的主要思想是将程序的控制流程显示地传递给下一个函数,而不是通过函数调用栈来控制。 在CPS编程中,每个函数都需要一个额外的参数,这个参数被称为 \"continuation\" (或者我们一般叫他 k…",
"sources": [
"xlog"
],
"external_urls": [
"https://lemonhx.xlog.app/CPS-xi-lie-1-CPS-yan-xu-chuan-di--dui-zheng-chang-ren-lai-shuo-you-shi-mo-yong-"
],
"date_published": "2023-04-13T19:24:22.350Z",
"content": "\nCPS(Continuation Passing Style)编程是一种编程风格,它的主要思想是将程序的控制流程显示地传递给下一个函数,而不是通过函数调用栈来控制。\n\n在CPS编程中,每个函数都需要一个额外的参数,这个参数被称为 \"continuation\" (或者我们一般叫他 `k` ),它是一个函数,表示程序执行完当前函数后要继续执行的代码。\n在函数执行完之后,它会将结果传递给continuation函数,从而控制程序的执行流程。这样,函数调用就变成了一个连续的函数调用链,每个函数都负责将结果传递给下一个函数,从而实现了控制流的显示传递。\n\n我和主流作者不同的是我会用正常人看的编程语言来讲解CPS,而不是用一些奇怪的语言,比如 `Scheme` 。\n```js\n// 普通函数\nfunction add(a, b) {\n return a + b;\n}\n\n// CPS函数\nfunction _add(a, b, k) {\n return k(a + b);\n}\n```\n\n有人会问他们有什么显著区别吗?\n有!而且很大,举个例子\n\n```js\nfunction whatever() {\n const a = add(1, 2);\n const b = add(3, 4);\n return a + b;\n}\n```\n\n正常人写出这种代码是非常显而易见的\n\n```js\nfunction _whatever() {\n return _add(1, 2, \n (a) => _add(3, 4, \n (b) => \n a + b));\n}\n```\n\n这么写代码有什么优点呢?\n我可以毫不客气的讲,如果你这么写代码我是你上司我第一个刀了你。\n\n不过假如说你这么写的话就好看的多了\n```js\nfunction _whatever2() {\n // 手动柯里化一下\n const call = (f) => (...args) => (k) => f(...args, k);\n const add12 = call(_add)(1, 2);\n const add34 = call(_add)(3, 4);\n return add12((a) => add34((b) => a + b));\n}\n```\n\n你会发现这样你把一个连续的函数拆成了一个个的函数,这样你就可以在任何地方调用这个函数了,而不是只能在函数的最后调用。\n\n这样做的好处就是你可以任意的传递函数的控制流,而不是只能在函数的最后调用。\n所以这个函数可以被拆分成多个函数,这样就可以实现函数的复用。\n\n```js\nconst call = (f) => (...args) => (k) => f(...args, k);\nconst add12 = call(_add)(1, 2);\nconst add34 = call(_add)(3, 4);\n\nfunction _whatever3(add12, add34) {\n return add12((a) => add34((b) => a + b));\n}\n```\n\n假如有个逻辑改变,你只需要改变一个函数就可以了,而不是整个函数\n```js\n_whatever3(add12, add34);\n_whatever3((k)=>k(0), add34);\n// 思考一下下面的发生了什么?\n_whatever3((k)=>0, add34);\n```\n\n很简单,`k` 作为这个函数的延续,假如你并没有调用 `k` 就返回了一个值,那么这个值就会被当做结果返回\n\n这赋予了你更多的可组合性\n\n## 那这玩意儿有什么用呢?我之前代码写的好好的为啥学这个?\n\n比如说有个错误处理的例子非常好的说明了这个问题\n\n```js\n// 一会儿手动调节下看下效果\nvar fail = false;\n\nfunction maybe_error_function_(ok_do, error_do, k) {\n if (!fail) {\n ok_do(k);\n } else {\n error_do(()=>{});\n }\n}\n\nfunction main() {\n const ok_do = (k) => {\n console.log(\"ok\");\n k();\n };\n const error_do = (k) => {\n console.log(\"error\");\n k();\n };\n const before = call(_add)(1, 2);\n const maybe_err = call(maybe_error_function_)(ok_do, error_do);\n const after = call(_add)(3, 4);\n return before((befor_result) => maybe_err((maybe_err_result) => after((after_result) => {\n console.log(befor_result + after_result);\n console.log(\"done\");\n })));\n}\n```\n\n你会发现,诶!卧槽,怎么错误处理的逻辑写起来跟正常的逻辑一样了???\n\n我现在可以告诉你,这个就是 Rust 语言的 `?` 运算符的实现原理\n\n他就是在编译器层面把这个函数拆成了一个个的函数,然后把这些函数组合起来,然后再内联掉,这样就实现了 `?` 运算符的功能\n\n> 下一章我会讲什么叫 **限界延续(Delimited Continuation)** 可能会引入过多思考,请提前吃点核桃",
"attributes": [
{
"value": "CPS-xi-lie-1-CPS-yan-xu-chuan-di--dui-zheng-chang-ren-lai-shuo-you-shi-mo-yong-",
"trait_type": "xlog_slug"
}
]
}