clojure-site icon indicating copy to clipboard operation
clojure-site copied to clipboard

Document destructuring of singleton map sequences

Open lassemaatta opened this issue 1 year ago • 2 comments

Since clojure 1.11 (as per CLJ-2603), destructuring a sequence containing a single map can directly bind to the map contents. An example of this can be seen here. It might be a good idea to mention this in the destructuring guide as this behaviour might be surprising to some (at least it was for me).

lassemaatta avatar Feb 15 '24 08:02 lassemaatta

here are some relevant threads on clojurian slack highlighting the confusion:

https://clojurians.slack.com/archives/C03S1KBA2/p1668533720466509

https://clojurians.slack.com/archives/C03S1KBA2/p1707982404402549

https://clojurians.slack.com/archives/C03S1KBA2/p1714580544135239

https://clojurians.slack.com/archives/C03S1KBA2/p1715935559111129

NoahTheDuke avatar May 17 '24 13:05 NoahTheDuke

I just tripped over this issue today too.

i thought it's a bug, but @puredanger said it's intended behaviour: https://ask.clojure.org/index.php/12374/map-destructuring-works-on-singleton-lists?show=12380#c12380

so the Destructuring in Clojure guide should definitely mention these peculiarities!

it provides this example:

(defn configure [val & {:keys [debug verbose]
                        :or {debug false, verbose false}}]
  (println "val =" val " debug =" debug " verbose =" verbose))

so it should mention how it behaves the same way, as a function defined as:

(defn configure [val {:keys [debug verbose]
                      :or {debug false, verbose false}}]
  (println "val =" val " debug =" debug " verbose =" verbose))

when called as (configure 12 (list :debug true)) or (configure 12 (list {:debug true})), but not equivalent, when called as (configure 12 (vector :debug true)) or (configure 12 (vector {:debug true})).

im not sure what reasoning can be given for this behaviour though...

Pondering

given this situation:

(let [m {:x 1}
        {itself :x} m
        {from-list :x} (list m)
        {from-list-of-2 :x} (list m m)
        {from-vector :x} (vector m)]
    [(= itself from-list)
     (nil? from-list-of-2)
     (not= from-list from-vector)])

i'd say (= itself from-list) is surprising.

(nil? from-list-of-2) looks like magic.

(not= from-list from-vector) can easily result in hard to understand errors, since all u need is to realize some lazy sequence, by spilling it into a vector with vec or (into [] ,,,), instead of doall and BAMM your program might blow up at a distance.

in my specific situation the behaviour was even more magical, because i had function with an optional argument after the map destructuring one: (fn [arg1 {:as arg2 k :some/k} & [arg3]] ,,,), yet it behaved like (fn [arg1 & {:as arg2 k :some/k}] ,,,), which is apparently the same as (fn [arg1 {:as arg2 k :some/k}] ,,,), when called with a map or a list of a single map.

onetom avatar May 17 '24 13:05 onetom

Some extra details from me hitting it and getting some very helpful replies in Clojurians:

It is not limited to trailing maps:

;; Clojure 1.12.0
(defn get-k-destruct [{:keys [k]} _x] k)
(defn get-k-kw [m _x] (:k m))
(def one-el [{:k 1}])
(def two-el [{:k 1} {:k 2}])

;; on vec
(get-k-destruct one-el 3) ;; => nil
(get-k-destruct two-el 3) ;; => nil
(get-k-kw one-el 3)       ;; => nil
(get-k-kw two-el 3)       ;; => nil

;; on seq
(get-k-destruct (seq one-el) 3) ;; => 1, bugged?
(get-k-destruct (seq two-el) 3) ;; => nil
(get-k-kw (seq one-el) 3)       ;; => nil
(get-k-kw (seq two-el) 3)       ;; => nil

It is undefined behaviour:

Alex Miller (Clojure team) that associative destructuring of a sequential thing does anything outside the case of & args is not anything that has ever been documented. what do you expect it to even mean to do associative destructuring on a sequential collection of one element?

It is tested for in core: https://github.com/clojure/clojure/blob/da1c748123c80fa3e82e24fc8e24a950a3ebccd9/test/clojure/test_clojure/data_structures.clj#L1334-L1338

(deftest singleton-map-in-destructure-context
  (let [sample-map {:a 1 :b 2}
        {:keys [a] :as m1} (list sample-map)]
    (is (= m1 sample-map))
    (is (= a 1))))

filipesilva avatar Jul 01 '25 09:07 filipesilva