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 useful personal agent

I started building a useful personal agent. What makes it useful is that it runs on my computer, but this immediately raises concerns about data exfiltration. I use Apple reminders (among other things) to organise my life so I had it write a little script to be able to interact with those, which works great. One of the main things I want help with is getting a handle on my chaotic piles of todos. They are scattered all over the place and all mixed up. Part of the problem with my current system is that aspirational things are mixed up with hard deadlines, so on busy or unpredictable weeks things that aren’t essential just pile up and stay there and then the “today” list just keeps rolling everything over until there are dozens of things on it, which I couldn’t possibly get done in a single day if I tried. And then I’m just mentally keeping tabs on what is actually important or on a real deadline, which defeats the purpose of the system.

The problem is that since I had a baby every day is unpredictable. I used to have a pretty good handle on my life, but none of my old systems really work anymore. There’s no way to predict how my nights will go anymore so my energy levels are very inconsistent, and I can’t even know what the days will be like. Anyway sounds like I need a follow-up post on the woes of working parenthood. Point being, I need a new system and so far am having fun building an LLM- based one.

Like I said what makes the agent useful is that it runs on my computer. It has access to my data and the internet, and when it was just helping me with reminders it only had access to those, which I write, so I trust them.

The problem is that in the process of having it help me organize the reminders, I realized that the answers to most of the questions it was asking me could be found in my emails. It was still helpful and less overwhelming having a bot help me organize things, but it still needed a lot of information and context from me that it could have found in my emails if it’d had access. But the problem with giving a bot access to email is that emails come from other people. A way to inject a prompt to my bot is the missing piece of the lethal trifecta, so I need to find a way to do it safely. Turns out it’s just a hard problem, but there is some interesting research out there and I’m working on coming up with something that will work to safely allow the bot get answers from my email without a way to pass malicious or sensitive information it finds there forward to an agent that has ways to access the internet.

Permalink

Stringulation

I’ve been stringing you along for years. Time for a (breaking?) change?

Shall I compare thee to a … string?

When porting Clojure for the JVM to the CLR, the question often arises of when a particular aspect of computation is intrinsic to Clojure or just an exposure of a feature of the JVM. For the former, I try to duplicate behavior; for the latter, I try to expose the corresponding CLR behavior. I have run into this not infrequently, particularly in the early days of the porting effort.

One area where it came up very early was with string comparison. And, frankly, I did not give it a lot of thought. Where ClojureJVM used java.lang.String.compareTo, I used System.String.CompareTo. In other words, I made the decision to expose the underlying platform mechanism. In retrospect, this was probably not the right decision. These methods are significantly different.

The JVM String.compareTo is an ordinal comparison of UTF-16 strings. This is a straightforward lexicographic comparison, performed character-by-character. The CLR String.CompareTo is culture-sensitive, i.e., the result of comparing two strings varies depending on the culture in effect at that time, which is thread-dependent.

There are several consequences of using CompareTo on the CLR, of varying import.

ClojureJVM and ClojureCLR differ on things such as comparisons.

(compare "a\u00ADb" "ab") ;; => non-zero meaning not equal (JVM)
(compare "a\u00ADb" "ab") ;; => zero, meaning equal        (CLR) 

(\u00AD is the soft-hyphen character.)

And, thus, sort order:

(sort ["b" "B" "a" "A"]) ;; => ("A" "B" "a" "b")  (JVM)
(sort ["b" "B" "a" "A"]) ;; => ("a" "A" "b" "B")  (CLR)

compare and = don’t agree.

(compare "a\u00ADb" "ab") ;; => 0  (equal)     (CLR)
(= "a\u00ADb" "ab")       ;; false (not equal) (CLR)

The difference here is that compare uses String.CompareTo (culture-sensitive) and = uses String.Equals (ordinal comparison).

sorted-set and hash-set yield different sets on the same inputs.

 (count (sorted-set "a\u00ADb" "ab")) ;; => 1, one string silently dropped because it compares as equal (CLR)
 (count (hash-set "a\u00ADb" "ab"))   ;; => 2, because hash sets use ordinal = (CLR)

Culture-sensitivity bites in some related places where string comparisons occur.

For example,

(compare :b :B) ;; => 32 (positive, indicating :b > :B) (JVM)
(compare :b :B) ;; => -1 (negative, indicating :b < :B) (CLR)

Culture-sensitive string compares are thread-dependent.

For example, ASP.NET Core sets the culture per request from Accept-Language, so (sort names) silently follows each user’s collation. Picking up information from the thread is fine; I feel it is better for it to be a deliberate choice rather than delivered behind your back.

Culture-sensitive comparisons are more expensive than ordinal comparisons.

Sorting a bunch of strings or creating a sorted map under ordinal comparison executes roughly 54-61% fewer instructions (instruction counts measured on one machine, not timings) than a culture-sensitive comparison using en-US. Capturing one culture comparer at startup to avoid thread lookup of the culture decreases the instruction count by roughly 5% compared to String.CompareTo today.

