1
votes

I am working through Clojure for the Brave and True. In the chapter on macros there is this exercise:

Write a macro that defines an arbitrary number of attribute-retrieving functions using one macro call. Here’s how you would call it:

(defattrs c-int :intelligence
          c-str :strength
          c-dex :dexterity)

What these functions do is retrieve a value from a map. For example given: (def character {:name "Travis", :intelligence 20, :strength 23, :dexterity 13})

The result of (c-int character) would be 20 of course such a function could easily be defined as (def c-int #(:intelligence %))

This is the solution I came up with to the problem:

(defmacro defattrs
    [& attributes]
    `(let [attribute-pairs# (partition 2 (quote ~attributes))]
          (map (fn [[function-name# attribute-key#]]
                   (def function-name# #(attribute-key# %)))
           attribute-pairs#)))

The problem I am having is that def uses the generated symbol name instead of what it resolves to to define the function (which in hindsight makes sense given the usage of def). My attempts to use expressions with defining functions such as:

(let [x ['c-int :intelligence]]
  (def (first x) #((second x) %)))

Have resulted in this error: CompilerException java.lang.RuntimeException: First argument to def must be a Symbol, compiling:(/tmp/form-init5664727540242288850.clj:2:1)

Any ideas on how I can achieve this?

2

2 Answers

2
votes

You have found the use-case for the back-quote and tilde. Just try this:

(let [x ['c-int :intelligence]]
  (eval `(def ~(first x) #(~(second x) %))))

(def character {:name "Travis", :intelligence 20, :strength 23, :dexterity 13})

(c-int character) => 20

The back-quote is similar to the single-quote in that it makes the next form into a data structure of lists, symbols, etc. The difference is that the data structure is intended to be used as a template, where internal bits can be substituted using the tilde. The cool part is that the tilde doesn't just substitute items, but works for live code that can be any arbitrary Clojure expression.

2
votes

There are ordinary manipulations that you do with the attributes parameter that don't need to be generated as forms:

  • splitting the attributes into attribute-pairs; and
  • defining the function to generate a def form for each pair.

Applying the above to your code, we get ...

(defmacro defattrs [& attributes]
  (let [attribute-pairs (partition 2 attributes)]
     (map (fn [[function-name attribute-key]]
            `(def ~function-name #(~attribute-key %)))
          attribute-pairs)))
  • The scope of the back-quote is restricted to the def we wish to generate.
  • The values of the function-name and attribute-key parameters of the function are inserted into the def form.

There is one problem remaining.

  • The result of the map is a sequence of def forms.
  • The first one will be interpreted as a function to apply to the rest.

The solution is to cons a do onto the front of the sequence:

(defmacro defattrs [& attributes]
  (let [attribute-pairs (partition 2 attributes)]
    (cons 'do
          (map (fn [[function-name attribute-key]]
                 `(def ~function-name ~attribute-key))
               attribute-pairs))))

I've also abbreviated #(~attribute-key %) to the equivalent ~attribute-key within the back-quoted form.

Let's see what the expansion looks like:

(macroexpand-1 '(defattrs dooby :brrr))
;(do (def dooby :brrr))

Looks good. Let's try it!

(defattrs gosh :brrr)
(gosh {:brrr 777})
;777

It works.