30 秒看懂
一位 Jane Street 开发者比较了高阶函数、纯函数和惰性求值与表达能力强的静态类型在实践中的价值。讲者通过交易消息、映射合并和连接状态示例,展示类型如何使代码更清晰,并帮助防止无效状态。
- 1Hughes 提出的三个特性讲者重新审视高阶函数、纯函数和惰性求值,认为它们是实现模块化和管理复杂度的工具。
- 2交易系统的优先事项当小错误可能造成高昂损失,机会或监管要求又迅速变化时,正确性、清晰度、灵活应变能力和性能都很重要。
- 3表达能力强的静态类型居首对于 Jane Street 的工作,讲者认为类型比高阶函数、纯函数和惰性求值更重要。
- 4让编译器发现遗漏的情况在交易所消息变体中加入成交撤销后,编译器会指出需要更新的持仓处理代码。
- 5让非法状态无法表示Left、Right、Both 类型消除了映射合并中不可能出现的情况,连接状态变体则规定每种状态对应的数据。
- 6惰性求值与可预测性讲者认为惰性求值在某些情况下有用,但它的性能在波动市场中可能难以预测。
要点建议
- 高阶函数让程序员能够定义控制流并组合行为。
- 纯函数更容易推理,但真实系统必须容纳副作用。
- 交易系统需要正确、清晰、能够快速安全地修改,并具有可预测的性能。
- 选项类型和变体类型明确列出可能情况,让编译器发现遗漏。
- 通过类型使非法状态无法表示,可以减轻维护不变量的负担。
- 惰性求值可以分离关注点,但可能使性能更难预测。
按章节阅读整理后的全文
重新审视函数式编程
讲者回应 John Hughes 的论文并介绍其中三个特性,讨论实践中哪些特性最重要。
我是一名 Jane Street 开发者,工作日的大部分时间都在用 OCaml 编写函数式代码。这个标题有点设了个陷阱:我喜欢函数式编程,但我想解释它为何重要,以及为什么一些常见理由对我们来说没那么重要。
这场演讲回应 John Hughes 1988 年的论文《Why Functional Programming Matters》。Hughes 为函数式编程提出了有说服力的论证,我推荐大家阅读。我同意它很有价值,但我会将不同的理由放在首位。
Hughes 将高阶函数、纯函数和惰性求值列为标志性的优势。软件工程在很大程度上是通过分离关注点和组合模块来管理复杂度;他认为这些特性是让模块化成为可能的黏合剂。
高阶函数与纯函数
一等函数支持自定义控制流。纯函数更容易组合和推理,但真实系统需要副作用。
高阶函数将函数视为一等值:可以把函数存进哈希表、传给另一个函数,或放进引用。例如,OCaml 的 map 函数接收列表和一个函数,将该函数应用于表头,再递归地映射列表尾部。

高阶函数让我们能够定义自己的控制流,不必只依赖语言设计者提供的循环、迭代器或推导式。一个大型函数起初可能用布尔标志选择行为,后来改用变体;随着选择增多,传入函数就能让它交出控制权,待该函数返回后再继续执行。
纯函数的行为只依赖输入,唯一效果是返回结果。它不依赖计数器或一天中的时间,也不写文件或向屏幕打印。纯函数更容易推理,也能像数学函数一样组合。
可变操作可以通过共享状态在程序各部分之间建立通信通道。由于这个通道没有体现在函数的输入和返回值中,代码可能更难推理。尽管如此,真实系统需要副作用,例如与交易所通信,因此在所有作用域都强制保持纯净可能很困难。
惰性求值与模块化
惰性求值支持无限结构,并将国际象棋搜索与可行走法的生成分开。Jane Street 较少使用惰性求值。
惰性求值指表达式只在需要时才计算。可以把自然数无限列表定义为 n 后接从 n + 1 开始的数:严格求值会永远运行下去,而 Haskell 这样的惰性语言可以按需使用该列表。
Hughes 用国际象棋示例说明惰性求值如何将终止条件与循环分开。程序的一部分根据棋规定义可能走法的树,另一部分遍历它以寻找最佳走法。遍历时可以把树视为可用,而不必预先生成整棵树;没有惰性求值时,遍历还必须管理所需节点的生成。
我们同意高阶函数非常有用。纯函数在一定程度上有帮助,但可能让大型系统更难编写;Jane Street 相对较少使用惰性求值。我们的优先顺序反映了所构建的系统类型。
交易系统需要什么
公司的系统必须正确、清晰、容易安全地修改,并具有合理性能。除了速度,可预测性也很重要。
Jane Street 是一家自营交易公司和做市商,用自己的资金在全球交易所交易。演讲时,我们每天完成约 100 万笔交易,覆盖约 200 家交易所,并拥有约 1 PB 的历史数据。我们的系统使用 OCaml 编写。
我们首先关心正确性。系统处理数十亿美元,一个高频循环中的小错误可能在几秒内造成巨大损失。由于交易的是自有资金,重大损失会直接影响公司。清晰度也很重要:代码碰巧正确,但晦涩到没人能确定其正确原因,几乎和错误代码一样令人担忧。
我们也重视灵活应变能力,即快速、高效、安全地修改系统的能力。交易机会可能很快消失,监管变化也可能规定期限,过期后我们就不能交易。每位开发者负责许多系统,因此我们希望避免样板代码,并获得语言支持,防止意外后果。
性能同样重要,尽管我们的主要目标不是在降低交易所延迟的竞赛中获胜。交易代码必须有竞争力,而对 1 PB 数据的分析不能无限期运行。OCaml 不如 C 快,但性能合理且相对可预测。
OCaml 编译器相对简单,有助于我们理解机器层面执行的代码;源码的小改动通常不会导致巨大的性能变化。相比之下,我曾见过 Haskell 中细微的改动阻止编译器优化,造成审查时可能不明显的性能变化。这是一个权衡,并非因为性能就应拒绝函数式语言。
静态类型与空值安全
表达能力强的静态类型位列讲者的优先事项之首。选项类型明确表示值可能不存在,并要求处理这种情况。
我们的排序是:表达能力强的静态类型第一,高阶函数第二,其次是纯函数,惰性求值则低得多。表达能力强的类型系统可以表示元组、记录和代数数据类型;静态检查让编译器在程序运行前检查是否安全使用它们。
以空指针为例。程序员可能忽略值为空的情况,从而触发异常。Tony Hoare 曾将发明空引用称为自己的“十亿美元错误”。在 OCaml 中,类型为 t 的值就是 t,不会暗中可能为 null;选项类型则明确表示 Some 值或 None。
使用选项时,编译器要求我们考虑两种可能。在类型表达能力不足的语言中,注释可能保证函数绝不返回 null,但注释需要人们信任和维护。类型签名让这种可能性可见,并让编译器强制处理。
扩展交易所消息
交易所消息变体对确认、订单退出和成交建模。增加成交撤销后,编译器可以指出需要补充新分支的代码。
类型还帮助我们安全扩展系统。设想交易所发来的执行消息:确认表示已收到订单消息,订单退出表示订单不再有效,成交则报告带有数量和方向的一笔交易。持仓更新函数匹配这些情况:确认和退出不改变持仓,成交则改变持仓。

