返回

文章详情

一个脾气暴躁的人尝试语言服务器

Hacker News2026年8月26日 12:48

我写代码的方式大致与十年前相同。实际上,这在我开始起草这篇文章时是正确的。此后,我已经采用了机器人来完成更多实际的代码输入,除非是一个我关心代码质量或试图学习一些东西的项目。我在文本编辑器中进行编辑,切换到终端窗口,运行命令编译和执行代码。我查看结果,然后切换回编辑器。如果我对发生的事情不确定,我要么在代码中添加跟踪打印并重启程序,要么在调试器中重启程序。如果我遇到导致程序崩溃的异常,我会修复问题然后重启它。我羡慕 Lisp 程序员所做的事情。Lisp 开发通常是通过直接在 REPL 中输入代码来添加、删除和替换实时运行系统中的部分。在实际操作中,Lisp 程序员可能会把代码输入到编辑器中的草稿纸上,然后发送到 REPL,但这就好像他们直接在 REPL 中输入了一样。Lisp 程序员不需要“切换到”别的东西,因为他们已经在他们的程序过程中。他们从不“编译和执行”代码,因为代码已经在运行,并且他们通过热替换代码进行编辑。他们从不在调试器中重启,因为他们已经在他们的程序过程中,可以检查他们想要的任何东西。他们不必在异常时重启程序,因为条件系统允许在系统通过修复进行修补后,从堆栈的任何位置恢复崩溃的代码。这种工作方式的一个结果是,在 Lisp 项目的早期,甚至可能根本没有任何可以说的源代码。相反,系统的不断演变的定义仅存在于运行过程的内存映像中,其他地方没有。这与我们通常的开发方式截然不同,以至于人们表达了理解这到底意味着什么的困难。一个可能有帮助的比较是考虑人们在项目开始时对关系数据库的处理。通常,人们不会一开始就使用版本控制的模式定义;相反,演变的模式仅存在于正在运行的数据库中。当然,在某个时刻,当项目成熟时,数据库模式会转存到文件中,放在版本控制之下,从那时起,模式变更通过显式迁移进行。同样,一个成熟的 Common Lisp 项目最终会从内存映像转存到源代码,然后该源代码会进入版本控制,补丁会更加仔细地应用到实时系统上。但在早期阶段呢?哦,所有的一切都是动态演变的。我不是 Lisp 开发者,所以我将永远无法体验那种感觉。我会继续羡慕。但我也意识到我并没有真正尝试去接近它。也许我可以在我工作的 Haskell 语言中获得一些改进。我们在 Haskell 中能做什么?有些事情我们可以立即排除,还有一些事情看起来像是部分成功且不需要太多努力。在 Haskell 中没有办法让我们进入程序过程,所以我们仍需在调试器中重新启动。虽然我相信在 Haskell 中实现类似 Lisp 的条件系统是可能的,但常用库中使用的异常类型(EitherT 和异步异常)没有那样的系统。另一方面,由于强大的类型系统,Haskell 中导致异常的情况较少。这是一个不费吹灰之力的部分胜利。在过去的几年里,出现了一个 Haskell 语言服务器,即 Haskell 的 LSP 实现。这些天,Emacs 内置支持通过内置的 Eglot 客户端进行 LSP 对话。这似乎是一个便宜的部分胜利,可以获得比我习惯的更高质量的代码内省。然后有一个名为 ghc-id 的项目,它监视 Haskell 源代码文件的变化,并立即重新编译已更改的内容。仅此一点在功能上并没有给我们太多,超出了 hls 已经所做的工作,但 ghc-id 还可以在代码成功编译后运行函数。这很强大,因为它可以与 foreign-store 库(或高级的 Rapid 库)结合,即使替换了所有代码也能保持进程状态!因此,Lisp 程序员会逐步将定义发送到 REPL,而我们使用 ghc-id 和 foreign-store 的组合,自动重新编译并重启整个程序,而不会丢失重要状态。对于许多类软件,这两种方法应该会导致类似的工作流程。我们仍然无法做到的一件事是在正在运行的程序的 REPL 中评估代码——因为 ghc-id 不支持它。然而,我大多数时候是为了测试函数是否按预期工作,所以我们可以改为将该代码写成实际的单元测试用例,并让 ghc-id 在重新加载代码时也运行测试。我们最终形成的过程是我们仍然在编辑器中编写代码,LSP 服务器为我们提供有关我们的代码的重要信息。

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