一个自称“守旧派”的Haskell程序员,最近做了一件挺拧巴的事:他羡慕Lisp程序员那种“边跑边改代码”的活法,于是想借语言服务器把Haskell也调教成那样。结果调教到最后,真正撑住场面的,反而不是语言服务器。
这事有意思的地方就在这个反差上。
Lisp的活体编程,Haskell的死循环
Lisp开发者写代码,是直接冲进运行中的进程里改。加函数、删函数、换实现,全程不用重启,程序崩了还能从崩溃的那一层堆栈“续命”,打完补丁接着跑。项目早期甚至没有源码文件,整个系统的定义就活在内存镜像里,跟关系数据库项目早期schema只存在于运行库里是一个道理。
Haskell程序员的日常是另一套:编辑器改代码,切到终端编译执行,看结果,切回来。程序崩了,重启。这是编译型语言的老规矩,跟Lisp的活体编程隔着一层。
这位作者想干的事,是拿现成工具凑一凑,看能不能把这层隔阂磨薄一点。
语言服务器是锦上添花,不是主角
他找的第一个工具是Haskell Language Server,通过Emacs内置的Eglot客户端接进编辑器,拿类型提示、跳转定义、代码诊断。这些能力确实有用,但离“活体编程”还差着一整层——HLS给的是信息可见性,不是运行时状态。
真正干活的是第二套组合:ghcid监测文件变化自动重编译,配合foreign-store库把程序状态存到内存里,让代码整体替换重启时,之前跑起来的资源和数据不会丢。作者原本想用更高层的Rapid库省事,结果Rapid表现不如预期,他又退回去自己手搓foreign-store、IORef、Async、MVar拼生命周期管理——这本身就说明Haskell生态在“状态持久化重载”这件事上还没走到能直接拿来用的成熟度。
在他做的一个微分方程可视化玩具项目上,这套自制管线确实跑通了:改一行算法或常数,保存文件,ghcid重编译、跑单元测试,测试通过后旧的处理线程收掉,新代码接上原来的可视化窗口继续画图,窗口不用重建,状态没丢。
- 结论.能顶替Lisp REPL做“边跑边试”的,不是语言服务器,是这套自己拼的重载管线。
该赖的账,不该赖的账
作者自己吃了不少摩擦:Eglot拖慢了他习惯的类Vim快速跳转,慢到他不敢在其他项目里默认打开;构建失败或者依赖变动后,得手动执行eglot-reconnect,小项目重连也要等一会;靠Nix、direnv、Emacs插件拼起来的环境时常抽风,得手动开mode才能救回来。
Reddit上的haskell社区看完文章,没照单全收。有评论指出,这些安装和环境问题很大一部分该算在Nix、direnv、Emacs集成的账上,跟HLS本身关系不大——把环境配置的锅甩给语言服务器,是这类个人实验报告常见的归因模糊。
还有一条更扎实的纠正:文章里说ghci一次只能重载一个Cabal组件,逼得作者把整个项目塞进单一executable,这个限制在较新版本的Cabal里已经能靠--enable-multi-repl部分绕开。也就是说,文章暴露的一部分痛点,已经不算这场戏里最新的剧情。
语言服务器解决可见性,活体编程要的是可保存的状态。
这两件事在Haskell当前工具链里几乎是两条不相交的轨道:HLS负责让你在编辑器里看得更清楚,ghcid加foreign-store负责让程序活得更久。谁也替代不了谁,拼在一起才勉强凑出接近Lisp的手感。
谁该学,谁别学
这套组合的适用范围很窄。探索性、可视化、科研原型这类小项目最合适——反馈快、状态值钱、结构简单,正是它发挥的场景。
- 风险.单一executable、手工拼状态管理这些妥协,一旦挪到有独立库和多组件结构的生产项目上,代价会立刻放大,不适合直接照搬。
古人说“工欲善其事,必先利其器”,可这次的“器”并不是那把最显眼的语言服务器,而是作者自己在角落里锤出来的一副土办法。Haskell离Lisp式的活体编程还有一段距离,眼下能确定的是:路走对了方向,但还没铺完。
