一个老派程序员尝试 Language Server

A curmudgeon tries a language server

一个老派程序员尝试 Language Server

我写代码的方式十年来几乎没变:编辑、编译、运行、调试,循环往复。我羡慕 Lisp 程序员能在运行中的系统里直接热替换代码,无需重启。虽然 Haskell 无法完全做到这一点,但我尝试结合 Haskell Language Server、ghcid 和 foreign-store 库,构建了一套接近的 workflow。通过自动重载代码并保留状态,我能在不离开编辑器的情况下运行测试和更新应用。尽管配置 Eglot 和 Nix 环境过程曲折,但这套工具链确实让开发体验更接近我所向往的 Lisp 模式。

Lisp 程序员不需要切换到其他地方,因为他们已经在程序进程内部了。
  1. dieggsy

    作为一个 Common Lisp 开发者,文中关于 Lisp 的介绍在我看来大体上(?)是成立的。

    就我的经验而言,我们确实是在“编译并执行”代码,只不过由于文中描述的工作流,我们可以增量地完成这些操作,并且减少上下文切换。但事实上,编译和运行步骤往往是分开的,只是不在整个程序层面(比如编译一个函数并运行它,或者运行调用它的函数)。

    尽管人们经常把这吹嘘成一种优势,但我认为在实际执行过程中重新定义代码其实并没有那么常见。是的,偶尔会这么做,但很多程序从一开始就不适合这种范式。不过,对于某些游戏或长运行服务器来说,这确实行之有效。

    至于在没有源代码的情况下操作镜像(image):这听起来是个糟糕的主意。你可能为了那些琐碎的、一次性的实验这么做,但一般来说,尽早编写和组织代码会让你受益良多。我甚至不知道有什么特别简单或干净的方法能像文中描述的那样从镜像中提取源代码。这或许能做到,但我能想到的所有方法听起来都比其他任何东西更痛苦或更混乱。可能只是我的经验问题,或者我漏掉了某些商业 Lisp 的某个酷炫功能。

  2. cassepipe

    虽然不完全一样,但值得向各位 C/C++ 程序员提一下:gdb 实际上可以充当某种“廉价”的 C 和 C++ 实时环境。

    当在断点处停止时,你可以评估 C/C++ 表达式、修改状态、调用函数,并且配合 compile code 功能,让 GDB 在运行中的进程上下文中编译并执行新的 C/C++ 代码:

    (gdb) set variable state->counter = 42

    (gdb) call recompute_state(state)

    (gdb) compile code

    > printf("counter = %d\n", state->counter);

    > do_something(state);

    > end

    这带来了一种令人惊喜的“附加到运行中的 C 程序上的 REPL”体验。你可以探查堆内存、修改变量、调用现有代码,甚至在不重启进程的情况下注入一小段新代码。

    我对此有一段美好的回忆:当时我在实现不同的树结构,并添加了一个 print_dot 函数,用于以 .dot 格式打印树。每当我在 gdb 内部调用该函数时,xdot 窗口就会重绘并显示更新后的树状图。

    我知道这并不完全等同于 Lisp 镜像(在 Lisp 镜像中注入的代码是持久的),但这仍然是一个很好的功能。

    P.S:如果你是第一次尝试 gdb 的 tui 模式,且你的程序里还充斥着 print 语句,你可以在 printf 输出后按 Ctrl+L 或使用 refresh 命令来清理屏幕。

  3. PaulHoule

    天哪,这种基于镜像的编程方式之所以从未成为主流,就是因为它会制造一团糟。

    看看那些数据科学家用 Python 编写的令人噩梦般的 notebook 吧。他们想把答案直接烘焙在 notebook 里展示给别人看,却没意识到他们需要把代码和数据在版本控制中分开,并且如果他们想让技能真正落地,就必须有一个脚本能从头到尾每次运行。

  4. kaveh808

    我一直认为 Emacs/Slime/CL 开发环境比大多数其他语言的“编译/链接/执行”模式更加流畅,但我感觉有些人是在自己都没尝试过的情况下就狂热鼓吹“基于镜像的编程”。

    不维护文档完善的源代码这个想法让我感到恐惧。

    正如大家指出的那样,如果你不小心在 REPL 中执行了任意代码,你的源代码和镜像就会不同步。当我在开发 [1] 时,我会好几天不重启镜像,但所有的开发工作都是通过评估 Emacs 缓冲区中的代码并立即看到结果来完成的。偶尔,事情会变得过于复杂,我会重启进程作为一种 sanity check(理智检查)。

    对我来说,迭代修改实时镜像的真正好处在于它带来的心流(flow)感。我在其他语言中从未感受过这种感觉。我有时会用这种方式来解释这种感觉:CL 是唯一一种让我感觉不是在跟我作对的语言。

    [1] https://github.com/kaveh808/kons-9

  5. ux266478

    老实说,鉴于 GHC 会进行大量的结构破坏性优化,基于镜像的开发听起来是个非常糟糕的主意。Haskell 从系统层面来看是一种如此怪异且反直觉的语言,因为你几乎就像是在为一个你从未直接看到或交互的语言编写宏。从你的源代码到编译器输出的转换过程可能是高度非局部的且不可预测的,这甚至还没算上引入自定义重写规则之后的情况。

    在我看来,与 Haskell 合作的聪明做法恰恰与迭代相反。先在纸上推导,然后完善它,找到它的代数规则,再进一步完善,直到你确切知道自己在做什么,然后再开始写代码。我认为,屈服于“直接开始写代码”的诱惑,最终只会让你得到一团不透明的类型意大利面。这只是我的个人感受。

同日更多故事

2026-08-26