OCaml 中的尾调用与局部区域

原标题Tail Calls and Local Regions in OCaml

一个列表计数示例展示了局部闭包为何可能在尾调用时逃逸,以及 regional 模式与 [@nontail] 如何影响结果。

频道
Jane Street
发布
2024年5月7日
视频
15:59
阅读
5 分钟
幻灯片
7
原文
英语
在 YouTube 打开
往下滚动阅读时,视频会缩到右下角继续播放。字幕跟随阅读语言。
阅读语言共 2 种语言可读
由 AI 从英语原文翻译。打开“对照原文”可以逐段核对。
摘要

30 秒看懂

一位 Jane Street 讲者演示该公司 OCaml 编译器分支中的 local 模式。示例解释了尾调用优化、局部区域边界、隐藏的 regional 模式,以及禁用尾调用带来的栈空间权衡。

  1. 1逃逸的局部闭包即使外层函数返回整数,局部列表注解仍会暴露一个尾调用错误。
  2. 2区域边界调用者的局部区域在尾调用之前结束,因此无法在该处使用其局部闭包。
  3. 3使用 [@nontail]该注解将调用变为普通调用,增加一个栈帧。
  4. 4regional 模式来自局部参数的值可以保留允许尾调用的 regional 模式。
  5. 5递归的栈空间成本丢失 regional 信息并禁用递归尾调用可能消耗额外的 O(n) 栈空间。

要点建议

  1. 函数的局部区域在尾调用之前立即结束。
  2. 捕获局部值的闭包自身也必须是局部的。
  3. [@nontail] 注解阻止尾调用优化,使区域在调用期间保持可用。
  4. regional 信息允许某些参数经过尾调用而不逃离其来源区域。
  5. 禁用递归尾调用可能需要额外的 O(n) 调用栈空间。
全文

按章节阅读整理后的全文

只看原始文稿
01

统计列表中的出现次数

讲者构建并测试一个递归函数,统计首元素的出现次数。添加局部列表注解后,编译器报出值逃逸错误。

本集继续介绍 Jane Street 正在开发的 OCaml 编译器分支中的 local 模式。我将用一个小型列表处理示例,展示局部值与尾调用如何相互影响。

示例统计列表首元素出现的次数。对于 [3; 1; 3; 8; 3; 3; 4],结果应为 4。我们先用普通方式实现,再引入局部值并检查产生的错误。

对于空列表,count_firsts 返回零。否则,它将表头绑定为 x,并定义递归函数 count_occs 来处理尾部。空尾部返回一,计入原来的表头;之后每次匹配加一,不匹配则继续递归而不增加计数。

递归函数捕获 x,外层函数用剩余列表调用它。编译并运行程序会输出 4。这个特定示例的列表完全由常量构成,所以 OCaml 已能避免堆分配;一般列表则可能需要分配。

统计列表首元素
幻灯片 1 / 7统计列表首元素递归 OCaml 示例在捕获首元素的同时统计匹配次数。

我给 count_firsts 添加注解,让它接收局部列表并返回 int。此时编译器报告局部值逃离了其区域,并提示由于这是尾调用,该函数不能是局部的。乍看之下这很奇怪:返回的整数不可能包含列表,我们也没有把列表存入任何引用。

局部值逃离其区域
幻灯片 2 / 7局部值逃离其区域编译器标出了对局部闭包的尾调用。
02

闭包、区域和尾调用

闭包捕获了局部列表的首元素。尾调用优化使函数区域在调用之前结束,从而产生错误。

在本例中,对局部列表进行模式匹配会使 x 和 xs 都成为局部值。由于 count_occs 捕获 x,它的闭包也必须是局部的:如果将闭包放在堆上,局部区域结束后,堆中可能留下指向栈数据的指针。

把捕获的 x 换成常量 3 后,错误消失,这证实正是捕获的局部变量迫使闭包成为局部值。但我们实际要统计 x 的出现次数,因此需要弄清楚尾调用。

如果我先将 count_occs 的结果绑定到变量,再返回该变量,调用就不再处于尾位置,程序也能编译运行。区别源于尾调用优化。

返回前绑定结果
幻灯片 3 / 7返回前绑定结果这次重构让闭包调用不再处于尾位置。

当函数的最后一个操作是调用另一个函数时,尾调用优化可以直接把控制权交给被调用函数,而不必先返回调用函数。被调用函数最终返回给原始调用者,从而省去一个之后没有工作可做的调用帧。

函数的局部区域在其尾调用之前立即结束,如果存在尾调用的话。因此,局部 count_occs 闭包属于 count_firsts 的区域,但在尾调用中使用它会让它逃离该区域。先绑定整数结果则可行,因为返回的整数不保留闭包或列表。

03

禁止尾调用优化

[@nontail] 注解使调用成为普通调用,并增加一个栈帧。随后讲者询问,为什么看起来是局部值的递归参数仍能传入尾位置。

