Universal Infrastructure: Solving the Portability Gap with BigConfig

The primary challenge with Terraform and Ansible has always been portability. It is notoriously difficult to take a solution written for one environment and apply it to another without significant manual adjustments. Kubernetes achieved its massive success by leveling this playing field. With tools like Helm, you have a package manager that allows you to install applications without worrying about whether you are on-prem or using a specific hyperscaler.

However, in the world of Kubernetes, a common sentiment is to avoid stateful applications like databases unless you have mastered every technical nuance. Most internal platforms use Kubernetes for stateless services while relying on managed databases provided by the hyperscaler.

BigConfig Package changes this dynamic by enabling the creation of universal applications that are both stateful and stateless. Walter , the first application built with BigConfig, demonstrates this by deploying seamlessly across Oracle Cloud and Hetzner.

The Portability Challenge

Infrastructure is rarely uniform. When moving between providers, you encounter several inconsistencies:

  • The IP address property is named differently depending on the provider.
  • The default SSH user varies.
  • The UID of the default user is often inconsistent.

The solution involves defining a standardized schema for output parameters in every main.tf file:

Gluing Infrastructure to Configuration

This approach effectively glues the OpenTofu infrastructure step to the Ansible configuration step. By parsing the JSON output from the infrastructure layer, we can pass critical connection data directly into the workflow:

(defn opts-fn
[opts]
(let [dir (workflow/path opts ::tools/tofu)]
(merge-with merge opts {::workflow/params (if (fs/exists? dir)
(-> (p/shell {:dir dir
:out :string} "tofu output --json")
:out
(json/parse-string keyword)
(->> (s/select-one [:params :value])))
{:ip "192.168.0.1"
:sudoer "ubuntu"})})))

As long as the OpenTofu step adheres to the schema by providing an ip, sudoer, and uid, any new hyperscaler can be integrated into this BigConfig Package.

Handling Distribution Differences

What about variations in Linux distributions? This can be handled within Ansible or, similar to our Terraform approach, by using different source files based on the distribution.

(defn tofu
[step-fns opts]
(let [opts (workflow/prepare {::workflow/name ::tofu
::render/templates [{:template (keyword->path ::tofu)
:overwrite true
:hyperscaler "hcloud"
:transform [["{{ hyperscaler }}"]]}]}
opts)]
(workflow/run-steps step-fns opts)))

The power lies in the dynamic folder pathing. By using the template variable "{{ hyperscaler }}" for the hyperscaler, the directory containing the infrastructure code becomes dynamic. This allows us to share core Ansible logic while diverging where necessary, ensuring the code remains clean and manageable.

Conclusion

By standardizing the handshake between infrastructure provisioning and configuration management, BigConfig Package removes the friction typically found in multi-cloud deployments. This modular approach ensures that your automation remains truly portable, allowing stateful workloads to run wherever they are needed most without being locked into a single provider’s ecosystem.

Would you like to have a follow-up on this topic? What are your thoughts? I’d love to hear your experiences.

BigConfig Package Walter Fork this package or use the package template.

Permalink

Just What IS Clojure, Anyway?

Look at this line of code:

processCustomerOrder(customer, orderItems)

Any developer with six months of experience knows roughly what that does. The name is explicit, the structure is familiar, the intent is readable. Now look at this:

(reduce + (map f xs))

The reaction most developers have is immediate and unfavourable. Parentheses everywhere. No obvious structure. It looks less like a programming language and more like a typographer's accident. The old joke writes itself: LISP stands for Lost In Stupid Parentheses.

That joke is, technically, a backronym. John McCarthy named it LISP as a contraction of LISt Processing when he created it in 1958. The sardonic expansion came later, coined by programmers who had opinions about the aesthetic choices involved. Those opinions have not mellowed with time.

And yet Clojure – a modern descendant of Lisp – ranked as one of the highest-paying languages in the Stack Overflow Developer Survey for several consecutive years around 2019. Developers walked away from stable Java and C# positions to build production systems in it. A Brazilian fintech used it to serve tens of millions of customers. Something requires explaining.

The ancestry: Lisp reborn

Clojure only makes sense against the background of Lisp, and Lisp only makes sense as what it actually was: not merely a programming language, but a direct implementation of mathematical ideas about computation.

McCarthy's 1958 creation introduced concepts that took the rest of the industry decades to absorb. Garbage collection, conditional expressions, functional programming, symbolic computation – all present in Lisp before most working developers today were born. Many programmers encounter Lisp's descendants daily without being aware of it.

The defining feature is the S-expression:

(+ 1 2)

Everything is written as a list. This is not merely a syntactic preference. Because code and data share the same underlying structure, a Lisp program can manipulate other programs directly. This property – homoiconicity – is the technical foundation of Lisp macros: code that generates and transforms other code at compile time, with a flexibility that few conventional infix languages match. It is the reason serious Lisp practitioners regard the syntax not as a historical curiosity but as a genuine technical advantage.

Lisp also, however, developed a reputation for producing work that individual experts could write brilliantly and teams could not maintain at all. The tension between expressive power and collective readability never fully resolved. Clojure inherits this tradition knowingly, and is aware of the cost.

What Clojure actually is

Rich Hickey created Clojure in 2007. His central design decision was not to build a new runtime from scratch but to attach Lisp to an existing ecosystem.

Layer Technology
Runtime JVM
Libraries Java ecosystem
Language model Lisp

This host strategy gave Clojure immediate access to decades of mature Java libraries without needing to rebuild any of them. A Clojure developer can call Java code directly. The same logic drove two later variants: ClojureScript, which compiles to JavaScript and found real traction in teams already working with React, and ClojureCLR, which runs on .NET. Rather than fight the unwinnable battle of building its own ecosystem from scratch, Clojure attached itself to three of the largest ones that already existed.

Clojure does not attempt to displace existing ecosystems. It operates inside them.

Central to how Clojure development actually works is the REPL – Read–Eval–Print Loop. Rather than the standard write–compile–run–crash cycle, developers send code fragments to a running system and modify it live. Functions are redefined while the application continues executing. For experienced practitioners this is a material productivity difference: the feedback loop is short, and the distance between an idea and a tested result is small. Experienced Clojure developers report unusually low defect rates, a claim that is plausible given the constraints immutability places on the ways a programme can fail.

The Hickey doctrine: simple versus easy

Hickey's 2011 Strange Loop talk Simple Made Easy is the philosophical engine behind every design choice in Clojure. It draws a distinction that most language design ignores.

Term Meaning
Easy Familiar; close to what you already know
Simple Not intertwined; concerns kept separate

Most languages pursue easy. They aim to resemble natural language, minimise cognitive friction at the point of learning, and reduce the effort required to write the first working programme. This also means that languages favoured by human readers tend to be the hardest for which to write parsers and compilers.

Clojure instead pursues simple. Its goal is to minimise tangled interdependencies in the resulting system, even at the cost of an unfamiliar surface. Writing parsers for Lisps is comparatively straightforward, at the cost of human readability.

Hickey's specific target is what he calls place-oriented programming: the treatment of variables as named locations in memory whose values change over time – mutability, in more formal terms. His argument is that conflating a value with a location generates incidental complexity at scale, particularly in concurrent systems. When you cannot be certain what a variable contains at a given moment, reasoning about a programme becomes difficult in proportion to the programme's size.

The design of Clojure follows directly from this diagnosis. Immutable data, functional composition, minimal syntax, and data structures in place of object hierarchies are all consequences of the same underlying position. The language may not feel easy. The resulting systems are intended to be genuinely simpler to reason about.

The real innovation: data and immutability

Clojure's core model is data-oriented. Rather than building class hierarchies, programmes pass simple structures through functions:

(assoc {:name "Alice" :age 30} :city "London")

This creates a new map. The original is untouched. That is the default behaviour across all of Clojure's data structures – values do not change; new versions are produced instead.

This is made practical by persistent data structures, which use structural sharing. When a new version of a data structure is produced, it shares most of its internal memory with the previous version rather than copying it entirely. The comparison that makes this intuitive for most developers: Git does not delete your previous commits when you push a new one. It stores only the difference, referencing unchanged content from before. Clojure applies the same principle to in-memory data.

The consequence for concurrency results directly from this. Race conditions require mutable shared state. If data cannot be mutated, the precondition for the most common class of concurrency bug does not exist. This was Clojure's most compelling practical argument during the multicore boom of the 2010s, when writing correct concurrent code had become a routine industrial concern rather than a specialist one. Clojure let developers eliminate that entire class of problem.

The functional programming wave – and why easy beat rigorous

Between roughly 2012 and 2020, functional programming moved from academic discussion to genuine industry interest. The drivers were concrete: multicore processors created pressure to write concurrent code correctly; distributed data systems required reasoning about transformation pipelines rather than mutable state; and the sheer complexity of large-scale software made the promise of mathematical rigour appealing.

Clojure was among the most visible representatives of this movement, alongside Haskell, Scala, and F#. Conference talks filled. Engineering blogs ran long series on immutability and monads. For a period it seemed plausible that functional languages might displace the mainstream ones.

What actually happened was different. Mainstream languages absorbed the useful ideas and continued. And the majority of working programmers, it turned out, rarely needed to reason about threading and concurrency at all.

Java gained streams and lambdas in Java 8. JavaScript acquired map, filter, and reduce as first-class patterns, and React popularised unidirectional data flow. C# extended its functional capabilities across successive versions. Rust built immutability and ownership into its type system from the outset. The industry did not convert to functional programming – it extracted what it needed and kept the syntax it already knew.

A developer who can obtain most of functional programming's benefits inside a language they already know will rarely conclude that switching entirely is justified.

