Search code examples
clojuremacrosquoting

Clojure macros: quoting and syntax quoting


Say I have the following code:

(defmacro test1 [x]
  (list 'fn '[y]
        (if (pos? x)
          '(println y)
          '(println (- y)))))

It does what I need, composes a function based on x, and leaves no references to x. For example, (test1 1) macroexpands into (fn* ([y] (println y))).

Now, I'd like to rewrite it using syntax quoting. This is what I have so far:

(defmacro test2 [x]
  `(fn [y#]
     (if ~(pos? x)
       (println y#)
       (println (- y#)))))

This does exactly the same, with one exception: it leaves an (if true ..) expression in the expanded expression:

(fn* ([y__12353__auto__] (if true (clojure.core/println y__12353__auto__) (clojure.core/println (clojure.core/- y__12353__auto__)))))

This might not be an issue if the compiler can optimize it out. Still, is there a way I could omit it?


Solution

  • When you use test2 it will unquote the whole form (pos? x) which will work at compile time if it's a constant number or perhaps a gloabl that is already defined, but not if you pass a lexically scoped variable name that doesn't exist yet.

    Thus, you really want this instead:

    (defmacro test2 [x]
      `(fn [y#]
         (if (pos? ~x) ; just unquote x, not the whole predicate expression
           (println y#)
           (println (- y#)))))
    
    (macroexpand '(test2 y))
    ; ==>
    ; (fn* ([y__1__auto__] 
    ;   (if (clojure.core/pos? y)
    ;       (clojure.core/println y__1__auto__) 
    ;       (clojure.core/println (clojure.core/- y__1__auto__)))))
    
    (defn test-it []
      (let [y -9]
        (test2 y)))
    
    ((test-it) 5) ; prints "-5"
    

    Feel free to try this with your version. (hint: You'll get an Exception since clojure.lang.Symbol cannot be cast to java.lang.Number)

    UPDATE

    Since you want to make the function based on a constant you need to write it a little differently:

    (defmacro test3 [x]
      (assert (number? x) "needs to be a compile time number")
      (if (pos? x)
          `(fn [y#] (println y#))
          `(fn [y#] (println (- y#)))))
    

    Now you'll get an error if you use (test3 x) since x is not a number but get what you want when you evaluate (test3 -10) since -10 is a number we can work with compile time. I'm not sure you'll notice a speed improvement though since these are hardly heavy algorithms.