把一门纯函数式语言的绝对安全与优雅,和一门现代底层语言的原始执行速度放在一起,会打开一个此前难以触及的维度。此前我介绍过 phpurs,一个把 PureScript 的数学级安全带到仍由 PHP 驱动的七成网络世界的编译器后端。那一次的目标是证明:即使目标运行时乍看并不自然,也不必牺牲现代安全性与开发体验。

这一次要看的正好是光谱的另一端。如果 PHP 代表极致的可移植性与遗留兼容,那么当我们追求纯粹的、接近裸金属的速度时会发生什么?当我们想要真正的多核并发、静态原生二进制,以及为极端高吞吐设计的垃圾回收器时,答案会是什么?

打开网易新闻 查看精彩图片

这就是我最近的项目之一:gopurs,一个高度优化的 PureScript 到 Go 编译器。先把结论说在前面:把 PureScript 的高层语义纯度与 Go 的原始执行力结合起来之后,最终编译出的代码在纯计算基准上追平了 Chez Scheme。Chez Scheme 是函数式提前编译领域历史上的绝对冠军之一,背后是数十年的编译器优化积累。

突破提前编译天花板:TAST 如何改变局面

历史上,把动态或高度多态的函数式语言编译到 Go 这类静态类型语言,一直是个难题。早期尝试往往把所有东西映射到 Go 的 interface{},也就是 any。这样做能跑通,但代价是性能。每个赋给 interface{} 的原始值都会被装箱并逃逸到堆上,给 Go 的垃圾回收器带来巨大压力。

为了解决这个问题,gopurs 最初使用了一个扁平的 Value 结构体,这是一种受 V8 启发的带标签联合体,尽量把数据留在栈上。这已经快了很多,但仍然不够。真正的瓶颈在于缺少结构类型信息:PureScript 标准的 CoreFn AST 会擦除类型。

这正是类型化抽象语法树发挥作用的地方。gopurs 依赖 PureScript 编译器的一个定制分支,以及出色的 purescript-backend-optimizer,把深层类型信息一路保留到代码生成阶段。这让编译器能够执行严格的单态化。热路径上零装箱、零开销。抽象在编译期直接消融,只留下 Go 编译器可以激进优化的高速命令式循环。

无惧并发:Aff 遇上 Goroutine

PureScript 最强大的特性之一是 Aff 单子,一种强大且可组合的异步效果处理方式。在 JavaScript 世界里,Aff 被小心翼翼地映射到单线程事件循环上。而在 gopurs 的目标环境中,情况完全不同:Go 原生提供 Goroutine,这让 PureScript 的异步模型有机会直接落到真正的多核并发之上。

这种结合的意义在于,开发者仍然用 PureScript 的高层抽象编写异步逻辑,不需要手动管理线程或锁,但编译产物却可以利用 Go 运行时的调度器,把工作分布到多个核心上。最终得到的是静态原生二进制,启动快、部署简单,同时保留了函数式语言在组合异步操作时的表达能力。

从 phpurs 到 gopurs,这条路线展示的是同一种思路的两个方向:一端是兼容与普及,另一端是速度与并发。两者共同说明,PureScript 的语义安全并不必然以牺牲运行环境为代价。相反,通过保留足够的类型信息,编译器可以在完全不同的目标上释放出接近手写底层代码的性能。