30 秒看懂
在这期 OCaml Unboxed 中,Richard Eisenberg 演示了局部模式、柯里化与偏应用之间微妙的相互作用。接受局部参数的函数在偏应用时可能产生局部闭包,即使书写的类型中没有明显标出这一点。 他解释了为什么将该闭包传给 List.map 会失败,展示如何用 eta 展开让例子通过编译,并说明在这一扩展中,函数类型里的括号为何会改变含义。
- 1整数示例为何可行第一个列表被复制进全局结果,但整数元素通过模式跨越而被视为全局值。将其换成不受约束的多态值会导致逃逸错误。
- 2偏应用捕获输入在这个例子中,List.map 需要一个全局函数。对 append_ints 的局部首参数进行偏应用会创建可能保留该参数的闭包。
- 3隐藏的局部模式多参数函数的局部参数意味着偏应用具有局部模式,即使没有显式写出这个模式。
- 4eta 展开将调用放进显式函数后,包装函数运行时会提供第二个参数。演示中的程序因此能够编译。
- 5括号会影响含义给返回的函数类型显式加括号而不写 local_,表达的是另一种模式,示例实现无法满足它。
要点建议
- 多参数函数的局部参数可能使其偏应用产生的闭包也成为局部值。
- 捕获局部值的闭包不能逃逸到生命周期更长的上下文。
- 整数可以跨越模式,因此示例中可以把局部列表中的整数放入全局列表。
- eta 展开通过显式提供剩余参数,可以避免造成问题的偏应用。
- 在局部模式扩展中,给返回的函数类型加括号可能改变它的模式和含义。
按章节阅读整理后的全文
局部列表参数
Eisenberg 构建一个递归的整数列表拼接函数,其首参数为局部值。整数的模式跨越解释了为什么全局结果是安全的。
在这期 OCaml Unboxed 中,我要继续探索 Jane Street 的 OCaml 编译器分支中的局部模式,重点讨论函数类型。接受局部值的函数,其行为可能不会从写出的类型中立刻显现。
考虑一个递归函数 append_ints,它把一个整数列表拼接到另一个列表前面。如果第一个列表为空,就返回第二个列表;否则将第一个元素放在递归结果前面。尾递归可以改善实现,但不是这个例子的重点。

我将第一个参数设为 local_ int list 类型,第二个参数和结果则为 int list。编译器接受了这个定义。第一个列表被复制进结果,第二个列表没有,因此值得问一问:局部列表中的元素为什么可以进入全局结果?
答案是这些元素是整数。int 可以跨越模式并被视为全局值,因此可以放进全局列表。如果把元素类型改为多态类型,编译器就会报告值逃逸了其区域:任意局部元素都不能安全地放进全局结果。
偏应用导致的逃逸
将偏应用后的拼接函数传给 List.map 会触发逃逸错误。闭包必须保留第一个参数,但 List.map 需要一个全局函数。
现在用 List.map 把 [1;2;3] 拼接到一个列表集合中的每个列表前面。将 append_ints [1;2;3] 作为函数传入会产生区域逃逸错误。编译器解释说这是一次偏应用,并建议再提供一个参数。
偏应用会构建一个函数对象,它必须记住第一个列表,直到第二个参数到来。这个对象可能存活很长时间。虽然我们知道 List.map 不会保存传入的函数,但标准库中的类型没有局部注解来保证这一点,所以它要求全局函数参数。
乍看之下,剩余的函数类型似乎是全局的,因为旁边没有可见的 local 注解。但闭包必须保留第一个局部参数。如果堆上分配的闭包可以指向局部栈上的值,该指针可能比区域存活得更久;模式限制正是为了防止这种情况。
隐藏模式与 eta 展开
偏应用保留局部参数时,其推断出的模式为局部。示例调用通过 eta 展开显式提供缺少的参数。
关键在于中间函数有一个隐含的局部模式。多参数函数接受局部参数时,其偏应用是局部的,因为闭包会保存这个参数。在该位置显式写出 local 注解不会改变含义,编译器已经推断出了它。

这个特定调用可以通过 eta 展开解决:不直接传入 append_ints [1;2;3],而是传入一个接受 x 并调用 append_ints [1;2;3] x 的函数。示例能够编译,因为包装函数内部的调用提供了全部参数。在这种上下文中,编译器可以将列表视为全局值,并在接受局部参数的位置使用它。

给函数类型加上括号时,这个区别就显现出来了。通常我们会把嵌套箭头理解为柯里化函数,但在局部模式下,显式加括号的形式并不等同于不加括号的形式。
括号改变函数类型
显式用括号括起返回的函数,隐含的模式不同于不加括号的形式。Eisenberg 解释了由此产生的错误及编译器提示。

如果括号内返回的函数没有局部注解,这个类型表示应用第一个参数后会得到一个全局函数。我的实现需要一个保存局部参数的中间堆闭包,因此编译器正确地报告:偏应用时函数或其参数之一逃逸了所在区域。
括号实际上在该位置表达了全局模式,尽管那里没有写出 global 关键字。原来不加括号的类型中,中间的局部模式是隐含的。这解释了为什么最初的偏应用会失败,以及为什么在包装函数中提供剩余参数就能通过。
由于这个偏应用问题困扰过许多用户,编译器为它提供了专门提示。这是局部模式中一个不同寻常的角落,也促使我们考虑为不允许偏应用的函数定义和箭头设计新语法。
同频道的其他文章
1:34:29EN
1:34:36EN → 简体中文
15:48EN → 简体中文
1:20:57EN → 简体中文
1:12:58EN → 简体中文
58:47EN → 简体中文