36

我已经看到了这个问题,但它并没有解释我在想什么。

当我第一次从 Common Lisp 来到 Clojure 时,我很纳闷为什么它把符号和关键字当作不同的类型,但后来我想通了,现在我觉得这是一个绝妙的想法。现在我想弄清楚为什么符号和变量是独立的对象。

据我所知,Common Lisp 实现通常使用一个结构来表示一个“符号”,该结构具有 1)名称的字符串,2)在函数调用位置评估时指向符号值的指针,3)指向其值的指针评估外部呼叫位置,以及 4) 属性列表等。

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

在 Clojure 中,要计算一个符号,首先必须查找相应的 var,然后必须取消引用该 var。为什么 Clojure 会这样工作?这样的设计可能有什么好处?我知道 var 具有某些特殊属性(它们可以是私有的、常量或动态的……),但这些属性不能简单地应用于符号本身吗?

4

8 回答 8

54

其他问题涉及符号的许多真实方面,但我会尝试从另一个角度解释它。

符号是名称

与大多数编程语言不同,Clojure 区分事物和事物的名称。在大多数语言中,如果我说类似 的话var x = 1,那么说“x 为 1”或“x 的值为 1”是正确且完整的。但是在 Clojure 中,如果我说(def x 1),我已经做了件事:我创建了一个 Var(一个价值持有实体),并且我用符号 来命名x它。说“x 的值为 1”并不能说明 Clojure 的全部内容。一个更准确(虽然很麻烦)的陈述是“由符号 x 命名的 var 的值是 1”。

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

一个类比:我的名字是“卢克”,但这与我不同,与我作为一个人的身份不同。这只是一个词。在某个时候我可以更改我的名字并非不可能,而且还有很多其他人共享我的名字。但是在我的朋友圈中(在我的命名空间中,如果你愿意的话),“Luke”这个词的意思是我。在幻想的 Clojure 领域,我可以成为一个为你带来价值的 var。

但为什么?

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

一方面,并​​非所有符号都绑定到变量。在本地上下文中,例如函数参数或 let 绑定,代码中符号引用的值实际上根本不是 var - 它只是一个本地绑定,当它遇到编译器。

不过,最重要的是,它是 Clojure “代码即数据”理念的一部分。这行代码(def x 1)不仅仅是一个表达式,它也是一个数据,特别是一个由值defx和组成的列表1。这非常重要,特别是对于将代码作为数据进行操作的宏。

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

这就是符号在 Clojure 中是一个独特实体的主要原因。在某些情况下,例如宏,您希望获取名称并对其进行操作,与运行时或编译器授予的任何特定含义或绑定分离。名字一定是某种东西,它们是某种东西,就是符号。

于 2012-07-27T02:34:32.250 回答
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都应查找的当前值。(大概是以小幅性能损失为代价的。)

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

于 2012-07-27T01:58:06.700 回答
6
(ns a)

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


(ns b)

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

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

于 2012-07-26T04:04:04.377 回答
6

我从您的帖子中推测了以下问题(如果我不在基地,请告诉我):
为什么将符号映射到它们的基础值有两个间接级别?

当我第一次回答这个问题时,过了一会儿我想出了两个可能的原因:动态“重新定义”,以及相关的动态范围概念。但是,以下内容使我确信这些都不是使用这种双重间接的原因:

=> (identical? (def a 0) (def a 10))
=> true

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

对我来说,这表明“重新定义”和动态范围都不会对命名空间限定符号和它指向的 var 之间的关系产生任何改变。

在这一点上,我要问一个新问题:
命名空间限定符号是否总是与它所指的 var 同义?

如果这个问题的答案是肯定的,那么我根本不明白为什么应该有另一个级别的间接性。

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

总而言之,我想这是个好问题:P

于 2012-07-26T06:30:16.950 回答
6

主要好处是它是一个额外的抽象层,在各种情况下都很有用。

作为一个具体的例子,符号可以在创建它们引用的 var 之前愉快地存在:

(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

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

于 2012-07-26T07:06:23.533 回答
2

奇怪的是没有人提到这一点,但即使这种 var 间接寻址肯定有不止一个原因,一个重要原因是在运行时更改引用的可能性,同时在 repl 中开发。因此,您可以在修改运行程序时看到更改后的效果,这允许具有即时反馈的开发风格(或诸如实时编码之类的东西)。

这家伙解释得比我好得多:https ://www.youtube.com/watch?v=8NUI07y1SlQ (诚然,在这个问题发布近两年后)。他还讨论了一些性能影响,并举了一个例子,这个额外的间接成本大约是 10% 的性能。虽然考虑到你得到的回报,但这并不是那么糟糕。最大的损失是额外的堆使用和较长的 Clojure 启动时间,我认为这仍然是一个大问题。

于 2018-01-01T12:31:33.790 回答
1

对于 Common Lisp 或其他 lisp,请查看:Differences with other Lisps来自http://clojure.org/lisps

于 2012-07-26T04:03:50.550 回答
1

Clojure 是我迄今为止的第一个(也是唯一一个)lisp,所以这个答案是一个猜测。也就是说,clojure 网站上的以下讨论似乎是相关的(强调我的):

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

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

我怀疑与重新考虑不需要线程特定动态绑定的问题相比,这种工具很少(如果有的话)提供真正的好处,但如果你需要它,它就在那里。

于 2012-07-26T14:50:09.200 回答