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

The Pull Newsletter (Nov 5, 2026)

Welcome back to the Pull! Here’s what the Datomic team and community has been up to over the past month.

Datomic Team News

  • Datomic Pro 1.0.7745, featuring several performance improvements and other fixes, is now available.

  • In case you missed it, we’ve launched a new version of datomic.com. Let us know what you think!

  • We also have a brand-new Datomic Query Playground, a read-only interactive environment to try out Datomic queries and read APIs with Clojure. Take it out for a spin and send us any feedback.

  • At the 2026 Clojure Conj, Datomic Developer Success team member João Nascimento Mello led a Datomic Deep Dive workshop. If you weren’t able to attend, you can play along at home with the workshop lab materials.

Community Projects

Speaking of Clojure Conj 2026, speaker Wendy Randolph gave a talk there about "Modeling a Fabric Stash with Datomic," which shows "why Datomic’s attribute-based schema fits sparse, irregular data better than rigid relational tables."

There’s a new video and demo from UnifyBio and the RCRF Pattern Data Commons, which stores molecular and clinical data on rare cancers and is built around Datomic as its primary store and system of record. You can read more about the project on the #datomic channel on Clojurians Slack.

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

Permalink

Lean on the JVM: proved functions for Java and Clojure

Imagine a pricing service written in Java. Customers request a discount, and the business has two firm rules: a discount may never exceed the order subtotal, and it may never exceed the policy cap. Unit tests check a few dozen combinations of amounts, but the rules speak about every order. Sooner or later, a refactoring or a new promotion meets an input nobody tested.

Lean, a programming language and proof assistant, lets you state such a rule as a theorem and have the computer check a proof that it holds for every input. But adopting it usually means leaving the JVM. Lean compiles to native code, and connecting that code to a Java service is a project of its own.

The Ansatz Lean/JVM SDK removes that step. You write the function and its proofs in Lean, prepare the compiled code during your build, and call it from Java or Clojure like any other library. The deployed service needs no Lean installation. We are introducing this workflow with release 0.2.615, and this post follows the pricing rule from proof to production, then shows how Lean code can call back into the application.

Write the rule down, then prove it

First, the rule itself. Amounts are whole numbers in the currency’s smallest unit, such as cents. Save this as Pricing.lean:

namespace Pricing

def discount (subtotal requested cap : Nat) : Nat :=
  min subtotal (min requested cap)

theorem discount_le_subtotal (subtotal requested cap : Nat) :
    discount subtotal requested cap ≤ subtotal :=
  Nat.min_le_left _ _

theorem discount_le_cap (subtotal requested cap : Nat) :
    discount subtotal requested cap ≤ cap :=
  Nat.le_trans (Nat.min_le_right _ _) (Nat.min_le_right _ _)

theorem discount_eq_requested {subtotal requested cap : Nat}
    (h₁ : requested ≤ subtotal) (h₂ : requested ≤ cap) :
    discount subtotal requested cap = requested := by
  simp only [discount, Nat.min_eq_left h₂, Nat.min_eq_right h₁]

end Pricing

The definition takes one line: the discount is the smallest of the subtotal, the requested amount and the cap. The theorems are the interesting part. The first says that the discount never exceeds the subtotal, and the second that it never exceeds the cap. But a function that always returns zero would satisfy both bounds, so the third theorem adds that a request within both limits is granted unchanged. Lean checks all three for every possible input when it builds the file.

Of course, a proof is only as good as its statement. Lean guarantees that this definition has these properties; whether they are the right properties remains a business question. The guarantee also covers the Lean code only. The compiler, the JVM runtime and the integration that carry the result into Java are trusted rather than proved. Proofs are erased from the compiled code, so the service never runs a theorem prover on a request.

The example is small on purpose. A team can start with one rule like this, keep its database, HTTP handlers and deployment in Java or Clojure, and move more logic into Lean only where proofs pay off.

Prepare the code once, during the build

Next, add the SDK to an existing Maven project:

<repositories>
  <repository>
    <id>clojars</id>
    <url>https://repo.clojars.org/</url>
  </repository>
</repositories>
<dependencies>
  <dependency>
    <groupId>org.replikativ</groupId>
    <artifactId>ansatz-lean-jvm</artifactId>
    <version>0.2.615</version>
  </dependency>
</dependencies>

It runs on JDK 17 or later and brings Clojure and a JSON library along as ordinary Maven dependencies. It does not need the core Ansatz kernel or a Mathlib store. For the commands below, write the resolved classpath to a file:

mvn dependency:build-classpath -Dmdep.outputFile=target/lean-classpath
export SDK_CLASSPATH="$(cat target/lean-classpath)"

Preparation is the only step that needs native tools. It uses the official Lean 4.33.1 toolchain and a published bundle of helper programs that export what the JVM needs. For now, it runs on Linux x86-64 with prlimit available. Set LEAN_SYSROOT, PRODUCER_BUNDLE and COMPILER_CACHE to the toolchain, the extracted bundle and a cache directory. Then place Pricing.lean in a directory named pricing and prepare the function:

java -cp "$SDK_CLASSPATH" ansatz.lean.jvm.LeanPreparation \
  --source ./pricing \
  --sysroot "$LEAN_SYSROOT" \
  --producer-bundle "$PRODUCER_BUNDLE" \
  --cache "$COMPILER_CACHE" \
  --root-module Pricing \
  --entry Pricing.discount \
  --output ./pricing.leanjvm

The result is a single file, pricing.leanjvm. It contains Lean’s compiled intermediate code, together with the function types, data layouts and initialisation steps that the JVM needs. The command also prints two fingerprints, which are SHA-256 hashes. Keep the manifest hash in your application’s configuration. When the service loads the artifact, it checks the file against that fingerprint, so a changed or swapped file is rejected. One caveat applies to proprietary code: the artifact also contains the Lean source files and compiled modules, so treat it like the source itself. The preparation guide covers the details.

The native Lean build prepares an artifact. A JVM application loads it and calls the Java interpreter without a native Lean toolchain. Lean source + dependencies Official native Lean build Prepared artifact + hash IR, types, initialization Java IR interpreter SDK + Maven dependencies Java / Clojure application Calls, values, host services Native tooling belongs to preparation. The deployed artifact runs with the SDK and its ordinary JVM dependencies.

Call it like any other Java method

Then, in the deployed application, load the artifact and ask for the function by name. Save this as PricingApp.java:

import ansatz.lean.jvm.*;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;

public class PricingApp {
    public static void main(String[] args) throws Exception {
        try (Lean runtime = Lean.open();
             var input = Files.newInputStream(Path.of(args[0]));
             LeanProject project = runtime.loadArtifact(input, args[1])) {
            LeanFunction discount = project.function("Pricing.discount");
            BigInteger amount = (BigInteger) discount.invoke(10_000, 1_500, 1_000);
            System.out.println(amount);
        }
    }
}
java --class-path "$SDK_CLASSPATH" PricingApp.java \
  pricing.leanjvm "$EXPECTED_MANIFEST_SHA256"

It prints 1000, because the requested 1,500 is capped at 1,000. Behind the call, the SDK checks the arguments against the Lean signature and converts the result into a BigInteger. The same works for booleans, strings, lists, arrays, pairs, optional values and success-or-error results, and Lean records and recursive datatypes arrive as immutable Java objects.

Clojure developers resolve the same release:

What is this syntax?
{:deps {org.replikativ/ansatz-lean-jvm {:mvn/version "0.2.615"}}}
{:deps {org.replikativ/ansatz-lean-jvm {:mvn/version "0.2.615"}}}

and can call the function from the REPL:

What is this syntax?
require('[ansatz.lean.jvm :as lean], '[clojure.java.io :as io])

with-open [runtime lean/open(),
           input io/input-stream("pricing.leanjvm"),
           project lean/load-artifact(runtime,
                     input,
                     System/getenv("EXPECTED_MANIFEST_SHA256"))]:
  discount := lean/function(project, "Pricing.discount")
  [discount(10000, 1500, 1000) discount(200, 1500, 1000)]
