Statistics made simple

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

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

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

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

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

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

Existing options

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

What is going on?

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

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

My solution

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

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

Setup

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

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

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

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

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

Request types

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

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

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

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

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

Feedbin feed-id:1373711 - 142 subscribers

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

Graphs

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

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

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

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

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

Insights

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

Not implemented (yet)

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

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

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

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

One day.

How to get

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

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

Permalink

Lowering the Drawbridge

Drawbridge 0.4 is out! If your reaction is “Draw-what now?”, I can’t really blame you - Drawbridge is easily the most obscure project in the nREPL stable, and it has spent most of its life in a state best described as “technically maintained”. I’ve set out to change that recently, and this post is both a release announcement and the story of a 14-year-old project that never quite lived up to its potential. Hopefully, until now.

What’s Drawbridge, anyway?

In case you’ve never come across it: Drawbridge is an HTTP transport for nREPL, packaged as a humble Ring handler. You mount it in your web application, and suddenly you can REPL into that application over plain HTTP(S) - no open socket, no special network setup, just another route in your app:

(ns my-app.repl
  (:require [drawbridge.core :refer [secure-ring-handler]]
            [ring.adapter.jetty :refer [run-jetty]]))

(defn -main [& _]
  (run-jetty (secure-ring-handler :token (System/getenv "DRAWBRIDGE_TOKEN"))
             {:port 8080 :join? false}))

Chas Emerick created it way back in 2012, in the golden era of Heroku-style PaaS platforms.1 The problem it solved was very real: those platforms gave your application exactly one way in - HTTP on port 80/443 - and nREPL speaks bencode over a raw socket. If you wanted a REPL into your production app (and of course you did, this is Clojure), you needed the REPL to ride on HTTP. Drawbridge was that ride. The name suddenly makes a lot of sense, doesn’t it? It’s the thing you lower to let people into the castle.

When I took over nREPL’s maintenance in 2018, Drawbridge came along as part of the broader ecosystem cleanup and moved to the nrepl GitHub org. It got a couple of releases back then (0.2.0 and 0.2.1), and after that
 silence. Seven years of it.

The bridge that never got you across

Truth be told, Drawbridge never really delivered on its promise. Let me try to explain why.

First, security. A Drawbridge endpoint is remote code execution as a service - that’s the whole point - but the project shipped with no authentication story at all. “Bring your own middleware” was the official answer, and the number of people who got that right on the first try was, I suspect, not large. Exposing your production REPL on the public internet with hand-rolled auth is the kind of thing that keeps security teams up at night, and rightfully so.

Second, the tooling never came. CIDER, Calva, and practically every other nREPL client connect over a plain socket with a host and a port. Drawbridge connections only worked through lein repl :connect and hand-written clients - so the audience was limited to people willing to REPL without their editor. That’s a tough sell.

Third, the transport itself was long-polling: the client keeps asking the server “anything for me yet?” over and over. It worked, but it always felt a bit sluggish, and it could flood your server with polling requests in the process.

Given all that, the community sensibly routed around the problem. If you had SSH access to the box, tunneling the nREPL port (ssh -L) was simpler and more secure than anything Drawbridge offered. And nREPL itself eventually grew native TLS support, which covered another chunk of the use cases. Between those two, “secure remote nREPL” mostly stopped meaning “HTTP”.2

But the original niche never went away. Plenty of environments still give you HTTP ingress and nothing else - PaaS platforms, container services, corporate networks where the only thing allowed through the firewall is 443. In those places SSH tunnels and TLS sockets aren’t options, and Drawbridge remains the only game in town. It just needed to stop being a liability there.

Dusting it off

