A curmudgeon tries a language server
A Curmudgeon's Experiment with Language Servers
By kqr | Published: 2026-08-25 | Tags: #haskell #programming
For roughly a decade, my approach to writing code has remained stagnant. I write code the same way I did ten years ago. Well, that was the case when I began drafting this piece.
Since then, I've integrated "robots" (AI) to handle a significant portion of the actual typing, provided that the project isn't one where I am specifically trying to learn something or where code quality is the primary objective.
The Traditional Cycle
My legacy workflow followed a rigid, linear path:
- Modify code within the text editor.
- Toggle to a terminal window.
- Execute a command to compile and run the application.
If the behavior was ambiguous, I would either inject tracer prints and restart, or launch the program within a debugger. If an exception caused a crash, the cycle was: Fix Restart.
The Lisp Ideal
Lisp development operates on a fundamentally different philosophy. Instead of the "compile-run" loop, developers interact directly with a REPL (Read-Eval-Print Loop) to modify a live, running system.
Note: In practice, a Lisp programmer likely writes code in an editor scratchpad and "sends" it to the REPL, but the effect is as if they are typing directly into the heart of the process.
Why Lisp is Different:
- No Context Switching: You are already inside the program process.
- No Compilation Wait: Code is hot-swapped while running.
- No Debugger Restarts: You can inspect any state in real-time.
- Resilient Exceptions: The condition system allows a developer to patch a bug and resume execution from the point of failure without restarting the stack.
This leads to a strange phenomenon: early-stage Lisp projects often have no source code. The system exists solely as a memory image.
The Database Analogy
To understand this, consider how relational databases are often handled at the start of a project:
- Phase 1: The schema evolves dynamically within the running database (no version-controlled file).
- Phase 2: Once mature, the schema is dumped to a file, placed under version control, and managed via explicit migrations.
Common Lisp follows this same trajectory: .
Adapting the Workflow to Haskell
As a non-Lisp developer, I wondered if I could approximate this "live" feeling in Haskell. I started by auditing what was possible:
- Better Introspection: Use a Language Server.
- Rapid Iteration: Automate recompilation.
- State Persistence: Keep data across restarts.
- True Process Residency:
Impossible in Haskell. - Lisp-style Condition System:
Not supported by common libraries likeEitherT.
Fortunately, Haskell's robust type system means we encounter far fewer runtime exceptions than Lisp might.
The Toolchain
| Tool | Purpose | Benefit |
|---|---|---|
HLS | Haskell Language Server | High-quality code introspection |
Eglot | Emacs LSP Client | Built-in communication with HLS |
ghcid | Compiler Watcher | Immediate recompilation on save |
foreign-store / Rapid | State Management | Maintains process state across code replacements |
The New "Pseudo-Lisp" Loop
By combining ghcid with foreign-store, I can automatically recompile and restart the program without losing critical state. While I cannot evaluate code in the running program's REPL, I can substitute REPL experimentation with unit tests.
The resulting workflow:
- Write logic or experiments as unit tests.
- Save changes.
ghcidreloads, tests, and restarts the app.- State remains intact via
foreign-store.
Putting it to the Test: A Toy Project
To see if this worked in practice, I applied it to a project aimed at improving my understanding of non-linear dynamics and differential equations.
I've been studying Differential Equations (Blanchard, Devaney, Hall; 2011), which emphasizes computational methods. This is a departure from traditional academic courses that focus on analytic solutions. In the professional world, we solve these computationally because calculus often cannot provide a closed-form solution.
For example, solving a system where the rate of change is defined by:
I used the book's accompanying software as a guide to build my own implementation, utilizing my new Haskell workflow to iterate on the mathematical models.
-- Example of a simple test case for ghcid to run
testDynamics :: IO ()
testDynamics = do
let result = solveODE initialCondition timeStep
if result == expected
then putStrLn "Success!"
else error "Dynamics diverged!"