Why is it possible to pass in key value pairs to a function that destructures a map?

StackOverflow https://stackoverflow.com/questions/16602507

  •  29-05-2022
  •  | 
  •  

Вопрос

I thought I understood destructuring, but I was reading a clojure blog and this confused me. If you have a function written like:

(defn f [& {:keys [foo bar]}] 
  (println foo " " bar))

Why can you call it like this:

(f :foo 1 :bar 2)

My first thought was that my function was supposed to be called like this:

(f {:foo 1 :bar 2})
IllegalArgumentException No value supplied for key: {:foo 1, :bar 2}  clojure.lang.PersistentHashMap.createWithCheck (PersistentHashMap.java:89)

But obviously that doesn't work. I think this has something to do with the way & works. But I always thought that the thing after it is a vector and therefore you'd have to destructure anything after it like a vector.

Can someone explain to me how/why this definition works the way it does? Thanks

Это было полезно?

Решение

The & and destructuring form work sequentially:

  • The & gathers any arguments after it into a collection
  • The map destructuring form then takes the collection, makes a map out of it if required and binds the names to the keys listed in the vector.

The vector in the map destructuring form is just syntax used to build the desctructuring/binding and does not imply anything aobut the input form

The without the & in the defn the second form will work and the first will not.
With the & the first form will work and the second will not.

Другие советы

You can see what's going on under the covers by calling destructure manually. Let's start with a simpler example:

user> (destructure ['{foo :foo} {:foo 42}])
[map__26147 {:foo 42}
 map__26147 (if (clojure.core/seq? map__26147)
              (clojure.lang.PersistentHashMap/create
               (clojure.core/seq map__26147))
              map__26147)
 foo (clojure.core/get map__26147 :foo)]

This corresponds to (let [{foo :foo} {:foo 42}] ...) (as you can verify with (macroexpand-1 '(let [{foo :foo} {:foo 42}] ...)). The second line of the output is the important bit. A map binding form can work in two ways: if the value being bound is a seq, the seq will be 'poured' into a hash-map (as if by (apply hash-map the-seq). Otherwise, the value is assumed to be an associative and used directly. The seq 'pouring' feature was added in this commit.

Let's test this out:

user> (let [{foo :foo} {:foo 42}] foo)
42
user> (let [{foo :foo} (list :foo 42)] foo)
42
user> (let [{foo :foo} (apply hash-map (list :foo 42))] foo)
42

In the first case, the value is not a seq, so it's used directly. In the second case, a list is a seq, so it is 'poured' into a hash-map before being bound to {foo :foo}. The third case shows that this pouring is semantically equivalent to (apply hash-map the-seq).

Now let's look at something like your example:

user> (destructure '[[& {:keys [foo bar]}] args])
[vec__26204 args
 map__26205 (clojure.core/nthnext vec__26204 0)
 map__26205 (if (clojure.core/seq? map__26205)
              (clojure.lang.PersistentHashMap/create
               (clojure.core/seq map__26205))
              map__26205)
 bar (clojure.core/get map__26205 :bar)
 foo (clojure.core/get map__26205 :foo)]

The nthnext bit is from the & — in this case, because there are no fixed parameters before the &, we have an (nthnext vec# 0), which amounts to just converting args into a seq (if necessary). Then we have the map destructuring as above. Because the & guarantees we have a seq, the seq special case for map destructuring will always be triggered, and the args will always be 'poured' into a hash-map before being bound to the map form.

In case the relationship between this example and your original fn is not clear, consider:

user> (macroexpand-1 '(fn [& {:keys [foo bar]}]))
(fn* ([& p__26214] (clojure.core/let [{:keys [foo bar]} p__26214])))
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top