解释Clojure符号

33

我有一个符号"a"绑定到一个函数:

(defn a []
    (println "Hello, World"))

user=> a
#<user$a__292 user$a__292@97eded>
user=> (a)    
Hello, World
nil

我使用的是语法引用(syntax-quote),根据Clojure文档,“它在当前上下文中解析符号,产生一个完全限定的符号”。但为什么我不能像不限定符号一样使用它呢?

user=> `a
user/a
user=> (`a)
java.lang.IllegalArgumentException: Wrong number of args passed to: Symbol (NO_SOURCE_FILE:0)

第二个问题:如果我在列表中有一个符号,为什么不能像直接评估符号一样评估它?

user=> (def l '(a 1 2))
#'user/l
user=> 'l
l
user=> (first l)
a
user=> ((first l))
java.lang.IllegalArgumentException: Wrong number of args passed to: Symbol (NO_SOURCE_FILE:0)

我怀疑我的基本符号运作理解上有致命缺陷。以上代码有什么问题?

3个回答

37

REPL = 读取-求值-输出循环。逐步了解读取和求值过程。

读取:Clojure看到字符串"(`a)",对其进行解析,最终得到一个数据结构。在读取时,阅读器宏会被展开,除此之外不会发生太多其他事情。在本例中,阅读器展开反引号并最终得到以下结果:

user> (read-string "(`a)")
((quote user/a))

EVAL: Clojure会尝试评估这个对象。评估规则取决于所考虑的对象类型。

  • 一些对象作为它们自己进行评估(例如数字、字符串、关键字等)。
  • 符号通过在某个命名空间中解析来评估它,以获取某个值(通常情况下)。
  • 列表通过宏展开列表,直到没有宏剩余,然后递归地评估列表中的第一个项以获取某个结果值,然后使用列表中第一个项的来决定该做什么。如果第一个值是一种特殊形式,则会发生特殊情况。否则,第一个值被视为函数,并用列表其余部分的值(通过递归地评估列表的所有项)作为参数来调用函数。
  • 等等。

有关列表的评估规则,请参见Clojure源代码中的clojure.lang.Compiler/analyzeSeq,有关符号的评估规则,请参见clojure.lang.Compiler/analyzeSymbol。那里还有许多其他评估规则。

例子

假设你这样做:

user> (user/a)

REPL 内部最终会执行以下操作:

user> (eval '(user/a))

Clojure会发现你正在评估一个列表,因此它会评估列表中的所有项。第一个(也是唯一的)项目:

user> (eval 'user/a)
#<user$a__1811 user$a__1811@82c23d>
a不是一个特殊形式,这个列表不需要宏展开,因此符号a在命名空间user中进行查找,结果值是一个fn。因此调用这个fn

你的代码

但实际上你有这个:

user> (eval '((quote user/a)))

Clojure 评估列表中的第一个项,该项本身是一个列表。

user> (eval '(quote user/a))
user/a

这段代码评估了子列表中的第一个元素 quote,它是一种特殊形式,因此应用了特殊规则,并返回其参数(Symbol a)未经评估的结果。

在这种情况下,符号 a 是值,因为上面的 fn 是该值。 所以Clojure将符号本身视为函数并调用它。在Clojure中,任何实现Ifn接口的东西都可以像fn一样被调用。恰好clojure.lang.Symbol实现了Ifn接口。作为函数调用的符号期望一个参数集合,并在该集合中查找自己。它的使用方法如下:

user> ('a {'a :foo})
:foo

这里的操作是试图实现什么。但是您没有传递任何参数,因此会出现错误“传递给Symbol的参数数量不正确”(它需要一个集合)。

要使您的代码起作用,您需要两个级别的eval。 这将起作用,希望您能够看到原因:

user> (eval '((eval (quote user/a))))
Hello, world
user> ((eval (first l)))
Hello, world

请注意,在真正的代码中,直接使用eval通常是一个非常糟糕的想法。宏(macro)是更好的选择。我只是在此处使用它进行演示。

查看Clojure源代码中的Compiler.java,了解这一切是如何进行的。这并不太难跟随。


15

使用一个符号作为函数并不等同于对它进行求值。符号作为函数的工作方式与关键字作为函数的工作方式相同。像这样:

user=> (declare a)
#'user/a
user=> (def a-map {'a "value"})
#'user/a-map
user=> ('a a-map)
"value"
user=>

这不是你通常使用符号的方式。它们更常用于在命名空间中查找变量以及在宏中生成代码。

为了分解间接层,让我们将“x”定义为1并看看会发生什么:

user=> (def x 1)
#'user/x

使用def,我们创建了一个“var”。该变量的名称是符号user/x。 def特殊形式将变量本身返回给repl,这就是我们可以看到打印出来的内容。让我们尝试获取该变量:

user=> #'x
#'user/x

#' 语法是一个读取宏,表示“给我下一个符号所指向的变量”。在我们的例子中,该符号为 "x"。我们得到了与之前相同的变量。变量是值的指针,并且可以进行解引用:

user=> (deref #'x)
1

但是在引用之前需要找到变量。这就是符号的可调用性发挥作用的地方。名称空间类似于一个映射,其中符号是键,变量是值,当我们简单地命名一个符号时,我们隐式地在名称空间中查找其变量。就像这样:

user=> ('x (.getMappings *ns*))
#'user/x

实际上,情况可能更像这样:

user=> (.findInternedVar *ns* 'x)
#'user/x

现在我们已经完成了未引用符号的旅程,回到了起点:

user=> (deref (.findInternedVar *ns* 'x))
1
user=> x
1
两者并不完全相等。因为求值器会对所有符号进行操作,包括 dref 和 *ns*。
引用的问题在于你可以绕过这个机制,直接获得普通符号。就像 `#'` 阅读器宏获取纯变量一样,`和'`阅读器宏将分别获得带或不带名称空间限定的纯符号。
user=> 'x
x
user=> `x
user/x

7

用户=> (def l `(~a 1 2))

在这里,~将符号a解析为其对应的变量,并且反引号使取消引用工作。

一般来说,您必须理解变量(绑定到某些内容)和符号(永远不会绑定到任何内容)之间的区别。

我会尝试解释它(希望我的解释不会让您更加困惑):

user=> (def v "content")
#'user/content

-> 在当前命名空间下使用符号“v”(完全限定为“user/v”,假设这是当前命名空间)定义一个变量,并将其(变量,而不是符号)绑定到对象“content”。

user=> v
"content"

-> 将 v 解析为变量,并获取绑定的值

user=> #'v
#'user/v

-> 解析为变量本身

user=> 'v
v

->并不解决任何东西,只是一个普通符号(不幸的是,REPL没有显示这一点,打印“v as v”)

user=> `v
user/v

-> 正如您已经引用的那样,解析为当前上下文(命名空间)中的符号,但结果仍然是一个符号(完全限定),而不是变量user/v。

user=> '(v)
(v)

-> 纯引用,不解决任何问题

user=> `(v)
(user/v)

-> 语法引用,与引用相同,但将符号解析为命名空间限定符号

user=> `(~v)
("content")

- > 将符号解析为其变量(隐式取消引用),从而产生其绑定对象

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