Skip to content

Instantly share code, notes, and snippets.

@alandipert
Created January 30, 2013 00:28
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save alandipert/4669472 to your computer and use it in GitHub Desktop.
Save alandipert/4669472 to your computer and use it in GitHub Desktop.
ClojureScript macros: kinda, sorta, not really.

ClojureScript macros: kinda, sorta, not really.

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.

Problem

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 `~.

Conclusion

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.

@bhurlow
Copy link

bhurlow commented Feb 3, 2015

wow very interesting!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment