clojure-dev 2026-08-04

It's possible to run into a Duplicate key error when describing a map using the new Clojure 1.13 syntax:

(let [{:select book
       {:keys! [& :name]} :author
       {:keys! [& :name]} :publisher}
      {:title "Title"
       :author {:name "Author"}
       :publisher {:name "Publisher"}}]
  book)
Syntax error reading source at (REPL:10:37).
Duplicate key: {:keys! [& :name]}
One way to avoid that is to add :as bindings (possibly unused if the intent is only to describe a structure):
(let [{:select book
       {:keys! [& :name] :as _author} :author
       {:keys! [& :name] :as _publisher} :publisher}
      {:title "Title"
       :author {:name "Author"}
       :publisher {:name "Publisher"}}]
  book)
{:publisher {:name "Publisher"}, :author {:name "Author"}}
It was always possible, before Clojure 1.13, to write similar code with bindings in :keys, but there was no point in doing so, because the bindings would just shadow each other. When describing a structure, this seems much more common. What's the recommended way of dealing with that?

This feels like a bug to me, but I'll be interested to hear what @richhickey says. Introducing unused bindings as a workaround seems like it is surfacing an implementation detail here?

I agree that introducing unused bindings doesn't feel ideal, but it seems like we don't have many good options here, since the destructuring syntax is itself a map. I can think of three: 1) workarounds like :as _foo (not intuitive, especially if such syntax is going to be used somewhere else, like Specs), 2) using qualified keys (restrictive, not always possible), and 3) changing the way of stating nested keys (a big change from what we have now, needs thinking about backward compatibility). Speaking of the last option, I can imagine something like this:

{:select book,
 :keys! [id],
 :keys [title
        &
        :release-date
        {:author {:keys [& :name], :or {:name "Author name was missing"}}
         ;; publisher is optional, but if present - :name is required
         :publisher {:keys! [& :name] :as publisher}
         ;; sequential destructuring works here as well
         :tags [main-tag secondary-tag]}],
 :or {:author {:name "Author was missing"},
      :publisher {:name "Publisher was missing"}}}
The way to think about what comes after the &: it's all literal keys of the current level, either mentioned in plain form or expanded in a map, where the value is an ordinary destructuring form - associative or sequential. Of course it's necessary to support the usual {{:keys [foo]} :nested} as well, which creates two ways of speaking about nesting. I don't have a strong opinion on whether that's something that can't be accepted, but at least it's possible to keep things backward compatible. I haven't thought all of this through, but it seems like this (imaginary) syntax works for most cases (including the new stuff like :select, :defaults, :all etc). And it's pretty readable, at least to me. That said, this is all speculation on my part β€” I'd rather wait for Rich's take on it.

i don't think this is a bug because the map is a data literal. if it's quoted, how would you edit it otherwise? {:a 1 :a 1} isn't allowed, why would this be allowed?

πŸ‘ 2
πŸ‘πŸ» 1

Yeah, I see this more as a restriction of the current syntax than a bug.

Whether we call it a restriction or a bug, it is unexpected. I thought it might be due to :select, but no:

(let [{:keys [title]
       {:keys! [& :name]} :author
       {:keys! [& :name]} :publisher}
      {:title "Title"
       :author {:name "Author"}
       :publisher {:name "Publisher"}}]
  title)
;; throws same error
I'd argue this is a perfectly valid thing to "say".

πŸ‘ 1

> i don't think this is a bug because the map is a data literal. if it's quoted, how would you edit it otherwise? {:a 1 :a 1} isn't allowed, why would this be allowed? Ah... yes... I assumed the Duplicate key was on :name but it's on the entire "key": {:keys! [& :name]} So, yes, it's a reader error because a map literal has two identical keysβ€”it's not an error coming from the new-in-1.13 features. So, yeah, it is an existing syntax/reader restriction.

πŸ‘ 1
🀯 1
☝️ 1

But how, there are two different maps containing :keys!?

I didn't notice the problem was in the map literal itself either until Sean called it out directly.

The keys in this literal are identical:

{:keys! [& :name]} :author
       {:keys! [& :name]} :publisher

πŸ‘ 1

Ah, ok, I see it now πŸ™‚ Thanks.

Yeah, took me a while. If Maksim had posted the whole error, I might have twigged sooner...

The full error was there, but I should have pointed out that the duplicate key is {:keys! [& :name]} itself β€” it really is easy to miss πŸ™‚

πŸ‘πŸ» 1

Hey, just checking. If I want to run a post-process application (i.e., clojure-lsp clean-ns), would I craft something in post-process-fn to shell out to the terminal to invoke the client?