vim-slime

<!DOCTYPE html> <html> <head> <title>Tarn Barford</title> <meta charset="utf-8"/> <link rel="icon" type="image/x-icon" href="/favicon.ico"> <link href="/style.css" media="screen" rel="stylesheet" type="text/css" /> <link rel="alternate" type="application/atom+xml" title="Journals of Tarn Barford" href="/atom" /> <link href="/highlight.css" media="screen" rel="stylesheet" type="text/css" /> <link href="/highlight-console.css" media="screen" rel="stylesheet" type="text/css" /> </head> <body> <div id="container"> <div id="header"> <div id="header"> <p>From the <a href="/journal">Journals</a> of <a href="/">Tarn Barford</a></p> <h1> vim-slime </h1> <p> Mar 26, 2012 </p> </div> </div> <div id="post_content"> <html><body><p>Today I found the awesomeness that is <a href="https://github.com/jpalardy/vim-slime">vim-slime</a>, it's been an exciting day for me. <a href="http://common-lisp.net/project/slime/">Slime</a> is the "The Superior Lisp Interaction Mode for Emacs", I can almost hear the emacs crowd laughing.</p> <p>For those that use vim and haven't used Slime, vim-slime or <a href="https://github.com/vim-scripts/VimClojure">something similar</a>, this is why it's awesome:</p> <p><strong>Text can be sent from any process to the stdin of a <a href="http://www.gnu.org/software/screen/">gnu screen</a> or <a href="http://tmux.sourceforge.net/">tmux</a> session. The process in this case is vim and the screen/tmux session is a terminal</strong>.</p> <p>Screen is a <a href="/journal/oh-screen-where-have-you-been">really neat</a> terminal multiplexer (you can run multiple terminals in a terminal window). The multiplexed shell processes are children of the screen process, which itself is not a child of the terminal window process. This means a screen process and its child processes keep running if you close the terminal window. Later you can re-connect to it, this is what makes vim-slime possible.</p> <p>Here is an screen shot, on the left is me in gVim writing some awful Clojure <a href="#footnote-1">[1]</a>. On the right is a screen buffer in which I started a Clojure REPL. When I want to try run some code I can send any vim text selection to the REPL in a keystroke (or two).</p> <p><img alt="vim slime screenshot" src="screenshot.jpg"/></p> <p>It doesn't have to be a Clojure REPL either, we can send anything to a screen shell. We could run git commands, find, grep, sed, etc. Like with the Clojure REPL we can even interact with any terminal programs that use STDIN.</p> <p>This concept can be taken even further, You can even connect to a tmux session over SSH and share a terminal or a <a href="http://remotepairprogramming.com/remote-pair-programming-with-tmux-and-vim-the">terminal program like vim to do remote pairing</a>!</p> <p>Hopefully remote pairing is the topic of my next post as there are a couple geographically distant people I know who are keen to do some pair hacking. I stand to learn a lot!</p> <p><a name="footnote-1">[1]</a> I learnt almost everything I know about Lisp from <a href="http://www.ccs.neu.edu/home/matthias/BTLS/">The Little Schemer</a>. Great book.</p></body></html> </div> <div id="comments"> </div> </div> <div id="footer"> <p>&nbsp;</p> <p>Questions, comments, suggestions? Email me, <a href="mailto:tarn@tarnbarford.net">tarn@tarnbarford.net</a> (<a href="/pgp.txt">public key</a>)</p> <p>&nbsp;</p> </div> </body> </html>

Permalink

Swipe Keyboard

<!DOCTYPE html> <html> <head> <title>Tarn Barford</title> <meta charset="utf-8"/> <link rel="icon" type="image/x-icon" href="/favicon.ico"> <link href="/style.css" media="screen" rel="stylesheet" type="text/css" /> <link rel="alternate" type="application/atom+xml" title="Journals of Tarn Barford" href="/atom" /> <link href="/highlight.css" media="screen" rel="stylesheet" type="text/css" /> <link href="/highlight-console.css" media="screen" rel="stylesheet" type="text/css" /> <style> #swipe-canvas { position: relative; width: 900px; height: 300px; } #swipe-results { font-size: 30px; padding-left: 50px; padding-left: 50px; } #swipe-results ul { margin: 0px; padding: 0px; } #swipe-results li { float: left; background-color: #DDDDDD; list-style-type: none; padding: 10px; margin: 5px; border-radius: 5px; } #swipe { position: relative; } #swipe-loading { position: absolute; height: 50px; width: 300px; top: 85px; left: 300px; background-color: darkgray; border-radius: 10px; text-align: center; padding-top: 20px; border: black; border-width: 5px; } </style> </head> <body> <div id="container"> <div id="header"> <div id="header"> <p>From the <a href="/journal">Journals</a> of <a href="/">Tarn Barford</a></p> <h1> Swipe Keyboard </h1> <p> Apr 06, 2014 </p> </div> </div> <div id="post_content"> <html><body><p>When I first tried a <a href="http://www.swype.com/">Swype</a> keyboard I was impressed how effective it was. Even though I don't use the feature on my phone I was interested in how it could be built, so I <a href="https://github.com/tarnacious/swipe-keyboard">implemented this otherwise useless swipe-able keyboard</a> below. It probably doesn't work on mobile devices, but works on modern browsers with mouse pointers (although I've only really tried Chrome and Firefox).</p> <div id="swipe"> <canvas height="300px" id="swipe-canvas" width="900px"></canvas> <div id="swipe-results"></div> <div style="clear: both"></div> <h2 id="swipe-loading">Loading<noscript>Javascript is Required</noscript></h2> </div> <p>I initially tried to solve this using the technique Peter Norvig famously uses in his <a href="http://norvig.com/spell-correct.html]">spell checker</a>. He takes a sequence of characters and generates a set of word candidates by adding, removing and swapping characters in the original sequence, the generated candidates are removed if they are not found a dictionary. This can work but to be effective too many combinations need to be generated.</p> <p>If the dictionary is indexed into a <a href="http://en.wikipedia.org/wiki/Trie">trie</a> the number of combinations generated can be reduced significantly by traversing the trie and only generating valid letter combinations. This is a pretty bare implementation of that, it requires: </p> <ul> <li>The first and last characters of the initial sequence are used </li> <li>Intermediate characters in the initial sequence can be repeated or discarded </li> <li>No characters are added or swapped</li> </ul> <p>Basically, if you swipe through all the characters in a word in order, then the word will be found if it is in the index regardless how many characters are swiped in between. It is surprisingly quick and effective.</p> <p>This implementation uses <a href="https://raw.github.com/first20hours/google-10000-english/master/google-10000-english.txt">these 10000 words</a>, I intended to use digital books but never got around to it as these words demonstrate the concept well enough.</p> <p>This is the first thing I've written in <a href="https://github.com/clojure/clojurescript">ClojureScript</a> or <a href="https://github.com/clojure/clojurescript">Clojure</a>, so my code my vary from non-idiomatic to shamblolic. I initially used a <a href="http://clojuredocs.org/clojure_core/clojure.zip/zipper">zipper</a> to build the trie with immutable data structures, but found the indexing took to long with my zipper implementation so I <a href="https://github.com/tarnacious/swipe-keyboard/commit/6edd7b26e78121fbe8586b3f0ef54ca8277d9e32">switched to using native Javascript maps</a>.</p> <p>I found that <a href="https://github.com/clojure/core.async">core.async</a> library is really awesome, the <a href="http://docs.closure-library.googlecode.com/git/index.html">Google closure library</a> and <a href="https://developers.google.com/closure/compiler/">compiler</a> integration with <a href="http://leiningen.org/">Leiningen</a> the <a href="https://github.com/emezeske/lein-cljsbuild">cljsbuild plug-in</a> to be impressive. My main pains were the slow JVM start-up time, the advanced closure compiler build of the web worker script fails silently when run (but the main script works fine when compiled with the advanced compiler), and at times I felt some compile time type checking would be nice.</p> <p>I would like to extend this experiment to index the word occurrence counts and proceeding word counts in original text and rank the found words as most likely. Support casing, umlauts, special characters, spelling correction and compound words in the indexing and lookup. I think a live lookup while swiping would also be possible.</p> <p>Overall this was fun, turned out OK I think, and was a great learning experience.</p></body></html> </div> <div id="comments"> </div> </div> <div id="footer"> <p>&nbsp;</p> <p>Questions, comments, suggestions? Email me, <a href="mailto:tarn@tarnbarford.net">tarn@tarnbarford.net</a> (<a href="/pgp.txt">public key</a>)</p> <p>&nbsp;</p> </div> <script src="swipe.js" type="text/javascript"></script> </body> </html>

