0

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!

0

1 Answer 1

2

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))))
Sign up to request clarification or add additional context in comments.

2 Comments

Just for curiosity, why are you passing an empty string to clojure.string/join?
Fixed the answer to remove it. While writing the macro I had a separator, but I forgot to remove the argument. Thanks for the comment.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.