惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

W
WeLiveSecurity
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Hacker News - Newest:
Hacker News - Newest: "LLM"
Cloudbric
Cloudbric
V
Visual Studio Blog
L
LangChain Blog
A
About on SuperTechFans
B
Blog
T
Tenable Blog
罗磊的独立博客
Hacker News: Ask HN
Hacker News: Ask HN
Blog — PlanetScale
Blog — PlanetScale
博客园 - 三生石上(FineUI控件)
The Register - Security
The Register - Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Palo Alto Networks Blog
U
Unit 42
WordPress大学
WordPress大学
D
Darknet – Hacking Tools, Hacker News & Cyber Security
N
News and Events Feed by Topic
T
Threat Research - Cisco Blogs
C
Check Point Blog
Security Latest
Security Latest
M
MIT News - Artificial intelligence
Application and Cybersecurity Blog
Application and Cybersecurity Blog
宝玉的分享
宝玉的分享
P
Proofpoint News Feed
NISL@THU
NISL@THU
Forbes - Security
Forbes - Security
S
Securelist
Security Archives - TechRepublic
Security Archives - TechRepublic
Hugging Face - Blog
Hugging Face - Blog
aimingoo的专栏
aimingoo的专栏
Latest news
Latest news
GbyAI
GbyAI
T
Troy Hunt's Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LINUX DO - 热门话题
V2EX - 技术
V2EX - 技术
小众软件
小众软件
Google DeepMind News
Google DeepMind News
K
Kaspersky official blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
O
OpenAI News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
N
Netflix TechBlog - Medium
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Google DeepMind News
Google DeepMind News
P
Proofpoint News Feed

smallyu的博客

为什么买的加密货币一定要去中心化 RISC-V 虚拟机无法代替 EVM 虚拟机 Polkadot 的 Existential Deposit 机制错在哪里 利息,套利,交易策略,金融市场 LayerZero 从来不挑战比特币的地位 默认参数引起的以太坊节点运行错误 硬件钱包与资产安全 预测市场比币圈更赌场 为什么去中心化的跨链桥不可能实现 我对于 AI 时代的答案 不要投资任何隐私币 区块链技术世界的三大真理 以太坊 AA 钱包的致命问题是什么 基于 AI 语义执行的 MCP 区块链的设计 一个集成 Geth 和 CometBFT 的兼容层 我的加密货币定投策略(二) DeFi 基础: 理解 AMM 定价机制 Go 语言 GMP 调度器的原理是什么 Web3 项目分析计划 对 Psyche Network 项目的分析 如何开发一个比特币符文(Runes)协议 比特币脚本开发教程 我从王垠的计算机科学视频班学到了什么 区块链技术面试题(2025年版) Rust 语言容易让新手困惑的一个“过度优化” Solana 智能合约开发教程 (1) 尝试开发一个最小 EVM 虚拟机 基于 ZK 的链上身份系统设计 一个 Web3 打赏系统的设计 鼓吹 Cursor 的人技术能力都差 关于 Code Review 的礼节 假如启动一个新的以太坊 PoS 网络 发行加密货币的最好方式 所有 BFT 共识的区块链都是中心化的 对 2025 年区块链行业的预测 Restaking 项目的经济难题 如何看懂任意区块链项目的技术架构 为什么不要做以太坊的二次开发 为什么不要做智能合约开发和 DeFi 开发 我的加密货币定投策略(一) PoS 类型的区块链如何处理分叉 Ethereum Casper 为什么需要 EIP-7251 区块链中的 PBFT 不需要第二次投票 开发者的思维方式 发币的核心要点 炒币投资的小 tips 为什么说 PoW 比 PoS 更加去中心化 牛市对普通人来说意味着什么 如何健康地远程工作 为什么比特币不用概率加密函数 程序员的 “服从权威” 心理 区块链技术面试题 如何区分公有链和联盟链 对 Layer 2 项目创业想法的回复 对区块链共识机制的理解 Pebbling Game 鹅卵石游戏 PDP 文件证明的局限性 不要小瞧 ChatGPT 为什么炒币不是一个好主意 一种在区块链上生成随机数的机制 为什么以太坊的私钥计算不可逆 关于以太坊的私钥碰撞 “猜均值的2/3” dApp 游戏设计 Proof of Storage/Space/Replication 的区别 Proofs of Retrievability 文件证明的含义 对 S-PDP 文件证明的示例和解释 我的加密货币交易机器人 对区块链行业的见闻 随机确认块的共识机制 VRF + BFT 共识引起交易失败的问题 为什么要重视编程思想 对 Web 3.0 的理解 GitBook 好用吗? 一种区块链节点存储扩容的方式 一种基于“自我中心主义”的共识机制 链表常见算法题及解析 基于 Multi-Linked List 的区块链设想 理解哈希函数与序列化 联盟链比公有链差在哪儿 在 Dijkstra 算法中保存路径 为什么数字货币使用区块链是政治问题 区块链:下一代数字身份认证体系的基石 网页技术能实现 3D 建模吗? 给区块链一个定义 从 Erlang 开始了解 Actor 模型 一种侧边导航栏的交互方式 Rust 的 ownership 是什么? Haskell 中的 Monad 是什么? 浅析 Libra 背后的区块链技术 对区块链的理性认识 Rust 基础语法概述 基于 Java 的爬虫框架 WebCollector Kotlin:简化版的 Scala JavaScript 有关联数组吗? 主流编程语言的异常处理机制 Go 语言基本语法 Scala 语法基础 用 Scala 改写 Java 浅度实践 Python 获取海贼王更新信息 HTML5 音乐可视化
continuation 教程: 理解 CPS
2025-07-23 · via smallyu的博客

