From Prompt to Product: Spec-Driven Development with Agent Workflows
Spec-Driven Development at OTTO: How to build a frontend without vibe coding and without manually written code đ Read more now!
Spec-Driven Development at OTTO: How to build a frontend without vibe coding and without manually written code đ Read more now!
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).
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!
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.
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?
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.

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.
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.
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.
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?
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.
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.
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.
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.
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.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.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.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.deps.edn now, so you can run the bridge straight from a git
coordinate without installing anything.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.
A few scenarios worth trying:
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.
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!
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. â©
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. â©
Clojure 1.13.0-alpha4 is now available! Find download and usage information on the Downloads page.
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-2870 Exception phase during top-level eval is miscategorized
A provisional continuous difficulty model before item calibrationâand the simulation gate it failed.
Rebuilding three earlier Bayesian simulations with Kindly, Scittle, Reagent, and accessible SVG.
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.
Hi friends,
This newsletter weâre streamlining the process of creating circuit boards and revisiting the DIY electronic calipers project from two years ago.
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:
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:
panel_Edge.Cuts.dxf imports a group IDâd âpanelâ to the Edge Cuts layer)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 ÂŻ\_(ă)_/ÂŻ.
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:

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.
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:
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.)
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âŠ
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:
Accidental anonymity âSorry to say it but if you need your work to be polished and beyond reproach, thatâs a determination and character problem, not a skill problem.â
High Performance Motor Control From the Ground Up || Field Oriented Control (FOC)
Bio-Rad Laboratories HeliosÂź Gene Gun sounds like a joke, but itâs absolutely real and I desperately want to use it.
I didnât build a Full Body Ultrasound⊠but I know the people that did
AgentsView is a very nice local web UI from my friend Wes McKinney that makes it easy to review transcripts and token usage across Claude Code, Codex, etc. I used it to quote some of my LLM sessions above.
Code:
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
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))))]
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.
Notes
Clojure 1.13.0-alpha3 is now available! Find download and usage information on the Downloads page.
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