The modernization happened in two waves. A quiet 0.3.0 earlier this year dragged the foundations into the present - current nREPL, Ring and friends, Clojure 1.10+, Java 17+, a real test suite and CI. The fun stuff landed now, in 0.4, and it goes straight after the three problems above - reach, trust and speed.

  • The headline feature is the bridge (drawbridge.bridge) - a small local process that presents a plain nREPL socket on localhost and relays everything to a remote Drawbridge endpoint. This single thing unlocks the entire nREPL ecosystem: CIDER, Calva, rebel-readline, anything that speaks bencode can now reach a Drawbridge endpoint. It even writes a .nrepl-port file, so cider-connect picks it up automatically.
  • Security is finally built in, and it’s opt-out rather than opt-in. secure-ring-handler gives you a complete endpoint with bearer-token authentication (constant-time comparison, naturally) in one form, and it flat-out refuses to create an unauthenticated endpoint unless you explicitly pass :insecure true. Exposing an open REPL should be a deliberate act, not an oversight.
  • There’s a brand-new WebSocket transport that replaces long-polling with real server push - output streams to your REPL the moment it’s produced. The bridge picks it automatically for ws(s):// URLs, and the server pings idle connections so aggressive proxies (looking at you, 55-second router timeouts) don’t sever your session mid-thought.
  • The old HTTP transport is still there as the lowest-common-denominator fallback, and it got some love too - including a fix for a subtle session-affinity race that had been quietly lurking in the client since the beginning.
  • The bridge CLI tries to be a good citizen: it reads the token from DRAWBRIDGE_TOKEN (command-line arguments leak via ps), validates its arguments, has a --help, and when it can’t reach the remote endpoint it tells you why right in your REPL instead of silently dropping the connection.
  • There’s a deps.edn now, so you can run the bridge straight from a git coordinate without installing anything.
  • A couple of ancient issues got closed along the way, including the “Drawbridge clobbers my Ring session” bug that had been open since 2018. (It had actually been fixed for a while - now there’s a regression test proving it.)

The end-to-end experience today looks like this. On the server - the secure-ring-handler example above. On your machine:

$ export DRAWBRIDGE_TOKEN=<token>
$ clojure -Sdeps '{:deps {nrepl/drawbridge {:mvn/version "0.4.0"}}}' \
    -M -m drawbridge.bridge --url https://my-app.example.com/repl


