Carl Smotricz
Carl Smotricz

Reputation: 67760

In Clojure, how to define a variable named by a string?

Given a list of names for variables, I want to set those variables to an expression.

I tried this:

(doall (for [x ["a" "b" "c"]] (def (symbol x) 666)))

...but this yields the error

java.lang.Exception: First argument to def must be a Symbol

Can anyone show me the right way to accomplish this, please?

Upvotes: 19

Views: 7172

Answers (3)

Michał Marczyk
Michał Marczyk

Reputation: 84331

Updated to take Stuart Sierra's comment (mentioning clojure.core/intern) into account.

Using eval here is fine, but it may be interesting to know that it is not necessary, regardless of whether the Vars are known to exist already. In fact, if they are known to exist, then I think the alter-var-root solution below is cleaner; if they might not exist, then I wouldn't insist on my alternative proposition being much cleaner, but it seems to make for the shortest code (if we disregard the overhead of three lines for a function definition), so I'll just post it for your consideration.


If the Var is known to exist:

(alter-var-root (resolve (symbol "foo")) (constantly new-value))

So you could do

(dorun
  (map #(-> %1 symbol resolve (alter-var-root %2))
       ["x" "y" "z"]
       [value-for-x value-for-y value-for z]))

(If the same value was to be used for all Vars, you could use (repeat value) for the final argument to map or just put it in the anonymous function.)


If the Vars might need to be created, then you can actually write a function to do this (once again, I wouldn't necessarily claim this to be cleaner than eval, but anyway -- just for the interest of it):

(defn create-var
  ;; I used clojure.lang.Var/intern in the original answer,
  ;; but as Stuart Sierra has pointed out in a comment,
  ;; a Clojure built-in is available to accomplish the same
  ;; thing
  ([sym] (intern *ns* sym))
  ([sym val] (intern *ns* sym val)))

Note that if a Var turns out to have already been interned with the given name in the given namespace, then this changes nothing in the single argument case or just resets the Var to the given new value in the two argument case. With this, you can solve the original problem like so:

(dorun (map #(create-var (symbol %) 666) ["x" "y" "z"]))

Some additional examples:

user> (create-var 'bar (fn [_] :bar))
#'user/bar
user> (bar :foo)
:bar

user> (create-var 'baz)
#'user/baz
user> baz
; Evaluation aborted. ; java.lang.IllegalStateException:
                      ;   Var user/baz is unbound.
                      ; It does exist, though!

;; if you really wanted to do things like this, you'd
;; actually use the clojure.contrib.with-ns/with-ns macro
user> (binding [*ns* (the-ns 'quux)]
        (create-var 'foobar 5))
#'quux/foobar
user> quux/foobar
5

Upvotes: 7

Alessandra Sierra
Alessandra Sierra

Reputation: 10887

Clojure's "intern" function is for this purpose:

(doseq [x ["a" "b" "c"]]
  (intern *ns* (symbol x) 666))

Upvotes: 37

Brian Carper
Brian Carper

Reputation: 72926

Evaluation rules for normal function calls are to evaluate all the items of the list, and call the first item in the list as a function with the rest of the items in the list as parameters.

But you can't make any assumptions about the evaluation rules for special forms or macros. A special form or the code produced by a macro call could evaluate all the arguments, or never evaluate them, or evaluate them multiple times, or evaluate some arguments and not others. def is a special form, and it doesn't evaluate its first argument. If it did, it couldn't work. Evaluating the foo in (def foo 123) would result in a "no such var 'foo'" error most of the time (if foo was already defined, you probably wouldn't be defining it yourself).

I'm not sure what you're using this for, but it doesn't seem very idiomatic. Using def anywhere but at the toplevel of your program usually means you're doing something wrong.

(Note: doall + for = doseq.)

Upvotes: 3

Related Questions