Permalink

A look at the Clojure CLI REPL

Just before this year's Clojure/conj, the Clojure team released a new Clojure CLI REPL. Per the README:A REPL for the Clojure CLI featuring multi-line editing with proper indentation, bracket highlighting, structural editing, inline eval, doc lookup for Clojure and Java, a data inspector, configurable prompts, keybindings, and much more.

Permalink

I wanted Jepsen's Elle in CI without a JVM, so I wrote adya

Your database documentation says REPEATABLE READ. Your code assumes it. Have you ever checked?

I spent the last stretch building adya, a black-box checker for transactional isolation. You give it a log of transactions a database ran, and it tells you which isolation guarantees held. For each one that didn't, it prints the transactions involved and the chain of reads and writes that proves the violation.

It's an independent Rust implementation of the approach from Elle (Kingsbury and Alvaro, VLDB 2020), the checker behind Jepsen's database analyses. If you know Elle, adya reads the same history formats and takes the same flags. If you don't, keep reading.

The problem with "it passed the tests"

Isolation bugs don't show up in unit tests. They need two or more transactions to interleave in one particular way, and when they happen nothing crashes. You just get a balance that's off, or two people booked into the same seat.

The classic example is write skew. Two doctors are on call, and the rule says at least one must stay on call. Each doctor's transaction checks "is the other one still on call?", sees yes, and takes themselves off. Both commit. Now nobody is on call.

Snapshot isolation allows this. Postgres's REPEATABLE READ is snapshot isolation, so it allows this. If you thought REPEATABLE READ meant "safe enough", the bug ships.

Elle's insight was that you can catch this from the outside. Record what every client asked for and what it got back, infer the dependencies between transactions, and look for cycles that a given isolation level forbids. Atul Adya's 1999 thesis catalogued those cycles (G0, G1c, G-single, G2 and so on), which is where the name comes from.

Why another implementation

Elle is excellent. It's also a Clojure library on the JVM. People who want it outside a Jepsen test end up shelling out to elle-cli from a Go or Python harness. I found two open-source projects doing exactly that in CI (barn, bytecaskdb), and the bytecaskdb PR lists what went wrong: a blocked Clojars mirror, a crash when graphviz was missing, log lines corrupting the JSON output.

On my Windows machine, elle-cli 0.1.11 hung with no output on every anomalous history I gave it. On Linux it worked, but in my CI comparison it needed more than five minutes on five of forty random 300-transaction histories, and ran out of a 6 GB heap on one more. adya checked all forty in 0.2 seconds.

Elle also leaves the workload to you. You have to write the client that generates transactions, runs them against your database and records the history. That's the part most people never get to.

So adya ships both halves in one binary:

cargo install adya

# Run a workload against Postgres at REPEATABLE READ,
# then check the history against serializability.
adya run postgres --url postgres://localhost/test -i repeatable-read -c serializable

What it looks like

You don't need a database to try it. adya has a built-in simulated database that implements isolation levels the textbook way:

$ adya run sim -i snapshot-isolation -c serializable -n 300 -p 4 --seed 1
history.jsonl   false

G2-item #0
  Let:
    T234 = {"index":234,"process":1,"type":"ok","value":[["r",10,[4]],["append",3,6],["r",3,[4,5,6]]]}
    T237 = {"index":237,"process":3,"type":"ok","value":[["append",9,13],["append",10,5],["r",3,[4,5]],["r",10,[4,5]]]}
  Then:
    - T234 < T237, because T234 did not observe T237's append of 5 to 10.
    - However, T237 < T234, because T237 did not observe T234's append of 6 to 3: a contradiction!

That's the doctors problem with list keys. T234 read key 10 before T237 appended to it, so T234 has to come first. T237 read key 3 before T234 appended to it, so T237 has to come first. Both can't be true, so no serial order exists. Snapshot isolation allows that cycle. Serializability doesn't.

How it works

The trick that makes this tractable is the workload. adya's default, borrowed from Elle, is list-append: every key holds a list, transactions append unique numbers to lists and read whole lists back. Each read then tells you the order of every append before it. If one client read [1, 2] and another read [1, 2, 5], you know 5 came after 2, without asking the database anything.

From those orders adya builds a dependency graph over transactions, using Adya's three edge types:

  • ww: T2 appended right after T1's append to the same key.
  • wr: T2 read a list ending in T1's append.
  • rw: T1 read a list that T2 later appended to, so T1 didn't see T2's write.

If you're checking a model with real-time guarantees (strict serializability), it adds edges for "T1 finished before T2 started", using a transitive reduction so the graph stays roughly linear in the history.

Then it looks for cycles, one strongly connected component at a time. Each anomaly class is a cycle with constraints. G-single has exactly one rw edge. G2-item has at least two, with two of them adjacent. G1c has no rw edges and at least one wr. Instead of enumerating cycles and classifying them afterwards, adya runs a breadth-first search over pairs of (transaction, path state). The path state is seven bits: how many rw edges so far (0, 1, or 2+), whether the last edge was rw, whether the first one was, whether two were adjacent, whether it has seen a wr, and whether it has used a real-time edge. One BFS then returns the shortest cycle of exactly the shape you asked for.

Before searching, it runs cheaper existence checks. For each model it asks whether the subgraph that model forbids cycles in has a nontrivial SCC at all. If the ww/wr-plus-one-rw subgraph is acyclic, snapshot isolation holds as far as cycles go, and adya skips every search that could only find SI violations. It also searches the most severe anomalies first and skips anything they imply. A 100,000-transaction history (30 MB of JSON) checks in about 1.6 seconds on my laptop.

Is it right?

A checker that's wrong is worse than none, so this took most of the work.

  1. Elle's own expected results. elle-cli's test suite ships 56 list-append and rw-register histories along with the JSON verdicts Elle produced for them. adya matches all 56: same verdict, same anomaly types, and the same weakest models ruled out. Getting there taught me two Elle details I would never have guessed. It labels an edge that carries several relations by a fixed priority (ww before wr before rw before real-time), but tests whether a cycle exists using any relation the edge carries. And it counts a composed edge that loops back to the same transaction as a cycle.
  2. Elle itself, live. On every push, CI generates random histories with adya's simulator and checks each with both tools. In the first full run Elle finished 34 of 40, and adya agreed on all 34.
  3. Databases with known answers. The simulator implements serializable, snapshot isolation, read committed (with write locks), read uncommitted, and a deliberately broken "snapshot" that writes back stale state. Tests assert that correct runs produce zero anomalies at their own level and the expected ones above it. That test caught a bug in my simulator, not in the checker: my first read committed took no write locks, which is weaker than any real database.