and then cider-connect (or lein repl :connect http://localhost:7888, or whatever you fancy). Your editor neither knows nor cares that there’s HTTP in the middle.

Where you might actually use it

A few scenarios worth trying:

  • REPL into an app on a PaaS or container platform that only exposes HTTP(S) - the original 2012 use case, finally with your editor along for the ride.
  • Debugging a staging environment from behind a corporate firewall that only lets 443 through.
  • Ops-style poking at a running service where provisioning SSH access would be a bureaucratic adventure, but adding one authenticated route is a code review away.
  • The WebSocket transport within your own infrastructure, simply because server push makes for a much nicer remote REPL than polling ever did.

To be clear - if you do have SSH access or can expose a TLS socket, those remain excellent options, and I’m not here to talk you out of them. Drawbridge is for everywhere they don’t reach.

Feedback, please!

Drawbridge spent so long in hibernation that I’ve genuinely lost track of who still uses it and for what. If you take 0.4 for a spin - or if you looked at it years ago and bounced off - I’d love to hear about it on the issue tracker: what works, what doesn’t, what’s missing. I have some ideas of my own for where to take it next (smarter session handling is high on the list), but I’d much rather steer by real use cases than by my own guesses.

Big thanks to Chas Emerick for building Drawbridge (and nREPL!) in the first place - reviving a well-designed project is a pleasure, even 14 years later. And big thanks to Clojurists Together, Nubank, and all the other organizations and people supporting my Clojure OSS work - none of this would be happening without you.

In the REPL we trust! Keep hacking!

  1. The docstring of nREPL’s url-connect still lists http://your-app-name.heroku.com/repl among its examples, to this very day. 2012 was a different time - deploying to Heroku was the height of fashion and we all thought dynos were the future. ↩

  2. Fun trivia: Leiningen’s REPL client (REPL-y) has been quietly bundling Drawbridge all these years to make lein repl :connect http://... work. Every Leiningen user has been carrying a copy of Drawbridge 0.2.1 around without knowing it. ↩

Permalink

Clojure 1.13.0-alpha4

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

Destructuring changes and additions

Idents after & in :keys!/syms!/strs!/:keys/syms/strs must now be actual keys, not binding symbols. This is a change in syntax since alpha3. Note that symbol keys should be quoted as unadorned symbols are binding symbols.

:or now accepts key→val mappings in addition to binding→val.

Added a new :defaults name directive at top level to bind name to a map of defaults, key→val. Binding symbols in the :or map are transformed to the key value in the :defaults map. :defaults without :or is an error.

:select name, introduced in alpha3, now selects deeply, through nested maps, and fills in values for missing keys from :or. The :select map contains all keys mentioned anywhere in the binding form.

  • CLJ-2964 :select directive in map destructuring

  • CLJ-2966 :defaults directive in map destructuring

  • CLJ-2967 tests for nested destructuring

Other changes since Clojure 1.13.0-alpha3

  • CLJ-2870 Exception phase during top-level eval is miscategorized

Try it out

Update your deps.edn :deps with:

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

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

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

Permalink

Dual Duet

Two gigs in one night: not quite a first (the same happened in Kathmandu many years ago), but perhaps the first representing two completely different projects on the same stage. First up is Khyal Geometries with Shama Rahman on sitar, second is The Printer Jam with Evan Raskob on printer, both sets as part of ACM Creativity and Cognition 2026. The former will be live-coded (Max, Node.js, ClojureScript); the latter will be controllerism.

It’s only now that I realise that my carefully crafted blog taxonomy cannot deal with a single blog post that spans more than one project.

Permalink

rab Coding Agent

rab Coding Agent

2026-07-13 - Building a TUI-based coding agent in Rust from scratch - the agentic loop, tool dispatch, what went well and what didn't, and why I went back to pi
Keywords: rust, coding-agent, agentic-loop, tool-calling, llm, tui, clojure, pi

Permalink

KiCad helpers and caliper improvements

Hi friends,

This newsletter we’re streamlining the process of creating circuit boards and revisiting the DIY electronic calipers project from two years ago.

Kevin’s KiCad Helpers

I first started playing around with electronics via Arduino back in 2010, and I designed my first circuit board back in 2015 for my walnut and leather cell phone. Since then, I’ve made around a dozen PCBs, with a notable spike during the pandemic making weird mechanical keyboards.

Averaging roughly one PCB/year is a maximally frustrating frequency: I have 100’s of hours of cumulative experience, but it’s sufficiently spread across the forgetting curve that every time I start something the details are only vaguely familiar and I’ve got to re-orient myself again. It reminds me of filling out my taxes, where I also furiously consult my notes and attempt to interpret them against new versions of the UI where input boxes have been hidden across other forms and new sub-menus.

Anyway, armed with a coding agent, I externalized everything I kept forgetting how to do in a pile of scripts: Kevin’s KiCad Helpers.

This is tailored to my needs — designing PCBs in KiCad 10 to be manufactured by JLCPCB — but if yours are similar you might find ‘em helpful. Even if you don’t make PCBs, it might be good inspiration for how to apply LLMs to sand down rough edges of whatever convoluted infrastructure is required for your projects.

A quick tour of the tools:

DXF import

I specify all of my mechanical stuff — board outlines, mounting hole positions, etc. — in Autodesk Inventor since it has a constraint solver and allows me to directly reference complex geometry driven by other objects.

KiCad’s GUI has a DXF import tool, but it doesn’t have a mechanism to replace already imported geometry, which makes it tedious to iterate.

This script:

  • imports geometry from a DXF file into a board layer specified by the filename (panel_Edge.Cuts.dxf imports a group ID’d “panel” to the Edge Cuts layer)
  • deletes any geometry previously imported with that ID
  • polls the file for changes, so you get “live reload”

I combine this with a similar poll + export-to-file script in my mechanical CAD tool (shown above) to get live syncing to KiCad (shown below):

DXFs map into KiCad’s origin at top-left of the page outline and the DXF positive Y direction points up, so most of my PCBs end up drawn outside of the page ¯\_(ツ)_/¯.

STEP export

I also need the reverse direction: Import a 3D model of a circuit board and its components back into my mechanical CAD program so I can verify clearances, etc. KiCad has native STEP export, but unfortunately some of the parts have extremely detailed 3D models. My M1 Macbook Air running Windows CAD software in a virtual machine does not do well when every pin of every chip is a separate solid body.

This script generates a STEP export where all parts have been replaced with axis aligned bounding boxes:

Parts database

I design all of my boards to be assembled by JLCPCB, so it’s extremely helpful to have a low-latency way to query their parts. This script downloads CDFER’s daily JLCPCB parts sqlite database and consolidates everything into a single table with a numeric price column and lots of indexes so that searching is fast:

I use DB Browser for SQLite but you can of course use whatever interface you like.

It’s also extremely useful to point LLM agents at this database:

My dude, I need an H-bridge that can drive +/- 30 Volts, please query ~/foo/bar/parts.db and give me a table with 5 options for integrated ones showing price / stock / description. Please also make a table showing options for drivers with external transistors. Include links to the datasheets.

JLCPCB (EasyEDA) schematic and footprint import

Shout out again to CDFER for their JLCPCB KiCad Library, which has all of the “basic” jellybean parts and some of the extended ones as well.

For the parts that are not already available in here, I need to import them. Rather than draw them entirely from scratch, I use uPesy’s easyeda2kicad.py.

However I don’t always want to create an entirely new symbol and footprint if the part actually corresponds to something that’s in the KiCad standard library. So my import tool tries to match existing footprints (including 90 degree rotations) and spawns a terminal UI so you can interactively compare potential matches with the EasyEDA footprint:

Schematic analysis

The most interesting helper I’ve created so far is a general schematic analysis framework, which imports the KiCad netlist and schematic instance properties into a DataScript graph database to run various queries/checks.

For example, this lil’ function calculates the capacitance (of all the explicit capacitors, anyway) on a given net:

(defn net-capacitance
  [db net-name]
  (some->> (d/q '{:find  [?ref ?v]
                  :in    [$ ?net]
                  :where [[?n :net/name ?net]
                          [?n :net/nodes ?node]
                          [?node :node/pin ?pin]
                          [?i :instance/pins ?pin]
                          [?i :instance/ref ?ref]
                          [(clojure.string/starts-with? ?ref "C")]
                          [?i :instance/value ?v]]}
                db net-name)
           (keep (comp parse-capacitance second))
           seq
           (reduce + 0.0)))

