macrosclojuremetadatadefn

Clojure macro to process multiple function metadata


In Clojure, how do I make a library macro which processes supplied functions metadata and return some result? Amount of functions is unlimited and they should be passed without being boxed into a sequence ((my-macro fn1 fn2) instead of (my-macro [fn1 fn2]))

Say, we expect function vars having :foo keys in meta and the macro concatenates their values. The following snippet should work in REPL (considering my-macro is in the namespace):

user=> (defn my-func-1 {:foo "bar"} [])
(defn my-func-1 {:foo "bar"} [])
#'user/my-func-1
user=> (defn my-func-2 {:foo "baz"} [])
(defn my-func-2 {:foo "baz"} [])
#'user/my-func-2
user=> (my-macro my-func-1 my-func2)
(my-macro my-func-1 my-func2)
"barbaz"

I tried several approaches but was only able to process single function so far.

Thanks!


Solution

  • Try this:

    (defmacro my-macro [& fns]
      `(clojure.string/join (list ~@(map (fn [x] `(:foo (meta (var ~x)))) fns))))
    
    (defn ^{:foo "bar"} my-func-1 [])
    (defn ^{:foo "baz"} my-func-2 [])
    (my-macro my-func-1 my-func-2) ;; => "barbaz"
    


    How it Works

    If you expand the macro you can start to see the parts in play.

    (macroexpand '(my-macro my-func-1 my-func-2))
    
    (clojure.string/join
      (clojure.core/list (:foo (clojure.core/meta (var my-func-1)))
                         (:foo (clojure.core/meta (var my-func-2)))))
    


    (var my-func-1)
    

    Function metadata is stored on the var, so using (meta my-func-1) is not sufficient. But, var is a special form and does not compose like a normal function.

    (fn [x] `(:foo (meta (var ~x))))
    

    This anonymous function exists inside an escaped form, so it is processed inside the macro to produce the output forms. Internally it will create a the (:foo (meta (var my-func-1))) form by first backtick escaping the outer form to declare it a literal, and not evaluated, list and then unescaping the x var with a tilde to output the value instead of the symbol.

    `(clojure.string/join (list ~@(map (fn [x] `(:foo (meta (var ~x))))
                                       fns)))
    

    This entire form is backtick escaped, so it will be returned literally. But, I still need to evaluate the map function generating the (:foo (meta (var my-func-1))) form. In this case I have unescaped, and spliced (@) the result of, the map form directly. This first evaluates the map function and returns a list of generated forms, and then takes the contents of that list and splices it into the parent list.

    (defmacro test1 [x] `(~x))
    (defmacro test2 [x] `(~@x))
    
    (macroexpand '(test1 (1 2 3))) ;; => ((1 2 3))
    (macroexpand '(test2 (1 2 3))) ;; => (1 2 3)
    

    You could also split out the map function in a let statement before hand for slightly more readability.

    (defmacro my-macro [& fns]
      (let [metacalls (map (fn [x] `(:foo (meta (var ~x)))) fns)]
        `(clojure.string/join (list ~@metacalls))))