AI agents, Haskell, and the stubbornness of developers

Recently, I found an article about a member of the Haskell Foundation – who was moving his company away from Haskell. The company built a hardware product with Haskell, and they were moving away reluctantly.

I’m not going to repeat the whole thing here (you can read it yourself, but the TL;DR; is: the Haskell compiler and tooling ecosystem was too slow, and that became a bottleneck now that the LLM agent writing the code is so fast.

Now, let’s be clear: I don’t want to start a holy war about “static versus dynamic typing”, so I’m not going to do that. I’m not going to discuss if the merits of static typing are overrated or not, and I’m not going to discuss if dynamic languages are more or less flexible. This was discussed over and over again – and nobody knows the result. And for me, that in-determination is also a result.

What I am going to talk about is the stubbornness of software developers. It’s something that’s been bothering me for a couple of years now, and it still bothers me today. Looking at this article was no different.

The bottleneck is the compiler

So, again, the big TL;DR; of the article: the Haskell tooling is too slow, and the agents are really fast to write code.

If an agent produces working code in, say, 20 seconds, and the Haskell compiler takes 5, 10, 15 minutes to compile… then the compiler becomes a huge bottleneck in the loop where the agent writes code, tests it, sees if it works, and rewrites it to iterate. The agent is done, and the Haskell compiler still didn’t finish.

And here’s the part that got my attention: the author moved from Haskell to Python.

This seems like a weird move to me, because they’re moving from a static language to a dynamic one. But the thing is – it also matches my own experience (I also started with some static typed language, and moved to Ruby/Clojure). And here’s the stubbornness part: this is something that people who like dynamic languages (like me) have been saying for literal years – He experienced something that we iterate, and explain, and prove, again and again and people are still fighting to believe us.

It’s not the LLM

So what’s different right now? It’s not the LLM.

Even supposing the LLM could write excellent code – or even good code (which, to be honest, has not been my experience – It’s mostly code that works, but that I would not write: full of defensive coding, and sometimes even duplications), the real difference between an LLM and a human is: a human programmer is usually proficient in 3, 4, 5 languages, and plenty of programmers are proficient in one or two at most.

The agent, on the other hand, is proficient in multiple languages, and it writes with the same code quality in all of them. Be it good or bad, it’s the same. And that is the interesting part – you now have a mythical “person” (a mythical robot, maybe) – that can produce, with the same ease and the same quality, code in both Haskell and Python: which means you can finally compare the two while holding the developer constant.

And what they found is that the number of defects in the Python code, over a given timeline, was the same as the number of defects in the Haskell code over that same timeline. (Probably fewer in Python, actually – otherwise why would they be moving away from Haskell? But I’m not going to guess). Uhnnn… who could have predicted, except the academic study that studied that in 2016, and that ANOTHER study in 2019 found the same conclusion?

What about quality?

So here’s what makes a good quality software: it’s the developer. And in this case, the machine is the same developer producing the same quality code in two languages. Which means they can measure how much the compiler was actually influencing the time it takes to write something – and what they found is that the compiler wasn’t helping them as much as you’d expect.

I am not going to be that guy and say that the compiler wasn’t helping at all – maybe it was – but the actual time to produce code of quality X was the same in both a compiled, static language like Haskell and a dynamic, interpreted one like Python.

And this, again, matches my experience.

The number of hours that I saw people spend producing correct, and quality code in C# and Java was the same – if not higher – than the time I saw people doing quality code in Ruby. Yes, sure: if you did a very bad Ruby implementation, it was very, very hard to read. But I also saw very, very hard to read C# and Java code. The type system helped me understand what the code was doing – but it did not help me write the fixed version because, for that, I usually had to change the type of some function… and changing that type would cascaded compilation errors over and over the codebase, forcing me either to write “adapter”, or “facades” or other techniques to fix the issue (again, structural typing might help but not by much – you can have the proliferation of very small, specific types that you need to merge and split, essentially making the whole thing a duck-typed code anyway)

Also, if I am writing new types, cascading it over the code, can I actually guarantee the same code quality? After all, am I not changing the shape of the data I’m sending, essentially invalidating the whole “this data have this structure and we’re sure of it”?

Dehumanization, again

You know what bothers me? I met people that worked in Haskell. Most of them would not accept, at all, any argument for a language with “weaker typing” (I wrote how that term is meaningless in the past, so I’m also using it in a meaningless way, just to be 100% sure) than Haskell – some would argue that we need more types and that it’s impossible to guarantee any quality in languages Clojure (because it’s dynamic) and even worse in Ruby or Python (because they are imperative). Few people actually wanted to listen to a different approach, to listen to the “Clojure guy” (usually that was me, before I quit most of the groups because they ended up discussing into theorems, category algebra, etc and less about software) about some different approach to solve a problem.

But… apparently, a machine is ok.

And the worst part? It’s the honeymoon phase. They are doing what LLMs are very good at – capturing a code that already exists, that have very well-define semantics, and porting that to another language. The LLM will probably be able to solve some issues on that migration, and probably will offer a good path for the future.

And that will be attributed to the machine – not the humans that wrote the first Haskell code, well-written, in the first place.

Exactly like they listened to the machine saying that “A dynamic language can produce code faster, and we can iterate sooner on that result” than us, developers of said language saying the exact same thing over the years.

Permalink

Things I wish Datomic had: Map values

Whence this post

JEP 401 – a proposal to add value classes to Java – has been crystallizing for almost six years now, but I only learned about it this morning. I skimmed it, nodding along to myself as I thought “gee, I can’t imagine going back to Java” and “wonder how this might make Clojure more performant?”. But then I thought about some Datomic (actually, Datahike) data remodelling that I’d been working on recently, and I realized that the two areas are connected.

So here’s a braindump, in an effort to clear up my mental image of all this.

A simple example

Consider this Clojure value:

(def prog
  {:program/name    "Apache Maven"
   :program/url     "https://maven.apache.org"
   :program/version {:version/major 3
                     :version/minor 9
                     :version/patch 16}})

If you’re like me, you’ll have a warm, comfortable feeling in your heart looking at this. Plain data at rest. Accessible, transformable. What’s not to love?

Well, now try storing it in Datomic. Easy! Let’s first define a schema:

(def schema
  [{:db/ident :program/name
    :db/valueType :db.type/string
    :db/unique :db.unique/identity
    :db/cardinality :db.cardinality/one}
   {:db/ident :program/url
    :db/valueType :db.type/uri
    :db/cardinality :db.cardinality/one}
   {:db/ident :version/major
    :db/valueType :db.type/long
    :db/cardinality :db.cardinality/one}
   {:db/ident :version/minor
    :db/valueType :db.type/long
    :db/cardinality :db.cardinality/one}
   {:db/ident :version/patch
    :db/valueType :db.type/long
    :db/cardinality :db.cardinality/one}
   {:db/ident :program/version
    :db/valueType :db.type/ref
    :db/isComponent true
    :db/cardinality :db.cardinality/one}])

And now we can transact it (I’ll assume we have a DB connection, conn):

@(d/transact conn schema)
@(d/transact conn [prog])

Done. Now we can check what Maven’s version is:

(let [db (d/db conn)
      mvn (d/entity db [:program/name "Apache Maven"])]
  (:program/version (d/touch mvn)))
;=> {:db/id 17592186045419, :version/major 3, :version/minor 9, :version/patch 16}

It works!

Meh

But I’m not exactly happy about this.

What I’d really like to get is #:version{:major 3, :minor 9, :patch 16}. But Datomic models maps as entities – focal points that bind attributes with values. That’s fine for the top-level program map, but version is not an entity! It’s just a value, like a number or a string. It has internal structure, but, conceptually, it’s just an atomic value, an element of the set of all possible major.minor.patch version numbers.

Yet Datomic forces us to “reify” the version map as an entity. That means that it automatically gets a :db/id. If a new version of Maven gets released, and we transact that fact:

@(d/transact conn
             [[[:program/name "Apache Maven"]
               :program/version
               #:version{:major 3, :minor 9, :patch 17}]])

then Datomic will create a fresh artificial entity, give it the three version attributes, and associate it with the Maven entity. But the old one still exists in the DB! It’s “semi-orphaned” (detached from the rest of the object graph in the current state, but still reachable via history). If Maven for some reason goes back to 3.9.16 via a similar transaction, then we’ll get a third entity that is a duplicate of the first one.

Even worse, there’s nothing stopping us from changing attributes of that entity:

(let [db (d/db conn)
      mvn (d/entity db [:program/name "Apache Maven"])]
  @(d/transact conn [[(-> mvn :program/version :db/id) :version/major 4]]))

Now the identity of version hasn’t changed, but Maven is at 4.9.17, and every other entity that happened to be referencing the artificial version one is at 4.9.17 too! Clearly, this kind of thing should be disallowed.

Also note that I had to say :db/isComponent true in the schema for the transaction to have succeeded at all. isComponent means that the child entity only makes sense in the context of parent; otherwise, I’d have to lift the version map to the top-level of the transaction, give it a temporary id, and use that id to refer to it in the program map.

Two kinds of maps

At this point, I realize there are two kinds of maps: those that describe snapshots of state of some stateful entity at some point in time (like program), and those that are merely juxtapositions of named values (like version). For want of better names, for now on I’ll call these “snapshot maps” and “plain maps”, respectively.

Quoting from Clojure’s Approach to Identity and State:

We need to move away from a notion of state as "the content of this memory block" to one of "the value currently associated with this identity". Thus an identity can be in different states at different times, but the state itself doesn’t change. That is, an identity is not a state, an identity has a state. Exactly one state at any point in time. And that state is a true value, i.e. it never changes.

With this in mind, we could reformulate the distinction as follows: “snapshot maps” are the state of some identity, while “plain maps” are not.

In Clojure, the two kinds of maps look and work exactly the same, but as we saw, they are conceptually very different. It is typically obvious which kind a given map belongs to; e.g. snapshot maps typically have a :id field that holds an integer or a UUID. But sometimes the exact same map can be viewed as either (is {:x 1 :y 2} just a 2D point – a plain map – or a snapshot of a point moving around – a snapshot map?) It’s not just maps either; we could make the same distinction for vectors, or indeed scalar values, but maps are where it’s most commonly encountered.

I find the distinction philosophically interesting. For example, could we envision a variant of Clojure that makes it explicit? Or discriminate between them based on the value’s metadata? Should deref automatically set that metadata? Should operations on the map preserve that metadata?

Besides semantic version numbers, here are some more examples of plain maps that appear frequently:

  • Temporal values. These are typically disguised as instances of java.util.Date or java.time.* classes, but are conceptually immutable collections of fields. For example, java.time.Instant values look like {:epochSecond long, :nano int}.
  • Prices, consisting of an amount and a currency. Back when Fy! used Datomic, we modelled prices as “artificial entities” as described in this post, leading to a massive proliferation of duplicate/orphaned price entities accruing in everyday operation.

What else could we do?

Going back to Datomic: if we want to avoid artificial entities, what alternatives do we have?

  • Store them as strings instead, like "3.9.16". Strings are plain values and they map cleanly onto how we typically denote versions. This may be the right solution if our app also presents versions this way and doesn’t do any fancy processing of it. A potential problem is that strings don’t sort correctly as versions: "11.0.2" is lexicographically smaller than "2.2.1", so the values will appear in an incoherent order in the AVET index. If we want to extract programs with versions in a certain range, the index can’t help us – we’ll need to do a full scan of the DB, then parse and sort manually.

  • Store them as tuples instead, like [3 9 16]. Datomic supports short vectors as values that don’t have an identity. In our case, we can represent versions numbers as 3-tuples of longs (either homo- or heterogeneous ones will do), [major minor patch]. This gives us the correct sorting. The downside is that the rest of our app probably doesn’t think about versions this way, so we need to translate the “domain” values that we use elsewhere to tuples before transacting, and translate it back in the data access layer.

    This leads to increased verbosity (one of the nice things about Datomic is that, most of the time, it lets you get away without having a data access layer at all, as you can use the entities returned by d/entity directly in your domain logic code). Still, I think it’s the right thing to do.

  • Flatten the data: don’t have the :program/version attribute at all, and instead associate the :version/* attributes directly with the program entity. This may make sense in some cases, but loses grouping, which can make it harder to programmatically process such data. Also, what if the program could have many versions?

  • Use Datahike instead of Datomic, and use its unstructured input feature in the “content identity” mode. This still creates artificial entities, but avoids duplication because it automatically infers ids from the map content, so two maps with the same content will refer to the same entity. However, it still has the “accidental mutability” problem with the artificial entity. You have to either opt in to content identity for all sub-maps of the map being transacted, or opt out of it en masse. Plus, it just feels hacky.

In an ideal world…

…I’d like to be able to just define new types in Datomic. Just like there’s built-in supports for java.util.Dates (as :db.type/inst) or java.net.URIs (as :db.type/uri), I’d like to somehow tell Datomic to “support instances of java.time.LocalDate as :db.type/date”. Or, “let :db.type/semver be a map mapping :version/major, :version/minor, and :version/patch to longs”.

How such an API could look like is open to discussion: I don’t have concrete ideas here. Whatever the design, though, I think it’d be a win.

Permalink

Making CIDER More Discoverable

This series about the notable changes in CIDER 2.0 continues with the change you’ll bump into first, whatever your workflow: the transient menus, and the broader push to make CIDER’s functionality discoverable.

The problem: CIDER is huge

CIDER has well over 300 interactive commands. I’ll admit something I’ve said before: there are features in CIDER that even I forget exist, and I wrote half of them. For users, historically, the options for finding functionality were:

  • memorize cryptic key chords (C-c C-w i, anyone?)
  • grep the (very long) manual
  • read the source
  • stumble on a feature by accident three years in and feel robbed

That’s not great for a tool whose whole pitch is making you more productive. The Emacs answer to this problem was demonstrated years ago by Magit: transient menus, which turn every prefix into a self-documenting popup. It took us embarrassingly long to follow suit, but CIDER 2.0 finally does.

Transient everywhere

Every command group in CIDER now opens a transient menu: cider-eval-menu at C-c C-v, cider-doc-menu at C-c C-d, and likewise for test, namespace, macroexpand, profile, trace and references. A top-level cider-menu ties them all together, and even the debugger (? mid-session) and the inspector (m) got menus of their own. Jack-in and connect live in cider-start-menu at C-c C-x.

Here’s the evaluation menu, which is a good illustration of the problem the menus solve - I doubt many people knew all of this was hiding behind C-c C-v:

The CIDER evaluation transient menu, listing every evaluation command

One design constraint was non-negotiable: your muscle memory is safe. These menus replace bare prefix keymaps, so every existing keybinding works exactly as before, at full speed - C-c C-v e still evaluates instantly, menu or no menu. And if you’d rather not see the menus at all unless you actually hesitate mid-chord, set transient-show-popup to a short delay and they’ll appear only in that moment of doubt - which is precisely when you need them.

Transient also gave us something the old keymaps never could: arguments. Menus now carry flags for the things that vary per invocation - pick a pretty-printer with --print-fn=, set test selectors with --include=/--exclude= and reuse them across runs, toggle cider-ns-refresh’s modes explicitly, pass Clojure CLI aliases at jack-in time. All those “this command behaves differently with a prefix argument” paragraphs in the manual are becoming visible checkboxes instead.

The test menu shows this nicely - set the selectors once and every run below them picks them up:

The CIDER test transient menu with its include and exclude selector arguments

Discovery beyond menus

The menus are the headline, but the discoverability push in the 2.0 cycle went wider:

  • A new keybindings reference page collects every binding in one place, and the printable refcard was brought back up to date.
  • The REPL’s shouty welcome banner is gone, replaced by a one-line hint; the getting-started material now lives in a summonable reference card (C-c C-h, or the ,refcard REPL shortcut) - available when you want it, invisible when you don’t.
  • CIDER now warns (once per session) when you use a deprecated keybinding, so bindings can actually be retired someday without silently breaking people. M-x cider-list-deprecated-keybindings shows what’s on the way out.
  • The 1.22 cycle’s big audit already made the mode menus expose dozens of commands that were technically present but practically invisible; 2.0 builds on that foundation.
  • Even cider-doctor is discoverability of a sort - it surfaces the problems in your setup that you’d otherwise discover one confusing bug report at a time.

The philosophy

If I had to compress the 2.0 discoverability work into one sentence: the features were always there; now the tool tells you about them. Documentation is where knowledge goes to be forgotten - the only reliable place to teach a user about a feature is inside the workflow itself, at the moment of hesitation. Transient menus are exactly that, and Magit proved the pattern scales to enormous command sets.

If some menu feels wrong - a missing command, a flag that should exist, a grouping that doesn’t match how you think - please file an issue. This part of CIDER is young and very much open to feedback.

The keybindings docs have the full picture. Keep hacking!

Permalink

Database adapters in Biff 2

I've released two new Biff libraries, both database adapters: biff.sqlite and biff.xtdb. Both of them implement some interfaces used by various other Biff libraries, and they also both implement additional functionality that can be useful to Clojure apps even if they aren't using Biff.

The interfaces

So far, modifying a Biff app to use a different database than the default has been kind of inconvenient, as a result of philosophy about modularity for Biff 1. The ability to swap out defaults was more of an escape hatch so that starting out with Biff doesn't mean your locked into all its choices forever. So that meant if you wanted to swap out the database, you had to, for example, copy and paste all of Biff's sign-in-via-email code and rewrite the queries.

With Biff 2, modularity is becoming more of a first-class citizen. Hence these new "interfaces." And the main interface here addresses the question of, you know, how do you put things in a database. And get them back out. For Biff 2, I wanted be able to package up shared application functionality that needs persistence (such as the authentication module) in a database-agnostic way.

So Biff 2 now defines a key-value store interface which can be implemented by database adapter libraries like the two I've just released:

And my two implementations: sqlite and xtdb. These KV-store functions are exposed via a biff.core module, for example.

In general I try to avoid adding layers to things unnecessarily, so it took a little thinking to arrive at this solution. My main thought was that I'm primarily interested in database-agnosticism for libraries, not applications. Migrating a single application's database is a different use case than wanting to provide functionality for multiple applications using different databases, and it's the latter use case that I'm trying to address.

So I didn't want to introduce some sort of "Biff query/transaction language" that you'd use whenever writing a Biff app; that would be overkill. The database-agnostic libraries I wanted to write have only basic persistence needs, so a key-value interface is sufficient. And that's an easy interface for database adapters to implement.

The second biggest interface-type-thing is that both adapters come with make-resolvers functions (sqlite / xtdb) which generate a set of biff.graph resolvers for the tables in your application schema.

The features

Besides the key-value functions and a couple other doodads, the remaining functionality in these database libraries is just whatever stuff I thought would be nice to have when writing a Clojure app using the respective databases. From biff.sqlite's README:

  • Sane defaults like WAL mode, STRICT tables, etc.
  • Backup/restore via Litestream.
  • Migrations via sqldef.
  • Rich schema types: define columns as e.g. booleans, instants, nested maps, etc; and biff.sqlite converts them to/from SQLite's supported types (ints, blobs, etc).
  • Validate transactions based on centralized authorization rules you define (helps to keep LLM code secure).

And for biff.xtdb:

  • Start up an in-process node with high-level config defaults.
  • Custom :biff/upsert and :biff/assert-unique transaction operations.
  • Optionally enforce Malli schemas on write.
  • Define centralized authorization rules for validating transactions.

biff.sqlite is a chonkier library due to all the logic needed for supporting rich schema types.

Write your own database adapter

Finally, I have written a database adapter guide which lists all the interfaces that adapters should implement and suggests additional features that may or may not be relevant for any given database. Between that guide and the two sqlite/xtdb reference implementations, I'm hoping it should be straightforward to implement an adapter for whatever database you want to use. I will probably only maintain the SQLite and XTDB adapters, but I might publish some as-is code for other databases which could be picked up and maintained anyone who so chooses.


Plug: my team is hiring for a senior software engineer, writing ClojureScript and Python. We make optimization software for clean energy projects.

Permalink

Python Fundamentals for a JavaScript Developer

I'll guide you through Python fundamentals by comparing concepts with JavaScript. Let's start!

1. Hello World & Basic Syntax

JavaScript

console.log("Hello World");
let x = 5;

Python

print("Hello World")
x = 5  # No semicolon, no let/const

Key Differences:

  • No semicolons in Python
  • Indentation matters (replaces curly braces)
  • Comments use # instead of //

2. Variables & Data Types

JavaScript

let name = "Alice";  // string
let age = 30;        // number
let isStudent = true; // boolean
let scores = [95, 87, 91]; // array
let person = {        // object
    name: "Bob",
    age: 25
};
let nothing = null;
let notDefined = undefined;

Python

name = "Alice"        # str
age = 30              # int (or float for decimals)
is_student = True     # bool (capital T/F)
scores = [95, 87, 91] # list (mutable)
person = {            # dict (dictionary)
    "name": "Bob",
    "age": 25
}
nothing = None        # Python's null/undefined

Key Differences:

  • Python uses snake_case (not camelCase)
  • True/False capitalized
  • None instead of null/undefined
  • Lists ≈ Arrays, Dicts ≈ Objects

3. Control Flow

JavaScript

// If-else
if (age >= 18) {
    console.log("Adult");
} else if (age >= 13) {
    console.log("Teen");
} else {
    console.log("Child");
}

// For loop
for (let i = 0; i < 5; i++) {
    console.log(i);
}

// While loop
let count = 0;
while (count < 5) {
    console.log(count);
    count++;
}

Python

# If-else (indentation instead of braces)
if age >= 18:
    print("Adult")
elif age >= 13:  # NOT else if
    print("Teen")
else:
    print("Child")

# For loop (more like for...of in JS)
for i in range(5):  # range(5) = [0, 1, 2, 3, 4]
    print(i)

# Iterate over list (like for...of)
for score in scores:
    print(score)

# While loop
count = 0
while count < 5:
    print(count)
    count += 1  # No ++ operator in Python

4. Functions

JavaScript

// Function declaration
function add(a, b) {
    return a + b;
}

// Arrow function
const multiply = (a, b) => a * b;

// Default parameters
function greet(name = "Guest") {
    return `Hello ${name}`;
}

Python

# Function definition (def instead of function)
def add(a, b):
    return a + b  # Indented body

# Lambda functions ≈ Arrow functions
multiply = lambda a, b: a * b

# Default parameters
def greet(name="Guest"):
    return f"Hello {name}"  # f-strings like template literals

# Multiple return values (tuples)
def get_coordinates():
    return 10, 20  # Returns a tuple (10, 20)

x, y = get_coordinates()  # Destructuring assignment

5. Data Structures Comparison

Arrays/Lists

// JavaScript Arrays
let arr = [1, 2, 3];
arr.push(4);          // [1, 2, 3, 4]
arr.pop();            // [1, 2, 3]
let sliced = arr.slice(0, 2);  // [1, 2]
# Python Lists
arr = [1, 2, 3]
arr.append(4)         # [1, 2, 3, 4]
arr.pop()             # [1, 2, 3] (removes last)
sliced = arr[0:2]     # [1, 2] (slicing syntax)
arr.insert(1, 99)     # [1, 99, 2, 3]

# List comprehension (unique to Python)
squares = [x**2 for x in range(5)]  # [0, 1, 4, 9, 16]

Objects/Dictionaries

// JavaScript Objects
let person = {
    name: "Alice",
    age: 30,
    greet() {
        return `Hello, I'm ${this.name}`;
    }
};
console.log(person.name);
console.log(person["age"]);
# Python Dictionaries
person = {
    "name": "Alice",
    "age": 30,
    "greet": lambda self: f"Hello, I'm {self['name']}"
}
print(person["name"])  # Access with brackets
print(person.get("age"))  # Safer access

# Methods don't naturally have 'this' context
# Usually you'd use classes for that (see below)

6. Classes & OOP

JavaScript (ES6+)

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    greet() {
        return `Hello, I'm ${this.name}`;
    }

    static species = "Human";
}

const alice = new Person("Alice", 30);

Python

class Person:
    species = "Human"  # Class attribute (static)

    def __init__(self, name, age):  # Constructor
        self.name = name  # Instance attribute
        self.age = age

    def greet(self):  # Methods always have self parameter
        return f"Hello, I'm {self.name}"

    @staticmethod
    def static_method():
        return "This is static"

alice = Person("Alice", 30)
print(alice.greet())  # No parentheses needed for self when calling

7. Modules & Imports

JavaScript (ES6 Modules)

// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }

// main.js
import { PI, add } from './math.js';
import * as math from './math.js';

Python

# math.py
PI = 3.14159
def add(a, b):
    return a + b

# main.py
from math import PI, add
import math  # Then use math.PI, math.add
import math as m  # Alias

8. Error Handling

JavaScript

try {
    throw new Error("Something went wrong");
} catch (error) {
    console.error(error.message);
} finally {
    console.log("Cleanup");
}

Python

try:
    raise Exception("Something went wrong")
except Exception as e:  # 'as' instead of variable declaration
    print(f"Error: {e}")
finally:
    print("Cleanup")

9. Async Programming

JavaScript (Promises/Async-Await)

// Promise
fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => console.log(data));

// Async/await
async function getData() {
    const response = await fetch(url);
    return await response.json();
}

Python (Async/Await)

import asyncio
import aiohttp  # External library for HTTP

async def fetch_data(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.json()

# Run async function
asyncio.run(fetch_data('https://api.example.com/data'))

10. Common Patterns & Tips

1. Type Checking (Python is dynamically typed but has type hints)

def add(a: int, b: int) -> int:  # Type hints (optional)
    return a + b

2. String Formatting (multiple ways)

name = "Alice"
# f-strings (Python 3.6+, like template literals)
print(f"Hello {name}")

# .format() method
print("Hello {}".format(name))

# % formatting (older style)
print("Hello %s" % name)

3. Tuple vs List

# List - mutable
my_list = [1, 2, 3]
my_list[0] = 99  # OK

# Tuple - immutable
my_tuple = (1, 2, 3)
# my_tuple[0] = 99  # ERROR!

4. Sets (unique unordered collection)

my_set = {1, 2, 3, 3, 2}  # {1, 2, 3} (duplicates removed)
another_set = set([1, 2, 3, 4])  # Alternative creation

Quick Reference Table

JavaScript Python Notes
let x = 5; x = 5 No declaration keywords
const arr = [] arr = [] No const, just assign
null / undefined None Single null value
=== strict equality == and is == value, is identity
array.length len(list) Function, not property
array.map() List comprehensions [x*2 for x in arr]
for (let i=0; i<n; i++) for i in range(n) Different pattern
function fn() {} def fn(): def keyword
obj.property dict["key"] or obj.attr Depends on type
class MyClass {} class MyClass: Colon and indentation

Practice Exercise

Convert this JavaScript code to Python:

function filterEvenSquares(numbers) {
    return numbers
        .filter(n => n % 2 === 0)
        .map(n => n ** 2);
}

const result = filterEvenSquares([1, 2, 3, 4, 5]);
console.log(result); // [4, 16]

Python solution:

def filter_even_squares(numbers):
    return [n**2 for n in numbers if n % 2 == 0]

result = filter_even_squares([1, 2, 3, 4, 5])
print(result)  # [4, 16]

Next Steps

  1. Install Python and a good IDE (VS Code with Python extension works well)
  2. Practice by rewriting your JS projects in Python
  3. Explore Python-specific features: decorators, generators, context managers
  4. Learn popular libraries:
    • Web: Flask/Django (Express equivalents)
    • Data: NumPy, Pandas
    • AI/ML: TensorFlow, PyTorch

The main mindset shift: Python emphasizes readability and simplicity over cleverness. You'll write less code to accomplish the same tasks!

Permalink

Looking for work

Hey, Niki here. This is a bit unusual. My sabbatical is coming to an end, and I am looking for a new opportunity. Full-time or contract, startup or research, remote or Berlin, individual contributor, ideally—tight team, ambitious product.

I am a software engineer first and foremost with 20+ years of experience. I work on technically challenging products, foundational technology, dev tools. I’ve been doing Clojure and web recently, but I'm also very excited to explore closer-to-the-metal programming.

I have an eye for design, user interfaces, UX, DX. I would love to work with a team that takes interface quality seriously. Or to work with graphics!

I am pretty sure I am good at explaining stuff, including what we are building, why, why this way, why is it important, etc. For example.

The overarching theme is to understand computers deeply, and then use that to make better and simpler software. If you care about that too, we might be a great match!

Recent work

Instant DB is a US startup building a modern Firebase. I worked on the sync algorithm, performance, DX. A summary of my commit log.

Roam Research is an OG personal knowledge manager. I worked on database optimization and a plugin system.

At JetBrains, I developed a new Skia renderer for Fleet and Jetpack Compose Desktop.

I’ve built many open-source libraries, including a database, a GUI toolkit, a Clojure dev environment, a React wrapper, a well-known font... More recently, Clojure+ gives you a taste of my approach to DX, and Fast EDN—to performance.

I maintain several active projects — AlleKinos.de, Grumpy Website, this site.

If you want to dive deeper, here’s the usual stuff: Projects / Talks / LinkedIn / GitHub

I also made a two-page PDF CV.

Why this post?

It’s an attempt to reach beyond my immediate network. I’ve been doing Clojure for a long time, and now want to explore.

If you are working on a compiler, a database, an IDE, a programming language or another technically ambitious product, touching graphics, typography, algorithms, low-level programming, and you think my experience can help, let’s talk: niki@tonsky.me.

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

Clojure 1.13.0-alpha5

Clojure 1.13.0-alpha5 is now available! Find download and usage information on the Downloads page.

Java 17 baseline

As of this release, Clojure requires a minimum Java 17 runtime and compiles to Java 17 bytecode.

  • CLJ-2872 Move to new ASM and emit Java 17 bytecode

  • CLJ-2383 Add new java.lang classes to automatic imports

  • CLJ-2892 Remove uses of Java’s security manager, which is going away

  • CLJ-2920 Javadoc support updated for Java 17+

Other changes since last alpha

  • CLJ-2968 Qualified :keys bindings in destructuring can only be unqualified symbols (regression fix)

  • CLJ-2969 Add :select base cases to tests

  • CLJ-2897 prepl is missing DynamicClassLoader and *repl\* binding from repl

Try it out

Update your deps.edn :deps with:

org.clojure/clojure {:mvn/version "1.13.0-alpha5"}

Start a REPL with the Clojure CLI (any version) with:

clj -Sdeps '{:deps {org.clojure/clojure {:mvn/version "1.13.0-alpha5"}}}

Permalink

Datalevin 1.0.0 Is Here: One Database for Application State and Agent Memory

Today, after six years of development, I am thrilled to announce the availability of Datalevin 1.0.0.

We started Datalevin in 2020 with a deceptively simple question: why should SQL databases remain the default center of application state?

Six years of research, engineering, benchmarking, production use, and community feedback later, Datalevin 1.0.0 is our answer. Datalevin is open source under the Eclipse Public License 2.0. It is a durable, high-performance, fact-first database that brings relational queries, graph traversal, logical reasoning, document access, full-text search, and vector search into one compact system.

The release completes the roadmap we set for 1.0: automatic path indexing for documents; write-ahead logging and transaction-log access; read-only replicas and high availability; a JSON API; broad libraries for Clojure, Java, Python, and JavaScript; and much more.

It also arrives with two new ways to learn Datalevin. The new Datalevin website contains the online guide, with examples in Clojure, Java, Python, and JavaScript. The complete book, Datalevin: The Definitive Guide to Logical and Intelligent Databases, is available in print and ebook formats. In addition to the full database guide, the book contains five chapters devoted to persistent memory for intelligent systems.

This is a release, a book, and a website. More importantly, it is the point at which the original Datalevin idea becomes a complete platform.

Replace SQL at the Center

Datalevin is not intended to be one more specialized database sitting beside a SQL system. Its goal is to replace SQL databases at the center of application state.

That does not mean recreating SQL with different syntax. It means replacing the table as the center of gravity with the fact. Datalevin stores small entity-attribute-value facts, or datoms. The same fact can participate in a row-like record, a graph edge, a nested document workflow, a search result, or a logical rule without being copied into a different data model.

Why make such a fundamental change? There are three main arguments.

1. SQL Is an Awkward Application Interface

SQL is a string-shaped language embedded inside programs. It has a large, English-like syntax, many dialects, and poor composition with host-language code. The enormous ecosystems of ORMs, query builders, migration tools, and object mapping layers are not signs that SQL is a natural application interface. They are evidence of how much machinery is needed to make it behave like one.

Datalevin queries are data. Datalog expresses the facts that must be true, while shared variables create joins implicitly. Rules package reusable logic, and recursive rules use the same form as ordinary queries. The programmer describes relationships instead of spelling out a sequence of join mechanics.

This smaller, more regular surface is easier for people to learn and easier for programs to construct. It is also a better target for AI-generated queries: fewer syntactic branches, fewer vendor-specific choices, and less planner-sensitive ceremony.

2. Table-Shaped Storage Makes Complex Query Planning Harder

Rows bundle many individual facts into containers. When data are sparse, skewed, or correlated, a SQL optimizer has a hard time estimating how many rows will survive each predicate and join. Those cardinality estimates often depend on histograms, independence assumptions, and other approximations. A bad estimate can turn a reasonable query into a huge intermediate result.

A fact-first store begins with explicit, independently indexed data items. Missing facts are absent rather than represented by positional NULL values. Datalevin can count and sample the same indexed facts that query execution will use, giving its cost-based optimizer better raw material for planning complex joins.

This is not only a theoretical advantage. In the Join Order Benchmark, Datalevin has demonstrated that a triplestore can outperform PostgreSQL and SQLite on complex relational queries. The same query engine also performs strongly on recursive logic and industry-standard graph workloads.

3. Stacking Extensions Creates an Integration Tax

Modern SQL databases can add JSON, full-text search, vector indexes, graph features, recursive queries, and procedural extensions. Each capability is useful. The trouble starts when one application question needs several of them at once.

Every extension tends to bring its own syntax, operators, index types, cost model, and operational rules. If the capabilities are split into separate services, the application must also synchronize copies of data and reconcile results across network boundaries. Either way, the glue moves into application code.

Datalevin makes these capabilities composable over one database state in the same elegant fact based model. A single query can ask for documents that contain a phrase, are close to a question in embedding space, belong to a particular entity in a graph, satisfy a nested document predicate, and pass exact permission and lifecycle rules. The database does the integration work where it belongs.

A Memory Substrate for AI Agents

That unified model matters even more for AI agents.

An agent needs more than a transcript and more than a vector database. It needs durable episodes, structured facts, goals, tasks, permissions, tool results, source documents, relationships, and a bounded view of what matters now. It needs to recall by similarity, but it must also know which facts are current, which source supports them, who is allowed to see them, and what they are connected to.

Datalevin is designed to be the memory substrate underneath that system:

  • Full-text search recalls information by words, phrases, and boolean search expressions.
  • Vector and embedding search recalls information by semantic similarity.
  • Logical access uses Datalog queries and rules to enforce exact conditions, derive facts, and reason recursively.
  • Graph access follows relationships among users, episodes, facts, goals, tasks, evidence, and documents.
  • Document access keeps nested EDN, JSON, and Markdown values intact while automatically indexing their paths.
  • Relational access joins structured application state without giving up the fact-first model.

These are not six disconnected products. They are six ways to see and retrieve the same durable fact based state.

That distinction is crucial. Similarity search can find plausible memories, but similarity alone cannot decide whether a fact is authorized, supported, superseded, or relevant to the active goal. Datalevin lets vector and full-text recall produce candidates, then lets logic, graph relationships, document predicates, and ordinary joins constrain and explain the result.

Datalevin does not try to be an agent runtime. The application still owns model calls, tool authorization, ingestion policy, consolidation, truth maintenance, and prompt assembly. Datalevin provides the durable, transactional environment in which those decisions can be stored, inspected, queried, and resumed. Its built-in MCP server can also expose this memory directly to MCP-compatible AI tools.

In other words, a context window is temporary attention. Datalevin is memory.

One Database, Almost the Same API Everywhere

Datalevin began as a Clojure library, but 1.0 is not limited to Clojure applications. The Clojure, Java, Python, and JavaScript APIs now cover almost the same public surface:

Capability Clojure Java Python JavaScript
Embedded and remote connections Yes Yes Yes Yes
Datalog query, pull, and explain Yes Yes Yes Yes
Synchronous and asynchronous transactions Yes Yes Yes Yes
Datoms, index reads, bulk loading, and re-indexing Yes Yes Yes Yes
Key-value APIs and explicit KV transactions Yes Yes Yes Yes
Full-text, vector, embedding, and idoc access Yes Yes Yes Yes
Standalone search and vector indexes Yes Yes Yes Yes
UDF registries and query, transaction, and analyzer UDFs Yes Yes Yes Yes
Backup, snapshots, transaction logs, replicas, and HA administration Yes Yes Yes Yes

The remaining differences are small and explicit. JavaScript does not expose the Datalog transaction callback because callback re-entry through the Node/JVM bridge can deadlock. Staged mutation of an existing entity object remains a Clojure-only convenience; Java, Python, and JavaScript use transaction maps or builders instead. The full, current list lives in the language compatibility matrix.

Whatever language you choose, the important parts do not change: the same facts, schema, transactions, Datalog queries, and indexes.

Embedded, Server, or Script: Choose at Deployment Time

The data model should not have to change when the deployment topology changes. Datalevin therefore supports three primary ways to run:

Mode Use it for
Embedded Link Datalevin into a Clojure, Java, Python, or Node.js process for fast local access, much like SQLite.
Server Share databases across processes or machines with remote clients, role-based access control, read-only replicas, and high availability.
Scripting Use the Babashka pod for fast-starting automation, command-line tools, data jobs, and operational scripts.

There is also an MCP server mode for local AI-tool integration. You can begin with an embedded prototype, move to a shared server as the application grows, and automate it from scripts without rewriting the data model or query language.

Deployment changes. The facts do not.

Start Building

Datalevin 1.0.0 is available now:

Reaching 1.0 took six years because the goal was never merely to ship another query language or another storage wrapper. The goal was to build one coherent place for application state: simple enough to embed, serious enough to run as a server, expressive enough for relational, graph, document, and logical work, and intelligent enough to become durable memory for the next generation of AI systems.

Thank you to everyone who tested Datalevin, reported issues, contributed code, shared benchmarks, trusted it in production, or simply asked hard questions. You helped turn an ambitious idea into a 1.0 database.

Datalevin 1.0.0 is here. Let us build applications and agents that remember.

Permalink

clj-suitable 0.8.0: Closing the Gap with Compliment

You had me at js/.

– Jerry Maguire, on ClojureScript interop completion

clj-suitable 0.8.0 is out! If the name doesn’t ring a bell, that’s rather the point - clj-suitable is the small library that quietly powers ClojureScript code completion in CIDER, Calva, and pretty much anything else that talks to a cljs REPL over nREPL. Think of it as the ClojureScript counterpart to what compliment1 does for Clojure. For years it lagged well behind its Clojure sibling, and this release is my attempt to finally close that gap - to make cljs completion, if you’ll forgive me, a touch more suitable.

This is also a direct follow-up to the Piggieback work I wrote about a couple of weeks ago. Once I had the cljs REPL plumbing back in decent shape, fixing up the completion story sitting on top of it was the obvious next move.2

The mission: catch up with compliment

Clojure programmers have had really good completion for years, thanks to compliment. ClojureScript programmers got a paler version of the same idea - the basics worked, but all the little touches that make completion feel smart were missing. My goal for this cycle was simple to state and less simple to deliver: teach clj-suitable the tricks compliment has had all along. I think we’re kind of there now.

Here’s what that means in practice, mostly borrowed straight from compliment’s playbook:

  • Fuzzy matching. You no longer have to type a prefix - cs now completes to clojure.string and rkv to reduce-kv, the same subsequence matching you’re used to on the Clojure side.
  • Smarter ranking. Candidates are ordered the way compliment orders them - vars from the current namespace first, then cljs.core, then everything else. The thing you actually want tends to be at the top instead of buried alphabetically.
  • Local bindings. Completion now sees the bindings from the surrounding form - let, loop, fn, for, doseq and friends - including destructured ones. Type first| inside (let [{:keys [first-name]} m] ...) and you’ll get first-name, which previously you would not.
  • Referred vars. Inside a (:require [clojure.string :refer [jo|]]) clause you now get join, scoped to that one namespace rather than the whole world.
  • Context awareness. Special forms are only offered at the head of a list, so if, let and company stop showing up as candidates in argument position where they make no sense.

None of these are revolutionary on their own, but together they’re the difference between completion that feels like an afterthought and completion that feels like it belongs.

A bit of backstory

There’s a nice irony in chasing compliment, because for a while ClojureScript completion actually lived inside it. Back in 2019 Andrea Richiardi ported the cljs-tooling completion machinery - the same code CIDER used for cljs at the time - straight into compliment, and there was even a follow-up attempt to pull clj-suitable’s JavaScript interop completions in alongside it. (You can still spot the heritage: a few functions in clj-suitable’s current source are marked “Ported from compliment.”)

In the end we went the other way around: instead of growing compliment to cover ClojureScript, we consolidated the ClojureScript side in clj-suitable and reverted the port. That sounds like wasted effort, but it wasn’t - compliment’s pluggable custom source architecture is exactly what made the split clean. clj-suitable just registers itself as another source, so tools get Clojure and ClojureScript completion side by side without compliment having to know a thing about cljs.

Robert Krahn had started clj-suitable earlier that year for the dynamic, runtime-introspection side, and the static ClojureScript completion found its permanent home there too. In hindsight it was clearly the right call: cljs completion gets to grow (and break, and get fixed) on its own schedule, and compliment stays focused and lean. A good architecture is the kind that makes the split you didn’t plan for feel obvious after the fact.

Why ClojureScript makes this harder

Completion for Clojure is almost unfairly simple. Your code runs on the same JVM as the nREPL server, so compliment can just reflect on the live thing - real vars, real namespaces, real Java classes, all sitting in the same process. Ask a question, get an answer.

ClojureScript doesn’t get to be that lucky, because it lives in two worlds at once. The compiler is a Clojure program running on the JVM, and it’s the source of truth for namespaces, vars and their metadata - so static completion reads the ClojureScript compiler state, not your running program. But your actual program runs somewhere else entirely: a Node process, a browser tab, maybe a React Native app on a phone, reachable only across a REPL bridge. When you want to complete JavaScript interop - the methods on js/console, say - there’s nothing on the JVM to reflect on. You have to ship a bit of code across that bridge, run it in the JS runtime, and read back what a live object actually exposes.

That one fact is where all the complexity comes from. The bridge isn’t even a single thing - a piggieback-driven cljs.repl runtime evaluates differently from shadow-cljs, and clj-suitable has to speak both. The runtime can vanish under you - refresh a browser tab and the namespace you loaded is gone. And poking at a JS object to list its properties can have side effects, because a property getter is just code that runs. (If you’ve ever wondered why clj-suitable is so careful to only evaluate things that genuinely look like interop, that’s why.)

So a completion request that looks like one operation from the editor is really two very different machines under the hood - one reading compiler state on the JVM, one evaluating code in a JS runtime you don’t control. Here’s the whole picture:

  editor (CIDER / Calva)
     |   complete: prefix + context
     v
  nREPL server (one JVM) - cider-nrepl + clj-suitable
     |
     +-- static ---> compliment + clj-suitable's cljs source
     |               reads the ClojureScript compiler state
     |               (namespaces, vars, locals, keywords) - stays on the JVM
     |
     +-- dynamic --> only for JS interop forms
                        |   eval introspection code across the REPL bridge
                        v
              piggieback (cljs.repl)   or   shadow-cljs
                        |
                        v
              Node / browser / React Native  (the JS runtime)
              suitable.js-introspection reads a live object's
              properties and methods, and sends them back
     |
     v
  candidates from both paths, merged and returned to the editor

Two paths, one answer. Clojure completion has only ever needed the top half of that diagram.

Dynamic completion, tightened up

That dynamic path is the fiddly one, and it’s where this release did most of its sanding. The interop completion already worked - (.| js/console) would offer you log, warn and the rest - but it had some rough edges:

  • Completing interop no longer clobbers your REPL history. Poking at (.| js/some-obj) used to quietly overwrite *1/*2/*3 with the introspection result; now your last real value stays put where it belongs.
  • The introspection namespace is loaded once per session instead of on every single completion request. On a Node REPL that’s a needless round-trip gone from every keystroke.
  • The browser-runtime path got hardened. I chased down a couple of long-standing “no completions in the browser” reports, stood up a real headless-Chrome integration test to reproduce them, and fixed a lurking crash along the way. (The short version of the investigation: the old failures came from an inlined build that current CIDER no longer produces, so most of you were never affected - but now there are tests making sure it stays that way.)

The full changelog has everything that didn’t make the highlights.

One more thing

0.8.0 gets the headline, but it stands on the 0.7.0 release from a week earlier, which did the unglamorous groundwork: modern dependencies (ClojureScript 1.12, compliment 0.8.0, shadow-cljs 3.x), a move from CircleCI to GitHub Actions, a tools.build-based build, and - crucially for my sanity - actual integration tests that drive real Node and browser runtimes instead of trusting things to work.

Actually, I only thought about improving how clj-suitable works when I started to update its dependencies and CI setup.

Epilogue

No sufficiently useful system can be both complete and consistent.

– Kurt Gödel, subtweeting every autocomplete ever

If you use CIDER, you don’t have to do anything to get any of this - it’ll ship to you as part of the upcoming CIDER 2.1. Calva and other nREPL-based tools that depend on clj-suitable will pick it up on their own schedule.

As always, this stands on the shoulders of others. Huge thanks to Alex Yakushev, whose compliment is both the benchmark I was chasing and the source of a good chunk of these ideas; to Andrea Richiardi, who did much of the early work bridging ClojureScript completion and compliment; and to Robert Krahn for creating clj-suitable in the first place and giving me such a solid foundation to build on.

Is any of this complete? Of course not - completeness is a horizon, not a destination, and of all people a completion library should be the first to admit it (see the gentleman up top). But clj-suitable suits ClojureScript a good deal better than it did a month ago, and that was rather the point.

Keep hacking!

  1. The completion library, spelled with an i. Not the nice thing you say to someone, and - I really cannot stress this enough - not complement with an e

  2. One thing invariably leads to another with this stuff. You set out to fix a REPL env wrapper and three weeks later you’re writing a headless-Chrome test harness. No regrets. 

Permalink

Maybe not microservice: The Case for Pipes, Pipelines, and Functional Isolation

1. Subsystem Decomposition

1.1 The Decomposition Problem

A subsystem decomposes a codebase into smaller, cohesive units. Two primary axes of decomposition exist:

  • Technical axis: grouping by component type (controller, service, model, view)
  • Functional axis: grouping by business capability (cataloguing, circulation, etc.)

1.2 Tension Between Framework Prescriptions and Decomposition Strategy

Organizing top-level subsystems functionally may create friction with frameworks that prescribe a technical-first structure. Concrete examples:

  • Rails enforces model, view, and controller directories at the root level, making functional decomposition awkward without additional mechanisms like Rails Engines
  • Sinatra (a microframework) imposes minimal structure, leaving architectural decisions entirely to the team

Frameworks with rigid prescriptions constrain architectural choices. Frameworks with no structure shift the entire burden onto the team with no guidance. This second approach might be fine for teams that know what they are doing and how to shape the architecture properly. Not everyone needs guidance from the framework.

1.3 Contexts as a Middle Ground

Phoenix provides contexts as a compromise:

  • Explicit, guideline-oriented subsystems that enable functional decomposition without rigid enforcement
  • Contexts define functional boundaries while allowing technical organization to remain nested within them
  • Functional blocks may later evolve into microservices, but this is optional
  • The same decomposition serves equally well in a modular monolith or a distributed architecture
  • The choice depends on team needs, scaling requirements, and operational maturity, not on the decomposition strategy itself

2. Pipeline Topology and Data Flow

2.1 The Unix Pipeline Model

Unix pipelines model data flow through a single stream connecting stdout to stdin. This forms a linear chain where each stage's output becomes the next stage's input. Key characteristics:

  • Each stage has exactly one input and one output
  • Cognitive overhead is minimized because the topology is trivial to trace
  • The linear, single-stream characteristic is not mandatory for a pipeline, but it reduces complexity significantly

2.2 Arbitrary DAG Topologies

Orchestrators like Airflow allow arbitrary DAG topologies with fan-in and fan-out edges. Tradeoffs:

  • Powerful for expressing complex dependencies
  • DAGs with dense interconnections tend to become hard to read even with visual rendering
  • Complex function-call topologies with many parameters outside Airflow and Unix pipelines also produce unreadable code

2.3 Byte Streams and Opaque Containers

Unix-like pipelines connect programs by passing data through unidirectional byte-streams:

  • Programs at each end agree on a structure such as JSON, CSV, or tar archives
  • The pipe mechanism itself transports only raw bytes
  • This has a direct analogue in dynamically typed languages: in Lisp and Clojure, collections (lists, maps, vectors) serve as opaque containers that can hold almost arbitrary data
  • The consumer interprets the contents rather than the container dictating them

3. Typing Heterogeneous Pipeline Data

3.1 The Problem

Strict type systems introduce complications when handling heterogeneous data flowing through a pipeline where each stage transforms the shape slightly.

3.2 Failed Approaches

Single large type with many optional fields:

  • Creates dependencies between all pipeline steps
  • Loses the ability to reject illegal data
  • Makes reuse difficult
  • Changes to one field propagate everywhere

Many separate types for each step:

  • Exhaustive and adds noise to the program
  • Structures may not be mutually exclusive yet are treated as such
  • Maintenance burden grows with every new stage

Both approaches fail because each pipeline stage depends on more than it needs.

3.3 Partial Fixes From Functional Programming

Two techniques alleviate but do not fully resolve the problem:

  • Functional record update: enables creating modified copies without mutation, reducing coupling related to state changes
  • Sum types: restore the ability to discriminate valid from invalid data and support exhaustiveness checking

Remaining limitation: every step that pattern-matches on a sum type must know about all variants. Adding a new case still propagates changes through the pipeline.

3.4 Structural Type Compatibility

Structural type compatibility offers a complementary solution:

  • Independently defined types become compatible based on shape alone without requiring any inheritance relationship
  • A consumer can specify only the subset of fields it needs via a structural interface
  • Each step depends on a minimal projection of the data rather than the full type
  • This decouples pipeline stages more effectively than either naive approach or sum types alone

3.5 Python Implementation

Python implements several of these patterns:

  • dataclasses.replace(): supports immutable record updates
  • The | union operator: simplifies union type expressions
  • Protocol classes: enable structural subtyping, allowing independent types to satisfy contracts based on method and attribute signatures
  • Tagged unions: modeled using Literal discriminator fields on dataclasses or TypedDicts
  • typing.assert_never with mypy: enforces exhaustiveness checking on pattern matching or if chains, providing compile-time guarantees similar to sum types in functional languages

Combined approach:

  • Protocols decouple steps through structural conformance
  • Tagged unions enable variant discrimination with exhaustiveness checking
  • Functional record updates reduce mutation-related coupling

4. Microservices, Processes, and Isolation Patterns

4.1 The Shared Principle: Isolated State by Default

A microservice and a Unix process share architectural similarities:

  • Microservices: in well-designed architectures, a service does not share variables or databases with other services. Communication happens through well-defined interfaces. This is a best practice, not a hard technical constraint.
  • Unix processes: each process has its own virtual address space and does not share memory directly with other processes. Explicit sharing is possible through mechanisms such as shm_open (POSIX shared memory) or mmap.

4.2 Historical Lineage

Microservices on GNU/Linux are literally processes communicating via HTTP over TCP/IP. The historical chain:

  • TCP/IP: first implemented in 4.2BSD Unix in 1983
  • HTTP: developed at CERN in 1989 to 1990, building upon these networking foundations
  • Tim Berners-Lee wrote the first HTTP server and web browser on a NeXT workstation running NeXTSTEP in fall 1990
  • NeXTSTEP was heavily influenced by BSD Unix
  • GNU/Linux copied many initial ideas from Unix while remaining free and open

The modern distributed system traces an unbroken lineage back to Unix.

4.3 Pipes as an Alternative to Microservices

Piping via stdin-stdout chains is another mode of interprocess communication:

  • Not as powerful or generic as TCP/IP or HTTP
  • Easy to use and reason about
  • Naturally fits data pipelines
  • A data pipeline can be built using command-line tools piped together, running as processes on GNU/Linux instead of using microservices and a full orchestration system
  • Scalability can be achieved by SSH and distribution through GNU Parallel, which launches jobs across multiple machines accessed over the network

4.4 Erlang and Clojure as Additional Isolation Models

Erlang processes:

  • Lightweight alternative to Unix processes
  • Rich high-level interprocess communication via mailbox message passing
  • The Erlang VM enforces process isolation as a runtime guarantee

Clojure and other functional runtimes:

  • Do not have the same process isolation constraints as the Erlang VM
  • Provide lightweight isolation via persistent data structures and Software Transactional Memory (STM)
  • STM allows memory sharing while preventing conflicts even when multiple functions run in parallel or concurrently

Bottom Line: Think Twice Before Going Micro

Here is the real talk. You might want to pause before spinning up your first microservice. Ask yourself these questions:

  • Do I actually need physical isolation, or will logical separation suffice?
  • Can a simple pipe between processes do the job just as well?
  • Am I solving a scaling problem that does not exist yet?
  • Do I have the ops maturity to handle distributed tracing, service meshes, and deployment pipelines?
  • Will my team understand this architecture six months from now?

The truth is, Unix pipes have been doing data transformation reliably since 1973. Erlang processes have handled millions of concurrent connections since the 1980s. Functional isolation with STM has been working since Clojure showed up in 2009. None of these require Kubernetes. None of them need a dedicated platform team. And none of them will haunt you with debugging nightmares at 3am.

Microservices are not evil. They are just heavy. They are the nuclear option for isolation. Use them when the problem demands the weight. Otherwise, reach for the lighter tool. A pipe, a context boundary, a protocol type. Try the easy solution first. If it breaks, then scale up. Most teams never get to that point. And their systems stay simpler, cheaper, and easier to maintain because of it.

So yeah, think twice. Maybe thrice. Then build the smallest thing that could possibly work.

Permalink

Arbitrary Update: The Next One

Just a minor updates post. There are three tiny things.

Firstly, I now have a Printables account. I already have a Thingiverse profile that I haven't touched in a fair while. Not really sure why. If I had to speculate, I'd say that it has less to do with the MyMiniFactory buy-out and more to do with the fact that some of the creators I follow are more active on the Prusa site than Thingiverse. This makes me mildly sad because, as a Cory Doctorow fan, I'm more positively disposed towards the scrappy maker ethos exemplified by Thingiverse.

Secondly, the logo bar now includes the OpenSCAD logo. Given how much work I've been doing in it, and given the fact that PHP and Rebol are still up there, it was about time.

Thirdly, this blog is now written in Python. I've been holding on to Clojure for probably longer than was sensible. And in particular, given what this blog is, it was getting harder and harder to justify running a full JVM on my server for it. Deployment was kind of a pain and involved screen, and doing magic to make sure the blog came back up when I restarted the server. There is now a docker-compose.yml over in the repo, which should tell you exactly how I plan to use this. As per the usual, there was heavy LLM assistance here. I should note; I started this port sometime last year, and didn't get annoyed enough by server restarts to finish it until last week. The difference is staggering. Originally, I was doing the function-by-function thing, occasionally rewriting the output from ChatGPT entirely in order to make the Clojure to Python translation at all sensical. It kept trying to do weird things like reimplement the atom system, even where it was literally being used for plain mutable state (which Python has by default). That didn't happen this time; I fed the remaining files in, told it what I wanted and where the cleavage points were, and it spat out a 98% good blog server, along with deployment workflows and setup. As someone who always used programming as an instrumental skill in service of acts of creation, I'm pretty damn happy about this.

That's it for now. I'm working on a few things in the background. One of them might even eventually involve Clojure (or at least Clojurescript) in some capacity. But I wanted to get the update out before it got stale.

As always, I'll let you know how it goes.

Permalink

July 2026 Short Term Project Updates

Here are July’s updates for short term projects funded in Q2 2026. You can find overviews of these projects and the two others which will be reporting on a slightly different schedule in the original funding announcement. Thanks everyone!

Clojure LLM: Dragan Djuric
Malli: Ambrose Bonnaire-Sergeant
PluMCP: Shantanu Kumar

Clojure LLM: Dragan Djuric

Q2 2026 Report 2. Published June 30, 2026

The proposal was (in short):

The goal is to provide a high performance local LLM (large language model) AI solution, that supports mainstream open models, freely available at Hugging Face and elsewhere. Something like llama.cpp (https://llama-cpp.com/), but (hopefully!) simpler and faster, with both GPU and CPU support baked-in from the start.

I even have a catchy name for the library: iLLaManati :)

iLLaManati should:

  • work :)
  • be very fast,
  • have a very simple API (possibly even a NO-API if you use the default configuration),
  • have a fairly elegant implementation with not many lines of code, which will be a great showcase for Clojure as an enabling technology, and a good learning source for Clojurians.
  • integrate into the Clojure ecosystem naturally and seamlessly,
  • NOT require Clojurists to know anything about CUDA, ONNX, tensors, or linear algebra, to be able to use it (will require some of that if you want to extend it, though!),
  • run on your laptop, server, or cloud; wherever Clojure runs. It’s your choice.
  • be a great low-effort gateway for Clojurists to peek, as users, into high-performance and GPU computing,
  • be a very attractive topic to tell the world about!

Progress so far:

In the second month, main focus was on the hammock, but I also accomplished plenty of implementation. The initial prototype is almost there. When it start working correctly, I will be able to polish it a bit and try to squeeze as much performance is available in ONNX Runtime (not that much!) However, these challenges help me forge a better overall framework for more serious engines in the future.

The hammock

Lots of reading and thinking. And again.

Tokenizer

I polished the tokenizer a bit and integrated it with LLM. It works, and works well!

The original superfast token sampler

I polished this sampler and integrated it into the LLM loop. I fixed some correctness bugs and also supported float16 data (without losing perofmance). I didn’t have time to write up a scientific article, so that’s pushed into July (I hope!) so I didn’t publish the source yet.

The heart: LLM runner

This is still WIP, but i made lots of progress still. I implemented universal Clojure types that can cover both CPU and GPU implementations of prefill and decode. I connected that with the tokenizer and sampler, and got a consistent and meaningful stream of tokens out of it. So the first milestone for the functional part is there.

The KV manager also works well, having in mind that it has to cater to the ONNX Runtime constraint.

However, lots of challenges, especially with the CUDA EP. The ONNX Runtime has its own quirks, and of course the documentation is scarce and examples non-existing when it comes to integrating this with other CUDA code. I spent many hours debugging heisenbugs and trying to make it fits nicely. I made huge progress, but still have some quirks to solve before I get it to the same level of correctness that the CPU has. ONNX Runtime seems to have mind of its own with CUDA, cuBLAS, and cuDNN contexts and streams, and, of course it can’t be controlled fully from the outside, and of course it sometimes work this way, and sometimes that way…

So, the correctness part is not fully there, but I expect to solve it soon.

I’m less optimistic about the performance part. Of course it’s not expected of the default execution provider, but plenty of powerful providers are there: CUDA, TensorRT, OpenVINO, DNNL. Alas, none of these providers supports ALL Gemma3 operations, and this seriously sabotages the performance. Some less advanced models might be better supported (and even Gemma3 is last years news though) but overall it seems that ONNX Runtime struggles with up-to-date support for diverse LLM model architectures.

But does that make me pessimistic? On the contrary! This struggle gave me great insights into the challenges of running diverse LLMs (not only for text generation), and I have some concrete ideas about a solution with many backends, not unlike how Neanderthal and Deep Diamond solved this for matrices and tensors. The backend based on ONNX Runtime will be a good multiplatform baseline and the initial prototype, and then I can create backends based on industry heavyweights such as TensorRT-LLM for Nvidia GPUs, OpenVINO for Intel CPUs, MLX for MacOS, and, why not, even integrate Llama.cpp as an all-rounder.

Miscellaneous

To accommodate the requirements of iLLaManati, I worked on assorted improvements and upgrades in Uncomplicate libraries. I also spent a lot of time compiling upstream C++ code and dealing with cryptic C++ compiler shenanigans, that I am constantly reminded why Clojure is so great to work with :)

Of great importance is that I added support for Float16 to both Neanderthal and Deep Diamond!

I haven’t had time to make official releases, nor I committed all code to GitHub. I’m still in the middle of the battle.


Malli: Ambrose Bonnaire-Sergeant

Q2 2026 Report 2. Published July 17, 2026

In this project, I am tackling exponential growth related to Malli refs.

There has been a lot of progress to report in this second month of work. As before, I have been iterating on an implementation in this pull request, and I think I have carved out a design and implementation that addresses the main goals of the project, while streamlining and simplifying both the current and future design of Malli schemas.

If we view Malli schemas as a graph where nodes are schemas and edges point to their child schemas, then this graph is acyclic in Malli’s current implementation. Not only that, nodes that represent the exact same schema but are merely occurrences naming the same schema are not consolidated. This has caused many performance and usability issues with Malli that we have historically tackled by trying to consolidate this graph within Malli’s operations.

For example, when converting Malli schemas to value generators we add extra checks to essentially detect cycles in the graph of schemas in order to avoid generating unusably large values. We then solved the same problem separately for validators, explainers and transformers to fix memory leaks caused by recursing down large values, with each implementation being distict. The same problem would have to be solved for each current and future operation that we’d like to purge these issues of.

One particular symptom of this surprising duplication of effort is worth mentioning. Schema instances (nodes in the graph described earlier) each carry an internal cache for caching results. Notably absent is any use of this cache in the algorithms that exploit cycles in generators, validators or any other operation. Keeping the visualization of schemas as a graph in mind, each schema (node), and thus each schema’s cache, is self contained. Since there is no deduplication of schemas in the graph, even semantically identical schemas do not share a cache. This points to an elegant solution: upgrading Malli’s internal representation of schemas to reliably deduplicate schema instances such that the same schemas share the same cache.

My previous report explained a solution to this which still seems effective. I speculated that we could undo the duplication of effort in schema operations, and this month I’m happy to report exactly that: the new design since then reverts the custom ref validator implementation back to its original implementation while still tying the knot and thus avoiding memory leaks:

           (-validator [_]
             (let [validator (-memoize #(validator (rf)))]
               (fn [x] ((validator) x))))

Well, there is one subtle difference: we are memoizing a call to validator instead of -validator. The former caches the validator in the schema’s internal cache, which is now effective because we have deduplicated the graph of schemas and thus the same cache is used for semantically identical schemas.

In this new design, a schema like:

(m/schema
 [:schema {:registry {::list-of [:seqable ::element],
                      ::element :string}}
  [:tuple ::list-of
          ::list-of
          ::list-of]])

deduplicates the three ref occurrences of ::list-of in the :tuple to all point to the same :seqable schema instance, and thus the same cache. This means only the first ::list-of occurrence actually creates a validator, the second and third merely pull it from the shared cache. In contrast, the old implementation would create (at least) three distinct schema instances, and then the validator algorithm would manually deduplicate validators via a subtle algorithm. In this design, Malli’s own schema parsing logic performs deduplication, making the efficient implementation of operations much easier.

There are a few unknowns to resolve. Malli’s maintainers previously rejected this approach of caching recursive calls to validator, but have expressed interest in reconsidering the decision. The crux of the concern is that excessive caching of internal results will interfere with exotic registry implementations, such as those based on dynamic vars, and my stance is that Malli already uses caches too extensively for these kinds of registries to be reliable in these scenarios.

Also, I expected this new design to simplify the implementation of ref generators, but it caused some tests to fail and I reverted the change (you can see that here).

I suspect it may be hard to beat the current implementation mapping recursive refs to gen/recursive-gen using this new design, but I would like to at least know if the test failures are pointing to a problem in the schema deduplication algorithm itself. I’m curious if it will reveal differences between pointers and refs—or recursive and non-recursive refs—that I have missed.


PluMCP: Shantanu Kumar

Q2 2026 Report 1. Published July 1, 2026

I am grateful to Clojurists Together for sponsoring PluMCP during the 2026 Q2 cycle. The planned scope of work for this sponsorship is:

  • MCP spec 2025-11-25 implementation
  • PluMCP Usage documentation enhancement

Early in the sponsorship period I had to deal with unavoidable commitments outside the project, which delayed my progress. Kathy kindly granted me an extension, so this report is being submitted about two weeks later than originally planned.

MCP 2025-11-25 implementation

At the beginning of the sponsorship cycle, PluMCP v0.2.x supported the following MCP specification versions:

MCP spec version PluMCP implementation status
2025-11-25 (TODO) To be implemented during the sponsorship cycle
2025-06-18 (Done) Supported as the main spec version
2025-03-26 (Done) Supported in compatibility mode
2024-11-05 Not supported, no plan to support

At the start of the cycle, PluMCP advertised support for the 2025-11-25 specification during the MCP handshake but the implemented feature set corresponded entirely to the 2025-06-18 specification. This was functionally correct because the differences between the two specification versions were limited to optional features.

This sponsorship cycle closes that gap. PluMCP is implementing features listed in the MCP 2025-11-25 spec. The changes between 2025-06-18 and 2025-11-25 are captured here.

Progress so far

The larger part of the work is the MCP 2025-11-25 implementation, which can be tracked in PluMCP pull request #6.

This list does not include the schema updates required by the new MCP specification or the corresponding entity generator functions. Although implementing these required considerable groundwork, they provide a reliable foundation for the remaining implementation.

At this point, 5 of the 9 major feature changes and 5 of the 10 minor feature changes are complete. The completed work includes:

Major Changes completed

  • Allow servers to expose icons as additional metadata for tools, resources, resource templates, and prompts
  • Validate tool names as per the new spec
  • Update ElicitResult and EnumSchema to use a more standards-based approach and support titled, untitled, single-select, and multi-select enums
  • Add support for URL mode elicitation
  • Add tool calling support to sampling via tools and toolChoice parameters

Minor changes completed

  • Add utility function(s) to let servers using STDIO transport use STDERR for all types of logging, not just error messages
  • Add optional description field to Implementation (schema) interface to align with MCP registry server.json format and provide human-readable context during initialization
  • Have the servers respond with HTTP 403 Forbidden for invalid Origin headers in Streamable HTTP transport
  • Add support for default values in all primitive types (string, number, enum) for elicitation schemas
  • Establish JSON Schema 2020-12 as the default dialect for MCP schema definitions (2020-12 is the ONLY supported dialect for now)

Current status

10 of the 19 planned specification changes are complete, with Task orchestration currently under active development.

Remaining work

MCP 2025-11-25 introduced an experimental feature for task orchestration, which is also one of the largest additions in this release. This is the feature I am currently working on, and I hope to release a 0.3.0 alpha in about a week.

More than half of the specification’s feature set has now been implemented. What remains is to implement the rest of the specification along with the planned documentation improvements and example code.

I look forward to completing the remaining implementation and documentation work during the second half of the sponsorship period.

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.