What Postgres and MySQL did

CI runs 4,000 transactions from 10 clients over 6 hot keys against Postgres 17 and MySQL 8.4, at each isolation level, and checks every history against a ladder of models. List-append results:

database, level serializable snapshot isolation read committed
Postgres READ COMMITTED G-single, G2-item, internal, lost update G-single, internal, lost update valid
Postgres REPEATABLE READ G2-item valid valid
Postgres SERIALIZABLE valid valid valid
MySQL REPEATABLE READ G-single, G2-item, internal, lost update G-single, internal, lost update valid
MySQL SERIALIZABLE valid valid valid

Postgres did what its docs say. REPEATABLE READ is snapshot isolation, write skew included, and SERIALIZABLE came out strict serializable. I also restarted Postgres every few seconds during a 6,000-transaction SERIALIZABLE run (--fault "docker restart -t 0 pg"). Nine restarts, 2,957 commits, 3,043 failures, and the history still checked clean.

MySQL's REPEATABLE READ is weaker than its name. It lets a transaction lose another's update and see part of another transaction's writes, so it isn't snapshot isolation. Jepsen reported the same thing about MySQL 8.0.34 in 2023. adya reproduces it from a cold start in a few seconds.

Testing your own database

The built-in drivers cover SQLite, Postgres and MySQL. For anything else there's adya run exec: adya starts your client once per process, writes one JSON line per transaction to its stdin, and reads back one line saying what happened.

{"value":[["append",3,7],["r",4,null]]}
{"type":"ok","value":[["append",3,7],["r",4,[1,7]]]}

The repo has a 59-line Python client for SQLite as a template. Swap the SQL and you're testing your own database.

Limits

adya covers list-append and read-write register workloads. It doesn't do predicate reads, or Elle's bank, set and counter checkers. It prints text proofs instead of Graphviz plots. A clean result means it found no anomaly in that history. It doesn't prove your database is correct, so run it longer, with fewer keys for more contention, and with faults.

If you run it against something interesting, I'd like to hear what it found. Issues and PRs are open.

Permalink

What would a useful agent be like?

I use coding agents for basically all of my day day to work now. Recently I’ve been seeing more and more consumer-facing agent-like products and have been trying them out but find none are really able to do the things I want, mostly because they are all run by sketchy tech megacorps that I don’t trust and don’t want to connect my data to. Unfortunately Siri still really sucks, but in theory it’s more in the realm of what I actually want and Apple already owns my entire digital life anyway.

It made me wonder what a useful personal agent would be like. These are at least some of the requirements:

  • It needs access to all of the things I already use. For me that means protonmail for email, apple calendar and reminders, my obsidian personal wiki, and more. I’m not migrating my digital life to accommodate a bot.
  • It needs to remember things between sessions. LLMs are stateless and that makes them bad at long running tasks without harnesses that manage context.
  • It needs to be able to carry out long running tasks, like researching things for me or doing grunge work like organizing all my stuff. Getting a handle on the total dumpster fire of notes and todos I have scattered across a dozen different tools would be legitimately useful to me.
  • It needs to be able to schedule things for itself. Not everything needs to be a reminder visible to me. Some things I want are like “check if this computer is on sale yet”, “check for discounts on flights”. There are lots of little tasks I do throughout the day that are like this but I can’t be bothered to script them.
  • It needs to live in one place but be accessible anywhere, like on my computer and phone at least. Probably I’ll want to use it from multiple computers.
  • It would be really cool if I could even share them. There are some projects like volunteer orgs I’m a part of or home renovations that I collaborate with other people on.
  • It should at least be able to collaborate with other agents.
  • It should be self-managing and self-improving. I should never have to explain something twice. Over time it would just absorb my preferences and accumulate tribal knowledge about my life and projects, like a good assistant.
  • It needs to know how to use or maybe even make apps. I hate chat as a human-computer interface, it’s too unspecific and slow for most of what I want to do with computers.

Technically I think these requirements imply at least some of:

  • It needs to be able to read and write files.
  • It needs to be installed on one computer that never sleeps and be accessible to others over my tailnet, or similar.
  • It needs access to the internet though and probably needs a browser.
  • At least some of the agents need access to my actual computer. I don’t think there’s a practical way to give them access to my Apple or protonmail accounts but they could do everything they need from my personal Mac directly.

Anyway there are probably a lot more things that will come up but thinking about this has made me realize I want to try to build this. We’ll see how it goes!

Permalink

The Pull Newsletter (Oct 5, 2026)

Welcome to the first edition of The Pull, our new monthly Datomic newsletter rounding up projects and announcements from the Datomic community and the Datomic team.

Community Projects

  • EACL v8 for Datomic Pro is now available on Clojars as RC1. EACL (Enterprise Access ControL) is a situated ReBAC authorization library inspired by SpiceDB, built in Clojure and backed by Datomic Pro, Datahike, Datalevin or DataScript, though Datomic Pro remains the primary backend. Read the announcement or try the demo.

  • Trydatomic.org, an interactive website to learn how to query a Datomic database using Datalog, just got a content and design refresh.

Datomic Team News

  • In case you missed it: In April we released Datomic 1.0.7622, a big, feature-filled changelog with several performance improvements. It includes:

    • Feature: Read-only connections to storage and backups

    • Feature: rseek-datoms, reverse index iteration, a complement to seek-datoms

    • Performance: Reduce log write amplification for systems with many transactions

    • New API: list-backups, which lists the timepoints available in a backup repository

    • Performance: Reduce CPU and memory required to calculate index metrics

    • Fix: Regression introduced in 1.0.7556 which broke the ddb-local protocol

  • Also earlier this year, Joe Lane, principal engineer at Nubank on the Datomic Core Dev team, gave a talk at the Unlocked Conference about Immutability in Motion, which explores how immutability and multi-tier caching power database performance at massive scale.

  • Recently Nubank engineers João Nascimento Mello and Mateus Oliveira, supported by engineers Gabrielle Cadurim and Carolina Silva, presented an online Day of Datomic workshop in connection with the 2026 Clojure Conj, now available to watch on YouTube.

  • We’re hosting DatomicConf this December 11, 2026 in Durham, North Carolina. Registration and CFP are now open, so join us there. Details at conf.datomic.com.

Questions or suggestions? Reach out to the Datomic team and the rest of the community at the #datomic channel on Clojurians Slack.

Permalink

Understanding Datastar

Previously I wrote Understanding htmx which was meant to explain htmx conceptually, what the main tradeoffs are, and when you might want to use or not use it. This post is a followup to that for Datastar. Both of these posts describe how I personally think of these tools; if others disagree with my framing, I make no apologies.

Also note that the web apps I make do not typically include collaborative or real-time features, yet I still think Datastar is nice for this case. My explanation here focuses on things that matter for people like me who make boring apps.


htmx and Datastar are both tools for doing server-side rendering instead of e.g. using React; they both ask "what if we went back to how things were pre-jquery and tried to extend that model of thin-client web development instead of moving toward SPAs." They differ in how they implement that vision.

