30 秒看懂
一位 Jane Street 演讲者演示在其 OCaml 编译器分支中开发、并提议合并到上游的实验性 include functor 语法。他从数组映射接口出发,构造一个提供派生操作的函子,展示传统的包装模块方式,再用新语法取而代之。
- 1可映射接口接口提供 map、iter,以及同样传入键的 mapi 和 iteri。数组实现使用整数键。
- 2反向遍历反向数组实现处理空数组,并从末尾开始访问元素。当传入的函数有副作用时,遍历顺序很重要。
- 3派生其余操作忽略键参数即可从 mapi 和 iteri 构造 map 和 iter,因此可以用可复用的函子实现这一常见模式。
- 4包装模块的样板代码传统方法将最小定义放入一个模块,包含该模块,再包含将函子应用于它的结果。
- 5include functor提议中的语法使用当前模块中先前的定义来应用函子,省去了显式包装模块和额外的 include。
要点建议
- 模块接口可以暴露由较少核心操作派生出的便利函数。
- 在示例中,忽略键参数即可从 mapi 和 iteri 定义 map 和 iter。
- 函子可以将这些派生定义打包,供多个模块复用。
- 传统方法将已有定义放入模块,再分别包含该模块和函子应用的结果。
- 提议中的 include functor 语法直接将函子应用于当前模块中先前的定义,无需显式包装模块。
按章节阅读整理后的全文
提议中的模块便利语法
演讲者介绍在 Jane Street 编译器分支中开发、正考虑合并到上游的 include functor。Mappable 签名展示了重复的实现模式。
我想介绍 include functor,这项功能在 Jane Street 的 OCaml 编译器分支中开发,我们正在努力将其合并到上游。它简化了一种常见模式:编写模块的核心部分,再利用可复用的逻辑补全其余部分。视频简介中提供了演示所用编译器的链接。

考虑一个名为 Mappable 的模块类型,它包含数据类型、键和四项操作:map、iter,以及其函数还会接收键的 mapi 和 iteri。数组可以直接实现这个接口:键是 int,数据类型是数组,四个函数可以使用对应的数组操作。
反向数组与派生函数
反向实现从最后一个元素开始定义带索引的操作。忽略索引即可派生普通操作。
现在假设我们想要另一个从后往前遍历的数组模块。当迭代函数有副作用时,顺序很重要。键和数据类型保持不变,但 mapi 反向工作:它单独处理空数组,将函数应用于最后一个元素以初始化结果,然后依次向前填充剩余位置,直到索引零。

反向 iteri 更简单,它使用一个递减的 for 循环。但我不想再写一次 map 和 iter。只要忽略额外的键参数,就能从 mapi 和 iteri 派生它们;每次实现 Mappable 时都可以使用同样的模式。如果效率需要,我们仍然可以提供专门的实现。
可复用函子及其样板代码
签名被拆分为最小接口和额外操作。用传统方式应用函子需要包装模块和显式 include。
函子可以将这种常见派生方式打包。首先,我将接口拆成 Mappable_minimal 和完整的 Mappable:前者包含类型和带索引的操作,后者包含最小部分并增加 map 和 iter。函子 Make_map_iter 接收一个符合 Mappable_minimal 的模块 M。
在 Make_map_iter 内,map 调用 M.mapi,传入一个忽略第一个参数、并对其余输入应用指定函数的函数。iter 对 M.iteri 做同样的处理。编译器推断出预期签名,于是我们就有了一个可复用的方法来生成这两个普通操作。
用传统语法把该函子应用于反向数组定义比较繁琐。我需要把这些定义包装在模块 M 中,include M,再 include Make_map_iter(M)。这样可以工作,但需要额外的包装和显式包含;如果没有外层签名,M 本身也会保持可见。

使用 include functor
新语法将函子应用于当前模块中先前的定义,并包含结果。演讲者简要指出它也适用于模块类型。
include functor 省去了这个包装。将核心定义直接写在模块中之后,我写下 include functor Make_map_iter。从概念上说,它将函子应用于模块中已有的定义,并包含结果。因此我只需定义核心功能,就能获得其余部分,无须重复编写额外模块和 include。

该语法也适用于模块类型,在那里它会包含函子的模块类型,不过这种用法不如实现示例常见。文档中有更多相关细节。
同频道的其他文章
1:48:29EN → 简体中文
1:05:45EN → 简体中文
1:25:00EN → 简体中文
1:16:29EN → 简体中文
4:41EN → 简体中文
1:34:29EN → 简体中文