smallyu的博客

smallyu的博客

马上订阅 smallyu的博客 RSS 更新: https://smallyu.net/atom.xml

continuation 教程: 理解 CPS

2025年7月23日 12:12

这是一个 continuation 系列教程:

  1. continuation 教程:理解 CPS
  2. continuation 教程:用 yield 实现协程调度
  3. continuation 教程:用 call/cc 实现协程调度
  4. continuation 教程:用 shift/reset 实现协程调度
  5. continuation 教程:体验 Racket 语言
  6. continuation 教程:实现抢占式协程调度

我们来由浅入深地系统学习下 continuation 的原理以及应用场景。这个系列教程的内容和王垠的 continuation 专项班无关,是我自己学习和研究的成果,所以不会有版权问题。不过当然正是因为我学习了基础班,打下了坚实的基础,才知道该如何去自学和理解 continuation 这个概念。这篇文章会少量透露出基础班学到的技能,毕竟 continuation 属于基础班的进阶内容,无法跳过基础技能去理解。

递归

首先用递归的形式写一个阶乘函数 fact,我们已经很熟悉它的写法,不需要过多解释:

function fact(n){  if (n === 0)   {    return 1;  }  else  {    return n * fact(n - 1);  }}console.log("fact1=", fact(1)); // 1console.log("fact3=", fact(3)); // 6console.log("fact5=", fact(5)); // 120

尾递归

接着把 fact 函数改为尾递归的形式。尾递归会比递归多一个参数,新参数用来保存每个调用计算后的值:

function factTail(n, prod){  if (n == 0)  {    return prod;  }  else  {    return factTail(n-1, prod*n);  }}console.log("factTail1=", factTail(1, 1)); // 1console.log("factTail3=", factTail(3, 1)); // 6console.log("factTail5=", factTail(5, 1)); // 120

CPS 形式

我们基于 fact 函数的尾递归形式,再新增一个参数 k,这个 k 是一个函数,fact 不直接返回计算后的值,而是结果值对 k 函数的调用,像这样:

function factTailCPS(n, prod, k){  if (n == 0)  {    return k(prod);  }  else  {    return factTailCPS(n-1, prod*n, k);  }}factTailCPS( 1, 1, x => console.log("factTailCPS1=", x) ); // 1factTailCPS( 3, 1, x => console.log("factTailCPS1=", x) ); // 6factTailCPS( 5, 1, x => console.log("factTailCPS1=", x) ); // 120

这个 k 就是 continuation,意味着告诉 fact 函数,你执行完了计算出结果之后,应该如何进行下一步延续。不用怀疑,这个函数完全符合 CPS(Continuation-Passing-Style)的形式。

典型 CPS

但是用尾递归结合 continuation 参数的形式,显然不够简洁,并不算典型的 CPS 形式。典型的 CPS 形式比较难理解,所以不需要自己思考出来,直接看这个现成的例子,我们对递归形式的 fact 函数改进一下:

function factCPS(n, k){  if (n == 0)  {    return k(1);  }  else  {    return factCPS(n-1, r => k(n * r));  }}

可能看着有点懵,不要慌,我们拆解一下其中的内容。首先 k 仍然代表 continuation,并且...

剩余内容已隐藏

查看完整文章以阅读更多