clojure-dev 2026-07-29

I know the new destructuring features only consider :keys(!) and :or but this still surprised me:

(let [{:keys [rebilling repeat] referralamount :amount
         :or {referralamount 0 rebilling false repeat false}
         :all data} {:amount 13}]
    [data referralamount])
;;=> [{:amount 13, :rebilling false, :repeat false} 13]
Given that it binds referralamount and :or must refer to that and not amount, it took me a while to figure out why data contained :amount rather than :referralamount.

Data structures are maps of keys->values, binding names are only names of locals, never names in data. Everywhere we are talking about the members of data structures (`:defaults, :select, ;as, ;all`) keys will be used, not binding names. And note that :or can (now) refer to the default for referralamount/:amount by either the binding name or the key. The right way to think about it is that :keys et al just let you avoid restating the key when the binding name is the same, but defaults have always been about what to do when the (corresponding_) key_ was missing. It's a default for a missing key, not a default for a binding. A binding is initialized with the value at a key, or that key's default if missing. Now there are additional situations (beyond binding) when defaults are used (to construct select/all/defaults maps), and they remain defaults for keys. TL;DR associative destructuring is being enhanced to support specifying and checking structure. This may involve structure not involved in bindings. So there's new syntax for talking about structure directly via keys, and new features that build useful structures, containing keys.

:select and :all are views of the input data, which is unrelated to any bindings, and thus only contain keys (expected to be) in the input.

Yeah, the behavior of the new features is really emphasizing that and I suspect some of us are needing to undo our (incorrect) mental model of how we thought destructuring worked 🙂 I redid that code as follows—which now feels more intuitive:

(let [{:keys [& :rebilling :repeat] referralamount :amount
         :or   {:amount 0 :rebilling false :repeat false}
         :all  data} {}]
    [data referralamount])
The code in my OP was a halfway house from the original code that used :as and then explicitly rebuilt the map with the defaulted bindings; where I ended up was dropping the referralamount binding, adding :amount to :keys and then using data as-is with a set/rename-keys to turn :amount into :referralamount. The code is much cleaner than it was and a lot less repetitive!

@richhickey How do you feel about a :rename directive to perform key renaming (for :select and :all only)? That seems to be something I need fairly often when adapting some subsystem or API. Or is that a "bridge too far"?

@seancorfield I'm wondering what benefit you're getting out of the referralamount :amount rename in your latest snippet?

That isn't where the code ended up (see my note after that snippet). It was just part of a series of incremental changes as I adopted the new features. At one point I was building a new map using referralamount and wanted the bound name to match the key name within that function, around the call to the subsystem. Renaming the key, via clojure.set/rename-keys was where I ended up—hence asking Rich if he might consider supporting :rename directly within destructuring. I realize a lot of my activity and thought processes did not translate into the code snippets I posted 🙂

Thanks for the explanation. For some reason I couldn't fully connect the snippet and the original note

I think my long-standing mental model of destructuring was broken in a few ways, so shifting to the new model which Rich so clearly outlined here (and, to be fair, several times in other threads) is taking me several steps along the way...

@seancorfield I'm reluctant to go for :rename et al. It seems to me a shift from views to construction, with many candidate features like pruning and augmentation. At that point it clearly moves from what I want, a small set of features co-aligned with a smaller set of semantics, to what I don't want, a mechanical thing with an arbitrary feature set.

Fair. Thanks for the response. I'll have a deeper think about the situations where I feel I would reach for that "missing feature" 🙂

Someone https://github.com/clj-kondo/clj-kondo/issues/2928 relies on keys /`vals` returning nil for empty maps. Is this behavior you can rely on or undefined territory?

Given the docstring, it seems like an implementation detail that deep inside RT.java, those end up calling .seq() on the key seq inside the hash map object (and, thus, you get nil from an empty map). That said, I can't imagine that changing unless some people show it produces a real problem...

custom maps can override this implementation and not return nil for an empty map

Alex Miller (Clojure team) 2026-07-29T22:34:44.605309Z

I think the docstrings say they are sequences, and empty sequences are nil

Alex Miller (Clojure team) 2026-07-29T22:35:40.692609Z

but I'm sure there is plenty of inconsistency in other seq fns that technically return seqables

empty sequences can be truthy too right, () ?

I mean "are nil" doesn't sound accurate?

Alex Miller (Clojure team) 2026-07-29T22:36:43.885279Z

if you have called seq on something, it will never be ()

Alex Miller (Clojure team) 2026-07-29T22:37:41.656359Z

() is an empty list and nil is the empty seq

Alex Miller (Clojure team) 2026-07-29T22:38:34.944229Z

but as I said, there are plenty of doc strings that are somewhat vague or inconsistent between what they say and what they do

user=> (filter nil? '())
()
I'd call that an "empty seq" as well

The question boils down to: should I change the clj-kondo spec for keys to a nilable result. I guess so?

Alex Miller (Clojure team) 2026-07-29T22:40:00.098729Z

yes, many of the seq functions really return seqables, doing so increases laziness if you don't have to force it to a seq

Alex Miller (Clojure team) 2026-07-29T22:40:16.784099Z

(although the docstrings say they return a sequence)

Alex Miller (Clojure team) 2026-07-29T22:40:55.411259Z

I think having looked at the code, that right now you could and should be able to rely on this. but if you want a stronger guarantee, you should call seq on it

👍 1