ClojureScript does not have a standalone macro system. To write ClojureScript macros, one must write them in Clojure and then refer to them in ClojureScript code. This situation is workable, but at a minimum it forces one to keep ClojureScript code and the macros it invokes in separate files. I miss the locality of regular Clojure macros, so I wrote something called maptemplate
that gives me back some of what I miss. The technique may be useful in other scenarios.
Suppose you're wrapping functionality in another namespace or package so that you can have your own namespace of identically named but otherwise decorated functions:
ClojureScript:
(ns my.ns
(:require [their.ns :as their]))
(defn decorate [f] ,,, )
(def some-thing (decorate their/some-thing))
(def other-thing (decorate their/other-thing))
(def thing2 (decorate their/thing2))
;; ...
If there are tens of remote symbols you'd like to refer to locally, your def
.block will be pretty big and ugly. You may decide to write a macro in a separate Clojure file:
(ns my.ns.macros)
(defmacro make-defs [& names]
`(do
~@(map (fn [sym]
`(def ~sym (~'decorate ~(symbol (str 'their) (str sym)))))
names)))
Now, your ClojureScript file looks like:
(ns my.ns
(:require [their.ns :as their])
(:require-macros [my.ns.macros :refer [make-defs]])
(defn decorate [f] ,,, )
(make-defs
some-thing
other-thing
thing2)
This is certainly nicer to look at, and will be easier to read and change later on. However, you've lost something: knowledge about what's going on here is hidden in that Clojure macro in a separate file. Wouldn't it be nice if the code this expanded to were expressed meaningfully nearby, and if our macro were more general?
These were my aims with maptemplate
:
ClojureScript
(ns my.ns
(:require [their.ns :as their])
(:require-macros [my.ns.macros :refer [maptemplate]])
(defn decorate [f] ,,, )
(maptemplate
(fn [sym] `(def ~sym (~'decorate ~(symbol (str 'their) (str sym)))))
[some-thing other-thing thing2])
That's a macro in ClojureScript! Actually, no. It's just Clojure data written using reader macros that one usually only sees inside macro bodies. But if it isn't a macro, what is it? Smells like... eval.
Clojure
(defmacro maptemplate
[template-fn coll]
`(do ~@(map `~#((eval template-fn) %) coll)))
maptemplate
takes two arguments: template-fn
and coll
. template-fn
should be data representing a Clojure function that, when passed an unevaluated piece of data from coll
, returns appropriate ClojureScript code.
When the macro is used in ClojureScript, template-fn
is data representing a Clojure function, not a function object. It's converted into a Clojure function during compilation by being passed to eval
.
Once template-fn
is an actual function, it is invoked with items from coll. The return value is converted from Clojure's intermediate syntax-quote expansion into a regular-looking form with the application of `~.
Because one can pass Clojure data to the ClojureScript compiler unevaluated, and because there's access to eval
in the Clojure compiler, ClojureScript has more of a macro system than one might think.
wow very interesting!