This function can then be used to check the total capacitance on, e.g., the power nets, and throw an error if it exceeds, e.g., the maximum 10uF allowed by the USB specification.

(defn check-total-capacitance!
  [db]
  (let [rows (->> ["VCC" "VBUS"] ;;TODO: make this configurable
                  (keep (fn [net]
                          (when-let [c (net-capacitance db net)]
                            {:net net :total-uF (format "%.2f" (* c 1e6))}))))]

    (when (seq rows)
      (print "total capacitance:")
      (clojure.pprint/print-table rows)

      (doseq [{:keys [net total-uF]} rows]
        (assert (< (Double/parseDouble total-uF) 10) (str "Net " net " exceeds USB spec 10uF capacitance"))))))

(It’s easy to accidentally exceed this limit if you keep incrementally adding ICs and their recommended bypass capacitors.)

Since KiCad allows one to add arbitrary key/value pairs to schematic instances, it’s easy to check and print other data as well. For example, I record the i2c address(es) of each chip this way (note i2c and max_mA fields in property inspector on left):

Within the schematic text labels I reference using the KiCad text variable format. (E.g., the above Addr: 0x49 label is defined as Addr: ${U1:i2c}.)

The analysis script throws an error if an address maps to multiple chips:

(defn i2c-addresses
  [db]
  (d/q '{:find [?hex-addr (distinct ?ref)]
         :where [[?instance :instance/ref ?ref]
                 [?instance :instance/attributes ?attribute]
                 [?attribute :attribute/name "i2c"]
                 [?attribute :attribute/value ?addrs]
                 [(clojure.core/identity ?addrs) [?addr ...]]
                 [(clojure.core/format "0x%x" ?addr) ?hex-addr]]}
       db))


