3
votes

I have a macro defprinter where I can define functions that: get a value in a dic based on destructuring + print that variable.

This looks like:

(defmacro defprinter [name pattern] 
  (list 'defn name [pattern] '(prn to-print)))

So I can do something like (defprinter print-lemons {to-print :lemons}) and then (print-lemons {:lemons "lemons"}) and it will print the correct thing (and I can define printers with any kind of destructuring on the first argument).

But now I want to give the option of that function maybe knowing how to print with a color as well, such as, if the symbol color is defined, it should do (prn color "-colored " to-print), but else just (prn color).

So, the function generated by (defprinter print-lemons {to-print :lemons}) should work the same as above, while (defprinter print-lemons {to-print :lemons, color :color}) would write a function that performs the colored version.

Moreover, I want the same effect if I do (let [color "black"] (defprinter print-lemons {to-print :lemons})) or (def color "black") (defprinter print-lemons {to-print :lemons}).

Is that possible?

I've tried writing it the following way:

(defmacro defprinter [name pattern] 
  (list 'defn name [pattern]
   '(try (eval '(prn (str color "-colored " to-print))) 
         (catch java.lang.RuntimeException _ 
           (prn to-print)))))

In my understanding the function the macro will write will try to eval the expression at runtime, fail with a RuntimeException if color is not defined and then execute the (prn to-print). And even though it would check for the existence of color at runtime, to-print (which always need to exist for that function) would be checked at compile time when the macro expands.

But what happens here is that I always get a RuntimeException, even when color is defined (even if I leave only to-print on the eval statement, it can't find it but the clause in the catch works fine). Seems like the symbol isn't being resolved as I would expect during eval, but I can't think of any other way to achieve this.

Any suggestions?

2

2 Answers

3
votes

First off, it probably makes sense to split apart the two concerns you're working with here: defining a function, and determining how to print based on what vars/locals are available. Making macros as simple as possible tends to make it easier to figure out what's going on.

When deciding what to print, you really have 3 cases:

  1. color is a local
  2. color is a var (or class, I guess?)
  3. color is undefined

&env is a useful and rarely-used tool (only available within macros) that lets you take a look at what's available as a local.

And resolve lets you look at what vars there are with that name.

So in this example, there are 3 potential expressions that could make up the body of the function defined with def-color-printer:

(defn make-print-expression [env]
  (if (contains? env 'color)
    `(prn (str ~'color "-colored " ~'to-print))
    (if-let [color (resolve 'color)]
      `(prn (str @~color "-colored " ~'to-print))
      `(prn ~'to-print))))

(defmacro def-color-printer [name pattern]
  (list `defn name [pattern]
        (make-print-expression &env)))

(def-color-printer print-limes {to-print :limes})

(let [color "green"]
  (def-color-printer print-limes-color-with-local {to-print :limes}))

(def color "greyish")
(def-color-printer print-limes-color-with-var {to-print :limes})

(print-limes {:limes "limes!"})
;=> "limes!"

(print-limes-color-with-local {:limes "limes!"})
;=> "green-colored limes!"

(print-limes-color-with-var {:limes "limes!"})
;=> "greyish-colored limes!"

I also wrote a blog about Clojure's quoting in case the syntax-quoting syntax is unfamiliar.

1
votes

the resolve function will likely help you out with this. It returns the thing a symbol represents in the current namespace, or it returns nil. You can feed it to an if expression:

user> (resolve 'asdf)
nil
user> (if (resolve 'asdf) :defined :not-defined)
:not-defined

Remember to quote the symbol you want to resolve in the test.