30 秒看懂
一位 Jane Street 演讲者演示实验性 OCaml 编译器分支中的 local 模式。构造临时 widget 列表的过程揭示了尾调用限制、global 模态、包装类型,以及对局部分配 map 的需求。
- 1不能逃逸的列表只有 get_best 承诺不保留中间 widget 列表时,才能将它作为局部值传入。
- 2尾调用限制局部实参不能用于该尾调用,因此示例选择关闭尾调用优化。
- 3局部列表中的全局元素非装箱包装类型中的 global 模态字段,使元素即使处于局部列表中也保持全局状态。
- 4local 不等于栈分配即使结果被视为局部值,普通 List.map 仍会在堆上分配。
- 5在调用方区域分配返回局部值的 map 在尾位置使用 exclave,在调用方区域内分配结果。
要点建议
- local 参数承诺其实参不会逃逸,因此有时需要避免尾调用。
- 通过带 global 模态字段的包装类型,局部列表可以包含全局元素。
- 非装箱单字段包装类型可以表达这种区别,而无需在运行时额外分配包装对象。
- local 标注表达的是不逃逸,本身不能保证值分配在栈上。
- 返回局部值的 map 可以使用 exclave 在调用方的区域内分配列表单元。
- 模式多态和更方便的模态语法是未来的可能改进,并非演示中已经完成的功能。
按章节阅读整理后的全文
临时 widget 列表
演讲者介绍示例,并尝试将中间列表设为局部值。local 参数和 nontail 调用解决了最初的逃逸错误。
在本系列目前关于 locals 的最后一集中,我想用 Jane Street 的 OCaml 编译器分支中的实验性功能,完成一个小型编程示例。这个例子是刻意构造的,但遇到的错误展示了使用局部值时可能出现的实际问题。
假设有一个用整数索引 widget 的映射,我们想找出与 100 以下素数相关的最佳 widget。素数列表已经准备好,get_best 函数接收 widget 列表并返回最佳元素。为了演示,我提供了一些可以编译、但并不打算运行的占位辅助函数。
初始实现遍历素数,查找每个 widget,将结果收集到列表中,再把列表传给 get_best。它可以编译,但中间列表只在该函数执行期间需要,因此我希望避免在堆上分配它。

把列表标为 local 会产生逃逸错误:原先的 get_best 签名要求全局列表,并没有承诺不会存储这个实参。将其列表参数标为 local,就向编译器提供了所需的不逃逸承诺。
这又暴露了另一个错误:get_best 处于尾位置,因此不能在该尾调用中传入局部实参。添加 nontail 标注可以保持相关区域存活。在这个非递归例子里,放弃尾调用优化是可以接受的。

全局元素与局部结构
从局部列表返回元素暴露了局部性问题。非装箱包装类型中的 global 模态字段可以保留元素的全局性。
占位签名现在接收局部列表并返回全局 widget,但我们还没有实现它。真正实现一个取第一个元素的函数后,就出现了下一个问题:从局部列表中取出的值会被视为局部值,因此以全局值形式返回会使它逃逸。
我把 get_best 的输入标为 local 后,它推断出的结果也变成局部值。然而,调用方希望返回原始表中的全局 widget。我们需要区分的是:列表单元是局部的,但元素是全局的。
演示中的解决方案是定义一个包装类型,并在其唯一字段上使用 global 模态。包装类型标为 unboxed,这是 OCaml 针对此类单字段记录提供的通用功能:包装值与包装类型具有相同的运行时表示,因此包装本身不需要额外分配。
global 模态表示,即使外层包装值是局部的,该字段仍是全局的。因此,这些包装值组成的局部列表可以包含全局元素。通过模式匹配提取字段后,get_best 就能全局返回该字段,同时不让局部列表本身逃逸。

不逃逸与内存分配
包装值后,接口恢复为所需形式,但仅靠 local 标注不能改变 List.map 的分配方式。返回局部值的 map 使用 exclave。
最初推断出的输入类型要求一个包装后 widget 的映射,这与原始输入不同。在查找后包装每个 widget,就能修正接口:输入仍是普通 widget 的映射,结果是全局值,中间列表是局部的。

这里还有一个微妙之处。local 标注表示列表不会逃逸,但它本身不能保证栈分配。普通 List.map 在堆上分配结果,而全局值可以被视为局部值。因此代码可能通过类型检查,却没有实现期望的分配行为。
为了真正让中间列表在局部区域分配,我另写了一个递归 map。在完整的 Jane Street 环境中,库函数可以提供这一功能,但本例使用标准库环境,因此自行定义一个版本。
map 的类型必须说明结果是局部的。列表的 cons 单元需要分配在调用方的区域内,所以实现会在尾位置使用 exclave。完成这一修改后,map 返回局部分配的列表,调用方无需显式标注也能推断出它的局部性。

实践中的不便
包装元素并维护单独的局部 map 增加了代码冗长程度。演讲者讨论未来可能的改进,同时强调该功能仍处于实验阶段。
这个例子暴露了两个不便之处:要在局部列表中保留全局元素,需要包装再解包;为了控制分配,普通 map 也需要一个返回局部值的对应版本。团队希望模式多态可以改善这一点,也在探索内联模态的可能语法,但这项工作仍处于实验阶段。
同频道的其他文章
15:33EN → 简体中文
1:48:29EN → 简体中文
1:36:37EN → 简体中文
1:05:45EN → 简体中文
1:25:00EN → 简体中文
1:16:29EN → 简体中文