(defn check-i2c!
  [db]
  (let [refs-by-addr (i2c-addresses db)]
    (when (seq refs-by-addr)
      (print "i2c addresses")
      (clojure.pprint/print-table (sort-by :addr (for [[addr refs] refs-by-addr]
                                                   {:addr addr :refs (clojure.string/join " "  (sort refs))})))

      (doseq [[addr refs] refs-by-addr
              :when (< 1 (count refs))]
        (throw (ex-info (str "Addr " addr " matches multiple refs: " refs)
                        {:addr addr :refs refs})))

      (println ""))))

This also prints out a helpful table of everything on the bus every time I build the project:

| :addr | :refs |
|-------+-------|
|  0x20 |    U3 |
|  0x49 |    U1 |
|  0x60 |    U2 |
|  0x61 |    U2 |
|  0x62 |    U2 |
|  0x63 |    U2 |
|  0x64 |    U2 |
|  0x65 |    U2 |
|  0x66 |    U2 |
|  0x67 |    U2 |
|  0x68 |    U2 |
|  0x69 |    U2 |
|  0x6a |    U2 |
|  0x6b |    U2 |
|  0x6c |    U2 |
|  0x6d |    U2 |
|  0x6e |    U2 |

(In this example, U2 is an LED driver and exposes each channel on its own i2c address, so its i2c property is specified as 0x60..0x6F.)

Check/build scripts

Speaking of builds, most of the functionality described above is packaged up as a script, so you just run kkh build in a folder and it’ll create an output directory next to every *.kicad_pro it finds in any subfolder. The outputs are named with the date, git revision, and also indicate whether there are unstaged changes in the repository working tree:

2026-07-07-73c5c1
├── bom.csv
├── designators.csv
├── netlist.ipc
├── positions.csv
├── receiver-gerbers-2026-07-07-73c5c1.zip
├── receiver.full.step
├── receiver.simplified.step
└── schematics
    ├── receiver-pcb-back.pdf
    ├── receiver-pcb-front.pdf
    └── receiver-sch.pdf

So one command runs DRC, ERC, and custom analysis checks and then builds all of the output files required to place a JLCPCB assembly order.

The build script also exposes the version string as a KiCad variable, so if you add ${KKH_VERSION_DATE} to your PCB silkscreen, the actual version will appear in the output Gerber files.

I hope by making a proper build script I will never again relive the shame of forgetting to run ERC and having a PCB manufactured where I literally forgot to connect some IC pins entirely


Caliper updates

About two years ago I had my first foray into digital signal processing and made some electronic calipers.

I haven’t touched the project since I published that post, but a few folks recently asked me about it, and since I had some spare LLM credits I figured I’d spend five minutes sending off the little dude at the problem.

And I literally mean five minutes — these are very rough, rambly dictated prompts that I didn’t even bother to edit. I’m sharing here to remind everyone (especially myself!) that not everything needs to be A Big New Project and sometimes you can have great success from a quick, low-effort attempt.

I spawned a Claude session in the project repo, dictated the following prompt, and went to brush my teeth:

This is a project that I worked on a while ago and the accuracy that came out was alright but I’m wondering if there’s anything I can do to improve the accuracy purely from a computational way rather than making new hardware. Be sure to read the blog post mentioned at the top of the read me to get a background

After I brushed my teeth, it had come up with a few plausible sounding ideas, so I replied with the following and went to sleep:

I’d like you to set this up and come up with a few different ideas. These are pretty good ones and what I want you to do is Wire them up so that I can test them out Individually as a series of experiments with a given protocol and I want you to set everything up as much as possible So I can do it Quickly on my end without having to get in and change the code or anything like that so what you can do is you can make a branch and then Have maybe different entry points or something for each of these different improvements and then Write like an overall program or something like that Which I can just run to test through it and then make it interactive I guess so that I can You know start it up in the hardware Let it sit still for a certain amount of time Move it a fixed amount and back and then You know or some protocol like that and then I’ll tell you what I’m done and then we can just run through that program To test out all of the different ideas and potentially combinations of ideas in like a 10 or 15 minute setting and then We’ll figure out from that which ones are working most effectively

Again, I’m really not trying that hard here.

