What’s the best way to pass a function into a macro, so that it can be called at macro-expansion time?
For context, I’m trying to develop the idea posted by @plexus here. Basically it’s a macro that takes a Java class and generates Clojure functions for all the public methods. I’d like to provide the ability to filter the methods during macro expansion to reduce the number of generated functions, in cases when not all of them are needed.
I’ve distilled the problem down, and basically I want to do something like:
(defn things [_] ["abc" "bcd" "cde" "xyz"]) ;; get things to define
(defn thing-name [thing] (symbol thing)) ;; given a thing, what should the name be?
(defn thing-body [thing] thing) ;; given a thing, what should the definition be?
(defmacro defthings [klazz filter-fn]
`(do ~@(->> (things klazz) ;; get the list of things
(filter (resolve filter-fn)) ;; only keep the ones we actually need
(map (fn [thing] ;; generate definitions for them
`(def ~(thing-name thing) ~(thing-body thing)))))))
This works when filter-fn
is a var:
(defn my-filter [thing] (re-find #"b" thing))
(macroexpand-1 '(defthings nil my-filter))
;; (do (def abc "abc") (def bcd "bcd"))
But not if I try to use any more complex expression, such as a function literal:
(macroexpand-1 '(defthings nil #(re-find #"b" %)))
;; Unexpected error (ClassCastException) macroexpanding defthings at (REPL:1:1).
;; clojure.lang.PersistentList cannot be cast to clojure.lang.Symbol
Obviously, filter-fn
is no longer a symbol, so it can’t be resolve
d. If I replace (resolve filter-fn)
with (if (symbol? filter-fn) (resolve filter-fn) filter-fn)
so that it’s only resolved when it is a symbol, that still fails, but with a different error:
(defmacro defthings [klazz filter-fn]
`(do ~@(->> (things klazz)
(filter (if (symbol? filter-fn) (resolve filter-fn) filter-fn))
(map (fn [thing]
`(def ~(thing-name thing) ~(thing-body thing)))))))
(macroexpand-1 '(defthings nil #(re-find #"b" %)))
;; Error printing return value (ClassCastException) at clojure.core/filter$fn (core.clj:2817).
;; clojure.lang.PersistentList cannot be cast to clojure.lang.IFn
Now the function definition is being treated as its unevaluated form. So maybe I have to eval
it?
(defmacro defthings [klazz filter-fn]
`(do ~@(->> (things klazz)
(filter (eval filter-fn))
(map (fn [thing]
`(def ~(thing-name thing) ~(thing-body thing)))))))
(macroexpand-1 '(defthings nil #(re-find #"b" %)))
;; (do (def abc "abc") (def bcd "bcd"))
(macroexpand-1 '(defthings nil my-filter))
;; (do (def abc "abc") (def bcd "bcd"))
That looks much better, but because eval
uses an empty lexical environment, this still fails if the expression uses anything from a surrounding let
binding:
(defn make-filter [pattern]
(fn [thing] (re-find (re-pattern pattern) thing)))
(let [pat "b"]
(macroexpand-1 '(defthings nil (make-filter pat))))
;; Syntax error compiling at (REPL:2:34).
;; Unable to resolve symbol: pat in this context
Although this almost works, it doesn’t feel right. Is there a better way to achieve what I want?
The other option would be to use a dynamic var. It would mean any calls to the macro that require a filter-fn
would have to be wrapped in a binding
form, but by its nature that would mean that no matter how the function is defined, it will have been evaluated by the time the macro sees it:
(def ^:dynamic *filter-fn* (constantly true))
(defmacro defthings [klazz]
`(do ~@(->> (things klazz)
(filter *filter-fn*)
(map (fn [thing]
`(def ~(thing-name thing) ~(thing-body thing)))))))
(let [pat "b"]
(binding [*filter-fn* (make-filter pat)]
(macroexpand-1 '(defthings nil))))
;; (do (def abc "abc") (def bcd "bcd"))
So, in describing my problem, I think I’ve convinced myself that using a dynamic var is the best way forward. But if anyone has any thoughts on this, or can think of a better way, I’d be interested to hear them.