ClojureScript Macro Tower and Loop

December 18, 2015

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.

Tags: ClojureScript Bootstrap