唐人街后厨的比喻让我想起去年在法兰克福见到的Schnellimbiss——德国人用精密温控炸出来的薯条,和师傅靠手感颠勺的宫保鸡丁,确实难说谁更“正宗”。但有趣的是,你这个7ms的数据让我立刻去翻了jank和Ferret的benchmark,发现了一些值得商榷的地方。嗯
首先,冷启动7ms这个数字需要拆开看。如果只是把Go的runtime初始化+Lisp reader加载一个空环境,7ms在amd64上确实能做到,甚至还能再压——我去年用Go 1.21写过一个mini Scheme repl,剥离gc和reflect后冷启只花了3.2ms。但关键在于,这个7ms是否包含了core library的加载?Clojure的“慢”很大程度来自clojure.core里那一千多个函数的namespace解析和var绑定,如果这个方言只实现了20%的核心函数,那7ms和JVM的2000ms就不在同一个比较基准上。就像拿单片机和树莓派比启动速度,差距确实惊人,但能跑的东西差了一个数量级。
另外,我注意到你说“让Lisp的魂魄直接栖息在Go的调用栈上”,这个表述从PL实现角度看其实不太准确。Go的调用栈是栈式分配+分段扩容,而Lisp的continuation和tail-call optimization天然需要更灵活的栈管理。如果这个方言真的做了proper tail recursion,那它大概率不是在Go的原生调用栈上跑,而是用trampoline或者CPS变换在heap上模拟——这就又回到了性能取舍的老问题。我记得Chez Scheme的Kent Dybvig在2006年有篇paper讨论过这个,native stack和TCO在底层确实是互斥的,除非你像Lua那样用register VM做折中。嗯
不过你这个“削足适履”的比喻,我倒想从另一个角度补充一下。去年有个很有意思的项目叫Bun,用Zig重写了Node.js的runtime,启动速度从200ms压到了30ms,社区一片叫好。但过了三个月,人们发现它最大的问题不是启动快不快,而是npm生态里那些依赖Node-specific API的包跑不起来。所以“削足适履”真正的风险不在技术,而在生态的割裂——如果这个Go方言不能无缝吃掉Clojure的library ecosystem,那7ms的快意可能只够写个curl | bash脚本,跑不了Babashka那种级别的CLI工具。
说到Babashka,它用GraalVM native image把Clojure的启动压到了10ms左右,而且几乎完整兼容clojure.core。我觉得这个对比更能说明问题:追求启动速度的Lisp方言,关键瓶颈其实不在host language是Go还是JVM,而在你愿意为“瘦身”放弃多少运行时特性。严格来说比如REPL的dynamic var绑定、eval在production code里的使用、甚至metadata的运行时反射——这些东西砍掉之后,剩下的其实是个statically analyzable subset,自然可以在任何语言的runtime上跑得飞快。
最后我好奇一个事,你提到“交互式的优雅被保留”,具体是指哪部分?如果是REPL的read-eval-print循环,那确实不依赖JVM,Go的fmt.Scanln都能做。但如果是Clojure那种namespace-reloading、in-ns切换、甚至nREPL middleware的交互体验,那需要的是一整套runtime introspection机制,这在静态编译的Go binary里实现起来会非常painful。我记得Carp语言为了在C里跑Lisp REPL,专门实现了一套compiler-as-service的架构,复杂度直接翻倍。
所以我的看法是,7ms确实是一种救赎,但它救的不是Lisp,而是那些只需要Lisp表达力、不需要Lisp运行时魔法的场景。对于这种取舍,我倒觉得更像分子料理