Welcome! Please see the About page for a little more info on how this works.

0 votes
ago in Sequences by
edited ago by

Hi,

I found out something weird in some old code, where I wanted to pass the first map in a sequence and by mistake the code was working while actually passing the sequence itself!

After digging to make sense of this, I found out it has to do with the {:as opts} destructuring.

Here's some code:

;; even if something is assigned sequential vs associative
;; the `:as` is transparent
(let [[:as opts] {1 2 3 4}]
  opts)
#_=> {1 2, 3 4}
(let [{:as opts} [1 2 3 4]]
  opts)
#_=> [1 2 3 4]
;; however with a seq it's returned as a map
(let [{:as opts} (seq [1 2 3 4])]
  opts)
#_=> {1 2, 3 4}

;; and it only gets weirder!
(let [{:as opts} (seq [{:k :v}])]
  opts)
#_=> {:k :v}
(let [{:as opts} (seq [{:k1 :v1} {:k2 :v2}])]
  opts)
#_=> {{:k1 :v1} {:k2 :v2}}
(let [{:as opts} (seq [{:k1 :v1} {:k2 :v2} {:k3 :v3}])]
  opts)
#_=> {{:k1 :v1} {:k2 :v2}, :k3 :v3}
(let [{:as opts} (seq [{:k1 :v1} {:k2 :v2} :not-a-map])]
  opts)
;; java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Keyword
(let [{:as opts} (seq [1 2 3])]
  opts)
;; java.lang.IllegalArgumentException: Don't know how to create ISeq from: java.lang.Long

In my case I didn't realize my code was wrong because {:as opts} {:k :v} is the same as {:as opts} (seq [{:k :v}]).

To me this is behaviour I would expect closer to & {:as opts} rather than just plain map destructuring {:as opts}. Maybe both behaviours should be separated?

I think this should be tackled either by
1. make :as transparent regardless of what comes in to always bind
2. throw an error earlier when it's not of the expected destructuring
3. Document this very unexpected edge case in https://clojure.org/reference/special_forms#associative-destructuring
4. Separate the keyword arguments use case from plain map destructuring? (Assuming both are now mixed together)

1 Answer

0 votes
ago by

A lot of this can be broadly considered "garbage-in, garbage-out". {:as opts} is map destructuring and so if you give it a non-map, you'll get undefined behavior.

In particular, try these expressions on Clojure 1.10 and you'll see that many of them throw exceptions.

Clojure 1.11 introduced a new meaning for some key/value destructuring -- see https://clojure.org/news/2021/03/18/apis-serving-people-and-programs -- which changed the undefined behavior, in order to support new use cases.

Bottom line: pass a non-map to map destructuring and you are in undefined behavior territory. It just happened to change between 1.10 and 1.11 so that some cases that threw exceptions now just produce "junk" instead.

ago by
edited ago by
Yeah, I understand that’s the situation, undefined behaviour territory. But on my particular case Clojure didn’t help to notice that there was garbage in at all, as the code was actually working, even if I forgot to call `first` on the seq.

Anyway, I think if changing the code to make it defined behaviour is not probable, it would be nice to add a line to the documentation that in this destructuring garbage-in, garbage-out.

However to me it seems reasonable also the choice of making :as always assign to the value regardless, or make sure that a vector destructuring always receives something sequential, and a map destructuring always receives an associative. Both would also remove this undefined behaviour for the time being.

Anyways, not a big deal, just a sharp edge that people should be aware, maybe addressable just by writing a sentence in the docs.

Bringing it up because this seems behaviour I would expect from `& {:as opts}` rather than `{:as opts}` directly.
...