用 [@nontail] 标记调用
幻灯片 4 / 7用 [@nontail] 标记调用该注解在此调用点禁用尾调用优化。

这个编译器分支提供了明确的替代办法:用 [@nontail] 标记调用。它会在该调用点阻止尾调用优化,使局部区域在普通调用期间保持可用,从而让程序通过编译。

这个注解会改变运行时行为:普通调用会增加一个调用栈帧。讲者说,对于这一次调用,额外分配耗时很少,但它确实会额外使用栈空间。

还有一个更微妙的问题:ys 来自局部列表,为什么 count_occs 仍能用它进行尾递归调用?其局部参数得到了正确推断,而且调用无需 nontail 注解就能通过。

04

regional 模式与递归空间

隐藏的 regional 模式保留了有关来自当前区域外部的值的信息。恒等函数示范了这些信息丢失后的结果。

regional 递归参数
幻灯片 5 / 7regional 递归参数示例将从参数得到的列表尾部标记为 regional。

原因是一个隐藏模式 regional,它介于 global 和 local 之间:global 是 regional 的子模式,regional 又是 local 的子模式。局部参数来自当前函数区域之外,因此可以逃离这一个区域。对参数进行匹配得到的尾部 ys 是 regional,所以允许尾调用。

为了演示,我定义恒等函数 lid,接收并返回局部值。将 ys 传入 lid,再把结果绑定为 zs,会丢失该值原本是 regional 的信息:类型允许这次调用,但只知道其结果是 local。

此时在尾递归调用中使用 zs 就会产生局部值逃逸错误。用 [@nontail] 标记该调用可使其通过编译,但在长列表的递归中反复这样做,可能多消耗 O(n) 的调用栈空间。保留 regional 信息可让尾递归分支避免这一成本。

丢失 regional 信息
幻灯片 6 / 7丢失 regional 信息将列表尾部传入局部恒等函数后,递归调用需要 [@nontail]。
05

非尾调用分支与现有限制

调用后还要做加法的表达式不处于尾位置。讲者总结规则,并讨论改善注解体验的必要性。

递归调用后还要加一的分支则不同:1 + count_occs zs 不是尾调用,因为还要执行加法。因此,即使没有 nontail 注解,也可以在那里传入局部值。

调用后执行加法
幻灯片 7 / 7调用后执行加法匹配分支在递归调用后加一,因此不是尾调用。

需要记住的规则是:函数区域在尾调用之前结束。显式使用 [@nontail] 注解,或在返回之前绑定调用结果,都能阻止该调用点按尾调用处理。

实践中,当前方法可能需要许多 nontail 注解。我认为在将这项工作合入上游之前,需要找到更好的方案;我们有一些启发式方法的想法,但未来如何发展仍未确定。

同频道的其他文章

6 张幻灯片15:33EN → 简体中文
JSJane Street· 2026年10月26日Figgie 策略:解读手牌、争夺多数与定价Bradley, Mike · Jane StreetJane Street 交易员 Bradley 和 Mike 分析三局游戏,展示私人信息、市场竞争和时机如何影响牌价。Decision MakingMathematicsFintech
1:48:29EN → 简体中文
JSJane Street· 2026年10月16日AI 如何改变编程:John Krazit 与 Ron Minsky 的对话Ron Minsky, John Krazit · Jane StreetJane Street 的 AI 工具负责人和 Signals and Threads 主持人探讨智能体基础设施、安全、代码审查,以及软件开发体验的变化。编程智能体Developer productivity评估+2
1:36:37EN → 简体中文
JSJane Street· 2026年9月23日Aaron Bauer:在 AI 时代教授软件工程Ron Minsky, Aaron Bauer · Jane Street当模型能够生成代码时,学习目标、主动练习和编辑器设计如何影响工程教育。AI in EducationSoftware engineeringEducation+2
9 张幻灯片1:05:45EN → 简体中文
JSJane Street· 2026年9月11日Cyrus Omar:实时编程与人与 AI 共享的计算公共空间Cyrus Omar通过 Hazel 演示,探讨即时反馈、结构化协作和精细权限如何帮助人类与 AI 智能体共同工作。Programming LanguagesHuman-AI Collaboration开发流程与测试+2
1:25:00EN → 简体中文
JSJane Street· 2026年9月2日Eric Mannes:如何将杂乱的外部数据用于交易研究Ron, Eric Mannes · Jane Street从商品期货和 Excel 风险系统,到时点数据集和 AI,Eric Mannes 解释 Jane Street 数据工程背后的领域知识。Data quality & curation开发流程与测试Investing+2
7 张幻灯片1:16:29EN → 简体中文
JSJane Street· 2026年8月26日Grant Sanderson 与 Alok Puranik:从群论看位置编码Grant Sanderson, Alok Puranik · 3Blue1Brown从注意力机制的假设出发,他们推导矩阵指数,并将旋转、衰减和 Jordan 块与 RoPE、ALiBi 联系起来。Transformers & attentionMathematics机器学习基础+1