为什么Clojure区分符号和变量?

39

我已经看过这个问题,但它没有解答我所困惑的。

当我从Common Lisp转到Clojure时,我很疑惑为什么Clojure将符号和关键字视为不同类型,但后来我明白了,现在我认为这是一个很好的想法。现在我正在试图弄清楚为什么符号和变量是分开的对象。

据我所知,通常情况下,Common Lisp实现使用一个结构来表示“符号”,其中包含1)名称的字符串,2)在函数调用位置求值时该符号的值的指针,3)在调用位置之外求值时该符号的值的指针,以及4)属性列表等。

忽略Lisp-1 / Lisp-2的区别,事实仍然是,在CL中,“符号”对象直接指向其值。换句话说,在CL中,将Clojure称为“符号”的内容和“变量”合并在单个对象中。

在Clojure中,要评估符号,首先必须查找相应的变量,然后必须对变量进行解引用。为什么Clojure要这样做呢?这种设计有什么好处?我知道变量有某些特殊属性(它们可以是私有的、常量的、动态的...),但这些属性难道不能直接应用于符号本身吗?


1
并非所有符号都会被解析为变量。例如,符号String会被解析为一个类。 - Alex Taggart
2
请注意,符号在编译时解析,变量在运行时存在。 - amalloy
@amalloy,那可能是真的——但也有可能是另一种方式。这并不能解释为什么Clojure的设计者选择与以前的Lisp不同。 - Alex D
我认为这是Lisp-1的结果,乘以需要具有Java互操作性(因为Java不是单命名空间语言)。 - Vsevolod Dyomkin
我曾经对它将符号和关键字视为不同类型感到困惑,但后来我弄明白了,现在我认为这是一个很棒的想法。非常晚的跟进,但你能详细说明或指向一个来源吗? - Tianxiang Xiong
显示剩余3条评论
8个回答

58

其他问题已经谈到了符号的许多真实方面,但我将尝试从另一个角度解释它。

符号就是名称

与大多数编程语言不同,Clojure区分事物事物的名称。在大多数语言中,如果我说类似于var x = 1这样的东西,那么正确且完整地说“x是1”或“x的值为1”就足以了。但是在Clojure中,如果我说(def x 1),我做了两件事情:我创建了一个Var(一个持有值的实体),并用符号x 命名它。在Clojure中说“x的值为1”并没有完全说明问题。更准确(虽然麻烦)的陈述应该是“由符号x命名的var的值为1”。

符号本身只是名称,而vars是持有值的实体,它们本身没有名称。如果扩展前面的例子,我说(def y x),我没有创建一个新的var,我只是给我的现有var起了第二个名称。这两个符号xy都是相同var的名称,该var的值为1。

一个类比:我的名字是“Luke”,但那不等同于我作为一个人。这只是一个词。在某些时候,我可以改变我的名字,还有许多其他人共享我的名字。但在我的朋友圈(在您的命名空间中),单词“Luke”表示我。在幻想的Clojure世界中,我可能是一个为您携带值的var。

但为什么?

那么,为什么会有这个额外的名称概念,而不是像大多数语言那样将两者混为一谈?

首先,不是所有的符号都绑定到变量。在本地上下文中,比如函数参数或let绑定中,您代码中所指的符号引用的值实际上根本就不是变量 - 它只是一个本地绑定,在编译器处理时将被优化并转换为原始字节码。

但最重要的是,这是Clojure的“代码即数据”的哲学的一部分。代码行(def x 1)不仅仅是一个表达式,它还是数据,具体来说是由值defx1组成的列表。这非常重要,特别是对于操作代码数据的宏来说。

但是,如果(def x 1)是一个列表,那么列表中的值是什么?特别是这些值的类型是什么?显然1是一个数字。但是defx呢?当我将它们作为数据进行操纵时,它们是什么类型?答案是,它们是符号。

这就是符号在Clojure中成为独立实体的主要原因。在某些上下文中,例如宏中,您想要获取名称并操作它们,使其与运行时或编译器授予的任何特定含义或绑定分离。而名称必须是某种东西,它们所代表的东西是符号。