The deeper reason functional languages lost the mainstream argument is not technical. It is sociological. Python won because it is, in the most precise sense, the Visual Basic of the current era. That comparison is not an insult – Visual Basic dominated the 1990s because it made programming accessible to people who had no intention of becoming professional developers, and that accessibility produced an enormous, self-reinforcing community. Python did exactly the same thing for data scientists, academics, hobbyists, and beginners, and for precisely the same reason: it is easy to learn, forgiving of error, and immediately rewarding to write. Network effects took care of the rest. Libraries multiplied. Courses proliferated. Employers specified it. The ecosystem became self-sustaining.

Clojure is the antithesis of this process. It is a language for connoisseurs – genuinely, not dismissively. Its internal consistency is elegant, its theoretical foundations are sound, and developers who master it frequently describe it with something approaching aesthetic appreciation. Mathematical beauty, however, has never been a reliable route to mass adoption. Narrow appeal does not generate network effects. And Clojure, by design, operates as something of a lone wolf: it rides atop the JVM rather than integrating natively with the broader currents of modern computing – the web-first tooling, the AI infrastructure, the vast collaborative ecosystems built around Python and JavaScript. At a moment when the decisive advantages in software development come from connectivity, interoperability, and the accumulated weight of shared tooling, a language that demands a clean break from everything a developer already knows is swimming directly against the tide.

Compare this with Kotlin or TypeScript, both of which succeeded in part because they offered a graduated path. A developer new to Kotlin can write essentially Java-style code and improve incrementally. A developer new to TypeScript can begin with plain JavaScript and add types as confidence grows. Both languages have, in effect, a beginner mode. Clojure has no such thing. You either think in Lisp or you do not write Clojure at all.

Where Clojure succeeded

Despite remaining a specialist language, Clojure has real industrial presence.

The most prominent example is Nubank, a Brazilian fintech that reached a valuation of approximately $45 billion at its NYSE listing in December 2021. Nubank runs significant portions of its backend in Clojure, and in 2020 acquired Cognitect – the company that stewards the language. That acquisition was considerably more than a gesture; it was a statement of long-term commitment from an organisation operating at scale.

ClojureScript found parallel influence in the JavaScript ecosystem. The Reagent and re-frame frameworks attracted serious production use, demonstrating that the Clojure model could be applied to front-end development at scale and not merely to backend data pipelines.

The pattern that emerges from successful Clojure deployments is consistent: small, experienced teams working on data-intensive systems where correctness and concurrency matter more than onboarding speed. That is a narrow niche. It was also, not coincidentally, a well-paid one – for a time.

Verdict: the ideas won

Clojure did not become a mainstream language. By any measure of adoption – survey rankings, job advertisements, GitHub repositories – it remains firmly in specialist territory. Even F#, a functional rival with the full weight of Microsoft's backing, has not broken through.

But the argument Clojure made in 2007 has largely been vindicated. Immutability is now a design principle in Rust, Swift, and Kotlin. Functional composition is standard across modern JavaScript and C#. Data-oriented design has become an explicit architectural pattern in game development and systems programming. The industry did not adopt Clojure, but it has been grateful for Hickey's ideas and has quietly absorbed them.

What did not transfer was the syntax – and behind the syntax lay an economic problem that no philosophical vindication could resolve.

A CTO evaluating a language does not ask only whether it is technically sound. The questions are: how large is the available talent pool? How long does onboarding take? What happens when a key developer leaves? Clojure's answers to all three were uncomfortable.

There is a further cost that rarely appears in language comparisons. A developer with ten years of experience in Java, C#, or Python carries genuine accumulated capital: hard-won familiarity with idioms, libraries, failure modes, and tooling. Switching to a Lisp-derived language does not extend that knowledge – it resets it. Clojure keeps the JVM underneath but discards almost everything a developer has learned about how to structure solutions idiomatically. The ten-year veteran spends their first six months feeling like a junior again. Recursion replaces loops. Immutable pipelines replace stateful objects. The mental models that took years to build are, at best, partially transferable. That cost is real and largely invisible in adoption discussions, and it falls on precisely the experienced developers an organisation most wants to retain. Knowledge compounds most effectively when it is built upon incrementally. Clojure does not permit that. It demands a clean break, and most organisations and most developers are not willing to pay that price.

The high wages Clojure commanded were not, from a management perspective, a straightforward mark of quality. They were also a warning of risk. They reflected something less flattering than productivity: the classic dynamic of the expert who becomes indispensable by writing systems that only they can maintain. At its worst this approaches a form of institutional capture – a codebase so entangled with one person's idiom that replacing them becomes prohibitively expensive, something uncomfortably close to ransomware in its commercial effect.

That position has been further undermined by the rise of agentic coding tools. The practical value of writing in a mainstream language has quietly increased, because AI coding assistants are trained on the accumulated body of code that exists – and that body is overwhelmingly Python, JavaScript, Java, and C#. The effect is concrete: ask a capable model to produce a complex data transformation in Python and it draws on an enormous foundation of high-quality examples. Ask it to do the same in idiomatic Clojure and the results are less reliable, the suggestions thinner, the tooling shallower. A language's effective learnability in 2026 is no longer a matter only of human cognition; it is also a function of training density. Niche languages are niche in the training data too, and that gap compounds. The expert moat – already questionable on organisational grounds – is being drained from two directions at once.

Clojure's ideas spread quietly through the languages that absorbed them and left the parentheses behind. Its practitioners, once among the best-paid developers in the industry, now find that the scarcity premium they commanded rested partly on barriers that no longer hold.

The language was right about the future of programming. It simply will not be present when that future arrives.

So, just what is Clojure, anyway? It is a language that was correct about the most important questions in software design, arrived a decade before the industry was ready to hear the answers, and expressed those answers in a notation the industry was never willing to learn. That is not a small thing. It is also not enough.

This article is part of an ongoing series examining what programming languages actually are and why they matter.

Language Argument
C The irreplaceable foundation
Python The approachable language
Rust Safe systems programming
Clojure Powerful ideas, niche language

Coming next: Zig, Odin, and Nim – three languages that think C's job could be done better, and have very different ideas about how.

Permalink

OSS updates January and February 2026

In this post I&aposll give updates about open source I worked on during January and February 2026.

To see previous OSS updates, go here.

Sponsors

I&aposd like to thank all the sponsors and contributors that make this work possible. Without you, the below projects would not be as mature or wouldn&apost exist or be maintained at all! So a sincere thank you to everyone who contributes to the sustainability of these projects.

gratitude

Current top tier sponsors:

Open the details section for more info about sponsoring.

Sponsor info

If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. Thank you!

Updates

Babashka conf and Dutch Clojure Days 2026

Babashka Conf 2026 is happening on May 8th in the OBA Oosterdok library in Amsterdam! David Nolen, primary maintainer of ClojureScript, will be our keynote speaker! We&aposre excited to have Nubank, Exoscale, Bob and Itonomi as sponsors. Wendy Randolph will be our event host / MC / speaker liaison :-). The CfP is now closed. More information here. Get your ticket via Meetup.com (there is a waiting list, but more places may become available). The day after babashka conf, Dutch Clojure Days 2026 will be happening, so you can enjoy a whole weekend of Clojure in Amsterdam. Hope to see many of you there!

Projects

I spent a lot of time making SCI&aposs deftype, case, and macroexpand-1 match JVM Clojure more closely. As a result, libraries like riddley, cloverage, specter, editscript, and compliment now work in babashka.

After seeing charm.clj, a terminal UI library, I decided to incorporate JLine3 into babashka so people can build terminal UIs. Since I had JLine anyway, I also gave babashka&aposs console REPL a major upgrade with multi-line editing, tab completion, ghost text, and persistent history. A next goal is to run rebel-readline + nREPL from source in babashka, but that&aposs still work in progress (e.g. the compliment PR is still pending).

I&aposve been working on async/await support for ClojureScript (CLJS-3470), inspired by how squint handles it. I also implemented it in SCI (scittle, nbb etc. use SCI as a library), though the approach there is different since SCI is an interpreter.

Last but not least, I started cream, an experimental native binary that runs full JVM Clojure with fast startup using GraalVM&aposs Crema. Unlike babashka, it supports runtime bytecode generation (definterface, deftype, gen-class). It currently depends on a fork of Clojure and GraalVM EA, so it&aposs not production-ready yet.