I woke up the next morning and spent about 20 minutes setting up the caliper PCBs and then running the programs it generated to collect new measurement data. It tried the following improvement hypotheses (LLM-generated text):

knob idea it tests
window size longer coherent integration (noise ∝ 1/√N), incl. one 50 Hz line period
mean_sub remove DC so ADC offset drift can’t leak into the phase
hann suppress spectral leakage from non-integer-cycle windows
smoothing averaging in I/Q (phasor) space instead of phase space; EMA vs block
hysteresis the current 0.1 rad dead-band vs smaller vs none

and based on the initial results and a bit more chatting, the agent proposed an improvement that is, of course, completely obvious in hindsight. My initial parameter sweep (two years ago) used a fixed 2kHz spacing. However, the actual signal and sampling timings generated by the microcontroller are driven by integer divisions of a fixed clock frequency — so most of these sampled timings don’t “line up” nicely in terms of an integer number of signal periods.

Thus, there’s always a bit of DC bias in the signal, which causes undesired noise.

With this change, the LLM-generated experimental code reports a noise floor of around 50um, which is about 10x better than what I was getting before. I’m currently on holiday away from my lab, but I expect it’ll take 30 minutes once I get back to code it up myself and validate.

Anyway, the main takeaways for me are:

  1. Write up your work in blog posts so that both people and LLMs can quickly get context and help you out
  2. There’s still plenty of low-hanging fruit out there to pick, and with LLMs it’s extremely cheap to just ask

Misc. stuff

Permalink

Q3 2026 Funding Survey

Greetings Clojurists!

Please take a moment to complete the Q3 2026 Funding Survey which helps inform our Q3 project awards. It is not a heavy lift - maybe 5 minutes of your time. Your input is invaluable! A link to the survey was sent to your email in the last few weeks - and just in case it made its way to spam, you can look for “We Need Your Input - Q3 2026 Funding”. The survey closes midnight PST on July 15, 2026.

Thanks as always for your support of Clojurists Together and for being a part of this awesome community.

Any questions, please email me at kdavis@clojuriststogether.org

Kathy Davis Program Manager Clojurists Together Foundation

Permalink

Destructuring with computed keys

In Clojure you can destructure a map using an arbitrary expression as the key. For example, here kw is a local binding.

(let [kw :key
      {a kw} {:key 1}]
  a)
;=> 1

Usually this syntax is demonstrated as {sym0 :kw0 sym1 :kw1 ...}, which doesn’t reveal that the keywords are actually in expression position, or an evaluation context. The reason why this more recognizable syntax works is because keyword literals are self-evaluating.

(let [{a :key} {:key 1}]
  a)
;=> 1

The basic rule for expanding these expressions is:

(let [{binding expression} map])
=>
(let [binding (get map expression)])

So that code is equivalent to:

(let [a (get {:key 1} :key)]
  a)

Symbols are not self-evaluating syntax in Clojure, so they must be quoted:

(let [{a 'key} {'key 1}]
  a)
;=> 1

Applying the rule makes the need more obvious:

