A clj snippet #4

Today's snippet is extracted from cider/orchard. A library that provides utilities for clojure tooling. It actually grew a lot longer than I intended it to be. We start off with a utility function that will come in handy later on.

(defn merge-meta
  "Non-throwing version of (vary-meta obj merge metamap-1 metamap-2 ...).
  Like `vary-meta`, this only applies to immutable objects. For
  instance, this function does nothing on atoms, because the metadata
  of an `atom` is part of the atom itself and can only be changed
  destructively."
  {:style/indent 1}
  [obj & metamaps]
  (try
    (apply vary-meta obj merge metamaps)
    (catch Exception e obj)))
0.1s
Clojure
'user/merge-meta

meta-merge is useful if you don't know (and don't always want to check) if the object you are working with supports metadata. It's sort of the vary-meta for lazy buggers like myself. Now comes the main hero of today's post.

(require '[clojure.walk :as walk])
(defn macroexpand-all
  "Like `clojure.walk/macroexpand-all`, but preserves and macroexpands
  metadata. Also store the original form (unexpanded and stripped of
  metadata) in the metadata of the expanded form under original-key."
  [form & [original-key]]
  (let [md (meta form)
        expanded (walk/walk #(macroexpand-all % original-key)
                            identity
                            (if (seq? form)
                              ;; Without this, `macroexpand-all`
                              ;; throws if called on `defrecords`.
                              (try (macroexpand form)
                                   (catch ClassNotFoundException e form))
                              form))]
    (if md
      ;; Macroexpand the metadata too, because sometimes metadata
      ;; contains, for example, functions. This is the case for
      ;; deftest forms.
      (merge-meta expanded
                 (macroexpand-all md)
                 (when original-key
                   ;; We have to quote this, or it will get evaluated by
                   ;; Clojure (even though it's inside meta).
                   {original-key (list 'quote (with-meta form nil))}))
      expanded)))
0.1s
Clojure
'user/macroexpand-all

So orchard's macroexpand-all behaves just as clojure.walk's macroexpand-all except that it preserves metadata of the expanded forms. On top of that, it is able to save the non-expanded version of the forms in the metadata. Where might this be useful?

Imagine you are working on some tooling the keeps track of the source location. The forms might look quite different when macroexpanded so you probably want to attach the source location beforehand. For the purpose of this post we will consider just the source location in one form. We will use a vector of integers to locate a subform in the top-level form. The first value describes the subform to move to in the toplevel form, the second value describes the subsubform to move to in the subform and so on.

Examples speak a thousand words. Consider the form

'(defn foo [x y] (+ 1 (- x y)))
Clojure

then the expression foo would have coordinate vector [1] and the subform (- x y) would have the coordinates [3 2]. For the second example you enter the top-level form move 3 expressions forward enter that form and then move 2 expressions forward.

For those interested, this type of source location is also used in the cider debugger.

(defn walk-indexed 
  ([f form] (walk-indexed [] f form))
  ([coor f form]
   (let [map-inner (fn [forms]
                     (map-indexed #(walk-indexed (conj coor %1) f %2)
                                  forms))
         result (cond
                  ;; here we are skipping quite a bit of clojure datastructures
                  (list? form) (apply list (map-inner form))
                  (seq? form)  (doall (map-inner form))
                  (coll? form) (into (empty form) (map-inner form))
                  :else form)]
     (f coor (merge-meta result (meta form))))))
0.1s
Clojure
'user/walk-indexed

walk-indexed takes a function f and a form and recursively applies f on every subform with the coordinate vector described above as first argument and the original subform as second argument. We now use walk-indexed to attach source location to Clojure forms.

(defn add-source-location [form]
  (walk-indexed (fn [coor sub-form]
                  (merge-meta sub-form {:coor coor}))
                form))
(-> (add-source-location '(defn foo [x y] (+ x y)))
  rest rest first first meta)
0.0s
Clojure
Map {:coor: Vector(2)}

Now let's put it all together. We first create a macro form, then add the source location as described above and finally macroexpand the form.

(require '[clojure.core.match :refer [match]])
(require '[clojure.pprint :as pprint])
(def match-form '(match [1 2]
                        [y 2] y))
(def expanded-match-form
  (-> match-form
   add-source-location 
   (macroexpand-all :original-form)))
(pprint/pprint expanded-match-form) 
3.0s
Clojure
nil

You can find the coordinate vectors attached as metadata to expressions that survived the macroexpansion in the pretty printed result.

Note that I modified macroexpand-all slightly, so as to make it non-dependent on other internal functions of cider/orchard. If you want to make sure that code works in all circumstances, please use the one provided by the library.

{:deps {compliment {:mvn/version "0.3.10"}
        org.clojure/core.match {:mvn/version "1.0.0"}}}
deps.edn
Extensible Data Notation
Runtimes (1)