logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·31.10.26
← Back to news

A curmudgeon tries a language server

Original article
entropicthoughts.com|82 points|53 comments|by crescit_eundo|Aug 26, 2026

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:

  1. Modify code within the text editor.
  2. Toggle to a terminal window.
  3. 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 →\rightarrow→ 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: Memory Image→Source Code→Version Control\text{Memory Image} \rightarrow \text{Source Code} \rightarrow \text{Version Control}Memory Image→Source Code→Version Control.

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 like EitherT.

Fortunately, Haskell's robust type system means we encounter far fewer runtime exceptions than Lisp might.

The Toolchain

ToolPurposeBenefit
HLSHaskell Language ServerHigh-quality code introspection
EglotEmacs LSP ClientBuilt-in communication with HLS
ghcidCompiler WatcherImmediate recompilation on save
foreign-store / RapidState ManagementMaintains 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:

  1. Write logic or experiments as unit tests.
  2. Save changes.
  3. ghcid reloads, tests, and restarts the app.
  4. 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: dydt=f(t,y)\frac{dy}{dt} = f(t, y)dtdy​=f(t,y)

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.

Conceptual Image of Differential Equation Plot

-- 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!"