在Clojure中,如何通过字符串定义一个变量?

19
给定一个变量名称的列表,我想将这些变量设置为一个表达式。
我尝试过这个:
(doall (for [x ["a" "b" "c"]] (def (symbol x) 666)))

...但这会产生以下错误

java.lang.Exception: def的第一个参数必须是Symbol类型

请问有谁可以展示正确实现的方法吗?

4个回答

37

Clojure的"intern"函数就是为了这个目的而存在的:

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

最简明直接的答案(可能?)我已经给了你打勾,希望sepp2k不会因为失去它而生气。谢谢! - Carl Smotricz

13
(doall (for [x ["a" "b" "c"]] (eval `(def ~(symbol x) 666))))

回应你的评论:

这里没有涉及到宏。eval是一个函数,它接受一个列表并返回执行该列表作为代码的结果。`和~是创建部分引用列表的快捷方式。

`表示以下列表的内容将被引用,除非前面有~。

~表示以下列表是一个要执行而不是引用的函数调用。

所以 ``(def ~(symbol x) 666)是包含符号def的列表,后跟执行symbol x的结果,然后是666。我也可以写成(eval (list 'def (symbol x) 666))`来达到同样的效果。


我知道这个答案某种程度上涉及到 eval,但忘记了 ~ 结构。 - JUST MY correct OPINION
哇,你知道吗,它就是有效的!我没想到需要降到宏层面来完成这个,但我很高兴将其包含在我的“我不完全理解的东西的食谱”中。谢谢! - Carl Smotricz
2
@CarlSmotricz:我刚刚给代码添加了一些解释。希望你现在能更全面地理解它。 - sepp2k
啊哈!我之前在宏的上下文中见过波浪号(~)。再次感谢! - Carl Smotricz
2
请不要使用 eval!使用 intern!但是要注意不要重新定义现有的变量! - kotarak

7

已根据Stuart Sierra的评论(提到clojure.core/intern)进行更新。

在这里使用eval是没有问题的,但值得注意的是,无论变量是否已知存在,都不需要使用它。实际上,如果它们已知存在,那么我认为下面的alter-var-root解决方案更加简洁;如果可能不存在,则我不会坚持我的替代命题要更加简洁,但是它似乎可以生成最短的代码(如果我们忽略三行函数定义的开销),所以我将其发布供您考虑。


如果变量已知存在:

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

因此,您可以这样做:

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

如果所有的变量都需要使用相同的值,你可以在map的最后一个参数中使用(repeat value)或者将其放入匿名函数中。


如果需要创建Vars,那么你可以编写一个函数来完成这个任务(再次强调,我并不一定认为这比eval更干净,但是出于兴趣,我们可以这样做):

(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)))

请注意,如果一个Var已经在给定的命名空间中使用相同的名称进行了内部化处理,则在单个参数情况下不会发生任何更改,在两个参数情况下仅将Var重置为给定的新值。通过这种方式,您可以像这样解决原始问题:
(dorun (map #(create-var (symbol %) 666) ["x" "y" "z"]))

一些额外的例子:
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

非常酷,非常有信息量,非常感谢!乍一看,这似乎是在做def代码所做的事情;我猜这是一个“分叉”的def。我可能会在我的下一个小项目中使用它。 - Carl Smotricz
1
一条经验法则:在必要时和确定自己在做什么的情况下才使用 eval。我会选择 intern 来解决目前的问题;这个答案很好地展示了 eval 在此处不需要使用。 - danlei
2
Var.intern 可以作为内置的 Clojure 函数 "intern" 使用。 - Stuart Sierra
@Stuart:感谢您的评论!我想我应该重新熟悉一下核心API...我会把这个加入到答案中。 - Michał Marczyk

3
常规函数调用的评估规则是:计算列表中的所有项,并将列表中的第一项作为函数并使用列表的其余项作为参数调用它。
但您不能对特殊形式或宏的评估规则做出任何假设。特殊形式或宏调用生成的代码可以计算所有的参数,也可以不计算它们,或者计算它们多次,或者只计算其中一些参数。def是一个特殊形式,它不计算它的第一个参数。如果它这样做,它就不能工作了。在(def foo 123)中计算foo通常会导致“没有这样的变量'foo'”错误(如果foo已经被定义,您可能不会自己定义它)。
我不确定您将其用于什么,但它似乎不是很惯用。除了程序的顶层之外,在任何地方使用def通常意味着您正在做错事情。
(注意:doall + for = doseq.)

谢谢你提醒我使用doseq!我在顶层定义了一些变量,只是想减少打字的次数。 - Carl Smotricz
2
为什么一开始不用符号而是字符串呢? - Brian Carper
因为我计划使用的名称本身是通过连接计算出来的(当然)。我强调,我这样做只是为了实验性的“快速脏代码”目的;有更好的方法来表达我正在做的事情。我发布这个问题只是因为我想在我的早期草案代码中这样做,并且很烦恼,否则优秀的Clojure语言似乎不支持这种特殊的jiggerpokery。 - Carl Smotricz
我仍在探索Lisp习语,特别是Clojure。我不打算将动态创建的命名全局变量作为我的常规编程技能之一。 - Carl Smotricz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接