Clojure in the age of language models

原始链接: https://yogthos.net/posts/2026-10-07-clojure-llms.html

相关文章

原文

In the age of generative models, the most important skill for a developer is to be able to recognize the shape of the problem and pick the correct way to express it. What's relevant today is the ability to do high level reasoning about algorithms, data structures, and data flows within the system. Imperative programming is quickly becoming akin to writing assembly because language models are quite competent at writing code in the small, while they stumble at high level design and architecture. So it is good to learn a language that operates at a higher level, such as Clojure, which is data-centric, composes functions declaratively, and keeps code close to the shape of the problem.

LLMs can generate code much quicker than I can, but the issue is how to test that the generated code does what I want. Before you can test anything in many languages, you have to recompile the program, and that may take quite a few minutes for larger projects. The length of that feedback cycle, in turn, sets the pace of your progress.

We also need to talk about the tedious reality of rebuilding application state. That matters even more when you are not writing the code yourself, so the output is inherently less intentional. Clojure collapses that loop because our workflow does not draw a hard line between when code is read, when it is compiled, and when it runs. Clojure runs in a live process, and you can redefine a function at the REPL and have the new version take effect immediately, without restarting. State stays in place while you change the code that operates on it. Inspecting the state to reproduce behaviors and verify fixes can be done instantly when you can reach into a running process.

An agent can similarly connect to a REPL to diagnose an issue and swap out the code without any downtime. Agents fundamentally need observability in order to get useful feedback about the changes they are making. Working with the REPL means the agent doesn’t have to go through all the steps of compiling and rebuilding the app, then logging its output to see the result. That translates into having to do fewer iterations to reach a working system, which becomes particularly valuable when working with a large codebase. The functionality of your system can keep evolving as you load new code into the running process without any restarts.

Another advantage comes from immutability, which helps reliably control the operating context of the program environment. When the majority of the logic in an application is written using pure functions, the agent can safely consider and test pieces in isolation without having to reason about the entire program. Agents can write functions one at a time, test them in the running program instead of making a whole bunch of changes, then running tests to find out if they worked.

At this point, I find anything with a compile cycle is a nonstarter if I have an option to have a live programming environment. It is not just compile and startup time that ends up being painful. A bigger problem is the tedious necessity of having to reconstruct the desired state every single run. When you have something small with limited functionality, that is fine, but as your application grows, rebuilding the state can take a significant effort. You might have to click through some menus in the user interface, wait for data to be processed from a service or a database, and so on. Being able to put your application in a particular state and make changes within that context is just a qualitatively better development experience.

Then there's the powerful macro system , allowing you to adapt the language to the problem domain, eliminating a lot of boilerplate code you would have to write otherwise. What makes this particularly powerful in Clojure is homoiconic syntax, where code is written using data structure literals. Since there is a common syntax for expressing both logic and data, a program can take any piece of code and manipulate it as it would with any other data structure, then evaluate it. This makes it incredibly easy to add new semantics because all you need is to make templates out of code. A macro works much like a function that accepts the code you wrote and produces the form that actually gets evaluated. The expression for printing a string does the printing when you evaluate it, but it is also nothing more than a list of the println symbol and the string itself.

One major advantage of S-expression based syntax is that it's easy for both humans and machines to read. And since the state can be trivially serialized as plain data structures, an agent can dump it in the REPL to inspect what’s happening in the application at any time. The data orientated nature of the language is a natural fit for LLMs since these models operate on text, making it exceptionally easy for them to see how data flows through the system without any opaque object graphs to worry about.

Furthermore, all the functions operate on a common set of data structures, allowing you to compose them together to transform data like Lego blocks. Clojure programs tend to be far more concise since the code is largely written through declarative composition of functions from the standard library, which encapsulate the implementation details. The result is that a codebase tends to be much shorter, leaving far less code to repeat. This conciseness matters since a smaller program costs fewer tokens, and fewer tokens leave more room in the context, making the language friendlier for local models.

In my experience, models lacking the broader context while making changes in a piece of code is one of the most common failure cases. Having a terse syntax means that more relevant code lives directly in the context window, directly addressing the problem. The model gets a significantly better view of what you are trying to do and can make much better decisions. If the whole call graph is sitting in the context window, the model sees how all the pieces fit together.

All these features combine to make a perfect environment for the agent to work in. Expressive syntax leads to less code repetition. Macros let you fold repeated patterns into new domain specific constructs. Code itself is just structured data that can be inspected and transformed. And the REPL ties it all together, providing you with a living system that evolves along with your code.

There are, however, a few drawbacks to the Java virtual machine, which Clojure traditionally runs on. From my experience, many developers, fairly or not, have an issue with requiring the JVM, and shy away from Clojure because of startup time, a somewhat heavyweight runtime, and perceived bootstrapping complexity.

One goal for Jolt in particular is to get more interest from outside the existing Clojure community by addressing these concerns. The compiler is a single binary, and it ships with all the tooling, such as dependency management and task running, baked in. It interops seamlessly with the native ecosystem via FFI, so you can use it in a way comparable to Python. Best of all, program distribution involves building a standalone binary similarly to Go. Thus, Jolt may remove the last big source of friction for trying Clojure.

In an age where writing code is cheap but verification and iteration remain expensive, the high level declarative style lines up exactly with what both agents and humans need to produce working code. Clojure is a great language to learn today because of the unique way it fits the era of language models.

联系我们 contact @ memedata.com