顽固派的语言服务器初体验
文章背景与核心概要
本文记录了一位传统 Haskell 开发者尝试将其工作流进行现代化改造的历程,旨在捕获类似 Lisp 开发中的人体工程学魔法。通过将 Haskell 语言服务器(Haskell Language Server,通过 Emacs 和 Eglot)、实时重载工具(ghcid)以及状态持久化技术(foreign-store)结合起来,作者为探索微分方程搭建了一个零摩擦的开发循环——而且完全不需要离开文本编辑器。
文章详细梳理了在 Haskell 中逼近 Lisp 动态体验的探索过程。尽管 Haskell 缺乏 Lisp 那样直接进入运行中进程的 REPL,但作者通过巧妙地结合 ghcid 的自动重编译触发机制和 foreign-store 的进程状态保留技术,实现了代码修改后自动重置并恢复状态的效果。此外,作者还记录了配置 Emacs、Eglot、Nix 以及处理各类环境陷阱的真实心路历程,为追求高效工作流的开发者提供了宝贵的实践参考。
导言:传统方式与 Lisp 嫉妒 (Introduction: The Old Ways and Lisp Envy)
I write code roughly the same way I did ten years ago: I make edits in my text editor, I switch over to a terminal window and I run a command to compile and execute the code. I look at the result, then switch back to my editor. If I am unsure of what happens, I either add tracer prints to the code and restart the program, or I restart the program in a debugger. If I hit an exception that crashes my program, I fix the problem and then restart it.
我编写代码的方式和十年前差不多:我在文本编辑器中进行修改,切换到终端窗口,然后运行一条命令来编译和执行代码。我查看结果,然后切回编辑器。如果我不确定发生了什么,要么在代码中添加追踪打印(tracer prints)并重启程序,要么在调试器中重启程序。如果遇到导致程序崩溃的异常,我会修复问题然后重新启动。
I’m envious of what Lisp programmers do. Lisp development typically happens by typing code directly into the REPL to add, remove, and replace parts of the live, running system.
我很嫉妒 Lisp 程序员的工作方式。Lisp 的开发通常是通过直接将代码输入 REPL 来添加、移除和替换正在运行的实时系统的各个部分。
- A Lisp programmer does not need to “switch over” to something else because they are already inside their program process.
- They never “compile and execute” the code because the code is already running and they edit it by hot-swapping code.
- They never restart in a debugger because they are already inside their program process and can inspect anything they want.
- They don’t have to restart the program on exceptions, because the condition system allows resuming the crashing code from anywhere in the stack after the system has been patched with a fix.
- Lisp 程序员不需要“切换”到其他地方,因为他们本来就身处自己的程序进程内部。
- 他们从不需要“编译并执行”代码,因为代码已经在运行了,他们通过热插拔代码来对其进行编辑。
- 他们从不在调试器中重启,因为他们已经身处程序进程中,可以检查任何想要的东西。
- 遇到异常时他们不必重启程序,因为条件系统(condition system)允许在用修复补丁修补系统后,从堆栈的任何地方恢复崩溃的代码。
A consequence of this way of working is that early in a Lisp project, there may not even be any source code to speak of. Instead, the evolving definition of the system exists only in the memory image of the running process and nowhere else. This is so unlike how we normally do development that people have expressed trouble understanding what that even means. A comparison that might help is to think of how people treat relational databases in the beginning of a project. Often, people don’t start out with a version-controlled schema definition; rather, the evolving schema exists only in the running database. Sure, at some point when the project matures, the database schema is dumped out to a file, put under version control, and from that point forward, schema changes are made through explicit migrations. Similarly, a Common Lisp project that matures will be dumped from memory image to source code eventually, and then that source code will go under version control, with patches applied more carefully to the live system.
这种工作方式的一个结果是,在 Lisp 项目的早期,甚至可能根本没有什么“源代码”可言。相反,系统不断演化的定义仅存在于运行进程的内存镜像中,别无其他。这与我们通常的开发方式大不相同,以至于人们表示很难理解这究竟意味着什么。一个可能有所帮助的类比是,想想人们在项目初期是如何对待关系型数据库的。通常,人们并不是从带有版本控制的模式(schema)定义开始的;相反,不断演化的模式只存在于运行中的数据库中。当然,当项目成熟到某个阶段时,数据库模式会被导出到文件中并纳入版本控制,从那时起,模式的变更通过明确的迁移(migrations)来进行。类似地,一个走向成熟的 Common Lisp 项目最终会从内存镜像转储为源代码,然后该源代码将被纳入版本控制,并对实时系统更谨慎地应用补丁。
But in the early stages? Oh, it’s all evolving live.
但在早期阶段呢?哦,一切都在实时演化。
I’m not a Lisp developer, so I won’t ever get that experience. I’ll stay envious. But I also realised I haven’t really tried to get closer to it. Maybe there are some improvements I can get even in my working language of Haskell.
我不是 Lisp 开发者,所以我永远无法获得那种体验。我只能继续羡慕。但我也意识到,我其实并没有真正尝试去接近它。也许在我的工作语言 Haskell 中,我也可以获得一些改进。
我们在 Haskell 中能做什么? (What Can We Do in Haskell?)
There are some things we can rule out immediately, and others that seem like partial wins with not too much effort.
有些事情我们可以直接排除,而另一些事情看起来只需花点力气就能取得部分成功。
- There’s no way to write Haskell that puts us inside the program process, so we will still need to restart in a debugger.
- Although I’m sure it’s possible to implement a Lisp-like condition system in Haskell, the types of exceptions (
EitherTand async exceptions) used in common libraries don’t have one. On the flip side, fewer things in Haskell lead to exceptions thanks to the powerful type system. That’s a partial win without lifting a finger. - For a couple of years, there has been a Haskell language server, an LSP implementation for Haskell. These days, Emacs has built-in support for speaking LSP through the built-in Eglot client. This seems like another cheap partial win to get higher-quality code introspection than I’m used to.
- Then there is a project called ghcid that watches for changes to Haskell source files and immediately recompiles things that have changed. That alone doesn’t give us much beyond what HLS already does, but ghcid can also run a function once the code has compiled successfully. That is powerful, because it can be combined with the foreign-store library (or the higher level Rapid library) to maintain process state even when all the code is replaced!
- 编写 Haskell 代码时,我们无法让自己直接进入程序进程内部,因此我们仍然需要在调试器中重启。
- 虽然我确信在 Haskell 中实现类似 Lisp 的条件系统是可能的,但常用库中使用的异常类型(
EitherT和异步异常)并没有这种系统。另一方面,多亏了强大的类型系统,Haskell 中导致异常的情况更少。这算是不费吹灰之力取得的局部胜利。- 几年来,一直有一个名为 Haskell language server 的项目,它是 Haskell 的 LSP 实现。如今,Emacs 已经通过内置的 Eglot 客户端内置了对 LSP 的支持。这似乎是另一个成本低廉的局部胜利,能让我获得比以往更高质量的代码内省(introspection)。
- 还有一个名为 ghcid 的项目,它会监视 Haskell 源文件的更改并立即重新编译发生变化的部分。单凭这一点带来的收益并不比 HLS 多多少,但
ghcid还可以在代码成功编译后运行一个函数。这非常强大,因为它能够与 foreign-store 库(或更高级的 Rapid 库)结合使用,即使在所有代码都被替换时也能保持进程状态!
Thus, where a Lisp programmer would incrementally send definitions over to the REPL, we use the combination of ghcid and foreign-store to automatically recompile and restart our entire program without losing important state. For many classes of software, these two approaches should lead to similar workflows.
因此,Lisp 程序员会增量地将定义发送到 REPL,而我们则结合使用
ghcid和foreign-store来自动重新编译和重启整个程序,同时不会丢失重要的状态。对于许多类别的软件而言,这两种方法应该会带来类似的工作流。
The one thing we still cannot do is evaluate code in the REPL of the running program – because ghcid doesn’t support it. However, I mostly do this to test that functions work as intended, so what we can do instead is write that code as an actual unit test case, and have ghcid also run tests when it reloads the code.
我们唯一仍然无法做到的是在运行中程序的 REPL 中评估代码——因为
ghcid不支持这一点。不过,我这么做通常只是为了测试函数是否按预期工作,所以我们可以改为将这些代码写成实际的单元测试用例,并让ghcid在重新加载代码时也运行测试。
We end up with a process where we still write code in our editor, with the LSP server giving us information about our code. Instead of playing around in the REPL to try things out, we write our experiments as unit tests. When we save our changes (tests or application logic), ghcid reloads the code, runs the tests, and restarts the application in a way that doesn’t lose important state. We never leave our editor. It’s neat on paper, but I wasn’t sure how it’d work in practice, so I tried it with a toy project.
最终我们得到了这样一个流程:我们仍然在编辑器中编写代码,LSP 服务器为我们提供有关代码的信息。我们不再在 REPL 中四处折腾来尝试各种想法,而是将实验写成单元测试。当我们保存更改(测试或应用程序逻辑)时,
ghcid会重新加载代码、运行测试,并以不丢失重要状态的方式重启应用程序。我们永远不需要离开编辑器。这在理论上很完美,但我并不确定它在实践中表现如何,所以我用一个玩具项目(toy project)进行了尝试。
玩具项目 (The Toy Project)
At the time of writing this, I was trying to patch up my lack of skill with differential equations and non-linear dynamics. The book I was reading has a pleasant focus on computational methods, and the book comes with software for performing the exercises, but I want to write the code on my own.
在撰写本文时,我正试图弥补自己对微分方程和非线性动力学知识的欠缺。我正在阅读的那本书很好地聚焦于计算方法,并且书里附带了用于完成练习的软件,但我希望自己来编写代码。
At least in the early chapters, the software is not exactly rocket surgery. Here’s an example of my program producing a slope field in light grey to visualise the general solution to a differential equation, and then in black the specific solution that passes through the coordinate under the cursor.
至少在前面几章中,该软件并不是什么高深莫测的火箭科学。以下是我的程序生成的一个示例:用浅灰色绘制斜率场(slope field)以可视化微分方程的通解,然后用黑色绘制通过光标所在坐标的特解。
When exploring ideas from the book, I want to write some code, write some tests, and then when I save, the window containing the visualisation should automatically update to reflect the latest changes. It’s not exactly a Lisp development flow, but contains some of its good parts; specifically those that are possible with the technologies mentioned above. It sounded easy on the surface to get there, but it wasn’t.
在探索书中的想法时,我希望编写一些代码、编写一些测试,然后当我保存时,包含可视化的窗口应自动更新以反映最新的更改。这虽然算不上标准的 Lisp 开发流,但包含了一些它的优点;具体来说,就是上述技术所能实现的那些部分。表面听起来要实现这一点很容易,其实不然。
如何让 Emacs 与 HLS 对话 (How to Get Emacs to Talk to HLS)
I’ll first focus on getting Eglot with HLS up and running, because that’s the less interesting part. The list below contains a lot of steps, because getting it installed from scratch with a new project takes a lot of steps. After this initial installation, only steps 2 and 4 are required to enable HLS in any project.
我首先将重点放在让 Eglot 配合 HLS 运行起来上,因为这是其中相对无趣的部分。下面的列表包含了很多步骤,因为从头开始为一个新项目安装它需要很多步骤。在完成最初的安装之后,在任何项目中启用 HLS 只需要步骤 2 和步骤 4。
- At first I ran
cabal initto create a standard empty Haskell project, split into executable, library, and test code.
I discovered later that ghci (and therefore also ghcid) don’t behave well when it comes to reloading multi-part projects like that. Thus, I ended up co-locating all code in one executable project. I wouldn’t want to do that for production code because it makes other types of development more difficult, but it works okay for this kind of toy project.
- I created a
flake.nixto wrap the cabal project and provide dependency management, including libraries and build tools. This way, I wouldn’t have to globally install ghcid and HLS, but could sandbox them to this project. Nix is great for fearlessly trying new things. - I installed
direnvwith my system package manager, and hooked it into my shell’s user profile. For some reason, I still had to manually call up the Nix development shell in my terminal before I ran the project. I’s not sure why, but it might mean hookingdirenvinto the shell is an optional step. No idea. - I created an
.envrcfile containing just the instructionuse flakein the project directory. This tellsdirenvthat when clients request the environment for any path under this project directory, it should automatically put the Nix flake development shell dependencies in the path. - I cloned
nix-direnvand imported it in mydirenvconfiguration. This is an optimisation that makesdirenvload Nix-based environments much faster. For terminal-only use, it’s optional, but it’s useful once we teach Emacs todirenv, because Emacs loads that environment often. - I installed the
envrcplug-in for Emacs and set up its hook. This means (some) Emacs commands will pick up thedirenv. This is necessary in order for Eglot to be able to connect to HLS, since we didn’t install it globally. - Manually connecting to HLS should be a matter of running
M-x eglot, and then functionality can be tested by e.g. hovering over a function call and runningxref-find-definition. Eglot also creates event buffers in Emacs containing all traffic between server and client, which can be read for additional troubleshooting information. - But at this point, I struggled for a while to get Eglot to work. It failed to find the HLS server. I manually enabled
envrc-global-modeand then I could runM-x eglotsuccessfully. That was probably a one-off problem, because theenvrchook hadn’t fired for the code buffer I already had open. - I still find I have to manually run
M-x eglot-reconnectto restart the LSP server sometimes when something gets stuck. The drawback of this is that the server takes a little while to restart, even for a tiny project. Restarting the server has been needed at least once after a general build failure, and often after I have added third-party libraries to the Cabal file. Maybe this is the system working as intended, but it’s annoying early on in a project when the dependency set is in flux. - Once I could reliably open files in the project and connect to the LSP server, I added an
eglot-ensurehook tohaskell-modeso it automatically connects when I open Haskell code. Unfortunately, this causes a small delay any time I open a Haskell file, and it triggers an ugly error if that project’s environment does not have HLS installed. I set up a condition in the Emacs code so it only tries to start Eglot if it detects there’s a good chance it might work. - I disabled Flycheck in Haskell buffers because it never worked well anyway, and if I stick with HLS, it would replace what I use Flycheck for anyway.
- I have configured Emacs to keep my cursor centred, with equal context above and below my edit. This interacts badly with the default documentation “pop-up” that Eglot uses, which changes the size of the editing window whenever it displays documentation. Fortunately, this was easy to fix by setting
eldoc-echo-area-use-multiline-ptonil. With that setting, it only shows one line of documentation while I’m editing: the type signature of the function under the cursor. That’s perfect for my needs.
- 起初,我运行
cabal init创建了一个标准的空 Haskell 项目,分拆为可执行文件、库和测试代码。我后来发现,ghci(因而也包括 ghcid)在重新加载这种多部件项目时的表现并不好。因此,我最终将所有的代码集中放在一个可执行项目(executable project)中。对于生产代码,我不会这么做,因为这会使其他类型的开发变得困难,但对于这种玩具项目来说,效果还可以。
- 我创建了一个
flake.nix来包装 cabal 项目并提供依赖管理,包括库和构建工具。这样一来,我就不必全局安装ghcid和 HLS,而是可以将它们沙盒化在这个项目中。Nix 对于无所畏惧地尝试新事物而言简直太棒了。- 我用系统的包管理器安装了
direnv,并将其挂钩到我的 shell 用户配置文件中。出于某种原因,在我运行项目之前,我仍然必须在终端中手动调用 Nix 开发 shell。我不确定为什么,但这可能意味着将direnv挂钩到 shell 是个可选步骤。完全没有头绪。- 我在项目目录中创建了一个
.envrc文件,其中只包含use flake指令。这告诉direnv,当客户端请求该项目目录下任何路径的环境时,它应该自动将 Nix flake 开发 shell 依赖项加入环境变量路径中。- 我克隆了
nix-direnv并将其导入到我的direnv配置中。这是一个优化项,能使direnv加载基于 Nix 的环境的速度快得多。对于纯终端使用来说,它是可选的,但一旦我们教 Emacs 使用direnv,它就很有用了,因为 Emacs 经常加载该环境。- 我为 Emacs 安装了
envrc插件并设置了其挂钩。这意味着(某些)Emacs 命令将捕获direnv的环境。这是 Eglot 能够连接到 HLS 的必要条件,因为我们没有全局安装它。- 手动连接到 HLS 应该只需要运行
M-x eglot,然后可以通过将鼠标悬停在函数调用上并运行xref-find-definition等方式来测试功能。Eglot 还在 Emacs 中创建了包含服务器和客户端之间所有流量的事件缓冲区,可以读取这些缓冲区以获取更多故障排除信息。- 但在此时,我费了一番周折才让 Eglot 运行起来。它找不到 HLS 服务器。我手动启用了
envrc-global-mode,然后就可以成功运行M-x eglot了。这可能是一次性的问题,因为envrc挂钩没有对我已经打开的代码缓冲区触发。- 我发现当某些东西卡住时,有时仍不得不手动运行
M-x eglot-reconnect来重启 LSP 服务器。这样做的缺点是,即使对于一个极小的项目,服务器重新启动也需要一点时间。在发生通用的构建失败后至少需要重启一次服务器,在将第三方库添加到 Cabal 文件后通常也需要重启。也许这是系统按预期工作的表现,但在项目早期依赖关系处于变动阶段时,这确实很烦人。- 一旦我能够可靠地打开项目中的文件并连接到 LSP 服务器,我便向
haskell-mode添加了一个eglot-ensure挂钩,这样当我打开 Haskell 代码时它就会自动连接。不幸的是,这导致每次我打开 Haskell 文件时都会产生微小的延迟,并且如果该项目的环境中未安装 HLS,它会触发一个丑陋的错误。我在 Emacs 代码中设置了一个条件,只有当它检测到很有可能成功时,才会尝试启动 Eglot。- 我禁用了 Haskell 缓冲区中的 Flycheck,因为它从来都没好用过;而且如果我坚持使用 HLS,它无论如何都会取代我使用 Flycheck 的场景。
- 我将 Emacs 配置为保持光标居中,在我的编辑位置上方和下方保留相等的上下文。这与 Eglot 使用的默认文档“弹出窗口”冲突,后者每次显示文档时都会更改编辑窗口的大小。幸运的是,通过将
eldoc-echo-area-use-multiline-p设置为nil,这很容易解决。采用该设置后,我在编辑时只显示一行文档:光标下函数的类型签名。这完美契合我的需求。
Overall, I like the idea of the Eglot-and-HLS experience. The additional formatting it adds to the buffer is not intrusive. Some of the hints and code actions are convenient. The one drawback I have trouble adjusting to is that it does add latency to editing in a way that sometimes really messes with me. I am used to Vim commands that zoom me around the editing buffer, but I can’t get them to fire as reliably when Eglot is enabled. I’m not sure why, but it’s bad enough to prevent me from running Eglot/HLS in other projects.
总的来说,我很喜欢 Eglot 配合 HLS 这种体验的构想。它为缓冲区增加的额外格式化并不唐突。一些提示和代码操作也很方便。我难以适应的一个缺点是,它确实增加了编辑延迟,有时这真的会弄乱我的节奏。我习惯了用 Vim 命令在编辑缓冲区中快速穿梭,但当启用 Eglot 时,我无法让这些命令像以前那样可靠地触发。我不确定原因是什么,但这已经糟糕到足以阻止我在其他项目运行 Eglot/HLS 的程度了。
If I could take the time to go on, I’d play around with it a little more to get a better understanding of its sharp corners. I’d also read the manual instead of improvising. The manual tends to be a good way to learn about sharp corners and how to sand them.
如果我有时间继续深入,我会再摆弄它一会儿,以便更好地理解它的棘手之处(sharp corners)。我也会去阅读手册,而不是即兴发挥。手册往往是了解棘手之处以及如何将其磨平的好途径。
使用 Ghcid 和 Foreign-Store 进行实时重载 (Live Reload with Ghcid and Foreign-Store)
One of the drawbacks of using ghci (and consequently ghcid) for live reload is that ghci can only track one set of source files at a time. This means that if the project is properly set up with distinct components for executable, library, and test code, ghci can only reload one of those components. To apply changes to any of the other components, the entire ghci process must be restarted.
使用 ghci(因此也包括
ghcid)进行实时重载的缺点之一是,ghci 一次只能跟踪一组源文件。这意味着,如果项目正确地配置了可执行文件、库和测试代码的独特组件,ghci 将只能重新加载其中一个组件。要将更改应用到任何其他组件,必须重启整个 ghci 进程。
Since part of the intended workflow was being able to edit both tests and program code and reload both, we are forced to co-locate all the code into one component. This is a non-starter for serious projects, but acceptable for the kind of toy project I was aiming for – and maybe even for the early evolutionary stages of experiments where this workflow is most powerful.
由于预期工作流的一部分是能够同时编辑测试和程序代码并重新加载两者,我们被迫将所有代码集中放在同一个组件中。这对于严肃的项目来说是行不通的,但对于我所追求的那种玩具项目来说是可以接受的——甚至对于实验的早期演化阶段来说可能也是如此,而这种工作流在那个阶段最具威力。
The code for the part that live-reloads ends up looking something like this, with comments explaining the mechanism.
实现实时重载的部分代码最终看起来大致如下,其中包含了解释其机制的注释。
module Main (main) where
import Control.Concurrent
import Control.Concurrent.Async
import Control.Exception (bracketOnError)
import Data.IORef
import Foreign.Store
import Test.Hspec (describe, hspec, it)
import Test.Hspec.QuickCheck (prop)
-- The main function runs on reload, and has the
-- effect of first running the tests. If they
-- succeed, it runs the update function which makes
-- sure to restart the processing thread while
-- reusing resources.
main :: IO ()
main = do
spec
update
-- Unit tests in a mix of example-based and property
-- tests. These are written instead of poking about
-- in the REPL.
spec :: IO ()
spec = hspec $ do
describe "forward euler solution" $ do
prop "ever increases with example study" $ \x y ->
-- ...
-- A function that must start our process if it is
-- not running, or restart it (and reuse its
-- resources) if it is running.
update :: IO ()
update =
let
-- This contains an MVar with the resources
-- needed to run SDL. It also serves as a lock
-- that prevents multiple processes from running
-- simultaneously.
resourceStore = Store 0
-- This contains the Async of the process thread,
-- for cancelling during an update.
asyncStore = Store 1
-- Wait for resources to be available, reserve
-- them, and start a new thread that uses them.
withResources action = do
withStore resourceStore $ \resources -> async $
-- Catch async exceptions during execution,
-- such as when this thread is cancelled by
-- the update procedure. The exception
-- already causes the action function to stop
-- running, so then we release its resources.
bracketOnError
(putStrLn "Running action." >> takeMVar resources)
(\res -> putStrLn "Action interrupted!" >> putMVar resources res)
action
start = do
-- Waits for resources and then runs the render
-- loop with them.
withResources $ \(window, renderer, texture) -> do
renderLoop renderer texture (State study Nothing)
putStrLn "Render loop exited naturally."
-- Getting here in the control flow means the
-- render loop terminated but not through an
-- async exception. That implies the user
-- requested an exit, e.g. by closing the
-- window. Thus we should destroy resources
-- rather than release them back for reuse.
destroySdl window renderer texture
-- We also need to delete all stores so the
-- next time the update function is called,
-- it sees a blank slate and recreates
-- everything all over.
deleteStore asyncStore
deleteStore resourceStore
in do
lookupStore (case asyncStore of Store i -> i) >>= \case
Nothing -> do
-- If the thread id store does not exist, it
-- means we're starting from a blank slate.
-- We should initialise resources fresh and
-- start a new thread to use them.
initialiseSdl >>= void . storeAction (Store 0) . newMVar
start >>= void . storeAction (Store 1) . newIORef
Just tidStore ->
-- If the thread id store exists, we cancel
-- the thread its referencing, which will
-- return the resources it used, and then we
-- start a new thread. The new thread picks
-- whatever resources were in store.
withStore tidStore $ \ref -> do
readIORef ref >>= cancel
start >>= writeIORef ref
I initially tried using the higher-level Rapid library for this, but I couldn’t get it to work properly. I found it much easier to get stability, resource clean-up, and support for stdout in all sub-processes when I implemented the plumbing myself with IORefs and Async.
我最初尝试为此使用更高级的
Rapid库,但我无法让它正常工作。我发现当我用IORef和Async自行实现这些底层支撑逻辑时,要获得稳定性、资源清理以及所有子进程对标准输出(stdout)的支持要容易得多。
The following invocation launches ghcid in a way that automatically reloads and runs the development main function:
以下调用启动
ghcid的方式可以自动重新加载并运行开发主函数:
ghcid --command "cabal repl --repl-options='-fobject-code -ferror-spans -fdiagnostics-color=always'" \
--reverse-errors \
--reload src \
--restart diffeq.cabal \
--test Main
This is the result of accretion-by-confusion. I couldn’t get something to work, so I tried another command line argument, and that happened a few times in a row. This is probably not the ideal way to write this command, but it seems to work and for now I want to play around with it and see how convenient it is, before I spend more time on it.
这是“困惑中不断堆砌(accretion-by-confusion)”的产物。有些功能我无法正常运作,于是我尝试了另一个命令行参数,这种情况连续发生了好几次。编写这条命令的方式可能并不是最理想的,但它似乎能用,目前我想先折腾它一下,看看它到底有多方便,然后再花更多时间去优化它。
However, at first I couldn’t get this to work at all. The ghcid process started all right, but it didn’t reload when code changed. It didn’t tell me why either. I actually gave up on using ghcid because I couldn’t get it to work, and then by accident I ran this command in a Nix development shell and it worked. I have no idea what’s going on. I thought direnv would make it unnecessary for me to first start a Nix development shell, but in this case it seems not.
然而,起初我根本无法让它运行起来。
ghcid进程启动得很正常,但当代码更改时它并不重新加载。它也没有告诉我原因。实际上,因为无法让它工作,我都打算放弃使用ghcid了,然后偶然间我在 Nix 开发 shell 中运行了这条命令,它居然成功了。我完全不知道是怎么回事。我原以为direnv会让我免于首先启动 Nix 开发 shell,但在这种情况下似乎并非如此。
So far I really like the ghcid-and-foreign-store experience for this type of programming. Instead of creating a complicated GUI for defining differential equations and initial conditions and whatnot, I can change constants in the code and the visualisation updates. When I learn new things in the book I’m reading, I can add code for them and the visualisation updates.
到目前为止,对于这种类型的编程,我非常喜欢
ghcid配合foreign-store的体验。我无需创建复杂的 GUI 来定义微分方程、初始条件之类的内容,只需更改代码中的常量,可视化便会自动更新。当我学到正在阅读的书中的新知识时,我可以为它们添加代码,可视化也会随之更新。