end
;; => [1000 200]
(require '[ansatz.lean.jvm :as lean]
         '[clojure.java.io :as io])

(with-open [runtime (lean/open)
            input (io/input-stream "pricing.leanjvm")
            project (lean/load-artifact runtime input
                      (System/getenv "EXPECTED_MANIFEST_SHA256"))]
  (let [discount (lean/function project "Pricing.discount")]
    [(discount 10000 1500 1000)
     (discount 200 1500 1000)]))
;; => [1000 200]

The Superficie tab shows the same Clojure forms in another notation. Both run on the same runtime.

Let Lean call the application

So far, information has flowed in one direction, from Java into Lean. Real business logic, however, also needs the application: a configuration value, a database lookup or an exchange rate. This is where running Lean inside the JVM pays off most. If you compile Lean natively instead, you load a platform-specific library through JNI or Panama, start Lean’s runtime and convert every value by hand, as the lean-android-compose demonstration does for an Android app. Calling back into Java then needs C glue code as well. The harder part is memory. Lean frees its objects by reference counting, while the JVM uses a garbage collector, and neither sees the other’s references. A Java object that holds a Lean value must release it when the collector reclaims the Java object, a Lean value that holds a Java object must keep a JNI reference until Lean frees it, and a cycle across the boundary is reclaimed by neither side. With the SDK, both directions use ordinary Java and Clojure values, and the garbage collector manages all of them.

Suppose the cap depends on the customer’s segment and lives in the application’s configuration. Add a declaration without a body to Pricing.lean, together with a function that uses it:

namespace Pricing

@[extern "ansatz_jvm_policy_cap"] opaque policyCap : String → IO Nat

def quote (subtotal requested : Nat) (segment : String) : IO Nat := do
  pure (discount subtotal requested (← policyCap segment))

end Pricing

Prepare it with --entry Pricing.quote --entry Pricing.policyCap. When the application loads the artifact, it supplies the implementation under the declaration’s name:

HostBindings bindings = HostBindings.builder()
    .io("Pricing.policyCap", "config/1", arguments -> {
        Long cap = caps.get((String) arguments.get(0));
        if (cap == null) throw LeanIOError.userError("Unknown segment");
        return BigInteger.valueOf(cap);
    })
    .build();
LeanProject project = runtime.loadArtifact(input, expectedManifestSHA256, bindings);
IOResult<?> result = (IOResult<?>) project.call("Pricing.quote", 10_000, 1_500, "retail");

With caps of 1,000 for retail and 2,500 for partner, the two quotes are 1000 and 1500. An unknown segment reaches Lean as an ordinary IO error and comes back to Java as a LeanException with its message. In Clojure, the binding is a map entry, {"Pricing.policyCap" {:implementation-id "config/1" :effect :io :fn (fn [segment] (get caps segment))}}, passed to lean/load-artifact.

Notice what happens to the proof. discount_le_cap holds for every cap, so it also covers whatever value the configuration returns, and the quote can never exceed the configured cap. A trust boundary appears only when host code replaces a Lean definition, because type checking a binding does not prove that it behaves like the code it replaces. For now, bindings are first-order and synchronous, so they cannot take functions as arguments or run asynchronously. The host-binding guide covers pure bindings, replacements with a Lean fallback and custom result types.

Run it in a service

In production, a few more questions come up. The first is lifetime. Open the Project, as the SDK calls a loaded artifact, once and reuse its function handles. Calls share its code and initialised globals and run one at a time, so use several Projects for parallel work. A new version of the code means loading a new Project; state migration and public execution forks are separate work.

The second is runaway computation. Each call runs under a budget, by default two million interpreter instructions and five seconds. When a workload needs more, raise the budget per call with InvocationOptions.empty().withBudget(instructions, duration) and invokeWith.

The third is failure. Invalid arguments are rejected before anything runs, and an IO function that returns an error reports it as a LeanException. In both cases, the Project stays usable. But when a call fails halfway, for example because it exhausted its budget, the SDK cannot know what state the Lean code left behind. It therefore refuses further calls with ProjectFailed until you call project.restart(). The restart builds a fresh engine from the same verified artifact and keeps your function handles. Module initialisers run again, so globals start over.

Finally, panics behave as in native Lean. panic! and an out-of-bounds xs[i]! print a message to stderr and continue with a default value. If you prefer a hard stop, an option turns a reached panic into a failure instead. The SDK guide covers all four questions.

Bring an existing Lean project

Most teams will not start from a blank file, so we also tried real library code. The insertion-sort module from Functional Programming in Lean uses polymorphic arrays, typeclass comparison, proofs about array size and a termination argument. We load the unchanged module as a dependency of a small Lake project and add one function for the JVM to call:

import Examples.ProgramsProofs.InstrumentedInsertionSort
namespace LakeTutorial

def sortAndOriginal (xs : Array Nat) : IO (Array Nat × Array Nat) :=
  pure (insertionSort xs, xs)

end LakeTutorial

The caller’s Lake configuration requires the pinned fp-lean examples package, which Lake builds first. The SDK’s builtLakeWorkspace() option, or the CLI flag --source-mode built-lake, then takes the built workspace as it is, without fetching or building anything itself. A Java call with List.of(3, 1, 2) returns [[1, 2, 3], [3, 1, 2]] as BigInteger values, and Clojure receives the same pair of vectors. Keeping the original array next to the sorted one also exercises the runtime’s copy-on-write behaviour. Because insertion sort is quadratic, a few hundred elements fit the default budget, while larger inputs need a raised one.

The module proves that sorting preserves the array’s size and terminates, although it does not prove that the result is sorted. It is the work of David Thrane Christiansen and the Functional Programming in Lean contributors. Our corpus pins the revision, and the attribution and CC BY 4.0 source licence document the original work. The Lake guide explains this path, and our native CI also runs selected argument-parsing functions from the unchanged lean4-cli library.

Why the interpreter can fork

Why interpret Lean at all, instead of only calling native code? The answer comes from our own product. Simmis is designed so that people and agents can try a change in a separate branch before anyone accepts it, and that branching should reach running computations as well as stored data. For Clojure, we have already extended SCI, a Clojure interpreter, so that a session can fork into independent histories. Simulations need the same ability. In our Stuttgart city model, a policy question is an intervention evaluated with the same random draws as the baseline, which is easiest when the what-if continues from the very same state.

We are not the first to run Lean’s compiled code on the JVM. Lean4J exports it as JSON and interprets it on GraalVM’s Truffle framework, which compiles hot code to machine code and lets Java, JavaScript and Python pass callbacks in. Its focus is speed and polyglot tooling such as debugging and profiling, it requires GraalVM, and its README states that it is not meant as a faithful drop-in for native Lean. Our SDK makes different trade-offs. It runs on any JDK 17, loads only artifacts that match a trusted fingerprint, takes its Java and Clojure types from Lean’s checked signatures, and refuses unsupported operations before a program starts. It also keeps the whole execution state forkable, which matters most for Simmis. Truffle-style compilation remains a promising route to speed, and we see it as a possible later backend rather than a competing design.

Because the SDK keeps Lean’s entire execution state as JVM objects, it can copy that state. Its engine can already fork a paused computation, at any point in its execution, into two engines that continue independently, and the engine tests check that control state, ownership and output stay separate. The public SDK does not expose forks yet, so this is groundwork rather than a feature of this release.

Internally, we also write most of our verified code in Clojure rather than in Lean’s own syntax. Core Ansatz implements Lean 4’s kernel in Java and offers the same type theory as Clojure forms, so definitions and theorems written in Clojure are checked against the same libraries, such as Mathlib. That lets us add types and proofs to existing Clojure code gradually, one function at a time, and call the results as ordinary Clojure. The SDK described here complements it for code that already exists in Lean. The two can run in the same application, although loading executable code with the SDK does not import its theorems into the Ansatz kernel.

What comes next

This release is a starting point, and two goals guide the next ones. The first is speed. Today the SDK interprets Lean’s compiled code, and the JVM’s just-in-time compiler optimises that interpreter. Our near-term aim is to be competitive with Lean’s own interpreter, lean --run. For workloads that need more, the direction is to compile and specialise Lean code for the JVM, aiming at compiled Lean performance for selected workloads. We have not yet published representative benchmarks. Startup is already cheaper: the SDK’s own Clojure layer ships precompiled, so a fresh JVM pays Clojure’s startup cost but does not compile the SDK.

The second goal is coverage of Lean’s language, standard libraries and runtime services. The current release supports common application types, custom datatypes, explicit IO services and Lean’s Nat, Int and unsigned fixed-width arithmetic. A program that needs an operation outside this profile is refused before it starts, not partway through. Public entry points use closed first-order signatures, and tasks, public forks and Mathlib-heavy projects remain outside this release. A prepared project may contain at most 128 modules besides Lean’s core library, Init. The compatibility ledger records the details.

Lean libraries written partly in C need more work still. Java’s Foreign Function and Memory API, available in JDK 22 and later, offers a route to native libraries. However, Lean’s object representation and ownership rules still need explicit bridges, and this release includes none.

The next useful compatibility target may be your application. Bring us the Lean project and the functions you want to run, together with the JVM environment and workload that matter to you. That tells us which language features, runtime operations and performance requirements to prioritise.

Permalink

Babashka vs cream

I&aposve given a lot of thought to whether cream should affect babashka. Cream is based on Crema, a native-image compatible bytecode interpreter, basically a JVM inside of native-image.

What it concretely would mean to move babashka to Crema is that we could run virtually any Clojure program without changes. This is exciting.

Crema is an impressive piece of engineering. Native-image could not define classes at run time before, which is the reason SCI exists. With Crema, eval, require and Java libraries from jars just work in a native binary, and cream starts as fast as babashka.

But it has serious downsides compared to using SCI.

Load time

The main downside is the load time of Clojure sources. Evaluating a fn in cream runs the Clojure compiler and emits a class, which just takes more work. Compilation accounts for about 1000ms of the load. Defining the 1670 classes takes the rest. SCI interprets the forms and skips both steps.

Here is the time to load malli 0.16.4 from source:

DEPS=&apos{:deps {metosin/malli {:mvn/version "0.16.4"}}}&apos
CP=$(clojure -Spath -Sdeps "$DEPS")
E="(time (require &aposmalli.core &aposmalli.generator))"

bb -cp "$CP" -e "$E"
clojure -Sdeps "$DEPS" -M -e "$E"
./cream -Scp "$CP" -M -e "$E"
bb     ## 0.06s
JVM    ############################ 0.93s
cream  ######################################## 1.34s
requirewhole process
bb55ms0.06s
JVM610ms0.93s
cream1310ms1.34s

Babashka on SCI is just way faster for loading bigger libraries from source and then executing some expressions, which is commonly called "scripting". When loading a bigger lib like malli, you might as well just use a normal JVM instead of cream.

The gap shrinks with smaller libraries, but it stays quite significant. Babashka loads medley 9x faster than cream and camel-snake-kebab 15x faster.

Startup is the same in both babashka and cream: about 10ms.

Cream loads malli in about 330ms from AOT-compiled classes, and a cache could do that for you. But caches are a hassle, and babashka is faster than the AOT case anyway.

Performance

Performance of cream is better than bb for hot JIT-ed code, but worse than a JVM.

bbcreamJVM
malli validate, 1M calls300ms70ms35ms
fib 35, hot1230ms815ms73ms
fib 27, cold26ms46ms5ms
(reduce + (map inc (range 10000000)))157ms725ms80ms

Cream&aposs JIT makes no difference in the last row, it takes 725ms with the JIT off too.

Cream&aposs JIT is still warming up during fib 27. For a script, the code does not run long enough for that JIT to pay off. Since the core feature of bb is scripting with good startup time, bb + SCI wins here.

Binary size and build time

Another major downside of including Crema is the binary size. I built bb with Crema and the binary goes up from 75MB to about 200MB. Cream itself is about 225MB. Maybe this isn&apost that important, but compile times also go up. I expect double or triple in CI, which isn&apost fun at all when you build a binary for several OSes. SCI is just way more light weight here too.

What bb gives up

Code that needs real JVM classes, or a Java library that bb doesn&apost ship, stays out of reach. Pods cover part of that. For the rest there is the JVM, or cream, which sits in the gray area between babashka and JVM Clojure.

Conclusion

This is not a verdict on Crema, which is still experimental and improving fast. A native binary with full Clojure and a working JIT is very interesting and exciting. Most of the load time comes from the Clojure compiler. It&aposs just not the right trade-off for babashka today. Perhaps with an experimental fork of Clojure that reduces the number of emitted classes, this picture will change. I do use a fork of Clojure for cream, but it only has very mild changes to make it work with Crema. I haven&apost changed anything significant in the compiler.

For a while I thought SCI would be replaced with Crema in bb, but it still wins for the scripting use case. And babashka is all about scripting. I will keep improving SCI to make it both load and run faster over time. I was planning to do that anyway. Now that SCI isn&apost going away any time soon, I&aposm even more determined to make babashka the best possible scripting experience for Clojure.

You can view more numbers in the cream README.

Permalink

Babashka vs cream

I&aposve given a lot of thought to whether cream should affect babashka. Cream is based on Crema, a native-image compatible bytecode interpreter, basically a JVM inside of native-image.

What it concretely would mean to move babashka to Crema is that we could run virtually any Clojure program without changes. This is exciting.

Crema is an impressive piece of engineering. Native-image could not define classes at run time before, which is the reason SCI exists. Instead of using eval, SCI allows you to interpret Clojure code inside of a native image. With Crema, eval, require and Java libraries just work in a native binary using the normal Clojure compiler (with mild patches). That&aposs the approach that cream takes.

But it has serious downsides compared to using SCI.

Load time

The main downside is the load time of Clojure sources. Evaluating a fn in cream runs the Clojure compiler and emits a class which then has to be loaded by a classloader. This takes longer than it takes SCI to construct an interpreted function.

Here is the time to load malli 0.16.4 from source:

DEPS=&apos{:deps {metosin/malli {:mvn/version "0.16.4"}}}&apos
CP=$(clojure -Spath -Sdeps "$DEPS")
E="(time (require &aposmalli.core &aposmalli.generator))"

bb -cp "$CP" -e "$E"
clojure -Sdeps "$DEPS" -M -e "$E"
./cream -Scp "$CP" -M -e "$E"
bb     ## 0.06s
JVM    ############################ 0.93s
cream  ######################################## 1.34s
requirewhole process
bb55ms0.06s
JVM610ms0.93s
cream1310ms1.34s

Babashka on SCI is just way faster for loading bigger libraries from source and then executing some expressions, which is commonly called "scripting". When loading a bigger lib like malli, you might as well just use a normal JVM instead of cream.

The gap shrinks with smaller libraries, but it stays quite significant. Babashka loads medley 9x faster than cream and camel-snake-kebab 15x faster.

Startup is the same in both babashka and cream: about 10ms.

With AOT-compiled classes, malli loads in about 300ms in both cream and the JVM. Cream spends 10ms on startup and 290ms on loading classes, the JVM 200ms on startup and 100ms on loading classes. Run-time performance is worse in cream, so if you AOT anyway, the JVM is the better choice. Cream could AOT-compile libraries on first use and cache the classes, but caches are a hassle, and babashka is faster than the AOT case anyway. So it seems something Crema-based in between bb and a JVM doesn&apost offer us much benefit here.

Performance

Performance of cream is better than bb for hot JIT-ed code, but worse than a JVM.

bbcreamJVM
malli validate, 1M calls300ms70ms35ms
fib 35, hot1230ms815ms73ms
fib 27, cold26ms46ms5ms
(reduce + (map inc (range 10000000)))157ms725ms80ms

Cream&aposs JIT makes no difference in the last row, it takes 725ms with the JIT off too.

Cream&aposs JIT is still warming up during fib 27. For a script, the code does not run long enough for that JIT to pay off. Since the core feature of bb is scripting with good startup time, bb + SCI wins here.

Binary size and build time

Another major downside of including Crema is the binary size. Even when I only use Crema for loading Java classes, as described below, the binary goes up from 75MB to about 200MB. Cream itself is about 225MB. Maybe this isn&apost that important, but compile times also go up. I expect double or triple in CI, which isn&apost fun at all when you build a binary for several OSes. SCI is just way more light weight here too.

Combining SCI and Crema

Another option is to combine both approaches: SCI keeps interpreting Clojure and Crema is only used to load Java classes that are not in the image. I tried this on the crema-hybrid branch and it works for many libraries. A tiny script with BouncyCastle, commons-compress or Caffeine takes about 40ms for the whole process, against about 320ms on the JVM. Loading malli is slow in cream because Clojure compiles every form and defines about 1670 classes. A Java library is already compiled, and a single call only loads the classes it needs.

Load time of Clojure code is not the problem here, since SCI still does that. The main downside of this hybrid is that a JDK package that wasn&apost preserved at build time aborts the process instead of throwing an exception, and you only find out by running the library. Of course we can protect users from this with extensive testing.

The binary size and build time downsides are the same as for cream. The 200MB applies to every user, even for scripts that never load a class, because Crema needs a table of all compiled methods and whole preserved JDK packages.

If GraalVM shrinks that table or turns the abort into an exception, I&aposll look at this again.

Babashka + SCI&aposs trade-off

Without Crema, babashka can&apost load JVM classes that aren&apost baked into the native image. However, babashka is extensible via pods and more recently FFI. Babashka code is fully portable with JVM Clojure so it should always be easy to migrate a bb scripting project to a full-blown Clojure JVM project, when a project outgrows the light-weight babashka runtime.

Conclusion

This article is not a verdict on Crema, which is still experimental and improving fast. A native binary with full Clojure and a working JIT is very interesting and exciting. Most of the load time comes from the Clojure compiler. It&aposs just not the right trade-off for babashka today. Perhaps with an experimental fork of Clojure that reduces the number of emitted classes, this picture will change. I do use a fork of Clojure for cream, but it only has very mild changes to make it work with Crema. I haven&apost changed anything significant in the compiler.

The hybrid is more tempting, since it keeps SCI&aposs load time and loads Java libraries faster than a JVM. But today it costs every user about 125MB and slower builds, and a library can still abort the process. So for now it stays on a branch.

For a while I thought SCI would be replaced with Crema in bb, but it still wins for the scripting use case. And babashka is all about scripting. I will keep improving SCI to make it both load and run faster over time. I was planning to do that anyway but the above gives me even more determination to make the babashka + SCI combination the best possible scripting experience for Clojure.

You can view more numbers in the cream README.

Permalink

kmet - Coding Agent Written in Clojure

kmet - Coding Agent Written in Clojure

2026-11-03 - A coding agent written in Clojure, the language I said couldn't do it - my own TUI stack, a Cordis-style kernel where everything is a plugin, a model that writes Clojure inside a sandbox, and a daily driver at the end instead of a retreat
Keywords: clojure, babashka, jolt, coding-agent, agentic-loop, tui, hiccup, cordis, sci

Permalink

Scittlecastr: The Scittling

Arab woman listening to Scittle podcast with earphones and dancing outdoors

This post was entirely written by a human, with no assistance from LLMs or other "AI" technology such as pervert glasses or autonomous weapon systems.

I&aposve mentioned cljcastr here before, but in a very tongue in cheek fashion. Since then, cljcastr has turned into a serious project to create a set of tools for producing and self-hosting podcasts. The project has two simple principles:

  1. No surveillance. Ever. cljcastr runs on your computer and only your computer. It does not connect to any external services, store data on the cloud (AKA other people&aposs computers), or track you in any way, shape, or form.
  2. No AI. All cljcastr code is written by hand, by humans. Nothing in this repository has been AI generated, and no AI generated pull requests will even be considered for inclusion.

cljcastr is hosted on Codeberg at jmglov/cljcastr.

In this blog post, I&aposm going to demonstrate the cljcastr ethos by implementing a simple podcast player in ClojureScript, using the mighty Scittle.

Building the skeleton

To get started, we&aposre going to need a Babashka project set up to serve files from a local webserver. Let&aposs create a cljcastr directory:

mkdir cljcastr

And now pop a bb.edn into it:

{:deps {io.github.babashka/sci.nrepl
        {:git/sha "80ec956af5e6610b040089ba62eecb5d223e9e76"}
        io.github.babashka/http-server
        {:git/sha "b38c1f16ad2c618adae2c3b102a5520c261a7dd3"}}

 :tasks
 {http-server {:doc "Starts http server for serving static files"
               :requires ([babashka.http-server :as http])
               :task (do (http/serve {:port 1341 :dir "resources/public"})
                         (println "Serving static assets at http://localhost:1341"))}

  browser-nrepl {:doc "Start browser nREPL"
                 :requires ([sci.nrepl.browser-server :as bp])
                 :task (bp/start! {})}

  -dev {:depends [browser-nrepl http-server]}

  dev {:doc "Development build. Starts webserver and watches for changes."
       :task (do (run &apos-dev {:parallel true})
                 (deref (promise)))}}
}

Now create a resources/public directory:

mkdir -p resources/public/

Drop an index.html in that directory:

<html xmlns="http://www.w3.org/1999/xhtml">

<head>
  <title>cljcastr episode player</title>

  <script src="https://cdn.jsdelivr.net/npm/scittle@0.8.33/dist/scittle.js" type="application/javascript"></script>
  <script>var SCITTLE_NREPL_WEBSOCKET_PORT = 1340;</script>
  <script src="https://cdn.jsdelivr.net/npm/scittle@0.8.33/dist/scittle.nrepl.js" type="application/javascript"></script>

  <script type="application/x-scittle" src="player.cljs"></script>
</head>

<body>
</body>

</html>

And finally, a player.cljs in the same directory that does naught but declare a namespace:

(ns cljcastr.player)

(js/console.log "Ready to play!")

Thus equipped, invoke Babashka from the top-level cljcastr directory thusly:

bb dev

Babashka will helpfully tell you where you can find your player:

Serving static assets at http://localhost:1341
nREPL server started on port 1339...
Websocket server started on 1340...

If you browse to localhost:1341, you&aposll see an amazingly blank page titled "cljcastr episode player". If you open the JavaScript console in dev tools (Ctrl + Shift + i in many browsers), you should see the player reporting for duty:

  Ready to play!                                                    core.cljs:80
> 

So let&aposs play!

Interactive development

In addition to the HTTP server serving up static files from our resources/public directory, we have an nREPL server listening on port 1339 and a websocket server on port 1340. When we loaded the index.html webpage in the browser, scittle.nrepl opened a websocket connection to port 1340 and started listening for commands. In order to send it some commands, we will need to connect an nREPL client to the nREPL server on port 1339. Let&aposs do it!

This section assumes that you are a reasonable person and are thus using Emacs with CIDER, but if you&aposve unreasonably chosen to use a lesser editor or one of those new-fangled "IDEs" like Cursive or Calva, you probably know how to operate nREPL in that editor, so just translate accordingly.

First, we open up player.cljs in our editor. Next, we use CIDER&aposs cider-connect-cljs command (C-c M-C) to connect to localhost on port 1339, choosing REPL type nbb when prompted. CIDER should open a REPL buffer that says something like this:

;; Connected to nREPL server - nrepl://localhost:1339
;; CIDER 1.12.0 (Split)
;;
;; ClojureScript REPL type: nbb
;;
nil> 

Now we&aposre all set up for interactive development, so let&aposs interact. First, execute cider-load-buffer (C-c C-k) in the player.cljs buffer. If you&aposre watching the JS console in your browser, you&aposll see something like the following pop up:

  Ready to play!                                                   nrepl.cljs:10
> 

And meanwhile in your terminal, you&aposll see the nREPL server log some output:

:msg "{:value \"nil\", :id \"246\", :session \"e443955d-e285-4d5b-bbde-9b7f21f1561c\", :ns \"cljcastr.player\"}"
:msg "{:status [\"done\"], :id \"246\", :session \"e443955d-e285-4d5b-bbde-9b7f21f1561c\", :ns \"cljcastr.player\"}"

We&aposve now completed the circle of LISP, meaning we can control everything on our player page with ClojureScript! 💥

Let&aposs try some simple arithmetic just to get a sense of the power at our fingertips. In the player.cljs buffer, we&aposll add a Rich comment and some code:

(comment

  (+ 1 2)

  )

Positioning our cursor right after the (+ 1 2) form, we hit C-c C-v f c e to execute cider-pprint-eval-last-sexp-to-comment, and are rewarded by a nice comment being inserted on the next line:

(comment

  (+ 1 2)
  ;; => 3

  )

We can also print this result to the browser console by adding another line inside the (comment) form:

(comment

  ;; ...
  
  (js/console.log (+ 1 2))

  )

If you&aposre using CIDER with an autocomplete setup, you should actually see completion candidates popping up as you type. 🤯

If you position the cursor after the form and evaluate it again with C-c C-v f c e, you&aposll get a comment on the next line saying

  ;; => nil

This is somewhat underwhelming, but if you take a peek at your browser console, you&aposll be more satisfied:

  3                                                                nrepl.cljs:10
> 

Can we have our cake and eat it too? (Note that from here on, I&aposll assume you&aposre whacking C-c C-v f c e after the new form you add, which will produce the ;; => comment on the next line.)

(comment

  ;; ...
  
  (doto (+ 1 2)
    js/console.log)
  ;; => 3

  )

Now we&aposre cooking with biofuel!

Let&aposs try something a little more complex:

(comment

  ;; ...
  
  (doto (map inc (range 5))
    js/console.log)
  ;; => (1 2 3 4 5)

  )

This is great, but what in the world is happening over in the browser console?

  ‣ $APP.gg {J: null, na: null, I: null, F: 32374988, ma: ƒ, …}    nrepl.cljs:10
> 

If we click on the ‣, things get even wilder:

  ‣ $APP.gg {J: null, na: null, I: null, F: 32374988, ma: ƒ, …}    nrepl.cljs:10
      F: 32374988
      I: null
      J: null
      M: 1
      ma: null
    ‣ na: $APP.lg {hc: kg, Zc: $…P.gg, J: null, I: null, F: 31850732, …}
    ‣ [[Prototype]]: Object
>

What we&aposre seeing is a JavaScript representation of a ClojureScript vector. This is of course super cool and all, but not so readable (unless you&aposre Rich Hickey and you don&apost even see the persistent data structures but just the woman in the red dress). Let&aposs see if we can do better:

(comment

  ;; ...
  
  (doto (->> (map inc (range 5))
             clj->js)
    js/console.log)
  ;; => #js [1 2 3 4 5]

  )

The browser console is looking good!

  ‣ (5) [1, 2, 3, 4, 5]                                            nrepl.cljs:10

But life inside our (comment) form is less idyllic. What&aposs up with this nonsense?

  ;; => #js [1 2 3 4 5]

Now we&aposre seeing ClojureScript&aposs JavaScript data literal. 😅

Let&aposs try one more thing:

(comment

  ;; ...
  
  (let [res (map inc (range 5))]
    (js/console.log (clj->js res))
    res)
  ;; => (1 2 3 4 5)

  )

That&aposs much better! Now how about we extract this to a function so we can use it to log stuff?

(ns cljcastr.player)

(defn log [v]
  (js/console.log (clj->js v))
  v)

(comment

  ;; ...

  (log (map inc (range 5)))
  ;; => (1 2 3 4 5)

  )

Good stuff. Thus equipped, we can start sprucing up this empty webpage.

Getting down to brass HTML

Let&aposs add an h1 HTML element to the page to let users know what&aposs up. The obvious way to do this would be to open up index.html and drop in something like this:

<body>
  <h1>cljcastr player</h1>
</body>

However, this would require a page reload for us to see the changes, and that would blow away the state we&aposve carefully built up in our REPL. 😭

Let&aposs see if we can do this in the REPL instead. The basic idea is this:

  1. Create an h1 element.
  2. Set its inner text to "cljcastr player".
  3. Grab the body element that we helpfully put in our HTML page skeleton.
  4. Add the new h1 element as a child of the body element.

And if we were to call an imaginary eng->clj function, that would look something like this:

(comment

  ;; ...
  
  ;; 1. Create an **h1** element.
  (def h1 (js/document.createElement "h1"))
  ;; => #&aposcljcastr.player/h1

  ;; 2. Set its inner text to "cljcastr player".
  (set! (.-textContent h1) "cljcastr player")
  ;; => "cljcastr player"

  ;; 3. Grab the **body** element that we helpfully put in our HTML page skeleton.
  (def body (js/document.querySelector "body"))
  ;; => #&aposcljcastr.player/body

  ;; 4. Add the new **h1** element as a child of the **body** element.
  (.appendChild body h1)
  ;; => #object[HTMLHeadingElement [object HTMLHeadingElement]]

  )

Et voilà! Our boring page has been transformed into an amazing feast for the eyes!

Before moving on, let&aposs take a brief detour into a new JavaScript literal:

  ;; => #object[HTMLHeadingElement [object HTMLHeadingElement]]

This is a bit opaque, but we can always feed it to our log function to make it nicely inspectable in the console:

(comment

  ;; ...
  
  (log h1)
  ;; => #object[HTMLHeadingElement [object HTMLHeadingElement]]

  )

The console will render this as:

  <h1>cljcastr player</h1>                                         nrepl.cljs:10

Play that funky music

Let&aposs find ourselves the URL of a random podcast episode and add an audio element to the page:

(comment

  ;; ...

  (def audio (js/document.createElement "audio"))
  ;; => #&aposcljcastr.player/audio

  (set! (.-controls audio) true)
  ;; => true

  (set! (.-src audio)
        "https://media.zencastr.com/audio-files/5d9048546bacd9001de88f1c/2abc6f33-6a0a-4658-a8e0-c811984c13a0.mp3")

  (->> audio
       (.appendChild body))
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

  (log audio)
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

)

Now we should see an audio player pop up on screen!

Simple podcast player with a heading and an audio player

And if we click play, we hear the defn theme music.

We could declare victory right here, but we&aposre missing a few things that a podcast player usually has, such as:

  1. Sweet custom controls instead of the ugly HTML5 default audio player.
  2. The ability to play any podcast episode, not just one we&aposve helpfully downloaded ahead of time.
  3. If a transcript is available, display it with clickable timestamps.

Let&aposs see what we can do about that.

Escaping from vanilla

The standard HTML audio control is functional, but a bit boring. In our hubris, we decide we can do way better than that!

Let&aposs look at a typical podcast player for inspiration:

Overcast podcast player

It looks like we&aposll want the following buttons:

  • Play
  • Pause
  • Skip backward
  • Skip forward
  • Playback speed

Let&aposs see if we can find some nice vector graphics for our buttons. SVG Repo is a great place to start, and a search there for "play" turns up a nice looking SVG named Play Circle Fill SVG, which is provided under the MIT Licence. That looks promising!

Better yet, the SVG is part of a collection called [Framework7 Filled Icons] (https://www.svgrepo.com/collection/framework7-filled-icons/). Let&aposs click into that and see if we can find some SVGs for other useful media player buttons. Searching within the collection, we find a nice Pause button, but nothing for the other three we need. 😭

No matter! We widen our search to all of SVG Repo and find nice graphics for the rest. Here&aposs where we land:

Let&aposs create a set of HTML images for each. We&aposll start by defining a map to describe them:

(comment

  ;; ...

  (def playback-controls
    {:play {:src "https://www.svgrepo.com/show/436852/play-circle-fill.svg"
            :label "Play"}
     :pause {:src "https://www.svgrepo.com/show/436829/pause-circle-fill.svg"
             :label "Pause"}
     :forward {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
               :label "Forward {{seek-dur-sec}} seconds"}
     :backward {:src "https://www.svgrepo.com/show/495036/backward-15-seconds.svg"
             :label "Back {{seek-dur-sec}} seconds"}
     :speed {:src "https://www.svgrepo.com/show/151785/speed-meter.svg"
             :label "Playback speed"}})
  ;; => #&aposcljcastr.player/playback-controls

  (def seek-dur-sec 15)
  ;; => #&aposcljcastr.player/seek-dur-sec

  )

Now we can map over the map (yeah yeah, I know) to create our img elements:

(comment

  ;; ...

  (def playback-buttons
    (->> playback-controls
         (map (fn [[control-name {:keys [src label]}]]
                (let [el (js/document.createElement "img")]
                  (set! (.-id el) (str "button-" (name control-name)))
                  (set! (.-classList el) "cljcastr playback-icon")
                  (set! (.-src el) src)
                  (set! (.-draggable el) false)
                  el)))
         log))
  ;; => #&aposcljcastr.player/playback-buttons

  )

Note that we&aposre adding a playback-icon class to the element in addition to our standard cljcastr class, and also giving the element an ID so it&aposs easy to grab it with a CSS selector later. We also set the draggable attribute to false to prevent drag-and-drop silliness from happening.

Now, how about a container to hold them?

(comment

  ;; ...

  (def playback-controls-container
    (-> (let [el (js/document.createElement "div")]
          (set! (.-id el) "playback-controls")
          (set! (.-classList el) "cljcastr")
          el)
        log))
  ;; => #&aposcljcastr.player/playback-controls-container

  (doseq [el playback-buttons]
    (.appendChild playback-controls-container el))
  ;; => nil

  )

Finally, let&aposs display the controls!

(comment

  ;; ...

  (.appendChild body playback-controls-container)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  )

Player with a huge play button

Let&aposs set the width of the images to a more reasonable size:

(comment

  ;; ...

  (doseq [el playback-buttons]
    (set! (.-style el) "width: 35px"))
  ;; => nil

  )

That&aposs much better! However, this black and white page is starting to bum us out.

Rainbows and unicorns

Since our controls are SVGs, we can use the magic of CSS filters to colour them.

Let&aposs start by picking a colour, and what better colour for the sake of this demo than a lovely amber sort of shade called #f7931e. Of course, CSS filters can&apost be specified in something as prosaic as hex colour codes, so we&aposll use a magic CodePen thingy to generate a filter from our hex code. (We could also port the JavaScript from this spectacular StackOverflow answer to ClojureScript, but we kinda don&apost feel like it.)

What we come up with is this:

filter: invert(59%) sepia(92%) saturate(611%) hue-rotate(343deg) brightness(97%) contrast(99%);

We can now add this as a style to our img tags:

(comment

  ;; ...

  (doseq [el playback-buttons]
    (set! (.-style el)
          (str "width: 35px; filter: invert(59%) sepia(92%) saturate(611%) "
               "hue-rotate(343deg) brightness(97%) contrast(99%);")))
  ;; => nil

  )

Better!

Player with a orange playback controls

One thing that&aposs slightly annoying is that we had to remember to include to include the width style when we added the filters. Why don&apost we write some code that adds a style to an existing element whilst preserving existing styles?

First, let&aposs help ourselves out by pulling in clojure.string:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...

(comment

  ;; ...

  ;; What do styles look like?
  (-> playback-buttons
      first
      (.getAttribute "style"))
  ;; => "width: 35px; filter: invert(59%) sepia(92%) saturate(611%)
  ;;     hue-rotate(343deg) brightness(97%) contrast(99%);"

  ;; Parse into a list of styles
  (-> playback-buttons
      first
      (.getAttribute "style")
      (str/split #";\s+"))
  ;; => ["width: 35px"
  ;;     "filter: invert(59%) sepia(92%) saturate(611%)
  ;;      hue-rotate(343deg) brightness(97%) contrast(99%);"]

  ;; Turn it into a map
  (let [styles (-> playback-buttons
                   first
                   (.getAttribute "style")
                   (str/split #";\s+"))]
    (->> styles
         (map #(let [[k v] (str/split % #":\s+")]
                 [(keyword k) v]))
         (into {})))
  ;; => {:width "35px",
  ;;     :filter "invert(59%) sepia(92%) saturate(611%)
  ;;              hue-rotate(343deg) brightness(97%) contrast(99%);"}

  )

Now we have a thing that we can easily turn into a function!

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; (defn log ...)

(defn get-styles
  "Returns the inline styles of an element as a map of style name to value."
  [el]
  (let [styles (-> el
                   (.getAttribute "style")
                   (str/split #";\s+"))]
    (->> styles
         (map #(let [[k v] (str/split % #":\s+")]
                 [(keyword k) v]))
         (into {}))))

;; ...

(comment

  ;; ...

  (-> playback-buttons
      first
      get-styles)
  ;; => {:width "35px",
  ;;     :filter "invert(59%) sepia(92%) saturate(611%)
  ;;              hue-rotate(343deg) brightness(97%) contrast(99%);"}

  )

How about going the other direction, i.e. turning a map of styles into a style string?

(comment

  ;; ...

  (->> {:width "35px", :height "35px"}
       (map (fn [[k v]]
              (str (name k) ": " v)))
       (str/join "; "))
  ;; => "width: 35px; height: 35px"

  )

From this, &apostis merely a hop, skip, and a jump to write a function that sets the styles of an element:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; (defn get-styles ...)

(defn set-styles!
  "Sets the styles of an element from a map of style name to value."
  [el styles]
  (let [styles-str (->> styles
                        (map (fn [[k v]]
                               (str (name k) ": " v)))
                        (str/join "; "))]
    (set! (.-style el) styles-str))
  el)

;; ...

(comment

  ;; ...

  ;; Back to black!
  (doseq [el playback-buttons]
    (set-styles! el {:width "35px"}))
  ;; => nil

  )

Finally, let&aposs put this together and create a function that adds a style instead of replacing all the existing ones:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn set-styles! ...)

(defn add-style!
  "Adds a style to an element"
  [el style-name style-value]
  (let [styles (get-styles el)]
    (set-styles! el (assoc styles (name style-name) style-value)))
  el)

;; ...

(comment

  ;; ...

  ;; Amber is the new black
  (doseq [el playback-buttons]
    (add-style! el :filter
                (str "invert(59%) sepia(92%) saturate(611%) "
                     "hue-rotate(343deg) brightness(97%) contrast(99%)")))
  ;; => nil

  )

Play it again, Sam

Now that we have visually stunning buttons, let&aposs actually make them do something.

Or rather, let&aposs do something that allows us to make them do something. 😅

Remember how we added a cljcastr class to the buttons so we could easier grab them with js/document.querySelector? Well, we didn&apost do this to our poor audio element, which will now cause us a problem:

(comment

  ;; ...

  (-> (js/document.querySelector "audio")
      log)
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

  )

Seems OK in the REPL, but if we look at the JS console, things (might not, depending on your browser and which extensions you have installed) look so good:

  <audio class="audio-for-speech" src></audio>                     nrepl.cljs:10

Seems like my browser is set up with a screen reader, which is very nice for accessibility, but not nice for Clojure programmers who didn&apost get enough hammock time. Let&aposs add a class so we can use a more precise selector:

(comment

  ;; ...

  (set! (.-classList audio) "cljcastr")
  ;; => "cljcastr"

  (-> (js/document.querySelector "audio.cljcastr")
      log)
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

  )

And now the console looks more like what we expected:

  <audio class="cljcastr" controls                                 nrepl.cljs:10
  src="https://media.zencastr.com/audio-files/5d9048546bacd9001de88f1c/2abc6f33-6a0a-4658-a8e0-c811984c13a0.mp3">
  </audio>

Now we can proceed with making our buttons come alive:

(comment

  ;; ...

  ;; Grab the play button
  (def play-button (js/document.querySelector "#button-play"))
  ;; => #&aposcljcastr.player/play-button

  ;; Add a click handler
  (.addEventListener play-button "click"
                     (fn [ev]
                       (log ev)
                       (.play (js/document.querySelector "audio.cljcastr"))))
  ;; => nil

  )

If we click the button, we hear the podcast start playing, and can see the playback position in the standard HTML audio element above.

Player playing an episode

Now we can do the same for the pause button:

(comment

  ;; ...

  ;; Grab the pause button
  (def pause-button (js/document.querySelector "#button-pause"))
  ;; => #&aposcljcastr.player/play-button

  ;; Add a click handler
  (.addEventListener pause-button "click"
                     (fn [ev]
                       (log ev)
                       (.pause (js/document.querySelector "audio.cljcastr"))))
  ;; => nil

  )

That was easy! Skipping forward will be a little more interesting, since there&aposs no JS function to tell the audio object "skip forward x seconds", there&aposs only a currentTime property that we can query and set:

(comment

  ;; ...

  (.-currentTime audio)
  ;; => 29.396358

  (set! (.-currentTime audio) 15.0)
  ;; => 15

  )

Given this, it&aposs not too hard to figure out how to skip forward and backward an arbitrary number of seconds:

(comment

  ;; ...

  ;; Forward
  (let [cur (.-currentTime audio)]
    (set! (.-currentTime audio) (+ cur seek-dur-sec)))
  ;; => 30

  ;; Backward
  (let [cur (.-currentTime audio)]
    (set! (.-currentTime audio) (- cur seek-dur-sec)))
  ;; => 15

  )

It&aposs probably a good idea not to skip forward past the end of the episode or backward past the beginning, so let&aposs see if we can put a stop to that silliness:

(comment

  ;; ...

  ;; Forward with guard
  (let [cur (.-currentTime audio)]
    (set! (.-currentTime audio) (min (+ cur seek-dur-sec)
                                     (.-duration audio))))
  ;; => 30

  ;; The end is nigh!
  (set! (.-currentTime audio) (- (.-duration audio) 5))
  ;; => 5365.949958

  ;; Don&apost worry, all is well
  (let [cur (.-currentTime audio)]
    (set! (.-currentTime audio) (min (+ cur seek-dur-sec)
                                     (.-duration audio))))
  ;; => 5370.949958

  ;; Hitting 88 miles per hour
  (set! (.-currentTime audio) 5)
  ;; => 5

  ;; Backward
  (let [cur (.-currentTime audio)]
    (set! (.-currentTime audio) (max (- cur seek-dur-sec)
                                     0)))
  ;; => 0

  )

Armed with this code, let&aposs make some functions:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn add-style! ...)

(defn skip-forward! [audio-el sec]
  (let [cur (.-currentTime audio-el)]
    (set! (.-currentTime audio-el) (min (+ cur sec)
                                        (.-duration audio-el)))))

(defn skip-backward! [audio-el sec]
  (let [cur (.-currentTime audio-el)]
    (set! (.-currentTime audio-el) (max (- cur sec)
                                        0))))

;; ...

(comment

  ;; ...

  (skip-forward! audio seek-dur-sec)
  ;; => 15

  (skip-backward! audio seek-dur-sec)
  ;; => 0

  )

Now we can wire up the skip buttons as well:

(comment

  ;; ...

  (def forward-button (js/document.querySelector "#button-forward"))
  ;; => #&aposcljcastr.player/forward-button

  (def backward-button (js/document.querySelector "#button-backward"))
  ;; => #&aposcljcastr.player/backward-button

  (.addEventListener forward-button "click"
                     (fn [ev]
                       (log ev)
                       (skip-forward! audio seek-dur-sec)))
  ;; => nil

  (.addEventListener backward-button "click"
                     (fn [ev]
                       (log ev)
                       (skip-backward! audio seek-dur-sec)))
  ;; => nil

  )

Look at this! All of our buttons work! (Except the playback speed one, but that&aposs a little more complicated, so let&aposs save it for later. 😉)

The only thing that&aposs obviously wrong with this amazing player is that it has two sets of controls, the standard HTML one and our amazing buttons. There&aposs also something that&aposs not obviously wrong to most readers, unless they happen to be using a screen reader: the accessibility of our buttons sucks! In fact, the only way we know they&aposre buttons is by their appearance, because we&aposre using img elements, which are semantically just images. They could be images of an elephant for all a screen reader knows.

Let&aposs see what we can do about that!

A11y spells ally

From the excellent MDN article on Accessibility:

Accessibility (often abbreviated to A11y — as in, "a", then 11 characters, and then "y") in web development means enabling as many people as possible to use websites, even when those people&aposs abilities are limited in some way.

For many people, technology makes things easier. For people with disabilities, technology makes things possible. Accessibility means developing content to be as accessible as possible, no matter an individual&aposs physical and cognitive abilities and how they access the web.

The W3C Accessibility Initiative puts it in even more inspiring terms:

The Web is fundamentally designed to work for all people, whatever their hardware, software, language, location, or ability. When the Web meets this goal, it is accessible to people with a diverse range of hearing, movement, sight, and cognitive ability.

We certainly want all people to adopt our superior podcast player, so let&aposs make sure it works for them, using the power of ARIA!

Arya Stark after killing the Frey soldier with a knife

No, not that one. 😉

This one: ARIA.

If we wrap each of our img elements in a button element, we&aposre now conveying semantic meaning to a screen reader (or any other user agent, for that matter; imagine a script that is trying to identify all the buttons on a page... such as the one we&aposre in the process of writing). And if we set that button&aposs aria-label attribute, we&aposre now identifying what the button is meant to do.

Remember the :label key we included in our playback-controls map but never used way back when?

(comment

  ;; ...

  (def playback-controls
    {:play {:src "https://www.svgrepo.com/show/436852/play-circle-fill.svg"
            :label "Play"}
     :pause {:src "https://www.svgrepo.com/show/436829/pause-circle-fill.svg"
             :label "Pause"}
     :forward {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
               :label "Forward {{seek-dur-sec}} seconds"}
     :backward {:src "https://www.svgrepo.com/show/495036/backward-15-seconds.svg"
             :label "Back {{seek-dur-sec}} seconds"}
     :speed {:src "https://www.svgrepo.com/show/151785/speed-meter.svg"
             :label "Playback speed"}})
  ;; => #&aposcljcastr.player/playback-controls

  )

Turns out we had accessibility in mind all along!

Let&aposs try wrapping a button:

(comment

  ;; ...

  (let [button-name :play
        {:keys [src label]} {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
                             :label "Forward {{seek-dur-sec}} seconds"}]
    (let [button-id (str "button-" (name button-name))
          button-el (js/document.createElement "button")
          img-el (js/document.createElement "img")]
      (set! (.-id button-el) button-id)
      (set! (.-classList button-el) "cljcastr")
      (set! (.-ariaLabel button-el) label)
      (set! (.-id img-el) (str button-id "-icon"))
      (set! (.-classList img-el) "cljcastr playback-icon")
      (set! (.-src img-el) src)
      (set! (.-draggable img-el) false)
      (.appendChild button-el img-el)
      (log button-el)))
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  )

All of this set! stuff is starting to get annoying. Why don&apost we write a nice function that we can call to give us back a fully baked DOM element?

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn add-style! ...)

(defn create-el [{:keys [tag attrs] :as args}]
  (let [el (js/document.createElement tag)]
    (doseq [[attr v] attrs]
      (.setAttribute el (name attr) v))
    el))

;; (defn skip-forward! ...)
;; ...

(comment

  ;; ...

  (let [button-name :play
        {:keys [src label]} {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
                             :label "Forward {{seek-dur-sec}} seconds"}]
    (let [button-id (str "button-" (name button-name))
          button-el (create-el {:tag "button"
                                :attrs {:id button-id
                                        :class "cljcastr"
                                        :aria-label label}})
          img-el (create-el {:tag "img"
                             :attrs {:id (str button-id "-icon")
                                     :class "cljcastr playback-icon"
                                     :src src
                                     :draggable false}})]
      (.appendChild button-el img-el)
      (log button-el)))
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  )

This looks pretty reasonable in the JS console:

  ‣ <button class="cljcastr" id="button-play"                      nrepl.cljs:10
            aria-label="Forward {{seek-dur-sec}} seconds">
      <img class="cljcastr playback-icon" id="button-play-icon"
           src="https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
           draggable="false">
    </button>

Except this bit, that is:

aria-label="Forward {{seek-dur-sec}} seconds"

Looks like we got a bit clever there with some Selmer-style templating to allow for changing the number of seconds we&aposre skipping around. Let&aposs quickly figure out how to interpolate such strings:

(comment

  ;; ...

  (let [s "Something {{something}} something"]
    (str/replace s "{{something}}" "SOMETHING"))
  ;; => "Something SOMETHING something"

  (let [s "Something {{something}} something"
        ctx {:something "SOMETHING"}]
    (str/replace s #"\{\{(something)\}\}" (fn [[_ k]] (get ctx (keyword k)))))
  ;; => "Something SOMETHING something"

  )

This may appear slightly inscrutable at first glance, but if we look at the documentation for clojure.string/replace, we see that the replacement argument can actually be a function, in which case it&aposs passed the match as an argument and whatever it returns is the replacement string. When the match contains a capturing group, the argument to the function is a vector of [match group1 group2 ...]. For example:

(comment

  ;; ...

  (let [s "It&aposs easy as 123, ABC, baby you and me"]
    (str/replace s #"(\d)+.+([A-Z]+)"
                 (fn [match-vector] (str "<<<" (pr-str match-vector) ">>>"))))
  ;; => "It&aposs easy as <<<[\"123, ABC\" \"3\" \"C\"]>>>, baby you and me"

  )

Let&aposs turn this into a proper function:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; (defn log ...)

(defn render
  "Given a string `s` and a context map `ctx`, replaces template variables of the
   form {{k}} with the value of the corresponding key in the context map. If
   the key `k` doesn&apost exist in the context map, the template variable won&apost be
   replaced (i.e. \"{{foo}}\" will remain \"{{foo}}\" in the interpolated
   string)."
  [s ctx]
  (str/replace s #"\{\{([^}]+)\}\}"
               (fn [[match k]] (get ctx (keyword k) match))))

;; (defn get-styles ...)
;; ...

(comment

  ;; ...

  (render "foo {{bar}} baz {{blah}} {{barf}}" {:bar 27, :blah 42})
  ;; => "foo 27 baz 42 {{barf}}"

  )

Now we can make use of our new render function in our button-wrapping code:

(comment

  ;; ...

  (let [button-name :play
        {:keys [src label]} {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
                             :label "Forward {{seek-dur-sec}} seconds"}]
    (let [button-id (str "button-" (name button-name))
          button-el (create-el {:tag "button"
                                :attrs {:id button-id
                                        :class "cljcastr"
                                        :aria-label (render label {:seek-dur-sec seek-dur-sec})}})
          img-el (create-el {:tag "img"
                             :attrs {:id (str button-id "-icon")
                                     :class "cljcastr playback-icon"
                                     :src src
                                     :draggable false}})]
      (.appendChild button-el img-el)
      (log button-el)))
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  )

Quoth the console:

  ‣ <button class="cljcastr" id="button-forward"                      nrepl.cljs:10
            aria-label="Forward 15 seconds">
      <img class="cljcastr playback-icon" id="button-forward-icon"
           src="https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
           draggable="false">
    </button>

Now we can extract a nice function to create a button:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn create-el ...)

(defn create-button
  ([button-name button]
   (create-button button-name button {}))
  ([button-name {:keys [label src click-handler]} ctx]
   (let [button-id (str "button-" (name button-name))
         button-el (create-el {:tag "button"
                               :attrs {:id button-id
                                       :class (render "cljcastr {{button-class}}" ctx)
                                       :aria-label (render label ctx)}})
         img-el (create-el {:tag "img"
                            :attrs {:id (str button-id "-icon")
                                    :class (render "cljcastr {{img-class}}" ctx)
                                    :src src
                                    :draggable false}})]
     (.addEventListener button-el "click" click-handler)
     (.appendChild button-el img-el)
     button-el)))

;; (defn skip-forward! ...)
;; ...

(comment

  ;; ...

  (->> (create-button :forward
                      {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
                       :label "Forward {{seek-dur-sec}} seconds"}
                      {:seek-dur-sec seek-dur-sec
                       :button-class "playback-button"
                       :img-class "playback-icon"})
       log)
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  )

We&aposve pulled another nice thing out of Clojure&aposs bag of tricks: multi-arity functions:


(defn create-button
  ([button-name button]
   (create-button button-name button {}))
  ([button-name {:keys [label src]} ctx]
   ;; ...
  ))

This allows us to call create-button without a context:

(create-button :play {:label "Play", :src "play.svg"})

This will call the version of create-button that takes two arguments, which in turn calls the three-argument version, passing along the button-name and button arguments as is, and adding an empty map as the ctx argument.

OK, now we can use our create-button function to sew new a11y&aposd buttons onto our playback-controls div:

(comment

  ;; ...

  ;; Throw away the current crappy buttons
  (.replaceChildren playback-controls-container)
  ;; => nil

  ;; Let&aposs fix the order of our buttons whilst we&aposre at it
  (def playback-buttons
    (->> [:backward :play :pause :forward :speed]
         (map (fn [button-name]
                [button-name (get playback-controls button-name)]))
         (map (fn [[button-name button]]
                (let [button-el (create-button button-name button
                                               {:seek-dur-sec seek-dur-sec
                                                :button-class "playback-button"
                                                :img-class "playback-icon"})]
                  (-> button-el
                      .-firstChild
                      (set-styles! {:width "35px"
                                    :filter "invert(59%) sepia(92%) saturate(611%) hue-rotate(343deg) brightness(97%) contrast(99%)"}))
                  button-el)))
         log))
  ;; => #&aposcljcastr.player/playback-buttons

  (doseq [el playback-buttons]
    (.appendChild playback-controls-container el))
  ;; => nil

  ;; Grab the new buttons
  (def play-button (js/document.querySelector "#button-play"))
  ;; => #&aposcljcastr.player/play-button

  (def pause-button (js/document.querySelector "#button-pause"))
  ;; => #&aposcljcastr.player/play-button

  (def forward-button (js/document.querySelector "#button-forward"))
  ;; => #&aposcljcastr.player/forward-button

  (def backward-button (js/document.querySelector "#button-backward"))
  ;; => #&aposcljcastr.player/backward-button

  ;; Attach the click handlers to them
  (.addEventListener play-button "click"
                     (fn [ev]
                       (log ev)
                       (.play (js/document.querySelector "audio.cljcastr"))))
  ;; => nil

  (.addEventListener pause-button "click"
                     (fn [ev]
                       (log ev)
                       (.pause (js/document.querySelector "audio.cljcastr"))))
  ;; => nil

  (.addEventListener forward-button "click"
                     (fn [ev]
                       (log ev)
                       (skip-forward! (js/document.querySelector "audio.cljcastr")
                                      seek-dur-sec)))
  ;; => nil

  (.addEventListener backward-button "click"
                     (fn [ev]
                       (log ev)
                       (skip-backward! (js/document.querySelector "audio.cljcastr")
                                       seek-dur-sec)))
  ;; => nil

  )

Player with buttons with backgrounds and borders

The only problem with this approach is that our buttons now look like, well, buttons. 😅

Let&aposs restyle them a bit so they look like they did before:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn create-el ...)

(defn create-button
  ([button-name button]
   (create-button button-name button {}))
  ([button-name {:keys [label src click-handler]} ctx]
   (let [button-id (str "button-" (name button-name))
         button-el (create-el {:tag "button"
                               :attrs {:id button-id
                                       :class (render "cljcastr {{button-class}}" ctx)
                                       :aria-label (render label ctx)}})
         img-el (create-el {:tag "img"
                            :attrs {:id (str button-id "-icon")
                                    :class (render "cljcastr {{img-class}}" ctx)
                                    :src src
                                    :draggable false}})]
     (set-styles! button-el {:padding 0
                             :margin 0
                             :border 0
                             :background "transparent"
                             :cursor "pointer"})
     (.addEventListener button-el "click" click-handler)
     (.appendChild button-el img-el)
     button-el)))

;; (defn skip-forward! ...)
;; ...

(comment

  ;; ...

  (.replaceChildren playback-controls-container)
  ;; => nil

  (def playback-buttons
    (->> [:backward :play :pause :forward :speed]
         (map (fn [button-name]
                [button-name (get playback-controls button-name)]))
         (map (fn [[button-name button]]
                (let [button-el (create-button button-name button
                                               {:seek-dur-sec seek-dur-sec
                                                :button-class "playback-button"
                                                :img-class "playback-icon"})]
                  (-> button-el
                      .-firstChild
                      (set-styles! {:width "35px"
                                    :filter (str "invert(59%) sepia(92%) saturate(611%) "
                                                 "hue-rotate(343deg) brightness(97%) contrast(99%)")}))
                  button-el)))
         log))
  ;; => #&aposcljcastr.player/playback-buttons

  (doseq [el playback-buttons]
    (.appendChild playback-controls-container el))
  ;; => nil

  (def play-button (js/document.querySelector "#button-play"))
  ;; => #&aposcljcastr.player/play-button

  (def pause-button (js/document.querySelector "#button-pause"))
  ;; => #&aposcljcastr.player/play-button

  (def forward-button (js/document.querySelector "#button-forward"))
  ;; => #&aposcljcastr.player/forward-button

  (def backward-button (js/document.querySelector "#button-backward"))
  ;; => #&aposcljcastr.player/backward-button

  (.addEventListener play-button "click"
                     (fn [ev]
                       (log ev)
                       (.play (js/document.querySelector "audio.cljcastr"))))
  ;; => nil

  (.addEventListener pause-button "click"
                     (fn [ev]
                       (log ev)
                       (.pause (js/document.querySelector "audio.cljcastr"))))
  ;; => nil

  (.addEventListener forward-button "click"
                     (fn [ev]
                       (log ev)
                       (skip-forward! audio seek-dur-sec)))
  ;; => nil

  (.addEventListener backward-button "click"
                     (fn [ev]
                       (log ev)
                       (skip-backward! audio seek-dur-sec)))
  ;; => nil

  )

Now our buttons look like they did before! 🎉

However, recreating these buttons is getting a little tedious. Let&aposs do some cleaning up.

Sweeping up the shop

First of all, our log function is a little limited. It would be nice if we could include a log message in addition to whatever data we&aposre logging. In other words, we&aposd like to do this:

(comment

  ;; ...

  (log "Something happened!" {:foo 1, :bar 2})
  ;; => "Something happened!"

  )

But if we look at the console now, we only see the message logged and not the data:

  Something happened!                                                 nrepl.cljs:10

Luckily, the fix is pretty straightforward:

(ns cljcastr.player
  (:require [clojure.string :as str]))

(defn log [& args]
  (apply js/console.log (map clj->js args))
  (last args))

;; (defn render ...)
;; ...

(comment

  ;; ...

  (log "Something happened!" {:foo 1, :bar 2})
  ;; => {:foo 1, :bar 2}

  )

The value returned is what we wanted, and the console looks good as well:

  Something happened!  ‣ {foo: 1, bar: 2}                             nrepl.cljs:10

Next annoyance: all those js/document.querySelector calls. Extraction!

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn render ...)

(defn get-el
  "Returns the element matching the specified CSS selector. If `selector` is
   already an element, just returns the element."
  [selector]
  (if (instance? js/Element selector)
    selector
    (js/document.querySelector selector)))

;; (defn get-styles ...)
;; ...

(comment

  ;; ...

  (get-el "#button-play")
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  )

Might as well have a function to get a list of elements too:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn get-el ...)

(defn get-els
  "Returns a sequence containing the elements matching the specified CSS
   selector."
  [selector]
  (->> (js/document.querySelectorAll selector)
       seq))

;; (defn get-styles ...)
;; ...

(comment

  ;; ...

  (get-els "#playback-controls > button")
  ;; => (#object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]])

  )

How about one to return the children of an element?

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn get-els ...)

(defn get-children [el]
  (-> (get-el el)
      .-childNodes
      seq))

;; (defn get-styles ...)
;; ...

(comment

  ;; ...

  (-> (get-children "#playback-controls")
      log)
  ;; => (#object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]] 
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]])

  )

We added another nicety there: by using get-el, our function can take either an element or a CSS selector as its argument. Let&aposs continue to use this pattern as we add a function for adding a child to an element:

But if we look at the console, we see something extremely odd:

                                                                      nrepl.cljs:10
  ‣ he {value: button#backward, Ee: ArrayIterator, Ag: null, F: 8388672, M: 0}

The answers, my friend, are blowing in the wind of get-children:

(defn get-children [el]
  (-> (get-el el)
      .-childNodes
      seq))

We&aposre taking the childNodes attribute of our HTMLDivElement, which is a NodeList object:

(comment

  ;; ...

  (-> (get-el "#playback-controls")
      .-childNodes
      log)
  ;; => #object[NodeList [object NodeList]]

  )

This looks great in the console:

                                                                      nrepl.cljs:10
    NodeList(5) [button#button-backward.cljcastr.playback-button,
    button#button-play.cljcastr.playback-button,
  ‣ button#button-pause.cljcastr.playback-button,
    button#button-forward.cljcastr.playback-button,
    button#button-speed.cljcastr.playback-button]

But we can&apost use it in Clojure all that well:

(comment

  ;; ...

  (-> (get-el "#playback-controls")
      .-childNodes
      count)
  ;; => "#error {:message \"No protocol method ICounted.-count defined for type object: [object NodeList]\", ...}"

  )

That&aposs why get-children adds the call to seq at the end. That lets normal Clojure code deal with the list of children, but as we&aposve seen, it doesn&apost look good in the console when logged.

Let&aposs see what we can do about this:

(comment

  ;; ...

  (-> (get-children "#playback-controls")
      vec
      log)
  ;; => [#object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]]

  )

Now the console is looking quite reasonable!

                                                                      nrepl.cljs:10
    (5) [button#button-backward.cljcastr.playback-button,
    button#button-play.cljcastr.playback-button,
  ‣ button#button-pause.cljcastr.playback-button,
    button#button-forward.cljcastr.playback-button,
    button#button-speed.cljcastr.playback-button]

Let&aposs see if we can apply what we&aposve learned here to our log function:

(ns cljcastr.player
  (:require [clojure.string :as str]))

(defn log [& args]
  (let [seq->vec #(if (seq? %) (vec %) %)]
    (apply js/console.log (map (comp clj->js seq->vec) args)))
  (last args))

  ;; (defn render ...)
  ;; ...

(comment

  ;; ...

  (-> (get-children "#playback-controls")
      log)
  ;; => (#object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]]
  ;;     #object[HTMLButtonElement [object HTMLButtonElement]])

  )
(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn get-children ...)

(defn add-child! [parent-el el]
  (let [parent-el (get-el parent-el)]
    (-> parent-el
        (.appendChild el))
    parent-el))

;; (defn get-styles ...)
;; ...

(comment

  ;; ...

  (->> (add-child! "#playback-controls"
                   (create-el {:tag "span"
                               :text "Some nonsense"}))
       (log "I got a new child"))
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  )

Wanna clear all the children of an element?

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn add-child! ...)

(defn clear-children! [el]
  (let [el (get-el el)]
    (.replaceChildren el)
    el))

;; (defn get-styles ...)
;; ...

(comment

  ;; ...

  (->> (clear-children! "#playback-controls")
       (log "Empty nest"))
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  )

Set the children of an element?

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn clear-children! ...)

(defn set-children! [parent-el els]
  (let [parent-el (get-el parent-el)]
    (clear-children! parent-el)
    (doseq [el els]
      (add-child! parent-el el))
    parent-el))

;; (defn get-styles ...)
;; ...

(comment

  ;; ...

  (->> (set-children! "#playback-controls" playback-buttons)
       log)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  )

Final annoyance: all the repetition in the click handlers. Let&aposs extract a function for that as well:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn create-el ...)

(defn add-event-listener! [el event-type f]
  (let [el (get-el el)]
    (.addEventListener el event-type
                       (fn [ev]
                         (log (str "Handling " event-type " on")
                              el ev)
                         (if f
                           (f ev)
                           (log "No event handler function defined"))))
    (log "Added listener for" event-type "events to" el)
    el))

;; (defn create-button ...)
;; ...

(comment

  ;; ...

  (add-event-listener! "#button-speed" "click"
                       #(log "Should probably do something with this event, eh?" %))
  ;; => #object[HTMLButtonElement [object HTMLButtonElement]]

  )

And if we now click the playback speed button, the console rewards us with some rich logging:

Console log of event handler

With all these awesome functions, we can finally recreate our buttons much more concisely:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn add-event-listener! ...)

(defn create-button
  ([button-name button]
   (create-button button-name button {}))
  ([button-name {:keys [label src click-handler]} ctx]
   (let [button-id (str "button-" (name button-name))]
     (-> (create-el {:tag "button"
                     :attrs {:id button-id
                             :class (render "cljcastr {{button-class}}" ctx)
                             :aria-label (render label ctx)}})
         (set-styles! {:padding 0
                       :margin 0
                       :border 0
                       :background "transparent"
                       :cursor "pointer"})
         (add-event-listener! "click" click-handler)
         (add-child! (create-el {:tag "img"
                                 :attrs {:id (str button-id "-icon")
                                         :class (render "cljcastr {{img-class}}" ctx)
                                         :src src
                                         :draggable false}}))))))

;; (defn skip-forward! ...)
;; ...

(comment

  ;; ...

  (def playback-controls
    {:play {:src "https://www.svgrepo.com/show/436852/play-circle-fill.svg"
            :label "Play"
            :click-handler #(.play (get-el "audio.cljcastr"))}
     :pause {:src "https://www.svgrepo.com/show/436829/pause-circle-fill.svg"
             :label "Pause"
             :click-handler #(.pause (get-el "audio.cljcastr"))}
     :forward {:src "https://www.svgrepo.com/show/495321/forward-15-seconds.svg"
               :label "Forward {{seek-dur-sec}} seconds"
               :click-handler #(skip-forward! (get-el "audio.cljcastr") seek-dur-sec)}
     :backward {:src "https://www.svgrepo.com/show/495036/backward-15-seconds.svg"
                :label "Back {{seek-dur-sec}} seconds"
                :click-handler #(skip-backward! (get-el "audio.cljcastr") seek-dur-sec)}
     :speed {:src "https://www.svgrepo.com/show/151785/speed-meter.svg"
             :label "Playback speed"}})
  ;; => #&aposcljcastr.player/playback-controls

  (def playback-buttons
    (->> [:backward :play :pause :forward :speed]
         (map (fn [button-name]
                [button-name (get playback-controls button-name)]))
         (map (fn [[button-name button]]
                (let [button-el (create-button button-name button
                                               {:seek-dur-sec seek-dur-sec
                                                :class "playback-control"})]
                  (-> (get-children button-el)
                      first
                      (set-styles! {:width "35px"
                                    :filter "invert(59%) sepia(92%) saturate(611%) hue-rotate(343deg) brightness(97%) contrast(99%)"}))
                  button-el)))
         log))
  ;; => #&aposcljcastr.player/playback-buttons

  (set-children! "#playback-controls" playback-buttons)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  )

OK, so we&aposve fixed the non-obvious (to those of us not using screen readers) problem with the player. Scratching our heads, we try to recall what the obvious problem was. 🤔

Oh yeah! Two sets of controls, right. Let&aposs fix.

Sweeping audio under the carpet

Making our audio element disappear isn&apost that hard. Let&aposs use this as an excuse to create a few more helper functions and spread the get-el pattern a little wider as well:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn get-els ...)

(defn get-attr
  "Returns the value of the specified attribute."
  [el attr]
  (let [el (get-el el)]
    (.getAttribute el (name attr))))

(defn set-attr!
  "Sets the value of the specified attribute."
  [el attr v]
  (let [el (get-el el)]
    (.setAttribute el (name attr) v)
    el))

(defn set-attrs! [el attrs]
  (let [el (get-el el)]
    (doseq [[k v] attrs]
      (.setAttribute el (name k) v))
    el))

(defn get-id
  "Returns the ID of an element."
  [el]
  (let [el (get-el el)]
    (.-id el)))

(defn set-id!
  "Sets the ID of an element."
  [el id]
  (let [el (get-el el)]
    (when-not (empty? id)
      (set-attr! el :id id))
    el))

(defn get-name
  "Returns the tag name of an element."
  [el]
  (let [el (get-el el)]
    (.-localName el)))

(defn get-text
  "Returns the inner text of an element."
  [el]
  (let [el (get-el el)]
    (or (.-textContent el)
        (.-innerText el))))

(defn set-text! [el text]
  (let [el (get-el el)]
    (when-not (empty? text)
      (set! (.-textContent el) text))
    el))

(defn get-classes
  "Returns the classes of the specified element as a list of class names."
  [el]
  (let [el (get-el el)]
    (->> (str/split (.-classList el) #"\s")
         (remove empty?))))

(defn set-classes!
  "Sets the classes of the specified element. Classes may either be a list or a
   string.

   Example:

     ;; These two are equivalent
     (set-classes el [\"foo\" \"bar\"])
     (set-classes el \"foo bar\")"
  [el classes]
  (let [el (get-el el)]
    (when-not (empty? classes)
      (set! (.-classList el) (if (string? classes)
                               classes
                               (str/join " " classes))))
    el))

;; (defn get-children ...)
;; ...
;; (defn set-children! ...)

(defn get-styles
  "Returns the inline styles of an element as a map of style name to value."
  [el]
  (let [styles (-> (get-el el)
                   (.getAttribute "style")
                   (str/split #";\s+"))]
    (->> styles
         (remove empty?)
         (map #(let [[k v] (str/split % #":\s+")]
                 [(keyword k) v]))
         (into {}))))

(defn set-styles!
  "Sets the styles of an element from a map of style name to value."
  [el styles]
  (let [el (get-el el)]
    (when-not (empty? styles)
      (set! (.-style el) (->> styles
                              (map (fn [[k v]]
                                     (str (name k) ": " v)))
                              (str/join "; "))))
    el))

(defn add-styles!
  "Given a map of style name to value, adds the styles to the element."
  [el styles]
  (-> el
      (set-styles! (merge (get-styles el) styles))))

(defn add-style!
  "Adds a style to an element"
  [el style-name style-value]
  (-> el
      (add-styles! {style-name style-value})))

;; (defn create-el)
;; ...

Now that we have all of this awesomeness, we can streamline create-el by accepting a few more keys:

  • :id "foo" instead of :attrs {:id "foo"}
  • :classes ["foo" "bar"] instead of :attrs {:class "foo bar"}
  • :text to set the text content of the element

We also have to update create-button to take advantage of this.

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn add-style! ...)

(defn create-el [{:keys [tag id classes text attrs] :as args}]
  (-> (js/document.createElement tag)
      (set-attrs! attrs)
      (set-id! id)
      (set-classes! classes)
      (set-text! text)))

;; (defn add-event-listener! ...)
;; ...

(defn create-button
  ([button-name button]
   (create-button button-name button {}))
  ([button-name {:keys [label src click-handler]} ctx]
   (let [button-id (str "button-" (name button-name))]
     (-> (create-el {:tag "button"
                     :id button-id
                     :classes ["cljcastr"
                               (render "{{button-class}}" ctx)]
                     :attrs {:aria-label (render label ctx)}})
         (set-styles! {:padding 0
                       :margin 0
                       :border 0
                       :background "transparent"
                       :cursor "pointer"})
         (add-event-listener! "click" click-handler)
         (add-child! (create-el {:tag "img"
                                 :id (str button-id "-icon")
                                 :classes ["cljcastr"
                                           (render "{{img-class}}" ctx)]
                                 :attrs {:src src
                                         :draggable false}}))))))

;; (defn skip-forward! ...)
;; ...

(comment

  ;; ...

  (add-style! audio :display "none")
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

  (def playback-buttons
    (->> [:backward :play :pause :forward :speed]
         (map (fn [button-name]
                [button-name (get playback-controls button-name)]))
         (map (fn [[button-name button]]
                (let [button-el (create-button button-name button
                                               {:seek-dur-sec seek-dur-sec
                                                :button-class "playback-button"
                                                :img-class "playback-icon"})]
                  (-> (get-children button-el)
                      first
                      (set-styles! {:width "35px"
                                    :filter (str "invert(59%) sepia(92%) saturate(611%) "
                                                 "hue-rotate(343deg) brightness(97%) contrast(99%)")}))
                  button-el)))
         log))
  ;; => #&aposcljcastr.player/playback-buttons

  (set-children! "#playback-controls" playback-buttons)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  )

Player with just buttons

Testing our controls, we verify that they still work. However, we&aposve lost three important things:

  1. The progress bar, which gave us a visual indication of where we are and a way to move to an arbitrary point in the episode by clicking or dragging.
  2. The display of the current position in the episode.
  3. The display of the duration of the episode.

Actually, on the third point, we like Overcast&aposs choice to display the time remaining instead of the duration, so let&aposs do that as well.

Having made these bold design decisions, let&aposs get to implementing! We&aposll start with the easy stuff, displaying the time elapsed and remaining:

(comment

  ;; ...

  (def elapsed-time (create-el {:tag "span"
                                :text (str (.-currentTime audio))
                                :classes ["cljcastr"]
                                :id "elapsed-time"}))
  ;; => #&aposcljcastr.player/cur-time

  (def remaining-time (create-el {:tag "span"
                                  :text (str (- (.-duration audio)
                                                (.-currentTime audio)))
                                  :classes ["cljcastr"]
                                  :id "remaining-time"}))
  ;; => #&aposcljcastr.player/remaining-time

  (doseq [el [elapsed-time remaining-time]]
    (add-child! "#playback-controls" el))
  ;; => nil

  )

Times displayed as fractional seconds

Well, this is something, at least. Something incredibly confusing and also ugly. 😭

Let&aposs enumerate the problems:

  1. Times are displayed as fractional seconds, not the more familiar h:mm:ss.
  2. The spans are side by side with no padding, so it&aposs not possible to tell where elapsed time ends and remaining time begins.
  3. The font size is too large compared to the buttons. And maybe black isn&apost the best choice of colour anyway.
  4. We&aposd like the times displayed above the buttons (and the progress bar, when we get around to adding it).

Now that we know what&aposs wrong, let&aposs tackle things one at a time.

Hours, minutes, and seconds, oh my!

How can we convert from seconds to h:mm:ss?

(comment

  ;; ...

  (set! (.-currentTime audio) 3645)
  ;; => 3645

  (let [secs (.-currentTime audio)
        h (-> secs (/ 3600) int)
        mm (-> secs (/ 60) (- (* h 60)) int)
        ss (-> secs (mod 60) int)]
    (str (int h) ":" (int mm) ":" (int ss)))
  ;; => "1:0:45"

  )

OK, not a bad start. Let&aposs zero-pad the minutes and seconds, though, because "1:00:45" looks way better than "1:0:45". 😉

(comment

  ;; ...

  (let [secs (.-currentTime audio)
        h (-> secs (/ 3600) int)
        mm (-> secs (/ 60) (- (* h 60)) int)
        ss (-> secs (mod 60) int)
        zero-pad #(-> % int str (str/replace #"^(\d)$" "0$1"))]
    (str (int h) ":" (zero-pad mm) ":" (zero-pad ss)))
  ;; => "1:00:45"

  (set! (.-currentTime audio) 46)
  ;; => 46

  (let [secs (.-currentTime audio)
        h (-> secs (/ 3600) int)
        mm (-> secs (/ 60) (- (* h 60)) int)
        ss (-> secs (mod 60) int)
        zero-pad #(-> % int str (str/replace #"^(\d)$" "0$1"))]
    (str (int h) ":" (zero-pad mm) ":" (zero-pad ss)))
  ;; => "0:00:46"

  )

Hrm, maybe one more tweak. It&aposs conventional to omit the hour(s) portion of timestamps less than an hour:

(comment

  ;; ...

  (let [secs (.-currentTime audio)
        h (-> secs (/ 3600) int)
        mm (-> secs (/ 60) (- (* h 60)) int)
        ss (-> secs (mod 60) int)
        zero-pad #(-> % int str (str/replace #"^(\d)$" "0$1"))]
    (str (if (pos? (int h))
           (str (int h) ":")
           "")
         (zero-pad mm) ":" (zero-pad ss)))
  ;; => "00:46"

  )

Nice! Now let&aposs turn this into a function:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn render ...)

(defn secs->ts
  "Given a number of seconds, returns a timestamp of the form h:mm:ss."
  [secs]
  (let [h (-> secs (/ 3600) int)
        mm (-> secs (/ 60) (- (* h 60)) int)
        ss (-> secs (mod 60) int)
        zero-pad #(-> % int str (str/replace #"^(\d)$" "0$1"))]
    (str (if (pos? (int h))
           (str (int h) ":")
           "")
         (zero-pad mm) ":" (zero-pad ss))))

;; (defn get-el ...)
;; ...

(comment

  ;; ...

  ;; Elapsed time
  (secs->ts (.-currentTime audio))
  ;; => "00:46"

  ;; Remaining time
  (secs->ts (- (.-duration audio) (.-currentTime audio)))
  ;; => "1:28:44"

  (-> elapsed-time
      (set-text! (secs->ts (.-currentTime audio)))
      log)
  ;; => #object[HTMLSpanElement [object HTMLSpanElement]]

  (-> remaining-time
      (set-text! (str "-" (secs->ts (- (.-duration audio) (.-currentTime audio)))))
      log)
  ;; => #object[HTMLSpanElement [object HTMLSpanElement]]
  
  )

OK, we&aposve solved problem number 1. 🎉

Problems 2 and 3 are just styling, so the fixes are pretty straightforward:

(comment

  ;; ...

  (doseq [el [elapsed-time remaining-time]]
    (add-styles! el {:margin "0 5px 0 5px"
                     :font-size "0.7rem"
                     :color "grey"}))
  ;; => nil

  )

Timestamps displayed in smaller, grey text

Whilst we&aposre at it, let&aposs fix another annoyance: with our current create-el function, we either have to call add-styles! (or set-styles!) afterwards, or do the stringifying of the styles ourselves:

(create-el {:tag "div"
            :attrs {:id "whatever"
                    :style "margin: 0 5px 0 5px; font-size: 0.7rm; color: grey"}})

We also have the same issue with classes and ID. 🙁

But instead of wallowing in misery, let&aposs modify create-el to take a :styles key:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn add-style! ...)

(defn create-el [{:keys [tag id classes styles text attrs] :as args}]
  (-> (js/document.createElement tag)
      (set-attrs! attrs)
      (set-id! id)
      (set-classes! classes)
      (set-styles! styles)
      (set-text! text)))

;; (defn add-event-listener! ...)
;; ...

(comment

  ;; ...

  (let [el (-> (create-el {:tag "span"
                           :id "elapsed-time"
                           :classes ["cljcastr"]
                           :text (secs->ts 123)
                           :styles {:margin "0 5px 0 5px"
                                    :font-size "0.7rem"
                                    :color "grey"}})
               log)]
    (add-child! "#playback-controls" el))
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  )

Now, let&aposs address the last problem.

Laying out the law

It is now time to employ the most awesome technology known to humankind to figure out our layout: ASCII art!

+-----------------------------------+
|            Podcast title          |
+-----------------------------------+
|            Episode title          |
+-----------------------------------+
|            Release date           |
+-----------------------------------+
|            Episode art            |
+-----------------------------------+
|     Elapsed time | Remaining time |
+-----------------------------------+
|            Progress bar           |
+-----------------------------------+
| Backward | Play | Pause | Forward |
+-----------------------------------+
|           Playback speed          |
+-----------------------------------+

We&aposve been using the body element to hold our player, but we should probably deploy a div at this point so we can style things according to our liking. We can also group similar things together in divs, something like this:

+ episode-container ----------------------------+
|                                               |
| + episode-intro ----------------------------+ |
| |                Podcast title              | |
| +-------------------------------------------+ |
| |                Episode title              | |
| +-------------------------------------------+ |
| |                Release date               | |
| +-------------------------------------------+ |
|                                               |
+-----------------------------------------------+
|                  Episode art                  |
+-----------------------------------------------+
|                Audio (hidden)                 |
+-----------------------------------------------+
|                                               |
| + progress ---------------------------------+ |
| |       Elapsed time | Remaining time       | |
| +-------------------------------------------+ |
| |                Progress bar               | |
| +-------------------------------------------+ |
|                                               |
+-----------------------------------------------+
|                                               |
| + playback-controls ------------------------+ |
| | Backward | Play | Pause | Forward | Speed | |
| +-------------------------------------------+ |
|                                               |
+-----------------------------------------------+

Let&aposs start by creating the new nested containers (we already have one for playback controls) and populating them with some hard-coded data:

(comment

  ;; ...

  (def episode-container (create-el {:tag "div"
                                     :id "episode-container"
                                     :classes ["cljcastr"]
                                     :styles {:width "100%"
                                              :display "flex"
                                              :flex-direction "column"
                                              :align-items "center"}}))
  ;; => #&aposcljcastr.player/episode-container

  (def episode-info
    (-> (create-el {:tag "div"
                    :id "episode-info"
                    :classes ["cljcastr"]
                    :styles {:max-width "420px"
                             :text-align "center"}})
        (set-children! [(create-el {:tag "h3"
                                    :id "podcast-title"
                                    :classes ["cljcastr"]
                                    :text "defn"})
                        (create-el {:tag "h2"
                                    :id "episode-title"
                                    :classes ["cljcastr"]
                                    :text "876b4c306172 Cora Sutton"})
                        (create-el {:tag "span"
                                    :id "episode-date"
                                    :classes ["cljcastr"]
                                    :styles {:color "grey"}
                                    :text "July 9, 2025"})])
        log))
  ;; => #&aposcljcastr.player/episode-info

  (def episode-art
    (-> (create-el {:tag "div"
                    :id "episode-art-container"
                    :classes ["cljcastr"]})
        (add-child! (create-el {:tag "img"
                                :id "episode-art"
                                :classes ["cljcastr"]
                                :styles {:width "400px"
                                         :border-radius "5%"
                                         :padding "10px"}
                                :attrs {:src "https://media.zencastr.com/image-files/5d9048546bacd9001de88f1c/a5b922ac-7294-4d58-9a33-4308f5d4f50b.png"}}))
        log))
  ;; => #&aposcljcastr.player/episode-art

  (def progress (create-el {:tag "div"
                            :id "progress"
                            :classes ["cljcastr"]
                            :styles {:width "390px"
                                     :margin "1.5em auto 0 auto"}}))
  ;; => #&aposcljcastr.player/progress

  (def playback-controls-container (-> (create-el {:tag "div"
                                                   :id "playback-controls"
                                                   :classes ["cljcastr"]})
                                       (set-children! playback-buttons)
                                       log))
  ;; => #&aposcljcastr.player/playback-controls-container

  (def playback-speed (create-el {:tag "div"
                                  :id "playback-speed"
                                  :classes ["cljcastr"]}))
  ;; => #&aposcljcastr.player/playback-speed

  (-> episode-container
      (set-children! [episode-info
                      episode-art
                      (-> audio
                          (set-styles! {:display "none"}))
                      progress
                      playback-controls-container
                      playback-speed])
      log)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (set-children! "body" [episode-container])
  ;; => #object[HTMLBodyElement [object HTMLBodyElement]]

  )

Player with defn episode

This is looking really good (relatively speaking), but there&aposs one major component missing.

Slipping and sliding

We have no idea where in the episode we are! Let&aposs see about getting that progress bar in place.

Luckily, HTML has got us covered! There just so happens to be a progress element designed exactly for what we want to do!

(comment

  ;; ...

  (def progress-bar
    (create-el {:tag "progress"
                :id "progress-bar"
                :classes ["cljcastr"]
                :attrs {:min 0
                        :max (.-duration audio)
                        :value (.-currentTime audio)}
                :styles {:appearance "none"
                         :height "8px"
                         :width "100%"
                         :border "0"
                         :border-radius "4px"
                         :margin "0"
                         :overflow "hidden"
                         :background-color "lightgray"
                         :color "#f7931e"}}))
  ;; => #&aposcljcastr.player/progress-bar

  )

Now that we have the progress bar, let&aposs add it to our #progress div, along with the current and elapsed time spans we created before:

(comment

  ;; ...

  (-> progress
      (set-children! [(-> elapsed-time
                          (add-styles! {:float "left"}))
                      (-> remaining-time
                          (add-styles! {:float "right"}))
                      progress-bar])
      log)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  )

Progress bar added

Hurrah! I mean, it&aposs a bit ugly, but at least it&aposs there! 😅

Let&aposs click the play button, then skip forward a few times. Now, we can try setting the value of the progress bar to the current time of the audio element:

(comment

  ;; ...

  (set! (.-value progress-bar) (.-currentTime audio))
  ;; => 370.277385

  )

We should see the progress bar move along a bit.

This is great and all, but it would be lovely to have a way to actually move forward and backward in the episode by clicking or dragging on the progress bar. You know, the way every audio player ever works?

Sadly, the progress bar itself isn&apost really designed for this. We could certainly hack it up with some "click" event listeners and a lot of arithmetic, but surely there must be a better way, right?

And indeed there is! HTML has an input element of type range that is designed for exactly this case! Let&aposs try it out:

(comment

  ;; ...

  (def progress-slider
    (create-el {:tag "input"
                :id "progress-slider"
                :classes ["cljcastr"]
                :attrs {:type "range"
                        :min 0
                        :max (.-duration audio)
                        :value (.-currentTime audio)
                        :step "any"
                        :aria-label "Seek"
                        :aria-valuetext (str (secs->ts (.-currentTime audio))
                                             " of "
                                             (secs->ts (.-duration audio)))
                        :aria-valuemax (.-duration audio)}
                :styles {:appearance "none"
                         :cursor "pointer"
                         :height "28px"
                         :width "100%"
                         :border "0"
                         :margin "0"
                         :touch-action "manipulation"}}))
  ;; => #&aposcljcastr.player/progress-slider

  (-> progress
      (set-children! [(-> elapsed-time
                          (add-styles! {:float "left"}))
                      (-> remaining-time
                          (add-styles! {:float "right"}))
                      progress-slider
                      progress-bar])
      log)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  )

OK, a blue dot has appeared. Let&aposs skip forward in the episode a bit more, then update the value of the slider to the current playback position. Oh yeah, might as well update the value of the progress bar as well, whilst we&aposre at it:

(comment

  ;; ...

  (doseq [el [progress-slider progress-bar]]
    (set! (.-value el) (.-currentTime audio)))
  ;; => nil

  )

Now, how about setting the playback position in the episode when we click or drag the slider?

(comment

  ;; ...

  (add-event-listener! progress-slider "input"
                       #(->> % .-target .-value (set! (.-currentTime audio))))
  ;; => #object[HTMLInputElement [object HTMLInputElement]]

  )

We now have a pretty good representation of playback position:

  • A span showing the elapsed time
  • A span showing the remaining time
  • A slider letting us control the playback position
  • A progress bar giving us a visual indication of the playback position

Let&aposs see if we can keep all of these in sync. First, some code to update them all in one go:

(comment

  ;; ...

  (let [secs (.-currentTime audio)
        duration-secs (.-duration audio)]
    (set-text! elapsed-time (secs->ts secs))
    (set-text! remaining-time (str "-" (secs->ts (- duration-secs secs))))
    (.setAttribute progress-slider "aria-valuetext"
                   (str (secs->ts secs) " of " (secs->ts duration-secs)))
    (doseq [el [progress-slider progress-bar]]
      (set! (.-value el) (.-currentTime audio))))
  ;; => nil

  )

Looks like that works, so let&aposs extract it into a function. And whilst we&aposre at it, maybe we can extract functions for the attribute and value setting so we don&apost have to expose the raw DOM interactions. And for the sake of completeness, we can add getters as well:

(ns cljcastr.player
  (:require [clojure.string :as str]))

;; ...
;; (defn set-text! ...)

(defn get-value
  "Returns the value attribute of the specified element."
  [el]
  (let [el (get-el el)]
    (.-value el)))

(defn set-value!
  "Sets the value attribute of the specified element."
  [el v]
  (let [el (get-el el)]
    (set! (.-value el) v)))

;; (defn set-classes! ...)
;; ...
;; (defn skip-backward! ...)

(defn update-playback-position-indicators!
  "Given a current playback time and duration (both in seconds) and a map of
   elements, updates the elements to reflect the current playback position. The
   element map should contain the following keys:
   `:elapsed-time` - text element (for example: label, span, etc.)
   `:remaining-time` - text element (for example: label, span, etc.)
   `:progress-slider` - input element of type range
   `:progress-bar` - progress element"
  [current-secs duration-secs
   {:keys [elapsed-time remaining-time progress-slider progress-bar]}]
  (set-text! elapsed-time (secs->ts current-secs))
  (set-text! remaining-time (str "-" (secs->ts (- duration-secs current-secs))))
  (set-attr! progress-slider :aria-valuetext
             (str (secs->ts current-secs) " of " (secs->ts duration-secs)))
  (doseq [el [progress-slider progress-bar]]
    (set-value! el current-secs)))

;; ...

(comment

  ;; ...

  (update-playback-position-indicators! (.-currentTime audio) (.-duration audio)
                                        {:elapsed-time elapsed-time
                                         :remaining-time remaining-time
                                         :progress-slider progress-slider
                                         :progress-bar progress-bar})
  ;; => nil

  )

There&aposs one final touch we should apply here before we move on. It&aposs getting a bit silly to have to manually update these elements, right? How about we update them automatically as the episode plays? For this, we can use the timeupdatechange event of our audio element:

(comment

  ;; ...

  (add-event-listener! audio "timeupdate"
                       #(update-playback-position-indicators!
                         (.-currentTime audio) (.-duration audio)
                         {:elapsed-time elapsed-time
                          :remaining-time remaining-time
                          :progress-slider progress-slider
                          :progress-bar progress-bar}))
  ;; => #object[HTMLAudioElement [object HTMLAudioElement]]

  )

Now things are running smoothly, but there&aposs another thing that&aposs bothering us. The progress slider that we use to control the playback position is above the progress bar, which just looks weird, and also goes against the expectations of our hordes of users that there should be a handle on the progress bar itself that they can drag around. You know, like this:

Overcast web player

Getting into position

What we&aposd really like is to set our slider down right on top of the progress bar. This will give us the seeking we seek (sorry) as well as a visual indication of the current playback position. In order to do this, we&aposre going to have to learn a bit about CSS positioning.

Basically, we&aposll need to create a new container to hold the progress bar and the slider, and set its position style to "relative". Then, we can add our progress bar and slider as children of it, setting their position to "absolute" (relative to their parent container). Yes, I know, this is enough to make our heads explode, but I guess it makes sense once you achieve enlightenment or something?

We also need to set the z-indexes of the two elements. We set the progress bar to 0 to put it at the bottom of the stack, and the slider to 1 so it&aposs on top. That means we need to make the background colour of the slider transparent so the progress bar can shine through in all its glory.

Finally, we position both elements 0 pixels from the left of the enclosing container, then the slider 0 pixels from the top and the progress bar 10 pixels from the top. Why 10 pixels? Well, because:

  • The slider is 28 pixels high.
  • The progress bar is 8 pixels high.
  • We want to vertically centre the progress bar underneath the slider.
  • 20 - 8 is 10.

With that advanced mathematics out of the way, let&aposs make it happen!

(comment

  ;; ...

  (def progress-container
    (create-el {:tag "div"
                :id "progress-container"
                :classes ["cljcastr"]
                :styles {:position "relative"
                         :height "28px"
                         :clear "both"}}))
  ;; => #&aposcljcastr.player/progress-container

  (-> progress-container
      (set-children! [(-> progress-bar
                          (add-styles! {:position "absolute"
                                        :top "10px"
                                        :left "0px"
                                        :z-index 0}))
                      (-> progress-slider
                          (add-styles! {:position "absolute"
                                        :top "0px"
                                        :left "0px"
                                        :z-index 1
                                        :background-color "transparent"}))])
      log)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  (-> progress
      (add-styles! {:position "relative"})
      (set-children! [(-> elapsed-time
                          (add-styles! {:float "left"}))
                      (-> remaining-time
                          (add-styles! {:float "right"}))
                      progress-container])
      log)
  ;; => #object[HTMLDivElement [object HTMLDivElement]]

  )

Player with a nice progress bar and slider

This is looking pretty good! There are a few more things we might want to do, but just as we&aposre about to start doing some of those things, a bone-chilling thought occurs to us: what if we reload the page? We&aposve been building our player up in the REPL one s-expression at a time, but our HTML file still has a body that looks like this:

<body>
</body>

Should a feline friend prance across the keyboard, stepping on the Ctrl and r keys simultaneously (or Apple-r, if you&aposre one of those too-cool-for-school types), all will be lost!

Before giving in to despair, we realise that we have the awesome power of ClojureScript at our fingertips, and thus can make a permanent record of what we&aposve done so far. And that, my dear readers, we shall endeavour to do in the new post in this series, which shall be called Scittlecastr: All You Need is Persistence.

Permalink

nREPL, Right in Your Browser

Last week nrepl.org got a makeover. The docs moved to docs.nrepl.org, and nrepl.org itself became a landing page that explains what nREPL is and how its protocol works, with “wire windows” showing real exchanges between a client and a server, as EDN or as the bencode that actually goes over the wire. Reading about a protocol only gets you so far, though, so the page now also has a “Try it” section with a real nREPL server running right in your browser. There’s one in this post as well! In the rest of it I’ll tell you how it came about, why it was harder than it looks, and how you can use the server behind it for your own projects.

Here it is:

Go ahead, evaluate something! Tick “show the messages” to see the requests and the responses going back and forth, and try (range) if you’re curious what happens to code that never finishes.

Picking an approach

An interactive nREPL playground always runs into the same problem: you need an nREPL server somewhere, and a server that evaluates code from random people on the internet is just about the worst thing you can host. (It would also cost money that I’d rather spend on something else.) So the server had to run in the browser, and I looked at a few ways to get there.

The first one was the real thing. CheerpJ is a JVM that runs in the browser, and nREPL 1.8 and Clojure 1.12 worked on it remarkably well. Every op I tried worked, stdin included! The problem was everything around that - about 8 seconds to a ready server on a fast machine, about 4.5MB to download, a busy loop that freezes the whole page unless you put the JVM in a Web Worker, and an interrupt that works only on code that’s sleeping.1 That’s a lot to ask of someone who just opened a landing page.

The second one was scittle, which runs SCI (Michiel Borkent’s Clojure interpreter) in the browser and comes with an nREPL server, sci.nrepl. It solves a different problem, though - it lets your editor evaluate code in a page, through a relay that runs in babashka or on the JVM, and its replies differ from nREPL’s in quite a few places. (I’ve filed some issues about those.) nbb has a pretty complete nREPL server of its own, but it runs only on Node.

None of those fit, so I ended up writing a small nREPL server for SCI that runs entirely in a web page, with the page as its client - nrepl-sci. SCI does all the heavy lifting, and evaluation, completion and docs lookup took just a few hundred lines of ClojureScript. The whole thing is about 230KB gzipped and it’s ready in a fraction of a second. Not bad, right?

The hard part

A basic nREPL server is easy enough. The trouble starts with two ops most people never think about - interrupt and stdin.

JavaScript gives you one thread, and SCI evaluates code in one go. While (loop [] (recur)) runs nothing else does, so the server can’t even read the interrupt request that would stop it, and code calling read-line can’t wait for input without blocking the very thread that would receive it. There’s a third problem hiding in there as well. nREPL sessions are supposed to be independent, so an eval in one session (say, your editor’s) shouldn’t hold up another one (say, a test runner connected to the same server).

Here’s how nrepl-sci deals with all of that:

  • The server runs in two Web Workers. A supervisor takes the requests, queues them, and answers interrupt and stdin itself, while an evaluator runs SCI.
  • The two share some memory (a SharedArrayBuffer) that serves as a mailbox. The evaluator waits for mail with Atomics.wait, which blocks a worker without spinning, so read-line waits for its input there, much like it would on the JVM.
  • SCI calls a hook of your choice on every function call and every recur. nrepl-sci uses it to check its mailbox, to see if it’s been interrupted, or if there’s a request from another session it could take in between. Checking on every call made code noticeably slower, so it checks on every 256th call, which still means every few microseconds. Benchmarks run about as fast as without any of this.
  • Endless seqs like (range) and (iterate inc 0) check as they go, so (count (range)) stops when you interrupt it.
  • Some code never calls back into SCI - a regex that backtracks forever, for instance. The supervisor gives it a second to stop, then kills the evaluator and starts a new one. Your sessions survive that, but everything you evaluated in them is gone.

In the end the supervisor, the evaluator and the mailbox took more code than the nREPL server itself, which tells you something about where the complexity is.

Note: All of this works only when the page is cross-origin isolated, as browsers hand out SharedArrayBuffer only to pages served with the Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers.

Sadly, GitHub Pages, which hosts both nrepl.org and this blog, can’t send those. Without them nrepl-sci falls back to doing one thing at a time: read-line throws, and interrupting code means starting over, which is exactly what you saw if you tried (range) above. Evaluation, output, errors, completion and doc work the same either way, though.

Testing with proof

Back in 2023 I listed the lack of a protocol compatibility test suite among the things people hate about nREPL, and asked if one of you was looking for a fun small OSS project. Eventually I took my own advice and started proof, and nrepl-sci is the first server I’ve written with proof around from day one. It made a huge difference! Building an nREPL server used to involve a lot of guessing, as the protocol is small, but what clients actually expect from a server is spread across their source code.

A proof profile is a short TOML file that tells proof how to start your server and how to say a few things in its language: a value, some output, an exception, reading a line, code that runs until it’s stopped. proof run takes it from there, with checks covering the protocol and the exact requests CIDER, Calva, Conjure, vim-fireplace, REPLy and neorepl send. When a check fails, proof also tells you why it matters:

FAIL   robust.sessions-at-once      An eval in one session doesn't hold up the others
       clone request got no reply with "done" within 10s
       why: Each client has sessions of its own (e.g. an editor and neorepl,
       connected to the same server), so a server that runs one eval at a time
       leaves every other client waiting for whatever the code in one of them
       does.

That’s a check you can easily shrug off when you’re writing a server for a playground, and it’s the reason nrepl-sci takes requests in between at all. Working on the server mostly came down to running make proof after every change. Today nrepl-sci passes all 73 checks that apply to it (the other 5 are about things like vim-fireplace, which needs Java). Without shared memory it fails exactly two: stdin.need-input and robust.sessions-at-once. I guess that’s a pretty good summary of what you give up without cross-origin isolation.

If you’re working on an nREPL server of your own, I can’t recommend proof enough. It also comes with a GitHub Action, so your CI can run it as well.

The tradeoffs

Nothing comes for free, and nrepl-sci is no exception:

  • It’s SCI, not Clojure. You’re getting ClojureScript as interpreted by SCI, so there are no Java classes, no JVM libraries, and some corners of the language behave differently. That’s fine for a playground, but not for anything that needs your real project.
  • There’s no JavaScript interop. SCI blocks it unless you allow it, and nrepl-sci doesn’t, so (js/alert "hi") and even (.toUpperCase "hi") fail. Admittedly, that’s a bit annoying, but it’s also what makes nrepl-sci safe to embed anywhere, as the code people type can’t touch the page, or even the worker it runs in. (clojure.string/upper-case works just fine.)
  • A request from another session runs on top of the code it interrupted, so that code can’t stop until the request is done, and a read-line taken in between holds it until it gets its input. You’d need two clients fighting over the same server to notice.
  • Starting over loses your definitions, which matters most without cross-origin isolation, where every interrupt starts over.
  • It’s about 1MB of JavaScript (230KB gzipped), loaded the first time you use the REPL. That’s nothing compared to CheerpJ, but it’s not free either.

Using nrepl-sci yourself

nrepl-sci was built for nrepl.org, but nothing in it is specific to the site.

You can embed a REPL in a page - documentation, a tutorial, a blog post. The worker takes the bencode of each request as a Uint8Array and posts back the bencode of each reply, so the whole integration boils down to something like this:

const worker = new Worker("/assets/js/nrepl-sci-worker-0.1.0.js");
worker.onmessage = (event) => console.log(decode(event.data));
worker.postMessage(new TextEncoder().encode(encode({ op: "eval", code: "(+ 1 2)", id: "1" })));

encode and decode are a few dozen lines of bencode, and the REPL in this post is about 170 lines in total, styles included - have a look. Grab nrepl-sci-worker.js from the latest release and serve it from your own site, as browsers start workers only from the page’s own origin. If you can send the cross-origin isolation headers, you get stdin and proper interrupts for free. nrepl.org’s playground is a bigger example, with completion, history and the wire windows.

You can also test nREPL clients without a JVM. The Node build is a regular nREPL server on a socket (node nrepl-sci.js --port 7888) that’s up in about a tenth of a second, which makes it handy for the test suites of clients and editor plugins.

Finally, if you’re after an nREPL server for some other language that runs on a single thread, the supervisor and the mailbox might give you a few ideas. The server itself knows nothing about threads or sockets, and the trick with the mailbox works for any interpreter that can call back now and then while it runs.

What’s next

The obvious next step is getting cross-origin isolation on nrepl.org. A service worker can add the headers GitHub Pages won’t send, at the cost of a reload when it’s first installed, so I might try that on a page of its own. I’d also like to allow some harmless interop, so that (.toUpperCase "hi") works as people expect, and to publish nrepl-sci on npm.

Epilogue

Back in 2018 I wrote that there was “the big dream to port nREPL to ClojureScript”, so it could be used in self-hosted environments, and in 2023 I was still hoping we’d see more native ClojureScript nREPL servers. nrepl-sci isn’t quite that dream - it’s a much smaller nREPL server for SCI - but it’s a step in that direction.

Big thanks to Michiel Borkent for SCI, which did most of the hard work here. None of this would exist without it! And, as usual, special thanks to Clojurists Together for supporting my work on nREPL!

That’s all I have for you today. Play with the REPL, try to break it, and let me know how it went! Keep hacking!

  1. CheerpJ’s free tier also covers only non-commercial open-source projects that load it from Leaning Technologies’ CDN. That would have been fine for nrepl.org, but it’s one more string attached. ↩

Permalink

neorepl 0.4: Tests, Inspector, Tracing and Agents

neorepl 0.4 came out last week, and since I never got around to writing about 0.2 and 0.3, it feels like a good moment to look back at what happened since the original announcement. Three releases in two weeks, and neorepl now has most of what I listed under “What’s next” back then - a test runner, a full-screen inspector and stacktrace browser, an MCP server and a skill for coding agents - plus a few things that weren’t on the list at all, like tracing and peeking into your coding agent’s REPL session.

Running tests

Running tests in neorepl

,test in the REPL, or neorepl test in your shell, runs the tests in the namespaces you name, the tests you name or, without either, the whole project’s. Failures come with their file and line, and errors with a stack trace that stops at the test that hit them, instead of wandering off into clojure.test’s guts. ,retest (neorepl retest) reruns only what didn’t pass, which is exactly what you want while you’re fixing things.

As usual, the server does the heavy lifting. cider-nrepl runs the tests when it’s around, and so does babashka’s nREPL server, which bundles CIDER’s test ops these days. Without either, neorepl falls back to plain clojure.test, as long as you tell it which namespaces to test. And in a ClojureScript session the same commands run your ClojureScript tests.

$ neorepl test
$ neorepl test my.app-test my.app.db-test
$ neorepl test --format json my.app-test/start-test

Stack traces and the inspector

,stacktrace shows the last error full-screen. Pick a frame, press Enter, and its source opens in your $EDITOR at that line (as a copy, for code in a jar), and when you quit the editor you’re back where you were. By default you see only the frames that matter, and a brings back the rest - the REPL’s own machinery and the repeated frames of the same function. With cider-nrepl, i inspects the cause you’re looking at and d its ex-data. After a test run with errors, ,stacktrace shows those, each under the test it came from. (You can see that at the end of the GIF above.)

,inspect puts cider-nrepl’s inspector (babashka has one too) in full-screen:

The neorepl inspector

The arrow keys (or j and k) move around, Enter digs into a value and Backspace goes back up, [ and ] page through big collections, v shows an object’s fields, P toggles pretty printing and g refreshes values that change, like an atom’s. The handiest one is d, which defines whatever you’re looking at as a var. Dig five levels deep into some big API response, press d, give it a name, and you can play with that piece in the REPL right away.

neorepl inspect does the same one step at a time, for scripts and agents, since the inspector keeps its state in the session:

$ neorepl inspect '(System/getProperties)'
$ neorepl inspect --push 3
$ neorepl inspect --def props

Tracing

Tracing in neorepl

,trace traces a function, or every function in a namespace, and from then on each call shows its arguments and what it returned, nested in the calls it’s part of. It’s often the quickest way to see what some code is actually doing, and there are no printlns to forget to remove afterwards. Calls on other threads, like an HTTP handler’s or a future’s, show up above the prompt too. ,trace without arguments lists what’s traced and ,untrace stops tracing. Scripts and agents get neorepl trace, with the calls showing up in the output of the neorepl eval that made them. Tracing needs cider-nrepl on the server.

(Fun fact: tracing a function that the tracing itself calls, like clojure.core/map, used to take the whole server down. neorepl refuses to trace those, and as of orchard 0.48 the server no longer falls over on them anyway.)

Coding agents

0.3 was all about coding agents. The CLI was usable by agents from day one, but now they get a lot more help:

  • neorepl mcp serves neorepl’s features as MCP tools over stdio, for agents that would rather call tools than run commands. With Claude Code it’s a one-liner:

    $ claude mcp add neorepl -- neorepl mcp
    

    The tools are eval, load, doc, source, where, apropos, macroexpand, reload, test, retest, pst, interrupt, check and ports. That’s fewer than the CLI’s commands on purpose: an agent pays for every tool’s description in its context on every turn, whether it uses the tool or not. Evaluations get cut at 10,000 characters and interrupted after 60 seconds by default, so a stray (range) or an infinite loop can’t eat the agent’s context or its session.

  • A skill teaches agents the workflow I’d like them to follow: after editing a file, check its parens, load it, run its tests, and read the stack trace when something fails. It costs nothing until the agent actually works on Clojure code. doc/agents.md explains how to set it up with Claude Code, Codex, Gemini CLI, Cursor and VS Code.
  • neorepl check --fix repairs unbalanced delimiters the way parinfer’s indent mode would, trusting the indentation over the parens. Mismatched parens are still the most common way for an agent to break a Clojure file, and now fixing them doesn’t take another trip through the model:

    $ neorepl check src/demo/orders.clj
    src/demo/orders.clj:3:1: Unclosed (
    $ neorepl check --fix src/demo/orders.clj
    src/demo/orders.clj:3:1: Unclosed (, repaired from the indentation
    

The CLI and the MCP tools share named sessions, the one called agent unless told otherwise, which brings me to my favourite addition in 0.4.

Looking over your agent’s shoulder

Stepping into an agent's session

,sessions lists the server’s named sessions, and ,sessions agent steps into the agent’s, with its name in the prompt. Everything the agent left behind is right there: its namespace, its *1, its *e. So when an agent tells you that a test fails with a NullPointerException, you can look at the exception yourself instead of asking it to paste the stack trace. ,sessions - takes you back to your own session.

The lookups take --session too, so neorepl doc --session agent foo looks up foo the way that session sees it, in its namespace and its language, ClojureScript included.

The REPL, round two

0.4 also spent a lot of time on the REPL itself:

  • ,ns switches namespaces, loading the one you name if it isn’t loaded yet, and lists the project’s namespaces when you don’t name one.
  • C-x C-s shows the source of the symbol at the cursor and C-x C-a the vars whose names contain it, next to the C-x C-d for docs that was there from the start. All three come from rebel-readline.
  • ,doc and the arglists that show up as you type cover Java classes, methods and fields too, with their Javadoc as text (that one needs cider-nrepl).
  • ,edit and the stacktrace browser open definitions that live in jars, instead of telling you they can’t.
  • Ctrl-C got a lot more reliable. An interrupt sent right after an evaluation could get lost when the server hadn’t started running it yet, the forms after an interrupted one ran anyway, and whatever an interrupted evaluation printed later could show up in the next one’s output. All fixed.
  • neorepl sends the forms you enter to the server one at a time now, which is a big part of the interrupt fixes, and as a bonus piggieback evaluates all of them, not only the first.
  • On Linux, every evaluation used to wait 40ms on the server’s socket, courtesy of TCP’s delayed ACKs. neorepl turns those off now, on the JVM and on babashka 1.13.227 or newer, and evaluating a file with 100 forms went from 4.5 seconds to about 150ms.
  • Lots of error messages got friendlier: a build tool that isn’t installed, a server that dies while starting, loading a file with unbalanced parens, a named session the server lost when it restarted, and so on.

So much for keeping neorepl small, by the way - the code has about doubled since 0.1. In my defense, it does a lot more than twice as much.

Making the neighbours better, round two

Just like last time, building neorepl turned up problems elsewhere, and a good part of these two weeks went into fixing them at the source:

  • I filed a batch of issues for babashka’s nREPL server: test runs that hung or killed the session, :project? being ignored, a session that never answered again after its client disconnected, a couple of JLine classes neorepl’s line editor needed. Michiel fixed every single one of them within days, and they’re all in babashka 1.13.227. Thanks, Michiel!
  • cider-nrepl 0.64 and orchard 0.48 came out this week with fixes for the problems I ran into: what tests print now comes back with the test results (it used to go to the connection of the session’s previous message, which for a CLI is long gone), the test op no longer goes silent for a namespace that isn’t loaded or runs every test when the one you asked for doesn’t exist, ClojureScript tests run on shadow-cljs, a traced call that throws shows up as such, and Javadoc no longer comes with stray spaces in it. CIDER gets all of that too, which is my favourite kind of side effect. neorepl 0.4.1, out the day after this post, uses cider-nrepl 0.64 when it starts a server.
  • In nREPL, the fix for an interrupted evaluation’s last response going to the wrong connection is merged, and so is TCP_NODELAY on the server’s sockets, which takes 100 evaluations with nREPL’s own client from 4.4 seconds to 90ms on Linux. Both will be in the next nREPL release.
  • The piggieback improvements I mentioned last time are a bit behind schedule. A lot of ClojureScript REPLs run on top of piggieback, so a mistake there has a pretty big blast radius, and evaluating every form of a message is something we never found a good way to do in the past. I’d rather take my time with those than rush them out. neorepl isn’t waiting on them, at least, since it sends the forms one at a time now.

What’s next

0.5 is about more runtimes and better navigation:

  • integration tests against real servers in CI: cider-nrepl and babashka first, then shadow-cljs and piggieback with Node
  • nbb and Basilisp in CI, with their gaps documented
  • cross-references (,refs), on top of cider-nrepl’s fn-refs and fn-deps
  • a full-screen namespace browser
  • a couple of ideas borrowed from clojure-cli.repl: evaluating the form at the cursor, and the last evaluation’s outcome and time in the prompt
  • indenting continuation lines, and a way to pick the vi keymap

Better Windows support, the one item from the 0.1 list that’s still open, is planned for 0.6, together with easier ways to install neorepl.

Try it

$ bbin install io.github.nrepl/neorepl
$ cd my-project
$ neorepl

The CHANGELOG has everything I skipped here, and the README has the details. Please keep the issues and ideas coming!

Keep hacking!

Permalink

My core.logic fork

My core.logic fork

My core.logic fork

A series on making Clojure's maps, vectors and sets relational in core.logic: every relation runs in every direction, and anything it can't decide yet, it waits on visibly instead of guessing. The code and an overview are in the repository README.

What it looks like

Each relation is a Clojure function read as a relation, so any argument can be the unknown one.

Maps. get, assoc, merge, conj and keys, forwards and backwards:

(run* [v] (geto {:a 1 :b 2} :b v))          ;;=> (2)
(run* [k] (geto {:a 1 :b 2} k 2))           ;;=> (:b)          which key holds 2

(run* [m] (assoco m :b 2 {:a 1 :b 2}))
;;=> ({:a 1} {:a 1, :b _0})                 ;; m had no :b, or any :b that assoc overwrote

(run* [m1 m2] (mergeo m1 m2 {:a 1}))
;;=> ([{:a 1} {}] [{} {:a 1}] [{:a _0} {:a 1}])

(run* [q] (fresh [v] (conjo q [:foo v] {:foo 1})))
;;=> ({} {:foo _0})                         ;; the wiki's test case, below

(run* [k] (== {:a 1 k 2} {:a 1 :b 2}))      ;;=> (:b)          unknown keys unify
(run* [ks] (keyso {:a 1 :b 2} ks))          ;;=> (#{:b :a})

(run* [m] (geto m :a 1) (geto m :b 2))
;;=> ((lmap {:b 2, :a 1} | _0))             ;; an open map: these keys, maybe more

Vectors. nth, conj, concat, subvec, with lengths solved exactly:

(run* [i] (ntho [:a :b :a] i :a))           ;;=> (0 2)
(run* [v x] (conjo v x [1 2 3]))            ;;=> ([[1 2] 3])
(run* [s e] (subveco [:a :b :c :d] s e [:b :c]))
;;=> ([1 3])                                ;; where the slice came from

(run* [a b] (concato a b [1 2 3]))
;;=> ([[] [1 2 3]] [[1] [2 3]] [[1 2] [3]] [[1 2 3] []])

(run* [v] (counto v 2) (ntho v 0 :a))       ;;=> ([:a _0])

(run* [x] (fresh [l r] (concato x [1] l) (concato [1] x r) (concato r [2] l)))
;;=> ()                                     ;; |x| + 1 ≠ |x| + 2, no search needed

Sets. Literals with unknown elements, conj, union, difference:

(run* [x y] (== #{x y} #{1 2}))             ;;=> ([1 2] [2 1])
(run* [s] (conjo s 3 #{1 2 3}))             ;;=> (#{1 3 2} #{1 2})
(run* [a] (uniono a #{2 3} #{1 2 3}))       ;;=> (#{1 3} #{1} #{1 3 2} #{1 2})
(run* [x] (subseto #{1 x} #{1 2 3}))        ;;=> (3 2)         x is distinct from 1

(run* [x] (differenceo #{1 2 3} x #{1}))
;;=> (((lset #{3 2} | _0) :- (clojure.core.logic/lackso _0 1)))
;; 2 and 3, plus a rest without 1

(run* [s] (containso s 1) (containso s 2))
;;=> ((lset #{1 2} | _0))

Together. The relations compose across kinds, and paths are relations too:

(def db {:users [{:name "ada" :tags #{:admin}} {:name "bob" :tags #{}}]})

(run* [n] (fresh [us u tags]
            (geto db :users us) (ntho us (lvar) u)
            (geto u :name n) (geto u :tags tags) (containso tags :admin)))
;;=> ("ada")

(run* [p] (get-ino db p "bob"))             ;;=> ((:users 1 :name))

;; the same question as a pattern: at least these keys, at least this element
(run* [n] (fresh [i u] (get-ino db [:users i] u) (matcho u {:name n :tags #{:admin}})))
;;=> ("ada")

Why this is the core.logic thing

A logic language takes its terms from its host. miniKanren's term is the Scheme pair, because Scheme's data is the pair: appendo and membero work so well because a partly known list has a natural shape, a known front and an unbound tail. core.logic brought miniKanren to Clojure, and the pair came along. Clojure's own data came in second class: two maps unified only if they had exactly the same keys, and apart from featurec reading values at known keys, nothing could take a map apart or build one.

But the data is what Clojure is. Literal, immutable maps, vectors and sets, used for everything: "just use maps", data over functions over macros, a program is a pipeline of get, assoc, merge, conj and union. Clojure programmers don't encode a record as an association list or a set as a list with no duplicates; they write {:name "ada"} and #{1 2}.

So a relational Clojure should be relational over that. This series makes it so: get, assoc, merge, nth, concat, union and friends, each as a relation that runs backwards. A query can ask which map an assoc came from, which key path leads to a value, or which two sets have this union, on the same data the rest of the program uses. That's the part of core.logic only Clojure can have, and what miniKanren-on-lists can only encode.

This isn't a new wish. In 2013 David Nolen put it on core.logic's own roadmap. The wiki page Constraints & modes opens: "core.logic unfortunately to this day continues to emphasize operating on sequence-like data structures due to its Prolog & Scheme roots", and asks for "constraint versions of Clojure functions". It sketches an assocc, finds that running it backwards branches, and settles on conjo as the test case:

(run* [q]
  (fresh [v o]
    (== o {:foo 1})
    (conjo q [:foo v] o)))

"So we should see the following results: ({} {:foo _0})." That is exactly what the fork answers. The companion page CLP(Set) asks for sets with unknown elements, (unionc #{:cat x y} #{:dog :bird z} o), and notes that such a union branches over which elements pair up. The fork runs that one too: with o = #{:cat :dog :bird}, x and y are :dog and :bird in either order and z is :cat. (Writing this found a bug: the fork used to let x be :cat too. Fixed.) Both pages are still on the wiki's roadmap, under "Immediate", and were never built. The machinery they sketch is not what this series builds (a defc with mode declarations, set domains like the FD domains), but their test cases are what it answers.

It also sets the bar. A relation that only works in some directions is a function with extra steps, so the series holds every relation to one rule: it answers correctly in every mode, or waits until it can (part 2).

  1. Relational maps: a tour of the API.
  2. No unsupported behaviour: why the design had to be redone, and how unknown keys, lazy checks and suspended goals fit together.
  3. Making it fast: from 33 s to 8 ms, and what was slow.
  4. Vectors: open vectors, lengths solved exactly, and get-in as a query language.
  5. Literature and miniKanren parity: where this sits in the research, and what core.logic still lacks.
  6. Towards relational hyperdimensional computing: the same data structures, encoded as hypervectors.
  7. Sets: maps without values, and what that buys.
  8. Side by side with miniKanren: faster-miniKanren's test suite run on core.logic, the bugs it found, and maps, sets and vectors next to their miniKanren encodings.
  9. A relational Clojure interpreter: evalo for a Clojure subset, with relational maps as environments; what it runs backwards, and where its search is still slow.

Next, maybe: records as types, and ClojureScript. See future work.


Generated by Claude on Benjamin Schwerdtner's behalf.

Date: 2026-10-28 Wed 00:00

Permalink

Mein core.logic-Fork

Mein core.logic-Fork

Mein core.logic-Fork

Eine Serie darüber, wie Clojures Maps, Vektoren und Sets in core.logic relational werden: Jede Relation läuft in jede Richtung, und was sie noch nicht entscheiden kann, darauf wartet sie sichtbar, statt zu raten. Code und Überblick stehen im README des Repositorys.

Wie es aussieht

Jede Relation ist eine Clojure-Funktion, als Relation gelesen, also kann jedes Argument das unbekannte sein.

Maps. get, assoc, merge, conj und keys, vorwärts und rückwärts:

(run* [v] (geto {:a 1 :b 2} :b v))          ;;=> (2)
(run* [k] (geto {:a 1 :b 2} k 2))           ;;=> (:b)          welcher Key hat 2

(run* [m] (assoco m :b 2 {:a 1 :b 2}))
;;=> ({:a 1} {:a 1, :b _0})                 ;; m hatte kein :b, oder ein :b, das assoc überschrieb

(run* [m1 m2] (mergeo m1 m2 {:a 1}))
;;=> ([{:a 1} {}] [{} {:a 1}] [{:a _0} {:a 1}])

(run* [q] (fresh [v] (conjo q [:foo v] {:foo 1})))
;;=> ({} {:foo _0})                         ;; der Testfall aus dem Wiki, unten

(run* [k] (== {:a 1 k 2} {:a 1 :b 2}))      ;;=> (:b)          unbekannte Keys unifizieren
(run* [ks] (keyso {:a 1 :b 2} ks))          ;;=> (#{:b :a})

(run* [m] (geto m :a 1) (geto m :b 2))
;;=> ((lmap {:b 2, :a 1} | _0))             ;; eine offene Map: diese Keys, vielleicht mehr

Vektoren. nth, conj, concat, subvec, mit exakt gelösten Längen:

(run* [i] (ntho [:a :b :a] i :a))           ;;=> (0 2)
(run* [v x] (conjo v x [1 2 3]))            ;;=> ([[1 2] 3])
(run* [s e] (subveco [:a :b :c :d] s e [:b :c]))
;;=> ([1 3])                                ;; woher der Ausschnitt stammt

(run* [a b] (concato a b [1 2 3]))
;;=> ([[] [1 2 3]] [[1] [2 3]] [[1 2] [3]] [[1 2 3] []])

(run* [v] (counto v 2) (ntho v 0 :a))       ;;=> ([:a _0])

(run* [x] (fresh [l r] (concato x [1] l) (concato [1] x r) (concato r [2] l)))
;;=> ()                                     ;; |x| + 1 ≠ |x| + 2, ohne Suche

Sets. Literale mit unbekannten Elementen, conj, union, difference:

(run* [x y] (== #{x y} #{1 2}))             ;;=> ([1 2] [2 1])
(run* [s] (conjo s 3 #{1 2 3}))             ;;=> (#{1 3 2} #{1 2})
(run* [a] (uniono a #{2 3} #{1 2 3}))       ;;=> (#{1 3} #{1} #{1 3 2} #{1 2})
(run* [x] (subseto #{1 x} #{1 2 3}))        ;;=> (3 2)         x ist verschieden von 1

(run* [x] (differenceo #{1 2 3} x #{1}))
;;=> (((lset #{3 2} | _0) :- (clojure.core.logic/lackso _0 1)))
;; 2 und 3, plus ein Rest ohne 1

(run* [s] (containso s 1) (containso s 2))
;;=> ((lset #{1 2} | _0))

Zusammen. Die Relationen lassen sich über die Arten hinweg kombinieren, und auch Pfade sind Relationen:

(def db {:users [{:name "ada" :tags #{:admin}} {:name "bob" :tags #{}}]})

(run* [n] (fresh [us u tags]
            (geto db :users us) (ntho us (lvar) u)
            (geto u :name n) (geto u :tags tags) (containso tags :admin)))
;;=> ("ada")

(run* [p] (get-ino db p "bob"))             ;;=> ((:users 1 :name))

;; dieselbe Frage als Muster: mindestens diese Keys, mindestens dieses Element
(run* [n] (fresh [i u] (get-ino db [:users i] u) (matcho u {:name n :tags #{:admin}})))
;;=> ("ada")

Warum das die eigentliche Sache von core.logic ist

Eine Logiksprache nimmt ihre Terme von ihrer Wirtssprache. Der Term von miniKanren ist das Scheme-Paar, weil Schemes Daten das Paar sind: appendo und membero funktionieren so gut, weil eine teilweise bekannte Liste eine natürliche Form hat, einen bekannten Anfang und einen ungebundenen Rest. core.logic hat miniKanren nach Clojure gebracht, und das Paar kam mit. Clojures eigene Daten waren Bürger zweiter Klasse: Zwei Maps unifizierten nur, wenn sie genau dieselben Keys hatten, und außer featurec, das Werte an bekannten Keys liest, konnte nichts eine Map zerlegen oder eine bauen.

Aber die Daten sind das, was Clojure ausmacht. Literale, unveränderliche Maps, Vektoren und Sets, für alles benutzt: "just use maps", Daten vor Funktionen vor Makros, ein Programm ist eine Pipeline aus get, assoc, merge, conj und union. Clojure-Leute kodieren einen Datensatz nicht als Assoziationsliste oder ein Set als Liste ohne Duplikate; sie schreiben {:name "ada"} und #{1 2}.

Ein relationales Clojure sollte also über diese Daten relational sein. Diese Serie macht das wahr: get, assoc, merge, nth, concat, union und Verwandte, jeweils als Relation, die rückwärts läuft. Eine Anfrage kann fragen, aus welcher Map ein assoc kam, welcher Key-Pfad zu einem Wert führt oder welche zwei Sets diese Vereinigung haben, auf denselben Daten, die der Rest des Programms benutzt. Das ist der Teil von core.logic, den nur Clojure haben kann, und den miniKanren auf Listen nur kodieren kann.

Der Wunsch ist nicht neu. 2013 hat David Nolen ihn auf die Roadmap von core.logic selbst gesetzt. Die Wiki-Seite Constraints & modes beginnt mit: "core.logic unfortunately to this day continues to emphasize operating on sequence-like data structures due to its Prolog & Scheme roots", und wünscht sich "constraint versions of Clojure functions". Sie skizziert ein assocc, stellt fest, dass es rückwärts verzweigt, und nimmt conjo als Testfall:

(run* [q]
  (fresh [v o]
    (== o {:foo 1})
    (conjo q [:foo v] o)))

"So we should see the following results: ({} {:foo _0})." Genau das antwortet der Fork. Die Begleitseite CLP(Set) wünscht sich Sets mit unbekannten Elementen, (unionc #{:cat x y} #{:dog :bird z} o), und merkt an, dass so eine Vereinigung darüber verzweigt, welche Elemente zusammenfallen. Auch die läuft im Fork: Mit o = #{:cat :dog :bird} sind x und y in beliebiger Reihenfolge :dog und :bird, und z ist :cat. (Beim Schreiben habe ich dabei einen Bug gefunden: Der Fork hat x früher auch :cat sein lassen. Behoben.) Beide Seiten stehen noch auf der Roadmap im Wiki, unter "Immediate", und wurden nie gebaut. Die Maschinerie, die sie skizzieren, ist nicht die, die diese Serie baut (ein defc mit Modus-Deklarationen, Set-Domänen wie die FD-Domänen), aber ihre Testfälle sind die, die sie beantwortet.

Sie legt auch die Messlatte. Eine Relation, die nur in manche Richtungen funktioniert, ist eine Funktion mit Extraschritten. Deshalb hält die Serie jede Relation an eine Regel: Sie antwortet in jedem Modus korrekt, oder sie wartet, bis sie es kann (Teil 2).

  1. Relationale Maps: ein Rundgang durch die API.
  2. Kein nicht unterstütztes Verhalten: warum das Design neu gemacht werden musste, und wie unbekannte Keys, lazy Checks und suspendierte Goals zusammenpassen.
  3. Schnell machen: von 33 s auf 8 ms, und was langsam war.
  4. Vektoren: offene Vektoren, Längen exakt gelöst, und get-in als Abfragesprache.
  5. Literatur und Parität mit miniKanren: wo das in der Forschung steht, und was core.logic noch fehlt.
  6. Auf dem Weg zu relationalem hyperdimensionalem Rechnen: dieselben Datenstrukturen, als Hypervektoren kodiert.
  7. Sets: Maps ohne Werte, und was das bringt.
  8. Seite an Seite mit miniKanren: die Testsuite von faster-miniKanren auf core.logic, die Bugs, die sie gefunden hat, und Maps, Sets und Vektoren neben ihren Kodierungen in miniKanren.
  9. Ein relationaler Clojure-Interpreter: evalo für eine Teilmenge von Clojure, mit relationalen Maps als Umgebung; was er rückwärts kann, und wo seine Suche noch langsam ist.

Als Nächstes vielleicht: Records als Typen, und ClojureScript. Siehe Future Work.


Generiert von Claude im Auftrag von Benjamin Schwerdtner.

Datum: 2026-10-28 Wed 00:00

Permalink

Clojure Explained: A Modern Lisp on the JVM

Introduction

Lisp is the second-oldest programming language still in use, and most programmers have never written it — yet its ideas (code as data, functions as values, macros) shaped everything after it. Clojure, released in 2007, is Lisp rebuilt for the modern world: it runs on the JVM, uses every Java library, and replaces Lisp's mutable lists with immutable data structures. It is the language that convinced a generation of sceptics that parentheses are not a problem — they are the point.

Immutable data, code as data, and the REPL

Clojure's model rests on a few distinctive ideas:

  • Immutable data by default — maps, vectors, and sets never change in place; "updating" returns a new version, so concurrent code shares data safely and old versions stay available.
  • Code as data — Clojure code is itself a data structure (lists), so programs can manipulate programs: macros, the feature other languages fake with code generation, are native.
  • The REPL — develop interactively: evaluate expressions, inspect state, and reshape a running program without restarting it. The REPL is not a toy; it is the workflow.
  • JVM hosted — every Java library works, so Clojure programs deploy wherever Java does, with none of the ecosystem risk of a niche runtime.

A tiny example shows the shape — a function, a map, and immutable transformation:

;; A user, and a function that "ages" them — returning a new map
(def user {:name "Ayesha" :age 25})
(defn birthday [u]
  (update u :age inc))
(birthday user)
;; => {:name "Ayesha" :age 26}
;; user itself is unchanged — still 25

No mutation, no classes, just data and functions — and the original value is still there, untouched, for any other code that needs it.

The honest trade

Clojure's trade is familiarity and hiring: parentheses and prefix notation take adjustment, startup time is JVM-slow, and the talent pool is small. Error messages, from the JVM stack, can be cryptic. The rule of thumb: Clojure when immutable data and interactive development matter most, especially for data pipelines and backends; a mainstream JVM language when team familiarity dominates.

Takeaway

Clojure is a modern Lisp on the JVM: immutable data, code as data, and REPL-driven development, with every Java library available. It trades familiarity for simplicity that scales — and for programmers who learn it, parentheses stop looking strange and start looking honest.

Originally published at https://practicaldevblog.blogspot.com/2026/10/clojure-explained-modern-lisp-on-jvm.html

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.