Clojure中符号和变量的区别

48

我一直对Clojure中的符号和变量有些困惑。

例如,可以说+是一个符号,用于表示一个变量,该变量指向一个能够相加数字的函数值吗?

那么,当我在REPL中输入“+”时,会发生什么呢?

  1. 该符号会被限定到一个命名空间中,这里是clojure.core。
  2. 然后,在某个符号表中,会有关于+引用变量的信息。
  3. 当此变量被求值时,结果是一个函数值?

5
你似乎相当理解这个 :) - Arthur Ulfeldt
5个回答

69

有一个符号 +,你可以引用它进行讨论:

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

4
补充这个出色的回答:您还可以限定符号来绕过let遮盖。(let [+ -] [(+ 1 1) (clojure.core/+ 1 1)]) => [0 2]。这说明符号在变量解析之前不会被限定。 - kotarak
@kotarak提到的合格符号与let绑定的符号不同的观点很好,但我不明白它与Var解析有什么关系。能详细说明一下吗? - Jouni K. Seppänen
@jouni-k-seppanen 这与问题中的第一个要点有关。我认为你很好地解决了所有其他问题,但这个问题只是略微提及了一下。(或者至少在开始时只是非常含蓄地提到了一下。) - kotarak
很棒的答案,但如果没有一些思维转换,很难理解“解析”、“引用”和“取消引用”之间的语义和区别。 - matanster

8
这个答案跟其他答案并没有太大的不同,只是它并不假设你最初希望学习几个新的函数和概念才能理解发生了什么:
  1. + 是一个符号,位于 clojure.core 中,默认情况下可以访问您的代码。
  2. 在您的代码中使用时,如果没有非常高级的意图(例如引用或查找其类),Clojure 将寻找它所指向的变量。
  3. 如果此变量是一个函数,则当 + 位于列表的头部位置时,Clojure 将尝试调用该函数(如果该变量没有指向一个函数,则会抛出 NullPointerException)。如果将其作为参数传递给另一个函数,该函数也可能这样调用它。这就是函数调用的工作原理。

进一步评论以总结:

大多数或所有编程语言都使用符号表。作为一种比较动态的语言,Clojure 使用这个额外的间接层(Symbol → Var → function,而不仅仅是 Symbol → function),以便更容易且更优雅地动态重写哪个函数与哪个符号相关联,并且这有时会引起初学者的好奇心。

正如其他答案过于强调的那样,您还可以执行一些 花哨的操作,例如引用它('+)以避免其评估,甚至使用 class 和/或 resolve 检查它,就好像您有兴趣验证它是什么(class)或它位于哪个命名空间中 (resolve)。您还可以通过 var#' 探查它所指向的变量。如果您编写宏或者在REPL中工作时,可能会进行这些花哨的操作;根据编写宏的方式,您实际上可能会在其中引用很多东西。

并且对于喜欢探索的人来说,这是一个花哨的插图:

Clojure 是一种比较灵活的语言,提供了API,使您可以自己进行 Symbol → Var → function 的遍历。通常情况下,您不会仅仅因为使用函数而这样做,因为显然这将是乏味和冗余的,但是可以在此处使用它来说明该过程:

(deref (resolve '+))

换句话说,符号首先被解析为其变量,然后到达指向该变量的内容。这只是说明了一个到达函数的两个步骤过程(符号→ 变量→ 函数),这些过程发生在幕后。希望您已经避免阅读这个额外的部分。


简而言之

对于原来的问题,答案很简单:是的。


这真的很有帮助,特别是我应该阅读的那部分。散步有助于理解三部分链。 - mpettis

8
请参阅名称空间文档
名称空间是从简单(未经限定的)符号到变量和/或类的映射。 变量可以使用def或其任何变体在名称空间中进行内部化,在这种情况下,它们具有简单的符号名称和对其所包含的名称空间的引用,并且名称空间将该符号映射到同一变量。 名称空间还可以通过使用refer或use将符号映射到在其他名称空间中内部化的变量,或者通过使用import将符号映射到Class对象。
因此,您的步骤1和2基本上是统一的:名称空间是符号表。
至于步骤3:我喜欢变量的定义,即它们是值名称的组合。 符号是变量的名称,评估它将导致其值。

0

调用

(ns-map *ns*)

获取命名空间中所有可用符号及其指向的变量的映射


它实际上是ns-map。 - Michiel Borkent

0

我认为理解符号、函数、字面量和变量之间的区别对于理解正在发生的事情是必要的。

  1. (def one (fn [] 1))#'example/one。它引用了函数#function[example/one]
  2. (def x one)#'example/x 引用了函数#function[example/one]
  3. (def y 'one)#'example/y 引用了符号one
  4. (def z #'one)#'example/z 引用了变量#'example/one

准确地说,one 是一个 符号,它 解析var #'example/one。该 var 引用 一个 函数#function[example/one],该函数 返回 字面值 1

每个 def 生成一个 var。在 #'example/x 中,var 由 #' 语法表示。每个 var 引用一个值。

根据Clojure自己的文档,符号在求值时会被解析为一个值、一个特殊形式或一个错误。因此可能有点令人困惑,因为没有提到 var:

  1. (type one)example$one
  2. (type x)example$one
  3. (type y)clojure.lang.Symbol
  4. (type z)clojure.lang.Var

在上述情况中,值为“由符号命名的全局变量绑定的值”。正如另一个答案更简洁地表述的那样:符号→变量→值。

  1. (one)1
  2. (x)1
  3. (y) ⇒ 无关的关联查找错误
  4. (z)1

请注意:#'example/z 引用了 var #'example/one#'example/x#'example/y 都引用了函数 #function[example/one]。 当将原始符号 interned 到新函数时,此差异的意义就会显现出来:(def one (fn [] 2))

  1. (one)2 ← 新值
  2. (x)1原始值
  3. (y) ⇒ 无关联的 associative lookup 错误
  4. (z)2新值

按照符号 → 变量 → 值的逻辑:

  • x(var x)(defn one [] 1)
    1. x 解析为 (var x)(“由符号命名的全局变量”)
    2. (var x) 解引用 x 的当前绑定:(fn [] 1)(“由符号命名的全局变量的绑定”)
  • z(var z)(var one).
    1. z 解析为其变量,该变量解引用为 (var one)
    2. one 解析为其变量,该变量解引用为 (fn [] 2)
最后需要注意的是,#'example/one仍然会评估并返回一个文字值(12),因为该变量位于列表的第一个位置((z))。这种行为类似于在列表的第一个位置放置关键字和在第二个位置放置映射表的行为:(:two {:one 1 :two 2})2

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