(let [a (get {'key 1} 'key)]
  a)

Destructuring is pleasingly compositional. This ability to drop down to computed keys makes destructuring available in many more situations than if all keys were required to be statically declared. I found a few examples in my own code and other libraries where this flexibility has been useful.

An example that dereferences the var u/expr-type to compute the key:

(let [{cargs :args
       res u/expr-type} (-> expr-noinline
                            ana2/unmark-top-level
                            ana2/unmark-eval-top-level
                            (check-expr expected opts))]

Another example that uses three class literals as computed keys:

(let [r (reflect-u/reflect cls)
      {methods clojure.reflect.Method
       fields clojure.reflect.Field
       ctors clojure.reflect.Constructor
       :as members}
      (group-by
        class
        (filter (fn [{:keys [name] :as m}] 
                  (if constructor-call
                    (instance? clojure.reflect.Constructor m)
                    (= m-or-f name)))
                (:members r)))]

A snippet of code that destructures nested maps using a mix of keywords, quoted symbols and computed vectors-of-locals as keys. Notice that the vectors are in binding position sometimes to introduce names, then in expression position to perform lookups.


(let [...
      {{[x1 x2] 'x} :fv
       {[y1] 'y [z1 z2] 'z} :idx} remap
      {{{[y1_x1 y1_x2] 'x
         [y1_y1 y1_y2 y1_y3 y1_y4] 'y} [y1]
        {[z1_x1] 'x
         [z1_y1] 'y} [y1 z1]
        {[z2_x1] 'x
         [z2_y1] 'y} [y1 z2]} :idx-context} remap]
  (is (= {:fv {'x [x1 x2]}
          :idx {'y [y1]
                'z [z1 z2]}
          :idx-context {[y1] {'x [y1_x1 y1_x2]
                              'y [y1_y1 y1_y2 y1_y3 y1_y4]}
                        [y1 z1] {'x [z1_x1]
                                 'y [z1_y1]}
                        [y1 z2] {'x [z2_x1]
                                 'y [z2_y1]}}}
         remap)))

You’ve probably seen code like this that destructures booleans from a group-by:

(let [...
      {anns false inits true} (group-by list? normalised-bindings)]

This Malli snippet nests :keys destructuring under a local binding key, method:


(-value-transformer [_ schema method options]
  (reduce
   (fn [acc {{:keys [name qname default transformers]} method}]

And this example elegantly destructures a nested map using keywords and locals, supporting the common pattern of updating a nested value in an atom then destructuring the swapped-in value’s relevant parts.


(defn remove-stale-cache-entries
  [nsym ns-form-str sforms slurped opts]
  {:pre [(simple-symbol? nsym)]}
  (when ns-form-str
    (let [{{{forms-cache ns-form-str} nsym} ::check-form-cache}
          (env/swap-checker!
            (env/checker opts)
            update-in
            [::check-form-cache nsym]
            (fn [m]
              (some-> m
                      (select-keys [ns-form-str])
                      not-empty
                      (update ns-form-str select-keys sforms))))]

Permalink

biff.graph: structure your data model as a queryable graph

The next Biff 2 library is ready to go: biff.graph

biff.graph makes your codebase more understandable/maintainable by helping you split up your code for reading/deriving data into small independent chunks. "Data-oriented dependency injection" is a term I've used to describe the approach.

biff.graph is a lightweight clone/variant of Pathom. I'm a huge fan of Pathom and wanted to include it in Biff by default, however it is a bit of a heavy abstraction and I was concerned if the benefits would be worth the learning effort for people working on small projects.

So biff.graph is an attempt to provide something similar to Pathom in as few lines of code as possible (~600 to be specific, with ~200 for the query engine). You still have to learn the same conceptual model, but learning what exactly it's doing under the hood should be easier. The tradeoff is that biff.graph has less functionality--most significantly there's no query planner, so biff.graph isn't as intelligent about how it executes your queries as Pathom is.

Permalink

libl.in, & Injee, Thanks To Clojure Community. Updates in Clojure Book.

Notes

Permalink

Clojure 1.13.0-alpha3

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

:select directive in map destructuring

The :select directive binds a name to a subset of the map being destructured containing only the keys mentioned (anywhere) in the binding form.

  • CLJ-2964 :select directive in map destructuring

  • CLJ-2963 Update specs for :select in destructuring

Other changes since Clojure 1.13.0-alpha2

  • RT.map, and thus reader, tracks new PAM thresholds

  • CLJ-1789 select-keys - improve performance (transients, etc)

  • CLJ-2958 ILookup on sets

  • CLJ-2902 pprint - prints arbitary objects in unreadable form

  • CLJ-2801 TaggedLiteral - doesn’t define print-dup

  • CLJ-2269 definterface - does not resolve parameter type hints

  • CLJ-2781 clojure.test/report - docstring has broken references

  • CLJ-2929 zipper - docstring typo

  • CLJ-2901 bytes, shorts, chars - docstring typos

  • CLJ-2811 scalb - docstring links to the documentation for nextDown

  • CLJ-2809 clojure.math/floor - docstring has line that should be on ceil docstring

Try it out

Update your deps.edn :deps with:

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

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

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

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.