Search code examples
clojuremacros

Understanding symbol resolution with macros in Clojure


I'm learning macros in clojure. I need help with symbol resolution in macros.

(ns macros.testing)

(def no (rand-int 10))

(defmacro drawer []
  `(do
     ~@(for [i (range no)]
     `(print ~i))))

(drawer) ;; This can resolve the global variable `no` properly


(defmacro drawer-2 [n]
  `(do
     ~@(for [i (range n)]
     `(print ~i))))


(drawer-2 10) ;; This works fine too
(drawer-2 n)  ;; This is not working, I'm unable to pass the global variable as an argument to the macro, although it's visible in the other case

The error message is class clojure.lang.Symbol cannot be cast to class java.lang.Number (clojure.lang.Symbol is in unnamed module of loader 'app'; java.lang.Number is in module java.base of loader 'bootstrap').

Why clojure is unable to resolve the global variable in the second case and why can it do so for the first macro?


Solution

  • When a macro is evaluated, its arguments are passed "as is", without symbol resolution, as if they're all quoted. So if you pass n, within a macro it'll be a symbol n and not a value contained in a relevant var. If you pass (inc 10), it'll be '(inc 10). And so on.

    So in drawer, (range no) works because no refers to a var that's bound to the result of (rand-int 10).

    In drawer-2, the argument n has the value of (symbol "n") (or just 'n) during macro evaluation. And (range 'n) can't work - that's exactly what that "can't cast a symbol to a number" exception that you saw is about.

    To fix that, you can explicitly resolve the passed symbol by using resolve (that @ is needed to get the value of the resolved var):

    (def value 10)
    
    (defmacro drawer-2 [n]
      (let [n (if (symbol? n)
                @(resolve n)
                n)]
        `(do
           ~@(for [i (range n)]
               `(print ~i)))))
    
    (drawer-2 value)