clojure 2026-10-06

aha so merge on 1.13 now does this?

(merge [1 2] [3 4]) => [1 2 3 4] ;; used to be [1 2 [3 4]]

i think merge is defined only for maps so this is UB?

Alex Miller (Clojure team) 2026-10-06T19:54:19.166649Z

correct - this always was undefined

i feel like i've seen this discussed before

Alex Miller (Clojure team) 2026-10-06T19:54:47.202649Z

merge docstring talks explicitly about maps

good to know

"Look, my setup works for me. Just add an option to reenable spacebar heating vec merging by conj'ing."

๐Ÿ”ฅ 1
๐Ÿ˜„ 8
Alex Miller (Clojure team) 2026-10-06T19:56:00.870339Z

note that the jank clojure test suite has many tests related to this - they mostly fail now, but should just be removed imo. haven't had a chance yet to file an issue about that

I'm fully on board with this, just need to know which edge cases to support in SCI ;)

๐Ÿ˜‚ 2
Alex Miller (Clojure team) 2026-10-06T19:57:24.025899Z

the case I have found in a few places is more commonly this (which used to do something):

user=> (merge {1 2} [3 4])
Execution error (IllegalArgumentException) at user/eval3 (REPL:1).
Don't know how to create ISeq from: java.lang.Long

let it crash seems good to me

๐Ÿ‘ 1
Alex Miller (Clojure team) 2026-10-06T19:58:17.000969Z

I have filed issues on a couple projects related to this

I can sharpen clj-kondo to detect these maybe

4

Ah, finally that multi-vector concat I've always wanted ๐Ÿ˜„

(merge [1 2] [3 4] [5 6]) => [1 2 3 4 5 6]

๐Ÿ™Œ 1
๐Ÿ˜„ 3

you mean, a multi-vector concat that will break in 1.14? ;)

1

Don't ruin it, I just got it.

๐Ÿ˜„ 3

some people just like living on the edge!

or is seq of 2-vector now also allowed?

ow yeah...

ERROR in (swagger-spec-test) (/Users/runner/.gitlibs/libs/metosin/malli/6b0862135ea58ef2ac92bda9ae605994a5fb20d6/test/malli/swagger_test.cljc:334)
generates swagger for ::parameters w/ basic schema + registry
expected: (= {:definitions {"malli.swagger-test.body" {:minLength 1, :type "string"}}, :parameters [{:description "", :in "body", :name "body", :required true, :schema {:$ref "#/definitions/malli.swagger-test.body"}}]} (swagger/swagger-spec {:malli.swagger/parameters {:body (m/schema :malli.swagger-test/body {:registry registry})}}))
  actual: java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Keyword
  clojure.core/merge                        - sci/impl/namespaces.cljc:64:1
  malli.swagger/dissoc-non-root-definitions - /Users/runner/.gitlibs/libs/metosin/malli/6b0862135ea58ef2ac92bda9ae605994a5fb20d6/src/malli/swagger.cljc:192:21
  malli.swagger/dissoc-non-root-definitions - /Users/runner/.gitlibs/libs/metosin/malli/6b0862135ea58ef2ac92bda9ae605994a5fb20d6/src/malli/swagger.cljc:156:1
  malli.swagger                             - /Users/runner/.gitlibs/libs/metosin/malli/6b0862135ea58ef2ac92bda9ae605994a5fb20d6/src/malli/swagger.cljc:191:17
  clojure.core/reduce-kv                    - clojure/core.clj:6993:1
  malli.swagger                             - /Users/runner/.gitlibs/libs/metosin/malli/6b0862135ea58ef2ac92bda9ae605994a5fb20d6/src/malli/swagger.cljc:180:10
  clojure.walk/postwalk                     - clojure/walk.clj:53:1
  malli.swagger/expand-qualified-keywords   - /Users/runner/.gitlibs/libs/metosin/malli/6b0862135ea58ef2ac92bda9ae605994a5fb20d6/src/malli/swagger.cljc:177:5
  malli.swagger/expand-qualified-keywords   - /Users/runner/.gitlibs/libs/metosin/malli/6b0862135ea58ef2ac92bda9ae605994a5fb20d6/src/malli/swagger.cljc:174:1
  malli.swagger/swagger-spec                - /Users/runner/.gitlibs/libs/metosin/malli/6b0862135ea58ef2ac92bda9ae605994a5fb20d6/src/malli/swagger.cljc:203:4
  malli.swagger/swagger-spec                - /Users/runner/.gitlibs/libs/metosin/malli/6b0862135ea58ef2ac92bda9ae605994a5fb20d6/src/malli/swagger.cljc:201:4
  malli.swagger/swagger-spec                - /Users/runner/.gitlibs/libs/metosin/malli/6b0862135ea58ef2ac92bda9ae605994a5fb20d6/src/malli/swagger.cljc:199:1
  malli.swagger-test                        - /Users/runner/.gitlibs/libs/metosin/malli/6b0862135ea58ef2ac92bda9ae605994a5fb20d6/test/malli/swagger_test.cljc:375:14
  clojure.core/list                         - clojure/core.clj:16:1

