我一直对Clojure中的符号和变量有些困惑。
例如,可以说+是一个符号,用于表示一个变量,该变量指向一个能够相加数字的函数值吗?
那么,当我在REPL中输入“+”时,会发生什么呢?
- 该符号会被限定到一个命名空间中,这里是clojure.core。
- 然后,在某个符号表中,会有关于+引用变量的信息。
- 当此变量被求值时,结果是一个函数值?
我一直对Clojure中的符号和变量有些困惑。
例如,可以说+是一个符号,用于表示一个变量,该变量指向一个能够相加数字的函数值吗?
那么,当我在REPL中输入“+”时,会发生什么呢?
有一个符号 +,你可以引用它进行讨论:
user=> '+
+
user=> (class '+)
clojure.lang.Symbol
user=> (resolve '+)
#'clojure.core/+
这个解析结果是 #'+,它是一个 Var 变量:
user=> (class #'+)
clojure.lang.Var
Var 引用该函数对象:user=> (deref #'+)
#<core$_PLUS_ clojure.core$_PLUS_@55a7b0bf>
user=> @#'+
#<core$_PLUS_ clojure.core$_PLUS_@55a7b0bf>
(@符号只是解引用的简写。当然,访问函数的常规方式是不引用该符号:)
user=> +
#<core$_PLUS_ clojure.core$_PLUS_@55a7b0bf>
请注意,词法绑定是一种不同的机制,它们可以屏蔽变量(Vars),但您可以通过显式地引用变量来绕过它们:user=> (let [+ -] [(+ 1 2) (@#'+ 1 2)])
[-1 3]
在最后一个示例中,甚至可以省略解引用操作:user=> (let [+ -] [(+ 1 2) (#'+ 1 2)])
[-1 3]
这是因为Var通过调用自身的deref方法并将结果转换为IFn类型,然后委托函数调用实现了Clojure函数的接口。
当您使用defn-定义私有函数时,可见性机制基于符号的元数据。您可以通过直接引用Var来绕过它,如上文所示:
user=> (ns foo)
nil
foo=> (defn- private-function [] :secret)
#'foo/private-function
foo=> (in-ns 'user)
#<Namespace user>
user=> (foo/private-function)
java.lang.IllegalStateException: var: #'foo/private-function is not public (NO_SOURCE_FILE:36)
user=> (#'foo/private-function)
:secret
let
遮盖。(let [+ -] [(+ 1 1) (clojure.core/+ 1 1)]) => [0 2]
。这说明符号在变量解析之前不会被限定。 - kotarak+
是一个符号,位于 clojure.core
中,默认情况下可以访问您的代码。+
位于列表的头部位置时,Clojure 将尝试调用该函数(如果该变量没有指向一个函数,则会抛出 NullPointerException
)。如果将其作为参数传递给另一个函数,该函数也可能这样调用它。这就是函数调用的工作原理。大多数或所有编程语言都使用符号表。作为一种比较动态的语言,Clojure 使用这个额外的间接层(Symbol → Var → function,而不仅仅是 Symbol → function),以便更容易且更优雅地动态重写哪个函数与哪个符号相关联,并且这有时会引起初学者的好奇心。
正如其他答案过于强调的那样,您还可以执行一些 花哨的操作,例如引用它('+
)以避免其评估,甚至使用 class
和/或 resolve
检查它,就好像您有兴趣验证它是什么(class
)或它位于哪个命名空间中 (resolve
)。您还可以通过 var
或 #'
探查它所指向的变量。如果您编写宏或者在REPL中工作时,可能会进行这些花哨的操作;根据编写宏的方式,您实际上可能会在其中引用很多东西。
Clojure 是一种比较灵活的语言,提供了API,使您可以自己进行 Symbol → Var → function 的遍历。通常情况下,您不会仅仅因为使用函数而这样做,因为显然这将是乏味和冗余的,但是可以在此处使用它来说明该过程:
(deref (resolve '+))
换句话说,符号首先被解析为其变量,然后到达指向该变量的内容。这只是说明了一个到达函数的两个步骤过程(符号→ 变量→ 函数),这些过程发生在幕后。希望您已经避免阅读这个额外的部分。
对于原来的问题,答案很简单:是的。
调用
(ns-map *ns*)
获取命名空间中所有可用符号及其指向的变量的映射
我认为理解符号、函数、字面量和变量之间的区别对于理解正在发生的事情是必要的。
(def one (fn [] 1))
⇒ #'example/one
。它引用了函数#function[example/one]
(def x one)
⇒ #'example/x
引用了函数#function[example/one]
(def y 'one)
⇒ #'example/y
引用了符号one
(def z #'one)
⇒ #'example/z
引用了变量#'example/one
准确地说,one
是一个 符号,它 解析 为 var #'example/one
。该 var 引用 一个 函数,#function[example/one]
,该函数 返回 字面值 1
。
每个 def
生成一个 var。在 #'example/x
中,var 由 #'
语法表示。每个 var 引用一个值。
根据Clojure自己的文档,符号在求值时会被解析为一个值、一个特殊形式或一个错误。因此可能有点令人困惑,因为没有提到 var:
(type one)
⇒ example$one
(type x)
⇒ example$one
(type y)
⇒ clojure.lang.Symbol
(type z)
⇒ clojure.lang.Var
在上述情况中,值为“由符号命名的全局变量绑定的值”。正如另一个答案更简洁地表述的那样:符号→变量→值。
(one)
⇒ 1
(x)
⇒ 1
(y)
⇒ 无关的关联查找错误(z)
⇒ 1
请注意:#'example/z
引用了 var #'example/one
。 #'example/x
和 #'example/y
都引用了函数 #function[example/one]
。 当将原始符号 interned 到新函数时,此差异的意义就会显现出来:(def one (fn [] 2))
。
(one)
⇒ 2
← 新值(x)
⇒ 1
← 原始值(y)
⇒ 无关联的 associative lookup 错误(z)
⇒ 2
← 新值按照符号 → 变量 → 值的逻辑:
x
→ (var x)
→ (defn one [] 1)
x
解析为 (var x)
(“由符号命名的全局变量”)(var x)
解引用 x
的当前绑定:(fn [] 1)
(“由符号命名的全局变量的绑定”)z
→ (var z)
→ (var one)
.
z
解析为其变量,该变量解引用为 (var one)
one
解析为其变量,该变量解引用为 (fn [] 2)
#'example/one
仍然会评估并返回一个文字值(1
或2
),因为该变量位于列表的第一个位置((z)
)。这种行为类似于在列表的第一个位置放置关键字和在第二个位置放置映射表的行为:(:two {:one 1 :two 2})
⇒ 2
。