make an MPA
each page is a resource
keep a stream open on the current state of that resource

-- a guy from the Datastar community

Imagine: it's 2005. You have an MPA. Each page in your web app--a social network for Tapirs--has a GET request handler (which returns a bunch of HTML for the page) and an assortment of POST request handlers that do stuff and then redirect back to that HTML endpoint. But then you run into the problems I brought up in Understanding htmx:

You need to implement the heart button so that people can heart their favorite posts. However, if your heart button is a plain-old-form that causes the entire page to reload, there are several potentially undesirable consequences:

  • All the posts in the feed will have to be fetched again.
  • The posts that get fetched might be different.
  • The user might lose their scroll position.
  • The user might lose their draft if they were in the middle of typing a post.

The Datastar approach looks something like this:

  1. The initial page load opens a long-lived SSE connection. Whenever backend state changes (e.g. a database transaction is committed), the entire page is re-rendered and pushed up to the client over the SSE connection—a bit like telling the client to refresh the page but without doing an actual browser refresh.

  2. When the user takes an action, like hearting a post, Datastar makes an ajax request to a POST request handler which updates the database. Since that triggers a re-render by the SSE connection, the POST handler doesn't redirect or return any HTML; it simply returns an empty 204 response.

  3. Any state on the page that you don't want to recompute (e.g. a list of recommended posts) can be stored in server-side "tab state," keyed by some ID that's unique to a single browser tab. For example, on page load, you could trigger an action/POST request that computes the recommended posts and stores them in tab state. The GET request handler does not compute recommended posts; it just reads tab state. If tab state hasn't been populated yet, it shows a loading indicator.

  4. If you have an interaction that needs to be fast and you don't want to wait for a server round-trip before re-rendering happens, you can have the interaction update some "signals," which are Datastar's mechanism for handling frontend-only state. A common use case for this is storing form field values: when you're typing into a text field, you don't want to wait for the network before the characters show up.

Your application code has the same structure as the MPA-in-2005-thing; the main differences are that your POST handlers return 204 instead of 303, and you instrument all your form fields so they're bound to signals. You get to keep the single page rendering endpoint/function which gives you the whole "view is a function of your state" thing, and then you can get rich interactivity on top thanks to the SSE stuff.

The cost: more moving parts on the backend, which means more work to set up your codebase and more opportunities to screw something up. And your page rendering function needs to be fast since it's going to get called a bunch of times; that could require some restructuring (such as the example above of using an action on page load to compute recommended posts).

htmx on the other hand doesn't try to overhaul your backend architecture. You're typically doing standard request/response rather than this SSE thing. The downside is that your application code has to do more stuff. You click a button, htmx triggers a POST request, then the backend request handler has to know what chunk(s) of html needs to be re-rendered and where those chunks should be inserted in the DOM. Which is kind of imperative! You no longer have this dead simple "the page is rendered by a single function" thing.

So when should you use Datastar? My take: I don't think the downsides of Datastar I've mentioned above are that big of a deal, especially if you're using a library that sets up the plumbing for you (ahem). The main situation in which I'd be tempted to not use Datastar is if I'm building something very small where plain-old form-posts-with-redirects is fine.

I'm not sure I see any cases where I would use htmx again: I figure if I'm making something complex enough to warrant htmx instead of plain form posts etc, then the overhead of setting up Datastar is probably negligible.

Permalink

2026 Board Nominations and Our Annual Meeting

Clojurists Together is having our sixth board election, and our sixth annual members' meeting.

Key dates

(All dates are EOD, in Pacific Time)

Board nominations close: Oct.16, 2026
Voting opens: a few days after submissions close and after the board has nominated candidates
Voting closes: November 9, 2026
Annual members meeting: Nov. 17, 2026 - 10 am Pacific time

Board Elections

As part of our commitment to transparency and community governance, Clojurists Together holds elections for board members. The Committee is responsible for governing the projects, selecting which projects are sponsored, administering the projects, and interacting with sponsors.

Committee members are elected for a two-year term. Each election cycle, half of our board seats come up for re-election. This year there are three seats available.

If you are interested in standing for election, please fill out this form by Oct. 16th, 2026 - 5 pm Pacific Time. If you can’t access the form, contact us, and we can accept your nomination by email. Nominations are open to anyone, you don’t have to be a Clojurists Together member to stand for election. Our bylaws do require you to be a member if elected to the board, though we provide a stipend that offsets the cost of your membership.

You don’t have to have lots of experience with Clojure to apply. We want a committee made up of a cross-section of the Clojure community so that we have a wide range of perspectives when making decisions on which projects to fund.

As part of the nomination, if we get more than 12 candidates for board membership, the board will nominate no more than 12 candidates. Our bylaws state:

The Board shall nominate no more than 12 candidates seeking board membership in any given election. In nominating candidates for Director positions and in choosing the number of candidates to nominate overall, the Board shall use reasonable efforts to maintain a Board composition consisting of at least: (1) 25% female Directors, (2) 25% non-Caucasian Directors, and (3) 35% from any category(ies) of persons (e.g., race, gender, ability) commonly considered to have suffered from discrimination at some time and then-currently under-represented in the technology industry, in each case as determined by the Board in its reasonable discretion.

The main responsibilities of a committee member are:

  • Participate in the general discussions of the month-to-month running of the program
  • Evaluate and vote on which open source projects to fund
  • Help in decision-making for the future plans of Clojurists Together
  • These responsibilities take roughly one hour/month, though there are peaks and troughs of activity as we go through our quarterly funding cycle. If you have more time to offer, there are lots more things that need developing, automating, designing, e.t.c. It would be great to have you help out with those things, but we don’t want to exclude people from standing because they don’t have a lot of spare time.

Our bylaws requires that we do not have more than two committee members from any one company. More than two people from a company can stand for election, but if more than two of these people were to be elected, only the top two ranked candidates would be elected and the other seats would go to the next most highly ranked candidates from other companies. If you have any questions about this, please get in touch.

Elections will be held once the candidates are announced, and all Clojurists Together members will be eligible to vote.

Annual Members Meeting

We are also holding our fifth members meeting at 10 am Pacific time, November 17, 2026. This will be an opportunity for Clojurists Together to share information about 2026 to date and discuss future plans, present the new board members, and most importantly take questions from and engage with members.

More details on this will follow including a videoconferencing link.

Please share this with anyone you think would be able to represent the interests of the Clojure community and Clojurists Together members. Thanks for your support of Clojurists Together, we appreciate it!

Permalink

On cowardness in Clojure code

Imagine you have a function accepting a value and doing something with it:

(defn double-number [x]
  (when x
    (* x 2)))

;; or

(defn get-user [id]
  (when id
    (jdbc/execute! *db* ["select..." id])))

A common pattern which comes all the time is to wrap the entire body with a (when id ...) form. You don’t want to process nil values so it’s safer to protect yourself against NPEs. Without (when ...), a nil value can ruin the entire pipeline, cause a null pointer error, trigger DB queries in vain and so on. These all sound reasonable, yet I’ve got my own term to describe such a kind of code: “coward style”.

A person who is wrapping the whole function with (when) isn’t getting one thing. If a function had been given nil, it should have never been called instead. Clojure provides a number of macros to call a function conditionally depending on arguments, for example:

(some-> (get-user-id) (get-user))

Should (get-user-id) return nil, the (get-user nil) form never gets called. The same applies to (cond->) and other macros that build an execution form conditionally.

