Clojure Macros with Private Helpers



Draft
An interesting consequence of a fundamental difference between symbols in different Lisps

Helper Functions for Macros    

In Lisps, it's common to define a helper function to provide the implementation of a macro.

Here's an example in Clojure:

(defn helper-for-with-print-length [n f]
  (binding [*print-length* n]
    (f)))

(defmacro with-print-length
  [n & body]
  `(helper-for-with-print-length ~n
                                 (fn [] ~@body)))

And a call:

(the-macro-ns/with-print-length 5
  (prn (range 1000)))
;; (0 1 2 3 4 ...)
;; => nil

One reason for doing this is to make REPL-based development work nicely when writing macros. Assuming the macro definition itself doesn't change, we can evaluate the definition of the helper function and all calls of the macro will get the new functionality. Without the helper function, when a macro's definition is changed all call sites would need to be re-evaluated.

Making the Helper Function Private    

We might want to make the helper function private.

(defn ^:private helper-for-with-print-length [n f]
  (binding [*print-length* n]
    (f)))

This works fine in Common Lisp, but in Clojure our call now gives an error:

;; CompilerException [...] helper-for-with-print-length is not public

There is an argument that macros should be syntactic sugar and that the underlying functionality should be made available with a function. But let's ignore that and assume we have our reasons for wanting to have the helper function private.

We Can Access Private Symbols    

We can get around this. Clojure allows us to access a private var by referring directly to the var. This is often used when testing private functions, but we can make good use of it here.

We end up with the following:

(defn ^:private helper-for-with-print-length [n f]
  (binding [*print-length* (inc n)]
    (f)))

(defmacro with-print-length
  [n & body]
  `(#'helper-for-with-print-length ~n
                                   (fn [] ~@body)))

Note that we now have a level of indirection that we didn't have before. That could possibly be a problem in a performance-critical situation.

What If Our Implementation is Itself a Macro?    

When using this technique of helpers for macros, sometimes our helper will itself be a macro. It turns out that the Clojure trick of using indirection through the var doesn't work for macros.

In this situation, it seems the only solution is to make our helper public.

Symbols in Clojure vs Symbols in Other Lisps    

This difference in behaviour between Clojure macros and Common Lisp macros is a consequence of a fundamental difference between symbols in the two languages; for more on this, see Symbols and Namespaces in Clojure and Common Lisp.