How do I generate a sequence of defs based on a parameter

Viewed 129

I can "generate" a def with a macro.

(defmacro my-def [my-name]
  `(def ~my-name 42))

(my-def a)

a; => 42

If I try to do something similar with a list

(defmacro my-defs [my-names]
  `(do
    ~@(for [name# my-names]
      `(def ~name# 42))))

(my-defs (a b c))

(macroexpand '(my-defs (a b c))); => (do (def a 42) (def b 42) (def c 42))

It works as long as I use a literal list as input. But as soon as I want to pass in a var

(def my-list '(a b c))

(macroexpand '(my-defs my-list)); => Don't know how to create ISeq from: clojure.lang.Symbol

I struggle to access the value of my-names. I can't use ~my-names as it is already used in a unquote-splice (~@) and would lead to an "Attempt[...] to call unbound fn".

What am I missing?

Do I need to use (var-get (resolve my-names))?

Do macros in these cases need to "detect" if the passed argument is a literal value or a var and act accordingly in order to work for both?

Or is it idiomatic to use eval to avoid this?

Addressing @Alan Thompson's question "[...] why [do] you want to do this?": I have a specification (a deeply nested map) of "resources" and it would be rather handy to have a macro generate defs (records) for these resources in order to use them down the line. So I guess no reason out of the ordinary "It would DRY up things". :) At this time I found a way by wrapping my-names in an eval. The question that remains is: Is this idiomatic, or is there a better way?

3 Answers

generally you can't employ macro to generate code based on runtime value, still your task doesn't require macro in clojure, since you can dynamically intern vars in namespaces:

(defn intern-vals [data]
  (doseq [[var-name var-val] data]
    (intern *ns* var-name var-val)))

user> (intern-vals {'some-val 10 'other-val 20})
;;=> nil

user> some-val
;;=> 10

user> other-val
;;=> 20

notice that this function interns values in the namespace it gets called from, thanks to *ns* dynamic var:

user> (ns a2)

a2> (user/intern-vals {'some-val "asd" 'other-val "xxx"})
;;=> nil

a2> some-val
;;=> "asd"

a2> user/some-val
;;=> 10

Macros play with symbols. When you call your macro with "my-names", that symbol goes straight into the macro, there's no lookup of the var as there would be in a function call. Then the macro says (for... and instead of a sequence there's a symbol!

As for what you should do... well, you may use resolve inside the macro, but then the macro will ONLY work if given a symbol.

Addressing @Alan Thompson's question "[...] why [do] you want to do this?": I have a specification (a deeply nested map) of "resources" and it would be rather handy to have a macro generate defs (records) for these resources in order to use them down the line. So I guess no reason out of the ordinary "It would DRY up things". :) At this time I found a way by wrapping my-names in an eval. The question that remains is: Is this idiomatic, or is there a better way?

I would make a single var that contains them all. And allow the macro caller to specify the var name they want. Thus making the macro "hygenic".

Is there any particular reason they have to be root vars?

The reason it doesn't work is the macro is passed the symbol my-list, not its value. So yes, you can eval it to find its value.

Considering that you are doing a (def my-list ...) first, why not make that the very def that declares your processed data struct? For example:

(def my-processed-set 
  (my-processing-macro '(a b c)))

or combining it

(defresources my-processed-resources '(a b c))

where defresources is your macro, and it binds the resultant set to the var referenced by the symbol passed in my-processed-resources

Then use them like (:resource-1 my-processed-resources)

Which brings you all the way back to just using a function.

(def my-processed-set 
  (my-processing-function '(a b c)))

data > functions > macros.

Related