In other words: a function must not check its input parameters for nils. But those people who call this function must.

Now let me explain the “coward style” I mentioned before. It’s when people write like this:

(defn double-number [x]
  (when x
    (* x 2)))

I don’t know what this code tells you, but to me, it’s clearly this: “Guys, I don’t want any problems. I don’t want any exceptions to be raised. If you supply me with a number, I’ll double it but won’t do anything if you pass nil. I cannot process it but won’t argue on you. Let’s keep it all quiet. Deal?”

This is a speech of a typical coward: I don’t want problems. I don’t want stack traces. I don’t want alerts and investigations. Let’s be quiet. The job is half-way done in fact as the function works partially. It won’t tell you when something is not quite right.

I’ve seen plenty of computation chains like this:

(-> some-param
    (parse-param)
    (pre-process-param)
    (get-use-id)
    (get-user-by-id)
    (send-user-somewhere))

Now imagine that every function starts with (when ...), and the final function crashes with NPE. It will be quite challenging to find who is guilty. These functions are real cowards: nobody wants to take blame. “I got nil → I returned nil. Not my business. I washed my hands”.

Thus, stop writing functions starting with (when ...). If a function silently swallows a nil value doing nothing, sooner or later you’ll pay for that. Or your teammates will.

There is still a way though to protect yourself against nils which I like a lot. Use built-in pre- and post conditions:

(defn double-number [x]
  {:pre [(number? x)]}
  (* x 2))

Now if you pass nil, you’ll get a clear error

(double-number nil)

;; Execution error (AssertionError) at … (REPL:211).
;; Assert failed: (number? x)

Preconditions help a lot with guessing types. Above, they clearly say x must be a number and nothing else. In addition to :pre and :post forms, the standard (assert ...) form might help in the middle of a function to interrupt execution when you know it makes no sense to go on with a weird value.