Here are updates about the projects/libraries I&aposve worked on in the last two months in detail.

  • NEW: cream: Clojure + GraalVM Crema native binary

    • A native binary that runs full JVM Clojure with fast startup, using GraalVM&aposs Crema (RuntimeClassLoading) to enable runtime eval, require, and library loading
    • Unlike babashka, supports definterface, deftype, gen-class, and other constructs that generate JVM bytecode at runtime
    • Can run .java source files directly, as a fast alternative to JBang
    • Cross-platform: Linux, macOS, Windows
  • babashka: native, fast starting Clojure interpreter for scripting.

    • Released 1.12.214 and 1.12.215
    • #1909: add JLine3 for TUI support
    • Console REPL (bb repl) improvements: multi-line editing, tab completion, ghost text, eldoc, doc-at-point (C-x C-d), persistent history
    • Support deftype with map interfaces (e.g. IPersistentMap, ILookup, Associative). Libraries like core.cache and linked now work in babashka.
    • Compatibility with riddley, cloverage, editscript, charm.clj
    • #1299: add new babashka.terminal namespace that exposes tty?
    • Add keyword completions to nREPL and console REPL
    • deftype supports Object + hashCode
    • #1923: support reify with java.time.temporal.TemporalQuery
    • Fix reify with methods returning int/short/byte/float
    • Full changelog
  • SCI: Configurable Clojure/Script interpreter suitable for scripting

    • Released 0.12.51
    • deftype now macroexpands to deftype*, matching JVM Clojure, enabling code walkers like riddley
    • case now macroexpands to JVM-compatible case* format, enabling tools like riddley and cloverage
    • Support async/await in ClojureScript. See docs.
    • Support functional interface adaptation for instance targets
    • Infer type tags from let binding values to binding names
    • defrecord now expands to deftype* (like Clojure), with factory fns emitted directly in the macro expansion
    • macroexpand-1 now accepts an optional env map as first argument
    • Add proxy-super, proxy-call-with-super, update-proxy and proxy-mappings
    • Support #564: this-as in ClojureScript
    • Store current analysis context during macro invocation, enabling tools like riddley to access outer locals
    • Full changelog
  • clj-kondo: static analyzer and linter for Clojure code that sparks joy.
    @jramosg, @tomdl89 and @hugod have been on fire with contributions this period. Six new linters!

    • Released 2026.01.12 and 2026.01.19
    • #2735: NEW linter: :duplicate-refer which warns on duplicate entries in :refer of :require (@jramosg)
    • #2734: NEW linter: :aliased-referred-var, which warns when a var is both referred and accessed via an alias in the same namespace (@jramosg)
    • #2745: NEW linter: :is-message-not-string which warns when clojure.test/is receives a non-string message argument (@jramosg)
    • #2712: NEW linter: :redundant-format to warn when format strings contain no format specifiers (@jramosg)
    • #2709: NEW linter: :redundant-primitive-coercion to warn when primitive coercion functions are applied to expressions already of that type (@hugod)
    • Add new types array, class, inst and type checking support for related functions (@jramosg)
    • Add type checking support for clojure.test functions and macros (@jramosg)
    • #2340: Extend :condition-always-true linter to check first argument of clojure.test/is (@jramosg)
    • #2729: Check for arity mismatch for bound vectors, sets & maps, not just literals (@tomdl89)
    • #2768: NEW linter: :redundant-declare which warns when declare is used after a var is already defined (@jramosg)
    • Add type support for pmap and future-related functions (@jramosg)
    • Upgrade to GraalVM 25
    • Full changelog
  • squint: CLJS syntax to JS compiler @tonsky and @willcohen contributed several improvements this period.

    • Add squint.math, also available as clojure.math namespace
    • #779: Added compare-and-swap!, swap-vals! and reset-vals! (@tonsky)
    • #788: Fixed compilation of dotimes with _ binding (@tonsky)
    • #790: Fixed shuffle not working on lazy sequences (@tonsky)
    • Multiple :require-macros with :refer now accumulate instead of overwriting (@willcohen)
    • Fix emitting negative zero value (-0.0)
    • Fix #792: prn js/undefined as nil
    • Fix #793: fix yield* IIFE
    • Full changelog
  • scittle: Execute Clojure(Script) directly from browser script tags via SCI

    • Support async/await. See docs.
    • Implement js/import not using eval
    • Support this-as
    • nREPL: print #<Promise value> when a promise is evaluated
  • nbb: Scripting in Clojure on Node.js using SCI

    • Support async/await. See docs for syntax.
    • Print promise result value in REPL/nREPL: (js/Promise.resolve 1) ;;=> #<Promise 1>
  • fs - File system utility library for Clojure

    • Released 0.5.31
    • #212: Introduce :keep true option in with-temp-dir
    • #188 copy-tree now throws if src or dest is a symbolic link when not following links (@lread)
    • #201 gzip now accepts source-file Path (@lread)
    • #207 review and update glob and match docstrings (@lread)
  • clerk: Moldable Live Programming for Clojure

    • Fix browse when using random port by passing 0, fixes #801
    • bb now supports editscript
  • neil: A CLI to add common aliases and features to deps.edn-based projects.

    • #258: neil test now exits with non-zero exit code when tests fail
  • cherry: Experimental ClojureScript to ES6 module compiler

    • Multiple :require-macros clauses with :refer now properly accumulate instead of overwriting each other