Culture-sensitive comparisons have not been consistent over time.

“Before .NET 5, the .NET globalization APIs used different underlying libraries on different platforms. … If you upgrade your app to target .NET 5 or later, you might see changes in your app even if you don’t realize you’re using globalization facilities.” (See Globalization and ICU - .NET). And you can switch globalization providers. ClojureCLR on .NET Framework uses NLS, not ICU, so Framework and .NET builds of ClojureCLR on the same machine can yield different results. That’s just on Windows. Let us not discuss Linux and Mac. Read it and weep.

Going deeper

The original focus in the benchmarking investigation surfaced the compare issue outlined above. After the initial results, I decided to expand the search to all string manipulation in the ClojureCLR, with comparisons against ClojureJVM where appropriate. Most of this internal string manipulation is related to reading data (which in Lisp-land includes programs) which arguably should not be culture/locale sensitive. It is not in ClojureJVM – all is ordinal. There are a few places where culture-sensitivity snuck into the ClojureCLR code, by carelessness or ignorance. Some are so marginal that I’m guessing they have never been encountered. For example, if we let <SHY> represent the soft-hyphen character, when reading source code:

  • JVM reads foo:<SHY> => creates symbol foo:<SHY>
  • CLR reads foo:<SHY> => throws an exception under en-US (at least)

The most significant culture-dependent bugs are

  • tr-TR: the flag to turn on direct linking in the compiler is ignored – the dotless ‘i’ (U+0131) makes an appearance.
  • sv-SE: (+ 1 1E-10M) doesn’t compile. Swedish uses a different minus sign character.

I consider these examples to be bugs. Fixing them is not a breaking change.

There are five functions (starts-with?, ends-with?, index-of, last-index-of, replace-first) in the clojure.string library that are in conflict with the JVM version and also demonstrably incorrect. These bugs are visible to users today, but they are bugs and should be fixed. Example: (replace-first "a<SHY>b" "ab" "X") gives "Xb" and corrupts the string. Results would change only for strings containing ignorable characters (soft hyphen, NUL, combining marks) and the changes would match the JVM’s answers.

A proposal

I plan to make two sets of changes. The first set is to fix the bugs above: the reader, the compiler, number literals, and clojure.string. I am not <SHY> about making these changes.

The second set of changes will be more user-visible and thus have the potential to break user code. This involves changing the definition of compare to be ordinal-based. To minimize impact, we can make this change selectable at startup. Set the switch to ‘culture’ and you get the current behavior; set it to ‘ordinal’ and you get ordinal-based comparisons. The ‘ordinal’ mode is pretty much identical to ClojureJVM behavior.

There is a third possibility: A captured culture mode which at startup creates a string comparator based on the CurrentCulture in effect at system startup. That is used by the compare function. If you change CurrentCulture, it will not be seen by this; there is no thread-dependency. You don’t get the large speedup, but do get the 5% savings from the no-thread-lookup. I’m not sure it is really worth it. The audience would seem to be a culture-mode user who sorts heavily and doesn’t care about threads. I’m not sure that’s much of a market. (This would require a little more testing before full validation as an option. )

For ordinal mode users, varying culture in things such as sorting is still possible. Most of the Clojure-defined functions, such as sort and sorted-set, have variations that take a comparator function. In fact, I think the general advice should be to be explicit in your sorting and comparator options always. To make this easier, we will add a culture-comparator function that will return a comparator function based either on the value of CurrentCulture when called or a supplied culture. You could write (sorted-set-by (culture-comparator "sv-SE")).

The choices

The bug fixes will happen regardless. For the compare changes, we have some choices.

  1. Do nothing. Not really an option, from my viewpoint.
  2. Put in the switch. Default is ‘culture’. If you want performance, you have to ask for it.
  3. Put in the switch. Default is ‘ordinal’. Performance and consistency are the default.

To be clear, my own preference is #3. The current situation has internal inconsistencies, is inconsistent with the JVM, and is slower overall.

An additional choice is

  1. Do we need the ‘captured culture’ mode?

I plan to put a link to this document up on the #clr channel in the Clojurians Slack for feedback. I’ll amend this post to reflect that discussion.

The result

TBD.

AI disclaimer/acknowledgement

I’ve been using AI coding tools extensively in my benchmarking work. There is a lot of tedious coding involved and the tools do it for me a lot faster and more correctly than I would on my own. This document reports on the results of that work. I drafted the document, then used the AI coding tool to vet it for accuracy, resulting in some typo corrections and a few suggested rewordings of inaccurate phrases. A stray comment in its review of my first draft sent me down a new line of inquiry that ended up in the two-phase approach here. The instructions to the agent include not drafting any code that might go into the ClojureCLR code base. It can identify locations and make prose suggestions only. (And it can write all the testing code its non-existent heart desires. It makes measurement runs on my commits as we go along.)

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

Clojure in the Age of Language Models

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.