非常好的表述。我对是否接受这个答案或我的答案感到犹豫。 - Alex D
1
这是一个很好的答案,但对我来说并没有澄清为什么Clojure选择了与Common Lisp不同的路线。 - Mars
2
如果扩展之前的例子并说(def y x),我没有创建一个新的变量,我只是给我的现有变量第二个名称。这是不正确的;x和y是两个不同的变量,而不是一个带有两个名称的变量。 - treat your mods well
@phyzome 这在技术上是正确的,但我认为这并没有任何区别。这对用户来说是不可见的。 - J Atkin
有人说得很正确——“如果你不能用简单的语言解释它,那就说明你对它的理解还不够深刻。” 这是最好的答案!感谢@levand。 - Deepak Tatyaji Ahire
显示剩余2条评论

11

在认真思考这个问题后,我想到了几个区分符号和变量的理由,或者像Omri所说的那样,使用“将符号映射到其基础值的两个间接层级”。我将保留最好的理由到最后...

1:通过将“变量”和“可以引用变量的标识符”这两个概念分离,Clojure 在概念上变得更加清晰。在 CL 中,当阅读器看到a时,它会返回一个符号对象,该对象携带指向顶级绑定的指针,即使a在当前作用域中局部绑定(在这种情况下求值器也不会利用这些顶级绑定)。在 Clojure中,符号只是一个标识符,没有其他含义。

这与一些张贴者提出的观点有关,在Clojure中符号也可以引用Java类。如果符号携带绑定信息,那么在符号引用Java类时,这些绑定信息就可以被忽略,但这在概念上会很混乱。

2:在某些情况下,人们可能希望将符号用作映射键等。如果符号是可变对象(如CL中),它们将无法很好地适配Clojure的不可变数据结构。

3:在(可能很少见的)情况下,符号用作映射键等,并且甚至通过API返回,Clojure中符号的相等语义比CL更直观。 (请参见@amalloy的回答。)

4:由于Clojure强调函数式编程,大量工作使用高阶函数如 partialcompjuxt等进行。即使您没有使用这些功能,您也可能将函数作为参数传递给自己的函数等。

现在,当你将my-func传递给一个高阶函数时,它并没有保留对被称为"my-func"的变量的任何引用。它只捕获当前的。如果你稍后重新定义my-func,这个改变不会"传播"到使用my-func值定义的其他实体。

即使在这种情况下,通过使用#'my-func,你可以明确请求每次调用派生函数时都要查找my-func的当前值。(可能会稍稍降低性能。)

在Common Lisp或Scheme中,如果我需要这种间接引用,我可以想象将一个函数对象存储在cons、vector或struct中,并在每次调用时从那里检索它。实际上,每当我需要一个可在代码不同部分之间共享的"可变引用"对象时,我都可以使用cons或其他可变结构。但在Clojure中,列表/向量等都是immutable的,因此你需要一些方法来明确地引用"可变的东西"。


9
主要好处是它是一个额外的抽象层,在各种情况下都非常有用。
举个具体例子,符号可以在引用它们的变量创建之前就存在。
(def my-code `(foo 1 2))     ;; create a list containing symbol user/foo
=> #'user/my-code

my-code                      ;; confirm my-code contains the symbol user/foo
=> (user/foo 1 2)

(eval my-code)               ;; fails because user/foo not bound to a var
=> CompilerException java.lang.RuntimeException: No such var: user/foo...

(def foo +)                  ;; define user/foo
=> #'user/foo

(eval my-code)               ;; now it works!
=> 3

就元编程而言,它的好处是显而易见的 - 在实例化并在完全填充的命名空间中运行代码之前,您可以构造和操作代码。


4
这里和CL没有区别: CL-USER> (defvar my-code `(foo 1 2)) MY-CODE CL-USER> my-code (FOO 1 2) CL-USER> (eval my-code); in: FOO 1 ; (FOO 1 2) ; ; caught STYLE-WARNING: ; undefined function: FOO ; ; compilation unit finished ; Undefined function: ; FOO ; caught 1 STYLE-WARNING condition ; Evaluation aborted on #<UNDEFINED-FUNCTION FOO {10174E4A93}>. CL-USER> (defun foo (a b) (+ a b)) FOO CL-USER> (eval my-code) 3 - Vsevolod Dyomkin