Contributions to third party projects:

  • ClojureScript:
    • Working on async/await support (CLJS-3470). I also implemented this in SCI, scittle, and nbb.
    • CLJS-3471: fix printing of negative zero
    • CLJS-3472: str on var that is set! returns empty string
  • editscript: Added babashka support, deps.edn for git dep usage, fixed CLJS tests
  • riddley: Added babashka compatibility, clj-kondo config
  • cloverage: Added babashka compatibility, migrated tools.cli from deprecated cli/cli to cli/parse-opts, bumped riddley
  • specter: Added babashka compatibility
  • compliment: Added babashka compatibility (PR #131)
  • rebel-readline: Removed JLine impl class dependencies for babashka compatibility, released 0.1.7
  • Selmer: Namespaced script tag context keys to avoid collisions, removed runtime require of clojure.tools.logging
  • charm.clj: Contributed JLine integration, FFM native terminal interface, babashka and native-image compatibility

Other projects

These are (some of the) other projects I&aposm involved with but little to no activity happened in the past month.

Click for more details - [pod-babashka-go-sqlite3](https://github.com/babashka/pod-babashka-go-sqlite3): A babashka pod for interacting with sqlite3 - [unused-deps](https://github.com/borkdude/unused-deps): Find unused deps in a clojure project - [pod-babashka-fswatcher](https://github.com/babashka/pod-babashka-fswatcher): babashka filewatcher pod - [sci.nrepl](https://github.com/babashka/sci.nrepl): nREPL server for SCI projects that run in the browser - [babashka.nrepl-client](https://github.com/babashka/nrepl-client) - [http-server](https://github.com/babashka/http-server): serve static assets - [sci.configs](https://github.com/babashka/sci.configs): A collection of ready to be used SCI configs. - [http-client](https://github.com/babashka/http-client): babashka's http-client - [html](https://github.com/borkdude/html): Html generation library inspired by squint's html tag - [instaparse-bb](https://github.com/babashka/instaparse-bb): Use instaparse from babashka - [sql pods](https://github.com/babashka/babashka-sql-pods): babashka pods for SQL databases - [rewrite-edn](https://github.com/borkdude/rewrite-edn): Utility lib on top of - [rewrite-clj](https://github.com/clj-commons/rewrite-clj): Rewrite Clojure code and edn - [tools-deps-native](https://github.com/babashka/tools-deps-native) and [tools.bbuild](https://github.com/babashka/tools.bbuild): use tools.deps directly from babashka - [bbin](https://github.com/babashka/bbin): Install any Babashka script or project with one command - [qualify-methods](https://github.com/borkdude/qualify-methods) - Initial release of experimental tool to rewrite instance calls to use fully qualified methods (Clojure 1.12 only) - [tools](https://github.com/borkdude/tools): a set of [bbin](https://github.com/babashka/bbin/) installable scripts - [babashka.json](https://github.com/babashka/json): babashka JSON library/adapter - [speculative](https://github.com/borkdude/speculative) - [squint-macros](https://github.com/squint-cljs/squint-macros): a couple of macros that stand-in for [applied-science/js-interop](https://github.com/applied-science/js-interop) and [promesa](https://github.com/funcool/promesa) to make CLJS projects compatible with squint and/or cherry. - [grasp](https://github.com/borkdude/grasp): Grep Clojure code using clojure.spec regexes - [lein-clj-kondo](https://github.com/clj-kondo/lein-clj-kondo): a leiningen plugin for clj-kondo - [http-kit](https://github.com/http-kit/http-kit): Simple, high-performance event-driven HTTP client+server for Clojure. - [babashka.nrepl](https://github.com/babashka/babashka.nrepl): The nREPL server from babashka as a library, so it can be used from other SCI-based CLIs - [jet](https://github.com/borkdude/jet): CLI to transform between JSON, EDN, YAML and Transit using Clojure - [lein2deps](https://github.com/borkdude/lein2deps): leiningen to deps.edn converter - [cljs-showcase](https://github.com/borkdude/cljs-showcase): Showcase CLJS libs using SCI - [babashka.book](https://github.com/babashka/book): Babashka manual - [pod-babashka-buddy](https://github.com/babashka/pod-babashka-buddy): A pod around buddy core (Cryptographic Api for Clojure). - [gh-release-artifact](https://github.com/borkdude/gh-release-artifact): Upload artifacts to Github releases idempotently - [carve](https://github.com/borkdude/carve) - Remove unused Clojure vars - [4ever-clojure](https://github.com/oxalorg/4ever-clojure) - Pure CLJS version of 4clojure, meant to run forever! - [pod-babashka-lanterna](https://github.com/babashka/pod-babashka-lanterna): Interact with clojure-lanterna from babashka - [joyride](https://github.com/BetterThanTomorrow/joyride): VSCode CLJS scripting and REPL (via [SCI](https://github.com/babashka/sci)) - [clj2el](https://borkdude.github.io/clj2el/): transpile Clojure to elisp - [deflet](https://github.com/borkdude/deflet): make let-expressions REPL-friendly! - [deps.add-lib](https://github.com/borkdude/deps.add-lib): Clojure 1.12's add-lib feature for leiningen and/or other environments without a specific version of the clojure CLI - [edamame](https://github.com/borkdude/edamame): configurable EDN and Clojure parser with location metadata and more - [CLI](https://github.com/babashka/cli): Turn Clojure functions into CLIs! - [quickblog](https://github.com/borkdude/quickblog): light-weight static blog engine for Clojure and babashka - [process](https://github.com/babashka/process): Clojure library for shelling out / spawning sub-processes - [deps.clj](https://github.com/borkdude/deps.clj): A faithful port of the clojure CLI bash script to Clojure - [reagami](https://github.com/borkdude/reagami): A minimal zero-deps Reagent-like for Squint and CLJS - [parmezan](https://github.com/borkdude/parmezan): fixes unbalanced or unexpected parens or other delimiters in Clojure files - [quickdoc](https://github.com/borkdude/quickdoc): Quick and minimal API doc generation for Clojure - [Nextjournal Markdown](https://github.com/nextjournal/markdown)

Permalink

Understanding Elixir's List.to_string

So I came across this (quite old) post on stackoverflow, someone wanted to print a list next to a string and he was struggling with it

16

I would like to print a list along with a string identifier like

list = [1, 2, 3]
IO.puts "list is ", list

This does not work. I have tried few variations like

# this prints only the list, not any strings
IO.inspect list
# using puts which also does

and I was a bit baffled as of why this is an issue at all, just convert your list to a string and print it, this logic should work in any language in any print function, should be simple and straightforward

Elixir surely have a function that convert a lists to string, and it does List.to_string

But to my surprise List.to_string was doing weird things

iex(42)> [232, 137, 178] |> List.to_string
<<195, 168, 194, 137, 194, 178>>
iex(43)> [1, 2, 3] |> List.to_string
<<1, 2, 3>>

That was not what I was expecting, I was expecting a returned value that is similar to what IO.inspect produce

So to verify my expectation, I checked what clojure does, because clojure to me is the epitome of sanity

Clojure 1.12.4
user=> (str '(232 137 178))
"(232 137 178)"
user=> (str '(1 2 3))
"(1 2 3)"

it was more or less what IO.inspect does
it returns something that look like how lists "look like" as code

So next step was more introspection

iex(47)> [232, 137, 178] |> List.to_string |> i
Term
  <<195, 168, 194, 137, 194, 178>>
Data type
  BitString
Byte size
  6
Description
  This is a string: a UTF-8 encoded binary. It's printed with the `<<>>`
  syntax (as opposed to double quotes) because it contains non-printable
  UTF-8 encoded code points (the first non-printable code point being
  `<<194, 137>>`).
Reference modules
  String, :binary
Implemented protocols
  Collectable, IEx.Info, Inspect, JSON.Encoder, List.Chars, String.Chars

What the heck is BitString?

So I kept fiddling with the function, until I finally got it

iex(44)> ["o","m",["z","y"]] |> List.to_string
"omzy"
iex(45)> ["o","m",["z","y"]] |> IO.inspect
["o", "m", ["z", "y"]]
["o", "m", ["z", "y"]]

List.to_string , does not transform a list to a string (preserving its structure), List.to_string flattens a list, take each element and transform it to a UTF-8 code point, and if you try to print that, you will get whatever string those codes points produce

iex(49)> [232, 137, 178] |> List.to_string |> IO.puts
è²
:ok
iex(50)> [91, 50, 51, 50, 44, 32, 49, 51, 55, 44, 32, 49, 55, 56, 93] |> List.to_string |> IO.puts
[232, 137, 178]
:ok

In retrospect this is what the docs says



but well, the docs wasn't telling what I wanted it to say 😀

And oh, before I forget, Elixir have the inspect function, which does exactly was I thought List.to_string would do

iex(53)> [232, 137, 178] |> inspect |> IO.puts
[232, 137, 178]
:ok
iex(54)> ["o","m",["z","y"]] |> inspect |> IO.puts
["o", "m", ["z", "y"]]
:ok

Permalink

I am sorry, but everyone is getting syntax highlighting wrong

Translations: Russian

Syntax highlighting is a tool. It can help you read code faster. Find things quicker. Orient yourself in a large file.

Like any tool, it can be used correctly or incorrectly. Let’s see how to use syntax highlighting to help you work.

Christmas Lights Diarrhea

Most color themes have a unique bright color for literally everything: one for variables, another for language keywords, constants, punctuation, functions, classes, calls, comments, etc.

Sometimes it gets so bad one can’t see the base text color: everything is highlighted. What’s the base text color here?

The problem with that is, if everything is highlighted, nothing stands out. Your eye adapts and considers it a new norm: everything is bright and shiny, and instead of getting separated, it all blends together.

Here’s a quick test. Try to find the function definition here:

and here:

See what I mean?

So yeah, unfortunately, you can’t just highlight everything. You have to make decisions: what is more important, what is less. What should stand out, what shouldn’t.

Highlighting everything is like assigning “top priority” to every task in Linear. It only works if most of the tasks have lesser priorities.

If everything is highlighted, nothing is highlighted.

Enough colors to remember

There are two main use-cases you want your color theme to address:

  1. Look at something and tell what it is by its color (you can tell by reading text, yes, but why do you need syntax highlighting then?)
  2. Search for something. You want to know what to look for (which color).

1 is a direct index lookup: color → type of thing.

2 is a reverse lookup: type of thing → color.

Truth is, most people don’t do these lookups at all. They might think they do, but in reality, they don’t.

Let me illustrate. Before:

After:

Can you see it? I misspelled return for retunr and its color switched from red to purple.

I can’t.

Here’s another test. Close your eyes (not yet! Finish this sentence first) and try to remember what color your color theme uses for class names?

Can you?

If the answer for both questions is “no”, then your color theme is not functional. It might give you comfort (as in—I feel safe. If it’s highlighted, it’s probably code) but you can’t use it as a tool. It doesn’t help you.

What’s the solution? Have an absolute minimum of colors. So little that they all fit in your head at once. For example, my color theme, Alabaster, only uses four:

  • Green for strings
  • Purple for constants
  • Yellow for comments
  • Light blue for top-level definitions

That’s it! And I was able to type it all from memory, too. This minimalism allows me to actually do lookups: if I’m looking for a string, I know it will be green. If I’m looking at something yellow, I know it’s a comment.

Limit the number of different colors to what you can remember.

If you swap green and purple in my editor, it’ll be a catastrophe. If somebody swapped colors in yours, would you even notice?

What should you highlight?

Something there isn’t a lot of. Remember—we want highlights to stand out. That’s why I don’t highlight variables or function calls—they are everywhere, your code is probably 75% variable names and function calls.

I do highlight constants (numbers, strings). These are usually used more sparingly and often are reference points—a lot of logic paths start from constants.

Top-level definitions are another good idea. They give you an idea of a structure quickly.

Punctuation: it helps to separate names from syntax a little bit, and you care about names first, especially when quickly scanning code.

Please, please don’t highlight language keywords. class, function, if, elsestuff like this. You rarely look for them: “where’s that if” is a valid question, but you will be looking not at the if the keyword, but at the condition after it. The condition is the important, distinguishing part. The keyword is not.

Highlight names and constants. Grey out punctuation. Don’t highlight language keywords.

Comments are important

The tradition of using grey for comments comes from the times when people were paid by line. If you have something like

of course you would want to grey it out! This is bullshit text that doesn’t add anything and was written to be ignored.

But for good comments, the situation is opposite. Good comments ADD to the code. They explain something that couldn’t be expressed directly. They are important.

So here’s another controversial idea:

Comments should be highlighted, not hidden away.

Use bold colors, draw attention to them. Don’t shy away. If somebody took the time to tell you something, then you want to read it.

Two types of comments

Another secret nobody is talking about is that there are two types of comments:

  1. Explanations
  2. Disabled code

Most languages don’t distinguish between those, so there’s not much you can do syntax-wise. Sometimes there’s a convention (e.g. -- vs /* */ in SQL), then use it!

Here’s a real example from Clojure codebase that makes perfect use of two types of comments:

Disabled code is gray, explanation is bright yellow

Light or dark?

Per statistics, 70% of developers prefer dark themes. Being in the other 30%, that question always puzzled me. Why?

And I think I have an answer. Here’s a typical dark theme:

and here’s a light one:

On the latter one, colors are way less vibrant. Here, I picked them out for you:

Notice how many colors there are. No one can remember that many.

This is because dark colors are in general less distinguishable and more muddy. Look at Hue scale as we move brightness down:

Basically, in the dark part of the spectrum, you just get fewer colors to play with. There’s no “dark yellow” or good-looking “dark teal”.

Nothing can be done here. There are no magic colors hiding somewhere that have both good contrast on a white background and look good at the same time. By choosing a light theme, you are dooming yourself to a very limited, bad-looking, barely distinguishable set of dark colors.

So it makes sense. Dark themes do look better. Or rather: light ones can’t look good. Science ¯\_(ツ)_/¯

But!

But.

There is one trick you can do, that I don’t see a lot of. Use background colors! Compare:

The first one has nice colors, but the contrast is too low: letters become hard to read.

The second one has good contrast, but you can barely see colors.

The last one has both: high contrast and clean, vibrant colors. Lighter colors are readable even on a white background since they fill a lot more area. Text is the same brightness as in the second example, yet it gives the impression of clearer color. It’s all upside, really.

UI designers know about this trick for a while, but I rarely see it applied in code editors:

If your editor supports choosing background color, give it a try. It might open light themes for you.

Bold and italics

Don’t use. This goes into the same category as too many colors. It’s just another way to highlight something, and you don’t need too many, because you can’t highlight everything.

In theory, you might try to replace colors with typography. Would that work? I don’t know. I haven’t seen any examples.

Using italics and bold instead of colors

Myth of number-based perfection

Some themes pay too much attention to be scientifically uniform. Like, all colors have the same exact lightness, and hues are distributed evenly on a circle.

This could be nice (to know if you have OCD), but in practice, it doesn’t work as well as it sounds:

OkLab l=0.7473 c=0.1253 h=0, 45, 90, 135, 180, 225, 270, 315

The idea of highlighting is to make things stand out. If you make all colors the same lightness and chroma, they will look very similar to each other, and it’ll be hard to tell them apart.

Our eyes are way more sensitive to differences in lightness than in color, and we should use it, not try to negate it.

Let’s design a color theme together

Let’s apply these principles step by step and see where it leads us. We start with the theme from the start of this post:

First, let’s remove highlighting from language keywords and re-introduce base text color:

Next, we remove color from variable usage:

and from function/method invocation:

The thinking is that your code is mostly references to variables and method invocation. If we highlight those, we’ll have to highlight more than 75% of your code.

Notice that we’ve kept variable declarations. These are not as ubiquitous and help you quickly answer a common question: where does thing thing come from?

Next, let’s tone down punctuation:

I prefer to dim it a little bit because it helps names stand out more. Names alone can give you the general idea of what’s going on, and the exact configuration of brackets is rarely equally important.

But you might roll with base color punctuation, too:

Okay, getting close. Let’s highlight comments:

We don’t use red here because you usually need it for squiggly lines and errors.

This is still one color too many, so I unify numbers and strings to both use green:

Finally, let’s rotate colors a bit. We want to respect nesting logic, so function declarations should be brighter (yellow) than variable declarations (blue).

Compare with what we started:

In my opinion, we got a much more workable color theme: it’s easier on the eyes and helps you find stuff faster.

Shameless plug time

I’ve been applying these principles for about 8 years now.

I call this theme Alabaster and I’ve built it a couple of times for the editors I used:

It’s also been ported to many other editors and terminals; the most complete list is probably here. If your editor is not on the list, try searching for it by name—it might be built-in already! I always wondered where these color themes come from, and now I became an author of one (and I still don’t know).

Feel free to use Alabaster as is or build your own theme using the principles outlined in the article—either is fine by me.

As for the principles themselves, they worked out fantastically for me. I’ve never wanted to go back, and just one look at any “traditional” color theme gives me a scare now.

I suspect that the only reason we don’t see more restrained color themes is that people never really thought about it. Well, this is your wake-up call. I hope this will inspire people to use color more deliberately and to change the default way we build and use color themes.

Permalink

Statistics made simple

I have a weird relationship with statistics: on one hand, I try not to look at it too often. Maybe once or twice a year. It’s because analytics is not actionable: what difference does it make if a thousand people saw my article or ten thousand?

I mean, sure, you might try to guess people’s tastes and only write about what’s popular, but that will destroy your soul pretty quickly.

On the other hand, I feel nervous when something is not accounted for, recorded, or saved for future reference. I might not need it now, but what if ten years later I change my mind?

Seeing your readers also helps to know you are not writing into the void. So I really don’t need much, something very basic: the number of readers per day/per article, maybe, would be enough.

Final piece of the puzzle: I self-host my web projects, and I use an old-fashioned web server instead of delegating that task to Nginx.

Static sites are popular and for a good reason: they are fast, lightweight, and fulfil their function. I, on the other hand, might have an unfinished gestalt or two: I want to feel the full power of the computer when serving my web pages, to be able to do fun stuff that is beyond static pages. I need that freedom that comes with a full programming language at your disposal. I want to program my own web server (in Clojure, sorry everybody else).

Existing options

All this led me on a quest for a statistics solution that would uniquely fit my needs. Google Analytics was out: bloated, not privacy-friendly, terrible UX, Google is evil, etc.

What is going on?

Some other JS solution might’ve been possible, but still questionable: SaaS? Paid? Will they be around in 10 years? Self-host? Are their cookies GDPR-compliant? How to count RSS feeds?

Nginx has access logs, so I tried server-side statistics that feed off those (namely, Goatcounter). Easy to set up, but then I needed to create domains for them, manage accounts, monitor the process, and it wasn’t even performant enough on my server/request volume!

My solution

So I ended up building my own. You are welcome to join, if your constraints are similar to mine. This is how it looks:

It’s pretty basic, but does a few things that were important to me.

Setup

Extremely easy to set up. And I mean it as a feature.

Just add our middleware to your Ring stack and get everything automatically: collecting and reporting.

(def app
  (-> routes
    ...
    (ring.middleware.params/wrap-params)
    (ring.middleware.cookies/wrap-cookies)
    ...
    (clj-simple-stats.core/wrap-stats))) ;; <-- just add this

It’s zero setup in the best sense: nothing to configure, nothing to monitor, minimal dependency. It starts to work immediately and doesn’t ask anything from you, ever.

See, you already have your web server, why not reuse all the setup you did for it anyway?

Request types

We distinguish between request types. In my case, I am only interested in live people, so I count them separately from RSS feed requests, favicon requests, redirects, wrong URLs, and bots. Bots are particularly active these days. Gotta get that AI training data from somewhere.

RSS feeds are live people in a sense, so extra work was done to count them properly. Same reader requesting feed.xml 100 times in a day will only count as one request.

Hosted RSS readers often report user count in User-Agent, like this:

Feedly/1.0 (+http://www.feedly.com/fetcher.html; 457 subscribers; like FeedFetcher-Google)

Mozilla/5.0 (compatible; BazQux/2.4; +https://bazqux.com/fetcher; 6 subscribers)

Feedbin feed-id:1373711 - 142 subscribers

My personal respect and thank you to everybody on this list. I see you.

Graphs

Visualization is important, and so is choosing the correct graph type. This is wrong:

Continuous line suggests interpolation. It reads like between 1 visit at 5am and 11 visits at 6am there were points with 2, 3, 5, 9 visits in between. Maybe 5.5 visits even! That is not the case.

This is how a semantically correct version of that graph should look:

Some attention was also paid to having reasonable labels on axes. You won’t see something like 117, 234, 10875. We always choose round numbers appropriate to the scale: 100, 200, 500, 1K etc.

Goes without saying that all graphs have the same vertical scale and syncrhonized horizontal scroll.

Insights

We don’t offer much (as I don’t need much), but you can narrow reports down by page, query, referrer, user agent, and any date slice.

Not implemented (yet)

It would be nice to have some insights into “What was this spike caused by?”

Some basic breakdown by country would be nice. I do have IP addresses (for what they are worth), but I need a way to package GeoIP into some reasonable size (under 1 Mb, preferably; some loss of resolution is okay).

Finally, one thing I am really interested in is “Who wrote about me?” I do have referrers, only question is how to separate signal from noise.

Performance. DuckDB is a sport: it compresses data and runs column queries, so storing extra columns per row doesn’t affect query performance. Still, each dashboard hit is a query across the entire database, which at this moment (~3 years of data) sits around 600 MiB. I definitely need to look into building some pre-calculated aggregates.

One day.

How to get

Head to github.com/tonsky/clj-simple-stats and follow the instructions:

Let me know what you think! Is it usable to you? What could be improved?

Permalink

Call for Proposals. Feb. 2026 Survey

Greetings folks!

Clojurists Together is pleased to announce that we are opening our Q2 2026 funding round for Clojure Open Source Projects. Applications will be accepted through the 19th of March 2026 (midnight Pacific Time). We are looking forward to reviewing your proposals! More information and the application can be found here.

We will be awarding up to $33,000 USD for a total of 5-7 projects. The $2k funding tier is for experimental projects or smaller proposals, whereas the $9k tier is for those that are more established. Projects generally run 3 months, however, the $9K projects can run between 3 and 12 months as needed. We expect projects to start at the beginning of April 2026.

A BIG THANKS to all our members for your continued support. We also want to encourage you to reach out to your colleagues and companies to join Clojurists Together so that we can fund EVEN MORE great projects throughout the year.

And Now the Survey…

We surveyed the community in February to find out what what issues were top of mind and types of initiatives they would like us to focus on for this round of funding. As always, there were a lot of ideas and we hope they will be useful in informing your project proposals.

A number of themes appeared in the survey results.

The biggest theme, by far, was related to adoption and growth of Clojure. Respondents repeatedly mentioned that Clojure is niche, and although they are happy with Clojure, that makes it harder to justify for projects, to find employment, and to persuade others than it is for more popular languages. Respondents want a larger community and wider adoption. In particular, they want more public advocacy for Clojure, including videos, tutorials, public success stories, starter projects, and outreach in general.

Another major theme was AI. Respondents were concerned about AI coding assistants being perceived as having weak Clojure support, and they expressed frustration that Python is perceived as the safe choice for AI despite how well Clojure works with AI tooling. Nonetheless, respondents would like to see more work on tooling, guides, and resources for using AI with Clojure.

ClojureScript and JavaScript interop received the most specific attention. Respondents want CLJS/Cherry/Skittle to provide frictionless support for modern JavaScript standards (ES6 and ESM) and would like an overall simplification of the build and run process.

Developer experience issues came up a number of times, including: confusing error messages, poor documentation, and under-supported libraries. Improvements to any of those would be welcome.

Difficulty finding Clojure employment was another recurring theme. Respondents were not sure how to solve it, but suggested a community job board might be helpful.


February 2026 Survey

88% of respondents use or refer to projects funded by Clojurists Together

time spent q2 2026


communication q2 2026


platform q2 survey


clojure improvements q2 2026


CLojure script improvement q2 2026


Plans for Conference Attendance in 2026 (number of mentions):

  • Clojure/Conj: 8
  • Dutch Clojure Days: 6
  • babashka conference: 5
  • Clojure South: 1
  • Clojure Jam: 1
  • reClojure: 1

If you were only to name ONE, what is the biggest challenge facing Clojure developers today and how can Clojurists Together support you or your organization in addressing those challenges? If you could wave a magic wand and change anything inside the Clojure community, what would it be? (select responses by category).

Adoption:

  • Advertising video like that one on Clojure Conj five years ago or so
  • Language Adoption and popularity, projects that helps to grow the popularity of the language or helps to start programming easily
  • Lack of widespread adoption is not a problem… until you want to convince others that Clojure is a technology you can count on and is worth developing with. Convincing others that Clojure is a great and solid technology that’s here to stay, regardless of low(er) adoption, is sometimes tough.
  • The fact that many teams and project would rule out Clojure as an option, being perceived as niche, far from mainstream, and thus risky
  • I would have more Clojure evangelism. More videos/blog posts/demos around using Clojure, both about whatever is currently at the peak of the broader tech hype cycle – LLMs currently – as well as uses and topics outside of the hype cycle.

AI/LLMs

  • Keeping relevant in a programming market is the top challenge. With the IA, everyone is moving toward the most popular languages. If nobody uses Clojure, it is more difficult to justify its use, no matter how much better it could be.
  • The biggest challenge is the spreading expectation that everything will be done in Python because AI will fix whatever problems Python will allegedly cause.
  • How will Clojure and the Community fare in the light of LLMs and coding assistants?
  • We are being encouraged to use Agentic AI coding assistants, but their support for Clojure is behind that of other languages.
  • How can we articulate the value of Clojure as a sustainable, modern solution when discussion about AI is taking all of the air in the room. We can for example fortify our tooling regards this. Projects such as Calva make Clojure easy to approach for newbies and ClojureMCP is a great tool for Agentic developers.
  • I am unsure about how LLM driven development fits with Clojure. I find myself building some things with JS and Python. For larger projects I am relying on Clojure for it’s correctness properties and lower likelyhood of bugs.
  • Support AI integration Clojure projects

Employment

  • Difficulty finding interesting and reliable work, but this isn’t just Clojure-specific, the whole industry is weird right now.
  • Finding employment writing Clojure code
  • I would say that the biggest challenge for Clojure developers is in the job search. I’m not certain of a solution to this challenge, but perhaps some kind of Clojurists Together Job Board?

Developer Experience

  • Developer tooling improvements competitive with modern JavaScript/TypeScript tooling
  • Missing parts of the data science stack
  • Better documentation of the tools and projects and more tutorials
  • Better integration with cljs/scittle/js/typescript - separate cljs compilation too complicated - scittle/squint/cherry with ES6 integration is the way clojurescript support for ESM libraries. It’s crazy the hoops you have to jump through to use ESM with clojurescript, most people probably assume it’s not possible at all because it’s so difficult.
  • Closer integration with JavaScript/TypeScript tooling -Seamless integration of cljs/cherry/scittle into the js-ecosystem with live repl and load-file support, standard sente/websocket communication included, standard/default solid telemetry/instrumentation API
  • Quicker resolution of outstanding Clojure (JIRA) issues
  • Have a official support program to people that focus on promote the language and/or community instead of library maintainers (like GDE from Google, MVP from Microsoft, Github Stars from Github)
  • I would encourage “cljc” as a default idiom. The linter could say, “This could be a cljc file!” or “Change this to that and suddenly it would be cljc-compatible”.
  • It remains Error Reporting imho, and anyone working on improving it would get my eternal gratitude.

What areas of the Clojure ecosystem need support? (select responses)

  • “I think something around marketing/evangelism; I have worked on several teams using Clojure/ClojureScript that have had to defend the use of Clojure/ClojureScript against more mainstream JVM/JS languages, and the core issue we’ve run up against is a confluence of the following three items:
    – 1. there are more Kotlin/Scala/TypeScript/Java developers than there are Clojure/ClojureScript developers
    – 2. The salary ranges for those languages tends to be lower than that for Clojure/ClojureScript
    – 3. The greatest benefits to be gained from using Clojure/ClojureScript – systems which are far easier to understand, maintain, and extend, thus accelerating business goals – are exceptionally difficult to quantify.”
  • “data.xml – My ticket has been rotting away for 14 months. :) (XML is a core technology at my company.)”
  • Repl tooling and setup, more official tutorials and guides. Data validation and schemas.
  • Data science, clojure for frontend
  • Guides for LLM driven development that don’t invoke huge piles of software just to modify code.
  • Growth to new domains and use cases, specifically scientific / academic / teaching

Permalink

Call for Proposals. Feb. 2026 Member Survey

Greetings folks!

Clojurists Together is pleased to announce that we are opening our Q2 2026 funding round for Clojure Open Source Projects. Applications will be accepted through the 19th of March 2026 (midnight Pacific Time). We are looking forward to reviewing your proposals! More information and the application can be found here.

We will be awarding up to $33,000 USD for a total of 5-7 projects. The $2k funding tier is for experimental projects or smaller proposals, whereas the $9k tier is for those that are more established. Projects generally run 3 months, however, the $9K projects can run between 3 and 12 months as needed. We expect projects to start at the beginning of April 2026.

A BIG THANKS to all our members for your continued support. We also want to encourage you to reach out to your colleagues and companies to join Clojurists Together so that we can fund EVEN MORE great projects throughout the year.

And Now the Survey…

We surveyed the community in February to find out what what issues were top of mind and types of initiatives they would like us to focus on for this round of funding. As always, there were a lot of ideas and we hope they will be useful in informing your project proposals.

A number of themes appeared in the survey results.

The biggest theme, by far, was related to adoption and growth of Clojure. Respondents repeatedly mentioned that Clojure is niche, and although they are happy with Clojure, that makes it harder to justify for projects, to find employment, and to persuade others than it is for more popular languages. Respondents want a larger community and wider adoption. In particular, they want more public advocacy for Clojure, including videos, tutorials, public success stories, starter projects, and outreach in general.

Another major theme was AI. Respondents were concerned about AI coding assistants being perceived as having weak Clojure support, and they expressed frustration that Python is perceived as the safe choice for AI despite how well Clojure works with AI tooling. Nonetheless, respondents would like to see more work on tooling, guides, and resources for using AI with Clojure.

ClojureScript and JavaScript interop received the most specific attention. Respondents want CLJS/Cherry/Skittle to provide frictionless support for modern JavaScript standards (ES6 and ESM) and would like an overall simplification of the build and run process.

Developer experience issues came up a number of times, including: confusing error messages, poor documentation, and under-supported libraries. Improvements to any of those would be welcome.

Difficulty finding Clojure employment was another recurring theme. Respondents were not sure how to solve it, but suggested a community job board might be helpful.


February 2026 Survey

88% of respondents use or refer to projects funded by Clojurists Together

time spent q2 2026


communication q2 2026


platform q2 survey


clojure improvements q2 2026


CLojure script improvement q2 2026


Plans for Conference Attendance in 2026 (number of mentions):

  • Clojure/Conj: 8
  • Dutch Clojure Days: 6
  • babashka conference: 5
  • Clojure South: 1
  • Clojure Jam: 1
  • reClojure: 1

If you were only to name ONE, what is the biggest challenge facing Clojure developers today and how can Clojurists Together support you or your organization in addressing those challenges? If you could wave a magic wand and change anything inside the Clojure community, what would it be? (select responses by category).

Adoption:

  • Advertising video like that one on Clojure Conj five years ago or so
  • Language Adoption and popularity, projects that helps to grow the popularity of the language or helps to start programming easily
  • Lack of widespread adoption is not a problem… until you want to convince others that Clojure is a technology you can count on and is worth developing with. Convincing others that Clojure is a great and solid technology that’s here to stay, regardless of low(er) adoption, is sometimes tough.
  • The fact that many teams and project would rule out Clojure as an option, being perceived as niche, far from mainstream, and thus risky
  • I would have more Clojure evangelism. More videos/blog posts/demos around using Clojure, both about whatever is currently at the peak of the broader tech hype cycle – LLMs currently – as well as uses and topics outside of the hype cycle.

AI/LLMs

  • Keeping relevant in a programming market is the top challenge. With the IA, everyone is moving toward the most popular languages. If nobody uses Clojure, it is more difficult to justify its use, no matter how much better it could be.
  • The biggest challenge is the spreading expectation that everything will be done in Python because AI will fix whatever problems Python will allegedly cause.
  • How will Clojure and the Community fare in the light of LLMs and coding assistants?
  • We are being encouraged to use Agentic AI coding assistants, but their support for Clojure is behind that of other languages.
  • How can we articulate the value of Clojure as a sustainable, modern solution when discussion about AI is taking all of the air in the room. We can for example fortify our tooling regards this. Projects such as Calva make Clojure easy to approach for newbies and ClojureMCP is a great tool for Agentic developers.
  • I am unsure about how LLM driven development fits with Clojure. I find myself building some things with JS and Python. For larger projects I am relying on Clojure for it’s correctness properties and lower likelyhood of bugs.
  • Support AI integration Clojure projects

Employment

  • Difficulty finding interesting and reliable work, but this isn’t just Clojure-specific, the whole industry is weird right now.
  • Finding employment writing Clojure code
  • I would say that the biggest challenge for Clojure developers is in the job search. I’m not certain of a solution to this challenge, but perhaps some kind of Clojurists Together Job Board?

Developer Experience

  • Developer tooling improvements competitive with modern JavaScript/TypeScript tooling
  • Missing parts of the data science stack
  • Better documentation of the tools and projects and more tutorials
  • Better integration with cljs/scittle/js/typescript - separate cljs compilation too complicated - scittle/squint/cherry with ES6 integration is the way clojurescript support for ESM libraries. It’s crazy the hoops you have to jump through to use ESM with clojurescript, most people probably assume it’s not possible at all because it’s so difficult.
  • Closer integration with JavaScript/TypeScript tooling -Seamless integration of cljs/cherry/scittle into the js-ecosystem with live repl and load-file support, standard sente/websocket communication included, standard/default solid telemetry/instrumentation API
  • Quicker resolution of outstanding Clojure (JIRA) issues
  • Have a official support program to people that focus on promote the language and/or community instead of library maintainers (like GDE from Google, MVP from Microsoft, Github Stars from Github)
  • I would encourage “cljc” as a default idiom. The linter could say, “This could be a cljc file!” or “Change this to that and suddenly it would be cljc-compatible”.
  • It remains Error Reporting imho, and anyone working on improving it would get my eternal gratitude.

What areas of the Clojure ecosystem need support? (select responses)

  • “I think something around marketing/evangelism; I have worked on several teams using Clojure/ClojureScript that have had to defend the use of Clojure/ClojureScript against more mainstream JVM/JS languages, and the core issue we’ve run up against is a confluence of the following three items:
    – 1. there are more Kotlin/Scala/TypeScript/Java developers than there are Clojure/ClojureScript developers
    – 2. The salary ranges for those languages tends to be lower than that for Clojure/ClojureScript
    – 3. The greatest benefits to be gained from using Clojure/ClojureScript – systems which are far easier to understand, maintain, and extend, thus accelerating business goals – are exceptionally difficult to quantify.”
  • “data.xml – My ticket has been rotting away for 14 months. :) (XML is a core technology at my company.)”
  • Repl tooling and setup, more official tutorials and guides. Data validation and schemas.
  • Data science, clojure for frontend
  • Guides for LLM driven development that don’t invoke huge piles of software just to modify code.
  • Growth to new domains and use cases, specifically scientific / academic / teaching

Permalink

What’s Next for clojure-mode?

Good news, everyone! clojure-mode 5.22 is out with many small improvements and bug fixes!

While TreeSitter is the future of Emacs major modes, the present remains a bit more murky – not everyone is running a modern Emacs or an Emacs built with TreeSitter support, and many people have asked that “classic” major modes continue to be improved and supported alongside the newer TS-powered modes (in our case – clojure-ts-mode). Your voices have been heard! On Bulgaria’s biggest national holiday (Liberation Day), you can feel liberated from any worries about the future of clojure-mode, as it keeps getting the love and attention that it deserves! Looking at the changelog – 5.22 is one of the biggest releases in the last few years and I hope you’ll enjoy it!1

Now let me walk you through some of the highlights.

edn-mode Gets Some Love

edn-mode has always been the quiet sibling of clojure-mode – a mode for editing EDN files that was more of an afterthought than a first-class citizen. That changed with 5.21 and the trend continues in 5.22. The mode now has its own dedicated keymap with data-appropriate bindings, meaning it no longer inherits code refactoring commands that make no sense outside of Clojure source files. Indentation has also been corrected – paren lists in EDN are now treated as data (which they are), not as function calls.

Small things, sure, but they add up to a noticeably better experience when you’re editing config files, test fixtures, or any other EDN data.

Font-locking Updated for Clojure 1.12

Font-locking has been updated to reflect Clojure 1.12’s additions – new built-in dynamic variables and core functions are now properly highlighted. The optional clojure-mode-extra-font-locking package covers everything from 1.10 through 1.12, including bundled namespaces and clojure.repl forms.2 Some obsolete entries (like specify and specify!) have been cleaned up as well.

On a related note, protocol method docstrings now correctly receive font-lock-doc-face styling, and letfn binding function names get proper font-lock-function-name-face treatment. These are the kind of small inconsistencies that you barely notice until they’re fixed, and then you wonder how you ever lived without them.

Discard Form Styling

A new clojure-discard-face has been added for #_ reader discard forms. By default it inherits from the comment face, so discarded forms visually fade into the background – exactly what you’d expect from code that won’t be read. Of course, you can customize the face to your liking.

Notable Bug Fixes

A few fixes that deserve a special mention:

  • clojure-sort-ns no longer corrupts non-sortable forms – previously, sorting a namespace that contained :gen-class could mangle it. That’s fixed now.
  • clojure-thread-last-all and line comments – the threading refactoring command was absorbing closing parentheses into line comments. Not anymore.
  • clojure-update-ns works again – this one had been quietly broken and is now restored to full functionality.
  • clojure-add-arity preserves arglist metadata – when converting from single-arity to multi-arity, metadata on the argument vector is no longer lost.

The Road Ahead

So, what’s actually next for clojure-mode? The short answer is: more of the same. clojure-mode will continue to receive updates, bug fixes, and improvements for the foreseeable future. There is no rush for anyone to switch to clojure-ts-mode, and no plans to deprecate the classic mode anytime soon.

That said, if you’re curious about clojure-ts-mode, its main advantage right now is performance. TreeSitter-based font-locking and indentation are significantly faster than the regex-based approach in clojure-mode. If you’re working with very large Clojure files and noticing sluggishness, it’s worth giving clojure-ts-mode a try. My guess is that most people won’t notice a meaningful difference in everyday editing, but your mileage may vary.

The two modes will coexist for as long as it makes sense. Use whichever one works best for you – they’re both maintained by the same team (yours truly and co) and they both have a bright future ahead of them. At least I hope so!

As usual - big thanks to everyone supporting my Clojure OSS work, especially the members of Clojurists Together! You rock!

That’s all I have for you today. Keep hacking!

  1. I also hope I didn’t break anything. :-) 

  2. I wonder if anyone’s using this package, though. For me CIDER’s font-locking made it irrelevant a long time ago. 

Permalink

How To Cleanly Integrate Java and Clojure In The Same Package

A hybrid Java/Clojure library designed to demonstrate how to setup Java interop using Maven

This is a complete Maven-first Clojure/Java interop application. It details how to create a Maven application, enrich it with clojure code, call into clojure from Java, and hook up the entry points for both Java and Clojure within the same project.

Further, it contains my starter examples of using the fantastic Incanter Statistical and Graphics Computing Library in clojure. I include both a pom.xml and a project.clj showing how to pull in the dependencies.

The outcome is a consistent maven-archetyped project, wherein maven and leiningen play nicely together. This allows the best of both ways to be applied together. For the emacs user, I include support for cider and swank. NRepl by itself is present for general purpose use as well.

Starting a project

Maven first

Create Maven project

follow these steps

mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=my-app -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

cd my-app

mvn package

java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App
Hello World

Add Clojure code

Create a clojure core file

mkdir -p src/main/clojure/com/mycompany/app

touch src/main/clojure/com/mycompany/app/core.clj

Give it some goodness…

(ns com.mycompany.app.core
(:gen-class)
(:use (incanter core stats charts)))

(defn -main [& args]
(println "Hello Clojure!")
(println "Java main called clojure function with args: "
(apply str (interpose " " args))))

(defn run []
(view (histogram (sample-normal 1000))))

Notice that we’ve added in the Incanter Library and made a run function to pop up a histogram of sample data

Add dependencies to your pom.xml

<dependencies>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>clojure</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>clojure-contrib</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>incanter</groupId>
<artifactId>incanter</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.clojure</groupId>
<artifactId>tools.nrepl</artifactId>
<version>0.2.10</version>
</dependency>
<!-- pick your poison swank or cider. just make sure the version of nRepl matches. -->
<dependency>
<groupId>cider</groupId>
<artifactId>cider-nrepl</artifactId>
<version>0.10.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>swank-clojure</groupId>
<artifactId>swank-clojure</artifactId>
<version>1.4.3</version>
</dependency>
</dependencies>

Java main class

Modify your java main to call your clojure main like in the following:

package com.mycompany.app;

// for clojure's api
import clojure.lang.IFn;
import clojure.java.api.Clojure;

// for my api
import clojure.lang.RT;

public class App
{
public static void main( String[] args )
{

System.out.println("Hello Java!" );

try {

// running my clojure code
RT.loadResourceScript("com/mycompany/app/core.clj");
IFn main = RT.var("com.mycompany.app.core", "main");
main.invoke(args);

// running the clojure api
IFn plus = Clojure.var("clojure.core", "+");
System.out.println(plus.invoke(1, 2).toString());

} catch(Exception e) {
e.printStackTrace();
}

}
}

Maven plugins for building

You should add in these plugins to your pom.xml

  • Add the maven-assembly-plugin

    Create an Ubarjar

    Bind the maven-assembly-plugin to the package phase this will create a jar file without the dependencies suitable for deployment to a container with deps present.

  <plugin>
    <artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>

<!-- use clojure main -->
<!-- <mainClass>com.mycompany.app.core</mainClass> -->

<!-- use java main -->
<mainClass>com.mycompany.app.App</mainClass>

</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
        <phase>package</phase>
        <goals>
<goal>single</goal>
</goals>
</execution>
</executions>
  </plugin>
  • Add the clojure-maven-plugin

    Add this plugin to give your project the mvn: clojure:… commands

    A full list of these is posted later in this article.

  •   <plugin>
        <groupId>com.theoryinpractise</groupId>
    <artifactId>clojure-maven-plugin</artifactId>
    <version>1.7.1</version>
    <configuration>
    <mainClass>com.mycompany.app.core</mainClass>
    </configuration>
    <executions>
    <execution>
    <id>compile-clojure</id>
            <phase>compile</phase>
            <goals>
    <goal>compile</goal>
    </goals>
    </execution>
    <execution>
    <id>test-clojure</id>
            <phase>test</phase>
            <goals>
    <goal>test</goal>
    </goals>
    </execution>
    </executions>
      </plugin>
    
  • Add the maven-compiler-plugin

    Add Java version targeting

    This is always good to have if you are working against multiple versions of Java.

  •   <plugin>
        <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.3</version>
    <configuration><source>1.8</source>
    <target>1.8</target>
    </configuration>
      </plugin>
    
  • Add the maven-exec-plugin

    Add this plugin to give your project the mvn exec:… commands

    The maven-exec-plugin is nice for running your project from the commandline, build scripts, or from inside an IDE.

  •   <plugin>
        <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.4.0</version>
    <executions>
    <execution>
    <goals>
    <goal>exec</goal>
    </goals>
    </execution>
    </executions>
    <configuration>
    <mainClass>com.mycompany.app.App</mainClass>
    </configuration>
      </plugin>
    
  • Add the maven-jar-plugin

    With this plugin you can manipulate the manifest of your default package. In this case, I’m not adding a main, because I’m using the uberjar above with all the dependencies for that. However, I included this section for cases, where the use case is for a non-stand-alone assembly.

  •   <plugin>
        <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>2.6</version>
    <configuration>
    <archive>
    <manifest>
    
    <!-- use clojure main -->
    <!-- <mainClass>com.mycompany.app.core</mainClass> -->
    
    <!-- use java main -->
    <!-- <mainClass>com.mycompany.app.App</mainClass> -->
    
    </manifest>
    </archive>
    </configuration>
      </plugin>
    

    Using Maven

    • building
    mvn package
    
    • Run from cli with
      • run from java entry point:

    java -cp target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar com.mycompany.app.App
    
  • Run from Clojure entry point:
  • java -cp target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar com.mycompany.app.core
    
  • Run with entry point specified in uberjar MANIFEST.MF:
  • java -jar target/my-app-1.0-SNAPSHOT-jar-with-dependencies.jar
    
  • Run from maven-exec-plugin
    • With plugin specified entry point:
  • mvn exec:java
    
  • Specify your own entry point:
    • Java main
  • mvn exec:java -Dexec.mainClass="com.mycompany.app.App"
    
  • Clojure main
  • mvn exec:java -Dexec.mainClass="com.mycompany.app.core"
    
  • Feed args with this directive
  • -Dexec.args="foo"
    
  • Run with maven-clojure-plugin
    • Clojure main
  • mvn clojure:run
    
  • Clojure test
    • Add a test

      In order to be consistent with the test location convention in maven, create a path and clojure test file like this:

  • mkdir src/test/clojure/com/mycompany/app
    
    touch src/test/clojure/com/mycompany/app/core_test.clj
    

    Add the following content:

    (ns com.mycompany.app.core-test
    (:require [clojure.test :refer :all]
    [com.mycompany.app.core :refer :all]))
    
    (deftest a-test
    (testing "Rigourous Test :-)"
    (is (= 0 0))))
    
  • Testing
  • mvn clojure:test
    

    Or

    mvn clojure:test-with-junit
    
  • Available Maven clojure:… commands

    Here is the full set of options available from the clojure-maven-plugin:

  • mvn ...
    
    clojure:add-source
    clojure:add-test-source
    clojure:compile
    clojure:test
    clojure:test-with-junit
    clojure:run
    clojure:repl
    clojure:nrepl
    clojure:swank
    clojure:nailgun
    clojure:gendoc
    clojure:autodoc
    clojure:marginalia
    

    See documentation:

    Add Leiningen support

    • Create project.clj

      Next to your pom.xml, create the Clojure project file

    touch project.clj
    

    Add this content

    (defproject my-sandbox "1.0-SNAPSHOT"
    :description "My Encanter Project"
    :url "http://joelholder.com"
    :license {:name "Eclipse Public License"
    :url "http://www.eclipse.org/legal/epl-v10.html"}
    :dependencies [[org.clojure/clojure "1.7.0"]
    [incanter "1.9.0"]]
    :main com.mycompany.app.core
    :source-paths ["src/main/clojure"]
    :java-source-paths ["src/main/java"]
    :test-paths ["src/test/clojure"]
    :resource-paths ["resources"]
    :aot :all)
    

    Note that we’ve set the source code and test paths for both java and clojure to match the maven-way of doing this.

    This gives us a consistent way of hooking the code from both lein and mvn. Additionally, I’ve added the incanter library here. The dependency should be expressed in the project file, because when we run nRepl from this directory, we want it to be available in our namespace, i.e. com.mycompany.app.core

  • Run with Leiningen
  • lein run
    
  • Test with Leiningen
  • lein test
    

    Running with org-babel

    This blog entry was exported to html from the README.org of this project. It sits in the base directory of the project. By using it to describe the project and include executable blocks of code from the project itself, we’re able to provide working examples of how to use the library in it’s documentation. People can simply clone our project and try out the library by executing it’s documentation. Very nice..

    Make sure you jack-in to cider first:

    M-x cider-jack-in (Have it mapped to F9 in my emacs)

    Clojure code

    The Clojure code block

    #+begin_src clojure :tangle ./src/main/clojure/com/mycompany/app/core.clj :results output 
      (-main)
      (run)
    #+end_src
    

    Blocks are run in org-mode with C-c C-c

    (-main)
    (run)
    
    Hello Clojure!
    Java main called clojure function with args:
    

    Note that we ran both our main and run functions here. -main prints out the text shown above. The run function actually opens the incanter java image viewer and shows us a picture of our graph.

    run.png

    I have purposefully not invested in styling these graphs in order to keep the code examples simple and focussed, however incanter makes really beautiful output. Here’s a link to get you started:

    http://incanter.org/

    Playing with Incanter

    (use '(incanter core charts pdf))
    ;;; Create the x and y data:
    (def x-data [0.0 1.0 2.0 3.0 4.0 5.0])
    (def y-data [2.3 9.0 2.6 3.1 8.1 4.5])
    (def xy-line (xy-plot x-data y-data))
    (view xy-line)
    (save-pdf xy-line "img/incanter-xy-line.pdf")
    (save xy-line "img/incanter-xy-line.png")
    

    PNG

    incanter-xy-line.png

    Resources

    Finally here are some resources to move you along the journey. I drew on the links cited below along with a night of hacking to arrive a nice clean interop skeleton. Feel free to use my code available here:

    https://github.com/jclosure/my-app

    For the eager, here is a link to my full pom:

    https://github.com/jclosure/my-app/blob/master/pom.xml

    Working with Apache Storm (multilang)

    Starter project:

    This incubator project from the Apache Foundation demos drinking from the twitter hose with twitter4j and fishing in the streams with Java, Clojure, Python, and Ruby. Very cool and very powerful..

    https://github.com/apache/storm/tree/master/examples/storm-starter

    Testing Storm Topologies in Clojure:

    http://www.pixelmachine.org/2011/12/17/Testing-Storm-Topologies.html

    Vinyasa

    READ this to give your clojure workflow more flow

    https://github.com/zcaudate/vinyasa

    Wrapping up

    Clojure and Java are siblings on the JVM; they should play nicely together. Maven enables them to be easily mixed together in the same project or between projects. For a more indepth example of creating and consuming libraries written in Clojure, see Michael Richards’ article detailing how to use Clojure to implement interfaces defined in Java. He uses a FactoryMethod to abstract the mechanics of getting the implementation back into Java, which make’s the Clojure code virtually invisible from an API perspective. Very nice. Here’s the link:

    http://michaelrkytch.github.io/programming/clojure/interop/2015/05/26/clj-interop-require.html

    Happy hacking!..

    Permalink

    Codex in the REPL

    As I started using coding agents more, development got faster in general. But as usually happens, when some roadblocks disappear, other inconveniences become more visible and annoying. Yeah… this time it was Clojure startup time.

    In one of my bigger projects, the Defold editor, lein test can spend 10 to 30 seconds loading namespaces to run a test that itself takes less than a second. This is fine when you run a full test suite, but painful for agent-driven iteration. And yes, I know about Improving Dev Startup Time, and no, it does not help enough.

    While it would be nice to improve startup time, it would be even better for an agent to actually use the REPL-driven development workflow that Clojure is designed for. Agents won’t naturally do it unless nudged in the right direction. So today I took the time to set it up, and I’m so happy with it that I want to share the experience!

    The Setup

    To make an agent use RDD, it needs to find a REPL, send a form to it, and get a result back. Doing that is surprisingly easy: one convention for discovery, one tiny script, and one skill prompt.

    1. Start a discoverable REPL in project context

    The important part is discoverability. I prefer to use socket REPLs, but I don’t want to pass ports around; the agent should find a running REPL by itself.

    To do that, I added a Leiningen injection in a :user profile that:

    • starts clojure.core.server REPL on a random port
    • writes the port into .repls/pid-{process-pid}.port in the current directory used to start a Leiningen project
    • ensures .repls/.gitignore exists and ignores everything in that directory
    • removes the port file on JVM exit

    This makes it easy to discover the REPLs programmatically while keeping git clean.

    2. Add a script that evaluates forms through that REPL

    To find the port, I vibe-coded an eval.sh script that:

    • looks for port files in .repls to find a running REPL
    • sends one or more Clojure forms to a running REPL using nc
    • prints a friendly message when no REPL is running

    The implementation is trivial, so there is no point in sharing it here. The idea is what matters: find the port in a known location and pipe input forms to the REPL server.

    3. Teach Codex to use it by default for Clojure work

    I added a Codex skill that points to this script and includes common patterns, such as:

    Evaluating in a namespace

    ./eval.sh '(in-ns (quote clojure.string)) (join "," [1 2 3])'
    

    Running tests in-process

    ./eval.sh '(binding [clojure.test/*test-out* *out*] (clojure.test/run-tests (quote test-ns)))'
    

    Some general advice

    Iterate in small steps, etc. You know the drill.

    Findings

    First of all, agentic engineering got noticeably faster. With a warm REPL, Codex can run many small checks while iterating on code.

    But what impressed me more is that it was actually quite capable of using the REPL to iterate in a running VM. For example, when it wanted to check a non-trivial invariant in a function it was working on, it used fuzzing to generate many examples to see whether the implementation worked as expected. That was cool and useful! I don’t typically do that in the REPL myself.

    Turns out Codex can do REPL-driven development quite well!

    Permalink

    Copyright © 2009, Planet Clojure. No rights reserved.
    Planet Clojure is maintained by Baishamapayan Ghose.
    Clojure and the Clojure logo are Copyright © 2008-2009, Rich Hickey.
    Theme by Brajeshwar.