Clojure macro local binding

Posted by on November 7, 2015

It was my turn to discover ~' pattern in Clojure macros. In the following toy problem we can bind sequence of Fibonacci numbers to symbols specified in the binding vector:

(defn- fib [a b]
  (list* a (lazy-seq (fib b (+ a b)))))

(defmacro fibonacci [bindings body]
  `(let [~@(interleave bindings (fib 0 1))]
       ~body))

So the code

(macroexpand-1 '(fibonacci [a b] (+ a b)))

expands to

       
(clojure.core/let [a 0 b 1] (+ a b))

Lets say we would like to know if the sequence was initialized with zero and we can define another symbol that will indicate if the Fibonacci numbers start with 0 and will be lexically bound to let’s say to ‘zero?’. We would do something like this:

(defmacro fibonacci-2 [bindings body]
  `(let [~@(interleave bindings (fib 0 1)) ~'zero? true]
       ~body))

And

(fibonacci-2 [a b c] (if zero? (+ b c) (+ a b)))

will expand to

       
(clojure.core/let [a 0 b 1 c 1 zero? true] (if zero? (+ b c) (+ a b)))

So indeed, why ~'? The idiomatic way in Clojure is to use syntax-quote and this will try to resolve/qualify all the symbols in the current context. Before that Clojure reader reads stream of characters and builds datastructures that evaluate to themselves except keywords. What macros recieve are those datastructures and unresolved and unevaluated keywords. So probably there are three key parts in the life of a keyword:

  1. Reading (parse text and create datastructures representing the code)
  2. Resolution
  3. Evaluating

More details on reader is available here. It slightly covers synax quoting reader macro (different from actual macros). However I found more examples here. Unrelevant here but the most interesting behavior of syntax-quoting when it comes to symbols that can’t be qualified.

When modifying the code within a macro you have a choice of resolving symbols and that is a normal behavior when using syntax-quote where the forms are not escaped using “~”. It will fully qualify symbols to their respective namespaces and, this is relevant to our case, it will qualify unresolved symbols to current namespace. So if we declared zero? instead of ~'zero? it would have been qualified as current-ns/zero? instead of creating local binding form. And finally, when the macro returns a datastucture it will be evaluated using the rules listed here.