Keen mind that :pre, :post, and assert forms rely on the global *assert* variable. It’s a good practice to rely on assertions a lot but wipe them off on production as they slow down the code. When baking an uberjar, set clojure.core/*assert* to false. If it’s ClojureScript with a shadow compiler, pass {:elide-asserts true} into the :compiler-options map for a production release.

I agree that pre/post and assertions take lines of code, and sometimes they make code a bit noisy. But they will save you hours of debugging. Don’t be a coward whose main goal is to avoid exceptions. Don’t hide weird things. Be simple and explicit, and let your code express these two qualities.

Permalink

Clojure 1.13.0-alpha8

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

Selectors

A developer may need to apply the same key-selection logic embedded in a map destructuring form (as directives, :keys!/:syms!/:strs!/etc., nesting, etc.) against multiple maps, or want to hand that selection logic to another part of the program. Currently that logic is oriented around binding in destructuring and not for programmatic use.

This release adds a new selector macro that takes a destructuring form and returns a function that can be applied to maps:

(selector [m])

  Builds a selecting-fn from m, a map destructuring form that must
  include one or more of the :select, :all, :missing, and :excess
  directives. The return function takes a collection, destructures it
  per m, and returns a map of the result(s).

  If m has exactly one directive, the result is the value that
  directive would yield. If m has more than one directive, then it
  returns a map of directives to values.

  As in destructuring, :missing controls whether missing required keys
  throw or are collected.

  While a map destructuring form may and sometimes must include
  bindings, selector doesn't produce bindings, thus ignoring the
  associated directive names.

  Throws an exception if the argument is not a map.

Transient map performance

This release contains many performance improvements around the use of maps, particularly transient maps, and functions like into, merge, and merge-with.

Transient array maps now grow larger before transitioning to transient hash maps (similar to recent persistent array map changes), and can be loaded significantly faster. The increased use of persistent and transient array maps will also make many more call sites inside the Clojure runtime monomorphic, encouraging greater JVM inlining. For more info see CLJ-2980.

Improvements and bug fixes

  • CLJ-2939 QualifiedMethodExpr field overload does not propagate field type - fixed

  • CLJ-1468 merge-deep, merge-deep-with - added recursive merge fns

  • CLJ-2981 merge - added tests

  • CLJ-2623 tagged literals and Java constructors - allow full unicode for first char

  • CLJ-2623 tagged-literal equality - check tag before form for performance

  • CLJ-2842 defn - does not needlessly initialize class when defn returns class

  • CLJ-2896 clojure.zip/next - only call zip/up once (not 3x)

  • CLJ-2915 ExceptionInfo - added to default imports, so no import or qualification needed

Try it out

Update your deps.edn :deps with:

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

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

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

Permalink

Superficie: Clojure you can read without learning Lisp

Syntactic sugar causes cancer of the semicolon.

Alan Perlis, Epigrams on Programming, 1982

Every Clojure example on this site opens in a tab called Superficie, with the original Clojure one click away. Perlis warned that syntactic sugar causes cancer of the semicolon, and Superficie is a lot of sugar, taken on one condition, that the two tabs show exactly the same program. This article explains why we built it, how that condition is kept, and how libraries teach it to write numerical kernels, proofs, and probabilistic models the way their fields do.

A few days is too long

Superficie began with a problem during a PhD in machine learning. The research code was written in Clojure, and the colleagues, supervisors, and domain experts who needed to read it worked in Python. Showing a function in a meeting, a paper, or a code review meant first explaining the parentheses.

That unfamiliarity passes. Most programmers read S-expressions comfortably after a few days. A meeting, a blog post, or a review with someone outside the team does not last a few days, though. The reader skips the code and trusts the prose around it, which defeats the purpose of showing code at all.

Our stack makes the problem larger. Datahike, Spindel, Yggdrasil, Raster, and Ansatz are Clojure libraries, and most people who read about them do not write Clojure. Simmis is meant to be usable without knowing Clojure, while deep work on the core stack still requires it. Superficie is the tool we use at that boundary. We keep writing Clojure, and we show it in a notation that readers of Python, Julia, TypeScript, or Lean can follow on first sight.

One program in two notations

Superficie is a bidirectional renderer. It prints Clojure forms as text with ordinary calls, infix arithmetic, and blocks, and it reads that text back into the same forms. Here is a function from our Stuttgart retail simulation that compares two shop names by the words they share:

What is this syntax?
defn jaccard [a b]:
  a := set(str/split(a, #" "))
  b := set(str/split(b, #" "))
  u := count(set/union(a, b))
  if zero?(u):
    0.0
  else:
    double(count(set/intersection(a, b))) / u
  end
end
(defn jaccard [a b]
  (let [a (set (str/split a #" ")) b (set (str/split b #" "))
        u (count (set/union a b))]
    (if (zero? u) 0.0 (/ (double (count (set/intersection a b))) u))))

A few rules cover most of what you see. A call is f(a, b). Arithmetic and comparisons are infix and need spaces around the operator, so a-b stays a Clojure name and a - b is subtraction. A block starts with a head and a colon and ends with end, which covers defn, if, let, loop, and every other form with a body. A let that ends a body is written as name := value statements, so a function reads from top to bottom instead of nesting one level deeper per binding.

To try it, paste any Clojure into the playground, which renders it live and includes a SCI REPL that evaluates Superficie in the browser.

Superficie has no runtime of its own. Any Clojure source prints as Superficie. Superficie source can also be read and evaluated directly, on the JVM, in Babashka, or in the browser through a SCI REPL, and it works with every Clojure library because what runs is ordinary Clojure. Macros and syntax-quote print as themselves rather than as their expansion, so a macro definition reads like the template its author wrote.

The same program, or it is a bug

A rendering that is almost right is worse than none, because a reader cannot tell which part changed. Superficie’s rule is exact roundtripping. We print the forms, read the text back, and compare the result with the original forms for equality. The comparison treats as equal only spellings that Clojure evaluates identically, such as the names Clojure’s reader invents for the parameters of #() or (new Foo x) against (Foo. x).

We check the rule against real code rather than hand-picked samples. In the current version, 421 of the 434 readable files in Raster and Ansatz roundtrip exactly, both in the compact form and in the width-aware layout used on this site. Across 13 open source Clojure projects, among them Datahike, DataScript, Malli, SCI, Babashka, and core.async, 622 of the 670 files our checker could read do. All 41 files of the Stuttgart simulation do.

Most of the remaining files use one of Superficie’s block words, such as match, let, ns, or end, as an ordinary name in a position where Superficie reads a block. The printer avoids this where it can. A local named end keeps its let block instead of becoming end := now(), which would close the enclosing block, and a statement that starts with such a word is parenthesised. Where the printer cannot avoid it, the constraint is documented, and code written in Superficie from the start does not run into it.

The corpora keep finding cases that a design review misses. A proxy block followed by a vector on the next line read its closing end as the name of a method. In the JavaScript build that renders this site, 4.0 printed as 4, because JavaScript has a single number type, and a reader of a numerical kernel would have seen integer arithmetic. Both are fixed, and the checker found both in code nobody wrote to test it.

The printer never fails, either. A form it has no block for prints as a call, f(a, b, c), which always reads back. That fallback is exact and hard to read, so most of the recent work went into making it rarer.

Where the notation comes from

Lisp was meant to have a second notation from the start. John McCarthy’s 1960 paper on Lisp wrote programs as M-expressions, such as f[x; y], and used S-expressions to represent data. In his History of Lisp, he recalled that translating M-expressions into S-expressions “was neither finalized nor explicitly abandoned. It just receded into the indefinite future, and a new generation of programmers appeared who preferred internal notation to any FORTRAN-like or ALGOL-like notation that could be devised.” Later attempts include Dylan, which replaced its prefix syntax with an Algol-like one in the 1990s, and the readable project’s SRFI 105 and SRFI 110 for Scheme. The most thorough recent one is Rhombus, a language built on Racket and described by Matthew Flatt and colleagues at OOPSLA 2023.

For a Clojure programmer, the interesting part is what a surface syntax has to rebuild. In an S-expression the parentheses are the tree, so Clojure’s reader stays small and knows nothing about operators. A notation with infix operators and blocks has to recover that tree in two steps, and Superficie takes both from existing work. First, following Rhombus’s shrubbery notation, it groups the text by its brackets without knowing what any operator means. A mismatched bracket becomes an error node in that tree while the rest of the file still parses, which is what makes useful error messages possible. Second, it decides precedence with Vaughan Pratt’s 1973 technique: each operator has a binding power, and the parser keeps consuming operators while their power exceeds the current minimum, so a + b * c groups correctly without a grammar rule per precedence level.

The visible choices are borrowed where readers already know them. A block header ends with a colon, as in Python, and a block closes with end, as in Julia, so indentation carries no meaning and copying code through HTML, chat, or a diff cannot change it. Arrow lambdas and indexing follow Julia, pattern-match arms and := follow Lean, and stores follow OCaml. Operators need spaces around them because Clojure names may contain -, *, ?, and <.

Libraries decide how their macros read

The Rhombus paper observes that “the message of macros has been difficult to detangle from Lisp’s minimalistic, parenthesis-oriented notation.” Languages without parentheses answer it in two ways, and Superficie takes the second.

In Rhombus, parsing is not finished before macros run. The reader leaves a sequence such as 1 + 2 * 3 flat, and the expander completes it while expanding macros, so a library can define a new operator together with its precedence. In Julia, the grammar is fixed. The parser builds the whole tree first, a macro call is marked with @, and the macro receives that tree as data and returns a new one.

Superficie parses first, as Julia does, but the data a macro receives is plain Clojure. a + b arrives as (+ a b) and a block arrives as the list it stands for, so a macro written for Clojure works unchanged and never sees Superficie. No @ is needed either. A macro call is written like any other call, and Clojure decides at expansion time that it names a macro. Superficie gives up Rhombus’s user-defined operators and gets every existing Clojure library in exchange. A macro can be written in Superficie too, with syntax-quote as its template:

What is this syntax?
defmacro unless [test & body]:
  `if not(~test):
     do(~@body)
   end
end

unless(ready?(job), log("waiting"), retry(job))
(defmacro unless [test & body]
  `(if (not ~test) (do ~@body)))

(unless (ready? job) (log "waiting") (retry job))

Most of our interesting code lives inside library macros. Raster’s deftm defines a typed function that Raster can compile into a GPU kernel, Ansatz’s a/theorem states and proves a theorem, and Spindel’s spin wraps a program such as the probabilistic model of the Stuttgart simulation. Without more information, Superficie can print a macro only as a call, deftm(weight, [att :- Double], :-, Double, ...), which is exact and unreadable.

A shape tells Superficie how a macro’s arguments split into a block header and a body. For unless, the shape [:form :body] lets unless(ready?(job), log("waiting"), retry(job)) read as unless ready?(job): followed by its body and end, and the macro still receives the same list. The shape changes the notation, never what the macro sees. Shapes for Raster, Ansatz, and Spindel ship with Superficie. A library can declare its own, in a resource file, in the macro’s metadata, or at runtime. The printer uses a shape only after checking that the block reads back to the original form, so a wrong shape can cost readability but not correctness.

A shape can also change how a block’s body is written. This kernel from the Stuttgart simulation computes, for every 100 m cell, the normaliser of the shop choice distribution:

What is this syntax?
deftm cell-totals!
    "Z_c for every cell: the normaliser of the choice distribution."
    [cell-lon :- Array(double),
     cell-lat :- Array(double),
     cand-lon :- Array(double),
     cand-lat :- Array(double),
     cand-att :- Array(double),
     params :- Array(double),
     totals :- Array(double),
     n-cells :- Long,
     nc :- Long] :- Void:
  alpha := params[0]
  beta := params[1]
  d0 := params[2]
  par/map-void! c n-cells:
    lon := cell-lon[c]
    lat := cell-lat[c]
    loop [q int(0), z 0.0]:
      if q < nc:
        recur(unchecked-add-int(q, 1),
          z + weight(cand-att[q],
                haversine-m(lon, lat, cand-lon[q], cand-lat[q]), alpha, beta, d0))
      else:
        totals[c] <- z
      end
    end
  end
end
(deftm cell-totals!
  "Z_c for every cell: the normaliser of the choice distribution."
  [cell-lon :- (Array double), cell-lat :- (Array double),
   cand-lon :- (Array double), cand-lat :- (Array double), cand-att :- (Array double),
   params :- (Array double), totals :- (Array double),
   n-cells :- Long, nc :- Long] :- Void
  (let [alpha (aget params 0) beta (aget params 1) d0 (aget params 2)]
    (par/map-void! c n-cells
      (let [lon (aget cell-lon c) lat (aget cell-lat c)]
        (loop [q (int 0) z 0.0]
          (if (< q nc)
            (recur (unchecked-add-int q 1)
                   (+ z (weight (aget cand-att q) (haversine-m lon lat (aget cand-lon q) (aget cand-lat q)) alpha beta d0)))
            (aset totals c z)))))))

Raster declares that inside its kernels x[i] means aget and x[i] <- v means aset. That declaration names functions; it does not fix what indexing means. Raster’s aget and aset dispatch on the array’s element type, much as Julia’s getindex and setindex! do, so the brackets are as polymorphic as the functions behind them. Outside a block that declares indexing, aget(U, i) stays a call, and in a block header name[x] is never read as an index.

Ansatz uses the same mechanism for a different field. Its terms follow Lean, so inside a/defn and a/theorem a name like Nat.succ(n) is a call to a dotted name rather than a Java method call, and a pattern match prints as Lean-style arms:

What is this syntax?
a/defn rb-size [t :- RBTree(Nat)] Nat:
  match t:
    | leaf => 0
    | node(color, left, key, right) => 1 + (rb-size(left) + rb-size(right))
  end
end

a/theorem map-preserves-len [f :- arrow(Nat, Nat) l :- List(Nat)] (llen(lmap(f, l)) = llen(l)):
  induction(l)
  all_goals(grind("lmap", "llen"))
end
(a/defn rb-size [t :- (RBTree Nat)] Nat
  (match t
    [leaf 0]
    [(node color left key right) (+ 1 (+ (rb-size left) (rb-size right)))]))

(a/theorem map-preserves-len [f :- (arrow Nat Nat), l :- (List Nat)]
  (= (llen (lmap f l)) (llen l))
  (induction l) (all_goals (grind "lmap" "llen")))

Choosing the notation

Each shorthand had to meet two conditions. A reader who has never seen Superficie should recognise it, and it must read back as exactly one form. Several candidates met the first condition and failed the second, and those failures shaped the syntax.

Anonymous functions have two spellings. A one-line function passed as an argument prints as an arrow, map(x -> x * x, xs), and its body runs to the next comma or closing bracket. The arrow needs a space on each side. Without them, a->b is one Clojure name, and base ->(raw, f()) is a name followed by a call to Clojure’s thread-first macro, which is how it appears in a let binding. Clojure’s #() literal keeps its own syntax with % parameters. By the time Superficie sees Clojure source, the reader has already expanded #(inc %) into a function whose parameter is called something like p1__123#. Only the #() reader produces names of that pattern, so Superficie can recognise them exactly and print #(inc(%)) again instead of the expansion.

What is this syntax?
defn summarize [xs]:
  {:squares map(x -> x * x, xs),
   :total reduce((acc, x) -> acc + x, 0, xs),
   :labels map(#(str("item-", %)), xs)}
end
(defn summarize [xs]
  {:squares (map (fn [x] (* x x)) xs)
   :total (reduce (fn [acc x] (+ acc x)) 0 xs)
   :labels (map #(str "item-" %) xs)})

Indexing was the harder decision. Brackets for Clojure’s general get would read naturally in data code, but x[i] would then have two meanings depending on where it appears, and get returns nil for a missing index where aget fails. Keyword lookup such as (:name user) is also several times more common than get in a rough count over the projects above, so a global index syntax would mostly dress up a less common idiom. Indexing therefore stays with the libraries that declare it.

Stores needed an operator that nothing else could be mistaken for. = is equality in Superficie, as in Clojure, and := introduces a binding. A store is neither, so it is written <-, as OCaml writes a.(i) <- v. Binding, comparison, and mutation each have one spelling.

The := statements are the newest rule. A let in the last position of a body prints as statements, consecutive statements read back as one let, and a let used as a value keeps its block. The printer declines to flatten when flattening would change the form. When a let’s only body is another let, the two would read back as one, so the inner one stays a block.

Where we use it

In Simmis, a Code View setting shows code in Superficie instead of Clojure, read-only. It applies in chat, in documents, and in the run inspector, where a person reviews the code an agent ran. Simmis is meant to be usable without knowing Clojure, and this is how someone who does not read Clojure can still check what an agent did.

On this site, Superficie renders every Clojure example at build time through its npm package, and the Superficie tab is shown first. An article can name the requires its snippets assume, so an excerpt without its namespace form still shows Raster kernels and Ansatz theorems as blocks. datahike.io uses the same package for its documentation. The project is on GitHub under the Apache 2.0 licence.

From reading code to changing it

Everything above concerns reading. Two open questions concern changing code, and we have not answered either.

The first is editing. Superficie reads back exactly the forms it printed, but not the comments and line breaks the author chose, because Clojure’s reader discards comments before Superficie sees the code. Editing a Clojure file in Superficie and writing Clojure back would need both preserved, so that a Clojure programmer still recognises the result as their own file. Until then, Superficie is a way to show existing code and to write new code, not to edit existing Clojure files.

The second is review. Simmis already shows the code an agent ran in Superficie. A proposed change is harder to show, because a line diff of the Clojure text does not map line by line onto Superficie. A diff over forms could, and it raises its own questions, such as how to show a changed argument inside an unchanged block and how to recognise a definition that moved. Both questions matter for the reason this project exists. More of the people who decide whether a code change should be adopted will not read Clojure.

Whatever the answers, the rule the rest of this article depends on stays. Superficie reserves its block words, a snippet without its namespace needs the requires it assumes, and a rendering that does not read back as the same forms counts as a bug.

Permalink

Stuttgart in a posterior: a city simulation you can question

What would happen to Stuttgart’s shops if the Milaneo shopping centre near the main station closed? What if the same floor space stood in Zuffenhausen instead? A simulation of the whole city, fitted to published data, answers with its uncertainty attached. If the Milaneo closed, about half of the 93 M€ a year that residents spend there would move to other shops in Mitte. A centre of the same size in Zuffenhausen would capture 59 % of that money, somewhere between 27 and 97 M€. This article describes the research demonstrator behind those numbers, and you can open the explorer and follow along. The model is a prototype, and its numbers are results under stated assumptions, not forecasts.

The city explorer.  Districts in the north are coloured green where simulated retail revenue rises after the Milaneo's floor space moves to Zuffenhausen; the centre is coloured red where it falls.  Rings mark individual venues.  A strip at the bottom reports revenue removed, the share captured in Zuffenhausen and the change in the expected clothing trip. The explorer with the Milaneo's floor space moved to Zuffenhausen. Green districts and rings gain resident spending, red ones lose it. Each number at the bottom comes with a 90 % band over the fitted parameters.

A city is a good test for a simulation because everyone has intuitions about it and some of those intuitions are measured. Stuttgart publishes how many people live in every 100 m square, how much floor space its shops have, and how much they sell by district and by kind of goods. A model that claims to explain the city’s retail has to reproduce those numbers, and where it cannot, the gap is information.

The demonstrator is built so that three things can be checked. Its inputs are measured, and each names its source. Its parameters are inferred from published figures, with the uncertainty those figures leave. A policy question is an explicit intervention on the model, evaluated with the same random draws as the baseline. The next three sections take them in turn.

A weekday for 936,597 people

The simulation builds a synthetic Stuttgart from published data. Residents are drawn cell by cell from the Zensus 2022 grid and in-commuters from the employment agency’s commuter statistics. The 3,637 shops come from Overture Maps and OpenStreetMap, with floor areas taken from building footprints and scaled to the city’s retail survey. The model page lists every input with its source.

Each person then lives a weekday from an activity diary that says when they leave home, go to work, shop or eat. German diary microdata is available only through a research data centre, so the diaries come from the Statistics Canada Time Use Survey 2022, reweighted to Stuttgart’s measured trip rate. That transfer is the model’s largest assumption.

When a diary says shop, the person picks a shop. The choice follows Huff’s gravity model, in which a shop’s pull grows with its floor area and falls with distance:

P(j∣c)∝Ajα⁢(1+dcj/d0)−β

Here j is a shop, c the 100 m cell the person is in, Aj the shop’s floor area and dcj the distance between them. The exponent α sets how much size matters, β how quickly distance puts people off, and d0 the radius within which distance hardly matters. The probabilities are normalised over all 3,637 shops. Nobody in this model optimises over the city. People weigh size against distance, which is bounded rationality in Herbert Simon’s sense, and the data have to decide how.

Shopping comes in three demand classes, because one kernel cannot fit Stuttgart: a kernel flat enough to fill the centre with clothing turnover makes grocery trips implausibly long. Food and daily needs, clothing and shoes, and long-lived goods such as furniture each get their own α, β and d0, their own share of every household’s purchasing power, and their own floor area per shop. That makes nine numbers.

Each shopping trip is decided once: its class, whether the purchase leaves the city, and the shop. The trips drawn on the map, the visits counted at each shop and the money those visits carry all come from that one decision, so the explorer shows a single simulated day rather than separate layers that merely agree on average.

Nine numbers the data has to decide

The city’s retail concept publishes turnover for each of 23 districts in each of the three classes, 69 observations in all. The model predicts the same 69 numbers by sending every resident’s spending through the choice kernel and summing where it lands. The prediction is an exact expectation rather than a sampled day, so it carries no Monte Carlo noise, and a likelihood compares it with the published values on a log scale.

Inference asks which values of the nine numbers make the published turnover plausible. The answer is a posterior distribution, a set of possible cities rather than one best fit, which the Bayesian inference article illustrates. Here it is computed with Spindel, the probabilistic programming runtime of the replikativ stack, as two independent runs of 48 Metropolis–Hastings chains, each of which runs the whole-city simulator at every step. The likelihood is evaluated on a GPU in about half a second per step, so a chain can take 200 steps in an hour. Started from different random points, the two runs end in the same distribution for every parameter. That checks where the chains end rather than how well each one explored, so it is a necessary test of convergence, not a proof.

size exponent α distance decay β flat zone d₀ short food, daily needs medium clothing, shoes long long-lived goods 0 0.75 1.5 1 2.25 3.5 200 m 630 m 2 km The 96 posterior draws of the nine parameters, each placed on the range the prior allowed. The published turnovers pin down the size exponent of long-lived goods and part of the clothing one. The distance parameters of every class stay spread over most of their range.

The figure shows what district totals can and cannot decide. They fix how strongly floor area attracts spending on long-lived goods. They leave the distance decay close to where the prior put it, because a steeper decay with a wider flat zone near home produces much the same district totals as a shallower one.

That points to what would sharpen the model. Measured shopping trip distances, such as those in the Mobilität in Deutschland survey, would constrain the distance decay directly. Visit counts per shop, anonymised card spending or pedestrian counts at a few dozen points would each separate kernels that district totals cannot. A simulation with an explicit likelihood can say which measurement would be worth collecting before anyone collects it.

Closing a shopping centre on paper

A policy question is an intervention. In the terms of the causal graph article, closing the Milaneo sets the venue set to a new value, do(venues := venues without the Milaneo), cuts nothing upstream, and recomputes every choice downstream. The residents and their money stay the same.

people, homes, money Zensus, employment, IFH shop floor area A OSM, retail survey kernel θ, 9 numbers inferred venue set do(venues := …) shop choice every trip, every person turnover by district compared with published The model's causal structure, simplified. Measured inputs and the inferred kernel feed every shop choice; a scenario changes only the venue set. The model page draws the full graph with its plates.

The simulator evaluates the change under every one of the 96 posterior draws and reports the difference per draw, so the spread is the uncertainty about the effect itself rather than two uncertainties added together. For the Milaneo, whose 103 shops within 170 m of Mailänder Platz hold about 24,000 m² of floor space:

  • Closing it removes about 93 M€ a year of resident spending from those shops. About half is spent elsewhere in Mitte and half in other districts. Mitte’s net loss is 47 M€, with a 90 % band from 35 to 62 M€. The expected straight-line trip for clothing gets 27 m shorter, because the centre had been pulling people past nearer shops.
  • Moving the same floor space to Zuffenhausen’s population centre captures about 59 % of that money there, 55 M€ with a band from 27 to 97 M€, and Mitte keeps 29 %.

The width of the Zuffenhausen band is the honest part of that answer. How much a large new centre would draw depends on exactly the parameters the district totals leave open, above all how steeply distance deters shoppers.

These are results of the model, and the model lacks things that would change them. It has no agglomeration: a centre on Königstraße benefits from the shops around it, and a box in Zuffenhausen would not. Only resident money moves in the scenario, while commuters and visitors keep spending where the baseline put them. Prices, opening hours and the competitors’ response are fixed. Read the Zuffenhausen figure as the capture a pure size-and-distance model allows, an upper bound on the pull of floor space alone.

Because every random draw in the simulator is a pure function of a seed, a person and a purpose, the same weekday can also be replayed under the changed venue set with the same draws for every person. The explorer’s visit-change layer shows that paired day hour by hour. It is not a minimal counterfactual: removing one shop shifts the choice intervals of others, so some people whose shop stayed open move too.

What the model cannot tell you

A simulation earns trust by being clear about where it stops. For this one:

  • Transferred behaviour. Daily rhythms come from urban Canada. Only the trip rate is Stuttgart’s.
  • Assumed shares. How shopping trips split between the three classes is a placeholder (65 %, 20 %, 15 %), and so is how leakage and commuter spending are attributed.
  • A static economy. Shops do not open, close or change prices in response. There is one representative weekday.
  • A posterior fitted to 69 district totals, which leave the distance decay close to its prior.

The model page lists every input as measured, assumed or fitted, and every derived data file carries a receipt naming its source. That table is the part of the project most worth arguing with.

Why build it this way

The interesting object here is not the Milaneo number. It is a model whose assumptions sit in one place, whose parameters are fitted to named evidence, and whose answer to a policy question comes with the spread the evidence leaves. Such a model can be disagreed with productively. Someone who thinks the centre has agglomeration effects can add them on a branch, refit, and compare. Someone with pedestrian counts can add an observation and see which parameters it narrows. That is the shared modelling practice we are interested in, for businesses asking what-if questions of their own operations, for city administrations and residents discussing a plan, and for language model agents that collect data, propose model changes and run the checks.

Later articles in this series will take the pieces apart: how the simulator runs and why one decision serves every layer, what the data can decide and which measurement would help most, how the evidence behind each input is kept, and how agents can take part in the work.

Try it and read further

  • The explorer: the two scenarios, the posterior, and the calibration against every published turnover.
  • The model: every equation, the full causal graph and the measured, assumed and fitted table.
  • The code: replikativ/city-rstr, a research prototype rather than a library. The simulator and the explorer are written in Clojure, on Spindel for inference, raster for the kernels that also run on a GPU, and Datahike for the evidence store.

Data: Zensus 2022 (© Statistisches Bundesamt), Landeshauptstadt Stuttgart (retail concept 2024, district boundaries), Statistik der Bundesagentur für Arbeit, Statistics Canada Time Use Survey 2022, Mobilität in Deutschland, © OpenStreetMap contributors, Overture Maps Foundation.

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.