ClojureScript Macro Tower and Loop
In ClojureScript, unlike in Clojure, you can't intermix macros and their use in the same compilation stage. In pragmatic terms, this often means you end up defining macros in files that are separate from the files that consume them.
There are two interesting corner cases that this post explores. I'll call them the tower and the loop.
Tower
Update April 15, 2016—See Collapsing Macro Tower.
In “regular” JVM-based ClojureScript, macros are written in Clojure. But, in bootstrapped ClojureScript, the macros are perforce written in ClojureScript.
This is true even if the macros are placed in a
*.clj
file. And, if placed in a*.cljc
file the:cljs
branch of reader conditionals will be used.
This raises the question: “Can a bootstrapped ClojureScript file that defines macros itself use macros?” The answer is “yes,” but the compilation stage rules still apply. So, in order to use macros, the macros-defining file must :refer-macros
from a “higher” level in the stage tower.
Here is an example:
baz/core.clj
:
(ns baz.core)
(defmacro unless [pred a b]
`(if (not ~pred)
~a
~b))
bar/core.clj
:
(ns bar.core
(:require-macros baz.core))
(defmacro abs [x]
`(baz.core/unless (neg? ~x)
~x
(- ~x)))
foo/core.cljs
:
(ns foo.core
(:require-macros bar.core))
(defn taxicab-norm [a b]
(+ (bar.core/abs a)
(bar.core/abs b)))
Then
cljs.user=> (require 'foo.core)
nil
cljs.user=> (foo.core/taxicab-norm -4 5)
9
Incidentally, I get a kick out of the amount of inlining in the code that is generated for taxicab-norm
:#object[foo$core$taxicab_norm "function foo$core$taxicab_norm(a,b){
return (((!((a < 0)))?a:(- a)) + ((!((b < 0)))?b:(- b)));
}"]
Nice!
Loop
Even though we have separate compilation stage rules, there is nothing that precludes a ClojureScript file from using macros defined in the same namespace.
Incidentally, if you do this, you can make use of implicit macro loading.
The normal way this is done is to, say, define functions in a foo/core.cljs
file and have the macros defined in a foo/core.clj
file.
But, can a ClojureScript namespace use macros from the same namespace, while having everything defined in the same file, while still abiding the compilation stage rules?
Yes, this can be done with a single foo/core.cljc
file along with appropriate reader conditionals. Here is an example that works in “regular” JVM ClojureScript:
(ns foo.core
#?(:cljs (:require-macros
[foo.core :refer [add]])))
#?(:clj
(defmacro add
[a b]
`(+ ~a ~b)))
#?(:cljs
(defn sum
[a b]
(add a b)))
Does this seem odd? Sure, but it is interesting to know that the ClojureScript compiler itself uses this technique. See David Nolen discuss this.