clojure-dev 2026-07-03

couple nits on the PAM threshold change: 1. it's supposed to be only for maps of keywords, but the impl is not that strict: it only validates that keys 8..32 must be keywords, 0..7 can be anything

user=> (class (assoc {"0" 0 "1" 1 "2" 2 "3" 3 "4" 4 "5" 5 "6" 6 "7" 7} "8" 8))
clojure.lang.PersistentHashMap
user=> (class (assoc {"0" 0 "1" 1 "2" 2 "3" 3 "4" 4 "5" 5 "6" 6 "7" 7} :8 8))
clojure.lang.PersistentArrayMap
2. the reader doesn't respect the new threshold, so literals > 8 are still PHMs regardless
user=> (class {:0 0 :1 1 :2 2 :3 3 :4 4 :5 5 :6 6 :7 7 :8 8})
clojure.lang.PersistentHashMap

1. Is not supposed to be only maps of keywords, that's an overstatement in the release note. It's behaving as designed, which is it's allowed to grow beyond 8 as long as all beyond are keywords. N.B. this is an implementation optimization, the details of which should never be relied upon!

👍 1
👍🏻 1

2. Reader and other alignments are TODOs.

We have Alpha 2 in production for one of our apps. No noticeable changes in behavior at the JVM level, positive or negative. It's not our highest traffic app—we'll start that rollout on Monday.

(defn- service-start
  [{:keys! [worldsingles-application members actioner database]
    {:keys! [redis-pubsub] pubsub :redis-pubsub} :worldsingles-application
    :as service}]
  ...)
Definitely a lot nicer than the destructuring + checking we were doing before! Unfortunately, clj-kondo/clojure-lsp don't understand the syntax yet so now I have red squiggles under uses of those bindings but I'm sure that'll happen soon 🙂 /cc @borkdude

@seancorfield is keys! [& binding meant to signal you're not going to use the binding in choose-package, but somewhere downstream?

@felipecortezfi Yes, it allows for a single, coherent check at the top of a call tree, that says "these keys must be present", but allows clj-kondo (for example) to know that these are not unused bindings.

💡 3

Previously, we had explicit checks in the code, asserting the presence of such keys (using our own assert-ex macro that throws IllegalArgumentException rather than AssertionError).

ha, I remember you mentioning not wanting keys! to be backed by AssertionError during the dev call

And the non-binding form is also a useful improvement:

(defn choose-package
  "Renders the html for choosing a package"
  [{:keys! [& cookie-alternative promo-action] :as m}]
 ...)

yes, I want this :)

A quick pass of our code at work: replaced dozens of destructure + key check code fragments with 30 uses of :keys! and a handful of & for non-binding key specifications. I suspect I could have been much more aggressive about that, and I'll probably default to :keys! going forward, and drop back to :keys for (what I hope are) the fewer cases where keys are optional. The non-binding & form is proving much more useful than I expected! We had quite a few places where were asserting the presence of keys in high-level functions, but not using their values. There were also several places where we genuinely had unused key bindings that were provided for documentation purposes, and where the keys were a mixed of required and optional. Thank you, core team!

👍 4
👍🏼 1
🚀 10

@seancorfield initial clj-kondo support on master. @richhickey found an edge case I think:

user=> (let [{:keys! [& &]} {:& 1}])
nil
user=> (let [{:keys! [& &]} {:&x 1}])
nil

in the sense that the spec may want to catch that one? not sure.

@borkdude we will preclude that, thanks