现在假设交易所还发送成交撤销消息,取消此前报告的一笔成交。这类消息出现得足够频繁,我们必须处理。将成交撤销加入变体类型后,编译器会指出未考虑新情况的匹配,包括系统其他位置的代码。我增加功能时会用这种技术找出所有需要重新考虑行为的地方。

让非法状态无法表示
映射合并和连接状态的例子展示类型如何消除不可能的组合,使不变量更清晰、更容易维护。

第二个例子是合并两个映射。两者使用相同的键类型;提供的函数针对某个键接收分别来自两个映射的可选值,并返回一个可选结果。合并操作考虑任一输入中存在的键,并根据函数结果构建输出映射。
调用者匹配两个可选值时似乎有四种情况,但两者都是 None 的情况不可能发生:该键必定至少出现在一个输入映射中。我们可以断言这种情况不可能,但读者或审查者仍需核实这个说法。

另一种做法是定义包含 Left、Right 和 Both 的类型,并将其传给组合函数。这样不可能出现的 None/None 情况就消失了。代码更清晰,审查者不再需要证明该分支不可达,我们也提前剪掉了组合空间中不必要的一支。
更一般的原则是让非法状态无法表示。如果某些值的组合不可能同时存在,就把这一事实编码进类型。也可以手工维护不变量,论证每个函数都保留它,但这很难长期坚持;合适的类型可以从构造上强制保证不变量。

例如,一条连接记录可能包含状态、服务器、ping 信息和时间戳。连接当前处于已连接状态时,断开时间意味着什么?如果连接已断开或从未收到 ping,最近一次 ping 又意味着什么?并非每个字段在每种状态下都有意义。
可以改用变体,让每种状态明确携带其中可用的信息。断开连接的分支包含其他分支没有的断开时间。这样不变量在类型中清晰可见,也能防止意外出现无效组合,从而提升正确性和清晰度。

最终排序
讲者解释惰性求值为何可能使性能复杂化,以及纯函数为何可能增加复杂度。表达能力强的静态类型和高阶函数是最重要的两项。
回顾一下,惰性求值可能有用,但它使性能难以推理。一个多次表现可预测的函数,在陌生且波动的市场环境中可能突然慢得多,而那恰恰是系统需要保持可用以进行交易的时候。
纯函数很有用,OCaml 也支持它;但如果强制使用纯函数引入更多复杂度,对我们来说就不值得。高阶函数很出色,而表达能力强的静态类型位列首位。幸运的是,OCaml 和 Haskell 等语言同时提供这些特性;这个排序描述的是如果必须选择,我们会如何选择。
同频道的其他文章
1:48:29EN → 简体中文
1:36:37EN → 简体中文
1:05:45EN → 简体中文
1:25:00EN → 简体中文
1:16:29EN → 简体中文
27:39EN → 简体中文