7
(ns a)

(defn foo [] 'foo)
(prn (foo))


(ns b)

(defn foo [] 'foo))
(prn (foo))

符号 foo 在这两个上下文中是完全相同的(即(= 'foo (a/foo) (b/foo))为真),但在这两个上下文中它需要携带不同的值(在这种情况下,指向两个函数中的一个的指针)。

1
所以符号本质上是变量的命名空间哈希表中的键?感谢您解释这一点。不过我的问题是:这种设计有什么好处? - Alex D

6
我从您的帖子中总结出以下问题(如果我理解有误请告诉我):
为什么需要两个间接级别将符号映射到其底层值?

起初我想回答这个问题,一段时间后我想出了两个可能的原因:"即时重新定义"和相关的动态作用域概念。然而,以下内容让我相信这两个都不是使用双重间接的原因:
=> (identical? (def a 0) (def a 10))
=> true

=> (declare ^:dynamic bar)
=> (binding [bar "bar1"]
     (identical? (var bar)
                 (binding [bar "bar2"]
                   (var bar))))
=> true

在我的看法中,这表明“重新定义”和动态作用域都不会改变名称空间限定符号与其所指向的变量之间的关系。

此时,我要提出一个新问题:
名称空间限定符号是否总是与其所引用的变量同义?

如果答案是肯定的,那么我就不明白为什么需要另一层间接性。

如果答案是否定的,那么我想知道在同一程序运行期间,名称空间限定符号会在什么情况下指向不同的变量。

总之,很棒的问题 :P


1
你完全理解了我的问题。我想我可能已经自己找到了答案……但在发布之前,我需要进行更多的研究来确认我的答案是否正确。 - Alex D

2
奇怪的是没有人提到这一点,虽然有很多原因可以解释这种变量间接引用,但其中一个重要原因是在REPL中开发时可以在运行时更改引用。因此,您可以在修改程序时看到运行程序中的更改效果,这允许一种具有即时反馈的开发风格(或类似于现场编码的东西)。
这位先生比我讲得更好:https://www.youtube.com/watch?v=8NUI07y1SlQ(尽管这个问题发布后将近两年)。他还讨论了一些性能影响,并给出了一个示例,其中这个额外的间接引用会导致大约10%的性能损失。不过考虑到您可以获得的收益,这并不算太糟糕。最大的惩罚是以额外堆使用和长时间的Clojure启动时间的形式出现,这仍然是一个很大的问题。

1

2
我的问题不是Clojure和CL关于“符号”的区别在哪里,而是为什么Clojure被设计成这样。这种设计有什么优势,如果符号和变量被合并成一个类型,就无法实现什么? - Alex D

1

Clojure是我目前为止唯一的lisp语言,所以这个回答有些猜测。尽管如此,来自clojure网站的以下讨论似乎是相关的(重点在于):

Clojure是一种实用的语言,它认识到偶尔需要维护对变化值的持久引用,并提供4种不同的机制以受控方式进行操作 - Vars、Refs、Agents和Atoms。Vars提供了一种机制,用于引用可动态重新绑定的可变存储位置(在每个线程的基础上)。每个Var都可以(但不必)具有根绑定,即所有没有每线程绑定的线程共享的绑定。因此,Var的值是其每线程绑定的值,或者如果未在请求值的线程中绑定,则为根绑定的值,如果有的话。

因此,将符号间接引用到Vars允许线程安全的动态重新绑定(也许可以用其他方式完成,但我不知道)。我认为这是核心Clojure哲学的一部分,严格和普遍地区分身份和状态以实现强大的并发性。

我怀疑这个功能只有在极少数情况下才能真正受益,如果重新思考问题以不需要线程特定的动态绑定,则更好,但如果需要,它就在那里。


感谢您的回复,但这仍然没有回答问题。为什么?因为附加到Var的每个线程数据允许在每个线程基础上重新绑定,理论上可以直接附加到符号上。 - Alex D

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