Search code examples
clojure

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


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?


Solution

  • 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