and:

n (request-parameter-definition-regression-test) (/Users/runner/.gitlibs/libs/metosin/malli/6b0862135ea58ef2ac92bda9ae605994a5fb20d6/test/malli/swagger_test.cljc:533)
collects :definitions for all parameters
expected: (= expected (fix (swagger/swagger-spec {:malli.swagger/parameters {:header [:map [:h :string]], :query [:map [:q :string]], :body (m/schema :malli.swagger-test/req-body {:registry registry})}})))
  actual: java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Keyword
  clojure.core/merge                        - sci/impl/namespaces.cljc:64:1

the problem seems to be here: https://github.com/metosin/malli/blob/76603d587553c5a8d60fa64c9e631fa3addb4b2e/src/malli/swagger.cljc#L201-L202 cc @joel.kaasinen - repro:

clojure -Sdeps '{:deps {org.clojure/clojure {:mvn/version "1.13.0-alpha8"} io.github.metosin/malli {:git/sha "76603d587553c5a8d60fa64c9e631fa3addb4b2e"}}}' -M -e '
(require (quote [malli.core :as m]) (quote [malli.swagger :as swagger]))
(swagger/swagger-spec
 {::swagger/parameters
  {:body (m/schema ::body {:registry (merge (m/default-schemas) {::body [:string {:min 1}]})})}})'

thanks! I thought we were running tests on 1.13 but maybe this didn't get covered

malli fix in master, I'll have a point release out Soon(tm)

๐ŸŽ‰ 2

thanks for pinging me @borkdude, I wasn't on top of this at all

Thanks for fixing!

out in malli 0.20.3

I keep finding myself running into a particular issue when using clojure.spec and namespaced maps. I often have data structures that go through several stages of processing--annotation, normalization, aggregation. At each stage, the data shape grows additively. This may happen at any level (or all levels) of a deeply-nested data structure. I would like to write specs to define the expected shape at each stage. But I get tripped up with Naming Things. Examples in ๐Ÿงต

Alex Miller (Clojure team) 2026-10-06T20:44:47.687809Z

not sure I need an example, this is a common issue

Alex Miller (Clojure team) 2026-10-06T20:46:58.578429Z

it is a good match for the things we are adding to Clojure 1.13 and thinking about right now with respect to destructuring. the way to think about this is that the data structure has a vocabulary of attributes and those attributes hopefully have a well-known shape (you may be in a nested variant of this). the key is to not try to spec the top-level wrt s/keys req/opt. instead, apply the Clojure 1.13 :keys! and/or :missing to specify the structural constraint in destructuring

Consider a system that receives orders, looks up prices, and annotates the order with these prices

;; incoming order
{:order/id 1234
 :order/items [{:item/id 1}
               {:item/id 2}
               {:item/id 3}]}
One way to spec this would be like so:
(s/def :item/id integer?)
(s/def :order/item (s/keys :req [:item/id]))
(s/def :order/items (s/coll-of :order/item))
(s/def :order/id integer?)

(s/def :incoming/order (s/keys :req [:order/id
                                     :order/items]))
Now let's say we annotate the order with price information
;; order after looking up prices
{:order/id 1234
 :order/items [{:item/id 1
                :item/price 100}
               {:item/id 2
                :item/price 300}
               {:item/id 3
                :item/price 250}]}
