Search code examples
clojuremacrosfunctional-programminglispclojurescript

When to use ~'some-symbol in Clojure Macro?


When I was reading The Joy of Clojure I came across some code.

(fn [~'key ~'r old# new#]
                  (println old# " -> " new#)

What is the exact behaviour of this declaration ~'some-symbol.

Differences between some-symbol# and '~another-symbol or gensym?

The Joy Of Clojure: (Did not understand)

You’ll see the pattern ~'symbol at times in Clojure macros for selectively capturing a symbolic name in the body of a macro. The reason for this bit of awkwardness[11] is that Clojure’s syntax-quote attempts to resolve symbols in the current context, resulting in fully qualified symbols. Therefore, ~' avoids that resolution by unquoting a quote.


Solution

  • You can see an example in the Tupelo library with the Literate Threading Macro. We want the user to type the symbol it and have it recognized by the macro. Here's the definition:

    (defmacro it->
      "A threading macro like as-> that always uses the symbol 'it' 
       as the placeholder for the next threaded value "
      [expr & forms]
      `(let [~'it ~expr
             ~@(interleave (repeat 'it) forms)
             ]
         ~'it))
    

    This is also referred to as an "anaphoric" macro. The user then creates code like this:

    (it-> 1
          (inc it)                                  ; thread-first or thread-last
          (+ it 3)                                  ; thread-first
          (/ 10 it)                                 ; thread-last
          (str "We need to order " it " items." )   ; middle of 3 arguments
    ;=> "We need to order 2 items." )
    

    The user includes the special symbol it in their code, which the macro is expecting (& is required in this case).

    This is a bit of a special case. In most cases, you want the macro to work no matter what symbols the user picks. That is why most macros use (gensym...) or the reader version with "#" suffix like this example:

    (defmacro with-exception-default
      "Evaluates body & returns its result.  In the event of an exception, default-val is returned
       instead of the exception."
      [default-val & body]
      `(try
         ~@body
         (catch Exception e# ~default-val)))
    

    This is the "normal" case, where the macro creates a "local variable" e# that is guaranteed not to overlap with any user symbol. A similar example shows the spyx macro creating a "local variable" named spy-val# to temporarily hold the result of evaluating the expression expr:

    (defmacro spyx
      "An expression (println ...) for use in threading forms (& elsewhere). Evaluates the supplied
       expression, printing both the expression and its value to stdout, then returns the value."
      [expr]
      `(let [spy-val# ~expr]
         (println (str (spy-indent-spaces) '~expr " => " (pr-str spy-val#)))
         spy-val#))
    

    Note that for the (println...) statement, we see the opposite syntax of '~expr -- but that is a topic for another day.