2025-07-23

这是一个 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)); // 1
console.log("fact3=", fact(3)); // 6
console.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)); // 1
console.log("factTail3=", factTail(3, 1)); // 6
console.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) ); // 1
factTailCPS( 3, 1, x => console.log("factTailCPS3=", x) ); // 6
factTailCPS( 5, 1, x => console.log("factTailCPS5=", 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,并且 k 是一个函数。然后我们这样来调用:

let factCPS1 = factCPS(0, x => x);
console.log("factCPS1=", factCPS1); // 1

let factCPS3 = factCPS(3, x => x);
console.log("factCPS3=", factCPS3); // 6

let factCPS5 = factCPS(5, x => x);
console.log("factCPS5=", factCPS5); // 120

关键在于调用的时候,传入函数的第二个参数是 x => x,如果结合函数内部的 r => k(n * r),也许一下子就糊涂了。

这确实是最难理解的部分。我们以计算 2 的阶乘为例,写一个拆解 factCPS 函数调用步骤的过程。这里用到的技巧是在基础班第一课就学过的 单步替换,对于理解递归非常有帮助。如果在基础班经过训练并且打好基础,确实会有助于理解更复杂的东西,比如这里的 CPS 调用:

let factCPS2 = factCPS(2, x => x);
console.log("factCPS2=", factCPS2); // 2

// n=2, k=x=>x, return factCPS(1, r => k(2 * r));
  // n=1, k=r=>(x=>x)(2*r), return factCPS(0, r => k(1 * r));
    // n=0, k=r=>(r=>(x=>x)(2*r)(1*r)), return k(1);
      // k(1) = r=>(x=>x)(2*r)(1*1)
      //      = (x=>x)(2)
      //      = 2

虽然我已经按照正确的思路拆解出了正确的步骤,但是从阅读者的角度,这仍然会非常难理解,可以自己拆解一下试试,逐步理解典型 CPS 的调用过程。理解这些步骤也许需要几个小时的时间,这是正常的。

总结来说,CPS 的每一次调用,都是在用闭包来储存当前步骤计算的值。尾递归是直接用参数传递值,而 CPS 是在用闭包传递给下个步骤值,就是这样的关系。当然理解这一点的前提是,知道闭包是什么,这个也是基础班学习的重点内容,尤其是会在实现解释器环节,自己实现闭包的语句,对于闭包的理解会很透彻。

fib 函数的 CPS

计算阶乘的函数 fact 特点是只在函数体内进行一次递归调用,我们再来看计算斐波那契数列的 fib 函数,它会在函数体内进行两次递归调用,CPS 该怎么处理这个情况。

fib 函数的递归形式的定义是这样:

function fib(n)
{
  if (n == 0)
  {
    return 0;
  }
  else if (n == 1)
  {
    return 1;
  }
  else 
  {
    return fib(n-1) + fib(n-2);
  }
}

console.log("fib(2)=", fib(2)); // 1
console.log("fib(5)=", fib(5)); // 5

这里直接给出 fib 函数的 CPS,然后理解一下 fib 函数的运作过程:

function fibCPS(n, k)
{
  if (n == 0)
  {
    return k(0);
  }
  else if (n == 1)
  {
    return k(1);
  }
  else
  {
    return fibCPS(n-1, r1 => fibCPS(n-2, r2=>k(r1+r2)) );
  }
}

可以看到,对于需要两次递归调用的情况,CPS 是把另一次递归调用,写在了原本的 r => k(r) 函数里,让第二次内部调用成为了递归调用 fib 时候的子调用。这句话有点绕,可以结合代码理解一下。

CPS 形式的 fib 函数这样来调用:

let fibCPS1 = fibCPS(1, x=>x);
console.log("fibCPS1=", fibCPS1); // 1

let fibCPS2 = fibCPS(2, x=>x);
console.log("fibCPS2=", fibCPS2); // 1

let fibCPS4 = fibCPS(4, x=>x);
console.log("fibCPS4=", fibCPS4); // 3

let fibCPS5 = fibCPS(5, x=>x);
console.log("fibCPS5=", fibCPS5); // 5

我们以计算 3 的斐波那契数为例,拆解一下具体的执行步骤。要注意的是,这个过程非常复杂,比 fact 函数还要复杂很多,只有自己亲自写一下才能搞清楚:

let fibCPS3 = fibCPS(3, x=>x);
console.log("fibCPS3=", fibCPS3); // 1+1=2

// n=3, k=x=>x, 
       // return fibCPS(2, r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) );
// n=2, k= r1 => fibCPS(1, r2=>(x=>x)(r1+r2)), 
       // return fibCPS(1, r1 => fibCPS(0, r2 => ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) )(r1+r2)) );
// n=1, k= r1 => fibCPS(0, r2 => ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) )(r1+r2)), 
       // return k(1)
       // return ( r1 => fibCPS(0, r2 => ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) )(r1+r2)) )(1)
       // return fibCPS(0, r2 => ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) )(1+r2))
          // n=0, k= r2 => ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) )(1+r2)
              // return k(0)
              // return ( r2 => ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) )(1+r2) )(0)
              // return ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) ) (1+0)
              // return fibCPS(1, r2=>(x=>x)(1+r2))
                  // n=1, k = r2=>(x=>x)(1+r2)
                  // return k(1)
                  // return (x=>x)(1+1)
                  // return 2

那么经过了 factfib 函数的训练,我们就已经知道 CPS 的形式是什么,以及具体的执行步骤是怎样了。理解 CPS 只是开始,接下来还会利用 continuation 实现更多有趣的程序。

练习题

已知一个递归形式的 sumFrom 函数,接收两个参数 ab,函数的功能是计算 a+(a+1)+...+(b-1)+b 的值,例如参数是 14,则计算 1+2+3+4 的结果:

function sumFrom(a, b)
{
  if (a == b) 
  {
    return a;
  }
  else
  {
    return b + sumFrom(a, b-1);
  }
}

console.log(sumFrom(1, 3));   // 6
console.log(sumFrom(2, 5));   // 14

练习的内容是,将 sumFrom 函数修改为 CPS 形式,补充 sumFromCPS 函数空白处的代码,让程序可以满足测试用例中的输出结果:

function sumFromCPS(a, b, k)
{
  // ____
}

sumFromCPS(1, 3, x => console.log("sumFromCPS(1, 3)=", x));   // 6
sumFromCPS(2, 5, x => console.log("sumFromCPS(2, 5)=", x));   // 14

延伸阅读

我们已经体验了手动将递归程序转变为 CPS 形式的过程,实际上存在能将代码自动转变为 CPS 形式的方法,也就是传说中 “王垠 40 行代码” 在干的事情。可以参考这两个链接查看更多内容:

因为 “自动 CPS 变换” 的难度比较大,我自己并不打算深入学习和实现。