ClojureScript Macros Calling Functions

January 5, 2016

Let's say you are writing a complex macro and decide to delegate some of its implementation to a function. When in bootstrapped ClojureScript, this can be fairly clean to do.

But first, let's back up and review how this can be done in regular ClojureScript—where macros are written in Clojure.




Regular ClojureScript

Consider src/foo/macros.clj, with

(ns foo.macros)

(defn add*
  [a b]
  (+ a b))

(defmacro add-now
  [a b]
  (add* a b))

The add-now macro delegates to an add* function defined in the same namespace. This will work fine if the values passed for a and b are compile-time constants. In a REPL, you can

(require-macros 'foo.macros)

and then

(foo.macros/add-now 1 2)

and get 3. What's really going here is Clojure produced the 3 at macroexpansion time. But you can't do

(let [x 3
      y 4]
  (foo.macros/add-now x y))

If you try it you'll see that it ends up trying to add the symbols x and y, not their values.

But, you can instead have a macro that expands to a call to a ClojureScript function. Let's say you add this to the foo.macros namespace:

(defmacro add
  [a b]
  `(foo.core/add* ~a ~b))

and introduce src/foo/core.cljs that defines a ClojureScript version of add*:

(ns foo.core)

(defn add*
  [a b]
  (+ a b))

Now if you

(require 'foo.core)

and

(require-macros 'foo.macros :reload)

then this works fine:

(let [x 3
      y 4]
  (foo.macros/add x y))

In regular ClojureScript, we need to play this game because of the use of Clojure for macros.

Bootstrapped ClojureScript

In bootstrapped ClojureScript, on the other hand, everything is pure ClojureScript and the following approach works just fine:

(ns foo.macros)

(defn add*
  [a b]
  (+ a b))

(defmacro add-now
  [a b]
  (add* a b))

(defmacro add
  [a b]
  `(add* ~a ~b))

The only real difference is that in the add macro, we are no longer referring to foo.core/add*, but can simply use the existing add* function definition in the foo.macros namespace.

With this in place, the add macro works just fine.

For the above to work as written, you'll need tools.reader 1.0.0-alpha3 or later for a syntax-quote behavior fix (TRDR-33).

Under the Hood

If you are interested in digging a little deeper into what is going on here, do

(macroexpand '(foo.macros/add 3 4))

and look at what you get:

(foo.macros$macros/add* 3 4)

This has to do with the fact that macros namespaces are compiled in a different stage, and end up in a pseudo-namespace involving a $macros suffix. (This implementation detail was exploited in a previous post.) In fact, an unfortunate consequence is that you cannot refer to add* as foo.macros/add*. You must instead let the compiler take care of var resolution for you (either directly or via syntax-quote).

Tags: ClojureScript Bootstrap