Lisp:糖霜还是蛋糕?
2024年春季Lisp游戏创作大赛(Lisp Game Jam)刚结束,提交了48款游戏,创下新高。在参与者相互体验和评分的过程中,文章作者观察到两种使用Lisp构建游戏的元模式,这些模式也适用于更广泛的Lisp应用。以下是比赛提交的语言分布概况:
| 语言 |
提交数 |
占比(约) |
| Guile |
15 |
31% |
| Fennel |
10 |
21% |
| Clojure |
5 |
10% |
| Common Lisp |
5 |
10% |
| Racket |
4 |
8% |
| Elisp |
4 |
8% |
| S7 |
3 |
6% |
| Kawa |
1 |
2% |
| OWL |
1 |
2% |
Guile首次成为最常用语言,这主要归功于11款基于Hoot(Scheme到WebAssembly编译器)的Web游戏。Fennel(编译到Lua的Lisp)通常是最流行的语言,排名第二。S7(可嵌入的Scheme)也引起了关注。
文章将两种模式描述为“Lisp作为糖霜”和“Lisp作为蛋糕”,两者均有效,各有侧重,并构成一个连续谱系。
Lisp作为糖霜(Icing)
此模式将Lisp用作“脚本”语言,嵌入到由C、Rust等静态语言编写的底层程序中。核心是嵌入Lisp解释器,适用于更关注高级应用逻辑的场景。其优势在于能快速利用现有静态语言工具链(如通过emscripten编译为WebAssembly进行Web部署),但深度依赖底层语言生态。
- 示例:S7是可嵌入的Scheme;Guile可用于扩展C程序;Fennel则为已有Lua扩展接口的应用提供Lisp语法。
- 典型堆栈:
- Fennel + love2d:love2d(C++程序)嵌入Lua解释器,是Fennel的完美目标,易于本地和Web部署。这是目前最成功的Lisp游戏开发堆栈。
- S7 + raylib:raylib(C库)与可嵌入的S7结合,易于通过emscripten部署到Web。
Lisp作为蛋糕(Cake)
此模式尽可能用Lisp实现整个软件栈,仅通过外部函数接口(FFI)最少化调用必要的共享库。这需要更长时间,因为可能需要为Lisp实现缺失库或包装C库,且Web部署更复杂。
- 示例:Common Lisp(如SBCL)具有良好的C FFI,可编译为原生可执行文件;Guile的理想用法也是从开始就用Scheme编写程序。
- 典型堆栈:
- Guile + Chickadee:Chickadee是一个用纯Scheme编写的Guile游戏库,仅包装少量C库用于底层任务。其渲染、数学计算等均用Scheme实现,推动了Guile编译器和虚拟机的改进,但开发缓慢。
- Hoot + HTML5 canvas:Hoot是完全用Scheme编写的Scheme到WebAssembly编译器,直接生成Wasm 2.0,无需打包垃圾收集器,二进制体积小(如<2MiB)。它与JavaScript互操作简便,使用浏览器内置HTML5 canvas API进行2D渲染。这相当于“密集的蛋糕加上一点糖霜”,因为其核心工具链是Lisp构建的,但仍依赖高级浏览器API。
蛋糕/糖霜连续谱
蛋糕方法有其局限性:现代Lisp系统最终仍建立在C等底层之上(如Emacs的C核心、Guile的C虚拟机、Hoot运行在V8等C++引擎上)。完全用Lisp重写所有底层库(如OpenGL、libpng)既不现实也无必要。
然而,将更多栈层从C回收为Lisp仍有意义:Lisp代码更易于修改、可运行时热重载,通常具有内存安全性(得益于GC),减少FFI调用可降低开销和提高安全性。随着栈中Lisp成分增加,它就更像蛋糕而非糖霜。
项目案例:
- Guix项目:使用Guile实现函数式包管理和init系统,取代了Nix和systemd。尽管初期被质疑“重复造轮子”,但十多年后,其最大化使用Lisp的做法为项目成功奠定了关键基础,为用户提供了极高的系统定制能力。
- Trial游戏引擎:用Common Lisp实现大量功能,而非包装C库。
- Pre-Scheme:有望通过NLnet资助复兴,实现用Lisp实现GC运行时以下的层级。
结论
作者个人倾向于“蛋糕”模式,希望看到项目不断拓展Lisp的能力边界。对于Lisp游戏创作大赛,他更看重推动Lisp栈层回收的小进步,而非游戏本身。虽然构建一个“Lisp蛋糕”并非易事,但作者坚信值得为之努力,并提倡用Lisp重写,而非Rust。