You cannot separately spec this data structure (let's call it a :priced/order ) and :incoming/order, because :order/items means different things in each context. OK, so maybe the right thing is to change the name and have something like this:
{:order/id 1234
 :priced.order/items [{:item/id 1
                       :item/price 100}
                      {:item/id 2
                       :item/price 300}
                      {:item/id 3
                       :item/price 250}]}
But this is awkward too. You can't just do (s/def :priced/order (s/merge :incoming/order (s/keys :req [:priced.order/items]))) unless you want to require both keys (`:order/items` and :priced.order/items). And also, all of your code that deals with order items now has to know both names, even if it has nothing to do with prices. (Again, unless you keep both keys, i.e., you separately keep copies of the annotated and un-annotated data at different locations in the tree.)

> the key is to not try to spec the top-level wrt s/keys req/opt. instead, apply the Clojure 1.13 :keys! and/or :missing to specify the structural constraint in destructuring Hmm ... maybe I'm missing something, but for me :keys! and destructuring fill a different need than specs (which I use for things like spec.gen and OpenAPI specifications)

Alex Miller (Clojure team) 2026-10-06T20:50:23.917469Z

we will (eventually) be connecting these

๐Ÿ˜ƒ 1
Alex Miller (Clojure team) 2026-10-06T20:51:50.944119Z

I think generally if you have a key that means different things in different contexts, you should use a different name

Alex Miller (Clojure team) 2026-10-06T20:53:02.425419Z

but what you have here seems like they are the same thing but have different sub-vocabularies of attributes (in different contexts)

Alex Miller (Clojure team) 2026-10-06T20:58:35.548359Z

in general, I would say we are moving towards s/keys with :req and :opt being something you should stop doing, and instead focus on using destructuring to specify your structural constraints at specific points where you want to enforce them (with :s/keys!/:missing).

I see--thank you! that's very interesting and it's good to hear that this is something that is being thought about and worked on.

I think in my contrived example it would be easy to just say, make :item/price optional and move on. But when I've run into this in real systems, the relationships between sets of optional attributes are more complicated. Like: in stage one you have attributes A, B, and C, stage two adds D and E, and stage three adds F. And those relationships also cross levels of nesting.

I'm curious/excited to see how it gets connected in terms of how nested non map collections get represented (e.g. s/coll-of/`s/cat` within a map, like :order/items above). To me that's the missing link right now, everything else makes perfect sense. But besides that, I think the destructuring forms are good at representing stuff like your example: > in stage one you have attributes A, B, and C, stage two adds D and E, and stage three adds F. I think part of the idea is you're sometimes better off not always naming the intermediate steps and just checking in the destructuring sites.

Alex Miller (Clojure team) 2026-10-06T21:30:35.802989Z

yes, the relationships are contextual so you want to specify structural constraints at the point of use, not globally

Alex Miller (Clojure team) 2026-10-06T21:31:03.572169Z

you can nest destructuring, although I think in this case you'd have to do it with two levels of destructuring - one for orders, one for items

Chiming in with the obvious recommendation here: @enn have you seen Rich's Maybe Not talk? It touches on this exactly. And the recent In Context talk from last week (but that's not out yet). Although Alex effectively summarises things here and how the ideas have evolved.

Alex Miller (Clojure team) 2026-10-06T21:34:31.985959Z

importantly, the talk from last week is an update and change from Maybe Not (which led to spec 2, which is now dead)

Alex Miller (Clojure team) 2026-10-06T21:34:58.663479Z

hoping to get Rich's talk up soon

2
4

also if you missed it, this post from Rich was really useful for understanding his more recent updated thinking wrt the stuff in spec2/Maybe Not https://clojurians.slack.com/archives/C06E3HYPR/p1783538440018859

๐Ÿ‘ 3

That post should come with a spoiler alert for the In Context talk ๐Ÿ˜„

๐Ÿ˜† 2

Ooh, I have watched "Maybe Not" (in fact, I think that talk is what made me feel like I should be able to define attributes once and then come up with many variant combinations of those attributes in different contexts) but had not seen that post from Rich. He describes exactly what's been tripping me up: > clojure.spec follows RDF in supporting global definitions of attributes independent of containers, which can then be arbitrarily composed (e.g. via spec/keys). But spec/keys still requires a named spec for each context. That's not so terrible when maps are flat, but becomes a serious problem as things become nested. Every context might need a new set of variant s/keys specs. Thinking about it a bit more, I do start to see how destructuring addresses these problems. Very excited for the new talk to drop (and I'm sad I could not make the Conj this year).

@enn One of the things in Alpha 8 that can be very useful here is selector. This takes a destructuring map, and produces a function that will take input and produce the result of the destructuring (via :select, :all, :missing, :excess). This allows you to create structural predicates that you can use to check, in code, for the "shape" of the data, much like you might previously have tested record-like Specs, and then of course use Spec to ensure the individual, named (qualified) items conform to their Specs a la RDF, i.e., at the "information" level.

1

We have some test code that currently uses Meander for structural pattern matching, and I realized I can replace it all with a few selector calls.

The 1.13 stuff is making me rethink a lot of life choices as far as our code at work goes ๐Ÿ™‚

I am a little curious how the new destructuring approach works for recursive data shapes

True, you're not going to be to specify a recursive structure. But you can check the relevant structure at the appropriate places in your code (which, I assume, is also recursive?).

I'm probably not getting something, so I'll wait till it's all as ready as can be, but I'm still confused about data generations with selectors. Or would using Spec+selectors, as Sean outlined, necessitate using custom generators?