Clojure: destructive macros


In this post we'll cover writing a macro that supports destructuring and does something with the bindings. More specifically we will write a macro that makes building maps from arbitrary data less verbose.

Consider the following code:

(let [{{:keys [year]}    :meta
       [{:keys [titles]}] :people}
      {:meta    {:year "1249"
                 :region "Estiria"}
       :people [{:titles ["Duke" "Silver Tongue"]
                 :name "John"}
                {:titles ["Queen" "Sun"]
                 :name "Jill"}
                {:titles ["Jarl" "Broken"]
                 :name "Vigo"}]}]
  {:year year :titles titles})

=>
{:year "1249", :titles ["Duke" "Silver Tongue"]}

This is fine. But, what we really want is to create a map with the keys that match the symbols we bind when destructuring. This will do away with the ceremony of building the map by hand {:year year :titles titles}.

defmacro and destructure to the rescue.

(destructure
 '[{{:keys [year]}    :meta
    [{:keys [titles]}] :people}
   {:meta    {:year "1249"
              :region "Estiria"}
    :people [{:titles ["Duke" "Silver Tongue"]
              :name "John"}
             {:titles ["Queen" "Sun"]
              :name "Jill"}
             {:titles ["Jarl" "Broken"]
              :name "Vigo"}]}])

[map__41580
 {:meta {:year "1249", :region "Estiria"},
  :people
  [{:titles ["Duke" "Silver Tongue"], :name "John"}
   {:titles ["Queen" "Sun"], :name "Jill"}
   {:titles ["Jarl" "Broken"], :name "Vigo"}]}
 map__41580
 (if
  (clojure.core/seq? map__41580)
  (clojure.lang.PersistentHashMap/create (clojure.core/seq map__41580))
  map__41580)
 map__41581
 (clojure.core/get map__41580 :meta)
 map__41581
 (if
  (clojure.core/seq? map__41581)
  (clojure.lang.PersistentHashMap/create (clojure.core/seq map__41581))
  map__41581)
 year
 (clojure.core/get map__41581 :year)
 vec__41582
 (clojure.core/get map__41580 :people)
 map__41585
 (clojure.core/nth vec__41582 0 nil)
 map__41585
 (if
  (clojure.core/seq? map__41585)
  (clojure.lang.PersistentHashMap/create (clojure.core/seq map__41585))
  map__41585)
 titles
 (clojure.core/get map__41585 :titles)]

Despite the noise we can see that destructure outputs a list of bindings and values. Some of theses are bindings that we care about year and titles. The others, which have generated names like vec__41582 and map__41585, are references to the collections being destructured.

We can filter these out to get the bindings we care about for building our map.

(->> (destructure
      '[{{:keys [year]}    :meta
         [{:keys [titles]}] :people}
        {:meta    {:year "1249"
                   :region "Estiria"}
         :people [{:titles ["Duke" "Silver Tongue"]
                   :name "John"}
                  {:titles ["Queen" "Sun"]
                   :name "Jill"}
                  {:titles ["Jarl" "Broken"]
                   :name "Vigo"}]}])
     (partition 2)
     (remove (fn [[k]] (re-find #"^(vec__|map__)" (name k))))))

=>
((year   (clojure.core/get map__41611 :year))
 (titles (clojure.core/get map__41615 :titles)))

We can define a macro that uses a map and into to build a map with keys that match the bound symbols.

(defmacro let->map [bindings]
  (let [dest-bindings (destructure bindings)]
    (->> (partition 2 dest-bindings)
         (remove (fn [[k]]
                   (or (= k '_)
                       (re-find #"^(vec__|map__)" (name k)))))
         (mapv (fn [[k v]] [(keyword k) v]))
         (into {}))))

=>
Syntax error compiling at (/private/var/folders/ms/x72d2hr9487980y4_dpy7gym0000gn/T/form-init17784029688269253732.clj:1:1).
Unable to resolve symbol: map__41669 in this context

Looks like this error is caused because the compiler can't find map__41669. This is one of the bindings destructure generates. As we haven't bound map__41669 when (clojure.core/get map__41669 :year) is called it throws an error.

We can fix this by binding all the of bindings outputed by destructure.

(defmacro let->map [bindings]
  (let [dest-bindings (destructure bindings)]
    `(let ~dest-bindings
       ~(->> (partition 2 dest-bindings)
             (remove (fn [[k]]
                       (or (= k '_)
                           (re-find #"^(vec__|map__)" (name k)))))
             (mapv (fn [[k v]] [(keyword k) v]))
             (into {})))))

Let's give it another go.

(let->map [{{:keys [year]}    :meta
              [{:keys [titles]}] :people}
             {:meta    {:year "1249"
                        :region "Estiria"}
              :people [{:titles ["Duke" "Silver Tongue"]
                        :name "John"}
                       {:titles ["Queen" "Sun"]
                        :name "Jill"}
                       {:titles ["Jarl" "Broken"]
                        :name "Vigo"}]}])

=>
{:year "1249", :titles ["Duke" "Silver Tongue"]}

It works!

What about with lists?

(map #(let->map [{name :name [first-title] :titles} %])
       [{:titles ["Duke" "Silver Tongue"]
         :name "John"}
        {:titles ["Queen" "Sun"]
         :name "Jill"}
        {:titles ["Jarl" "Broken"]
         :name "Vigo"}])

=>
({:name "John", :first-title "Duke"}
 {:name "Jill", :first-title "Queen"}
 {:name "Vigo", :first-title "Jarl"})

Magic.

In this post we've seen how to use destructure to write a macro that supports destructuring and does something useful with the bindings.

That being said. I'd probably think twice before using let->map everywhere as it's implementation is somewhat fragile (filter depending on collections starting with "map" or "vec") and is unlikely to cover all edge cases. The gains are also pretty minimal.