递归模式的隐秘历史
摘要
一场演讲,追溯从goto面条代码到结构化循环,再到递归模式的演化历程,展示控制流抽象如何映射数据结构,以及为何大多数语言仍把最好的组合子藏起来。
<p><a href="https://lobste.rs/s/0dej4c/secret_history_recursion_schemes">评论</a></p>
查看缓存全文
缓存时间: 2026/04/22 17:59
TL;DR:从面条式 goto 到结构化循环,再到尾递归,最终到递归方案,这场演讲追溯了我们如何学会用数学方式描述迭代,以及为何大多数语言仍把最好的抽象藏了起来。
## goto 深渊与结构化之路
条件分支赋予计算机力量,但裸机级的跳转会缠成难读的“意大利面条”。
早期程序员别无选择——直到 Fortran 54 带来符号表达式和自动寄存器分配,用 20% 的性能换取 5 倍开发速度和 20 倍代码压缩。
C 只多了指针和更大的生态;真正价值在于编译器管理的“无限寄存器”。然而我们仍保留无限制 goto。Dijkstra 1968 年的信指出:goto 密度与代码质量成反比——不受限的跳转抹掉了推理中间状态的坐标系。唯一可原谅的用法是:程序已注定失败时,向前跳转到单个清理标签。
Linux 里依旧 goto 遍地,说明内核开发者不信任编译器,乐于“裸骑硅片”。把编译器锁死在刚性控制流图上,会把意图冻在低层细节里,也失去让编译器变好的压力。
## 用方程代替循环
要让进展可见,我们把循环写成方程:把初值、更新函数、退出测试打包成一个自包含的**函数**。
若函数以自调用收尾,就是**尾递归**,靠谱编译器会把它变成跳转。老 C 编译器没做到,于是 Java 阵营仍念咒“递归跳湖去吧”。现代 GCC 与 Clang 已优化尾调用——只小心 Python,它明令禁止 TCO 以保留完整栈追踪。
看生成的汇编:GCC 假装优化尾递归版 Collatz,却直接返回 1(利用“无限递归未定义”),而 Clang 吐出干净循环。教训:在意循环形状就写尾递归,或至少写 do-while。
## tailRec:一个组合子统治全场
在 Haskell 里,我们可以一次性捕获模式:
```haskell
tailRec :: (s -> Either r s) -> s -> r
```
种子 `s` 要么被新种子替换(`Right`),要么产出最终结果(`Left`)。
Collatz 写成:
```haskell
collatz = tailRec $ \n ->
if n == 1 then Left 1
else Right (if even n then n `div` 2 else 3 * n + 1)
```
结构——种子类型、退出测试、更新——一行声明;干活的是纯函数,可组合。想加副作用用 `tailRecM`:
```haskell
tailRecM :: Monad m => (s -> m (Either r s)) -> s -> m r
```
`tailRec` 只适用于**定长状态**;不分配无界内存,性能可预测。多数语言,哪怕支持高阶函数,也不自带这个组合子——Scala 抄了 Haskell 的 `recursion-schemes` 库,只是例外。
## 当数据本身也递归
朴素递归求和会把整个列表骨架留在栈上。
尾递归能解决栈问题,却得把累加器和剩余列表搓成一对,“把圆钉敲进方孔”。真正的洞见:**控制结构应镜像数据结构**。
递归数据类型如
```haskell
data List a = Nil | Cons a (List a)
data Rose a = Node a [Rose a]
```
由构造子定义,用模式匹配消费。我们需要的是能**遍历**这些形状、同时管理累加器、副作用与提前终止的组合子——比 `tailRec` 丰富,却同样通用。这“更丰富的家伙”就是**递归方案**家族,主流语言仍把它藏起来的隐藏遗产。
来源:https://www.youtube.com/watch?v=JzMCwokUTl4
相似文章
Effectful 递归方案
《Effekt》编程语言博客文章演示了如何利用效应和处理器实现递归方案(特别是 catamorphisms),以此取代传统的基于函子的方法,从而避免了对无限递归类型的依赖。
Xavier Leroy关于编程中控制结构的新书
Xavier Leroy宣布了一本关于编程语言中控制结构的新书,涵盖从goto到代数效应的内容,提供CC许可下的免费预览。
五年尝试为lychee添加递归功能
Matthias Endler 回顾了在 lychee 中实现递归的五年的艰难历程,lychee 是一个被大型科技公司使用的 Rust 链接检查器,详细描述了架构上的挑战和多次失败的尝试。
从组合混乱到线性优雅:构建转换引擎架构
Minimal 的一篇博文,详细介绍了他们如何使用中间表示(Intermediate Representation)来线性管理复杂性,构建文件格式转换引擎,并与生物学的蝴蝶结架构进行了类比。
@hbouammar:也许长上下文推理别再靠模型自己写递归控制代码了。我们开源了 λ-RLM……
研究者发布 λ-RLM,一款开源的带类型 λ-演算运行时,用预验证组合子取代自写递归控制代码,将长上下文推理准确率最高提升 21.9%,在 36 项测试中赢下 29 场。