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

时间:2012-07-26 03:49:33

标签: clojure lisp symbols

我已经看到了this question,但它没有解释我在想什么。

当我第一次从Common Lisp来到Clojure时,我很困惑为什么它将符号和关键字视为单独的类型,但后来我想出来了,现在我觉得这是个好主意。现在我想弄清楚为什么符号和变量是单独的对象。

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

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

在Clojure中,要评估符号,首先必须查找相应的var,然后必须取消引用var。为什么Clojure以这种方式工作?这样的设计可能会带来什么好处?我理解vars有一些特殊属性(它们可以是private,或const,或动态......),但这些属性不能简单地应用于符号本身吗?

8 个答案:

答案 0 :(得分:48)

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

符号是名称

与大多数编程语言不同,Clojure区分了事物的事物名称。在大多数语言中,如果我说var x = 1之类的东西,那么说“x是1”或“x的值是1”是正确和完整的。但是在Clojure中,如果我说(def x 1),我已经完成了两个的事情:我创建了一个Var(一个值保持实体),我已经命名为< / em>它带有符号x。说“x的值是1”并不能说明Clojure中的整个故事。更准确(尽管很麻烦)的陈述是“由符号x命名的var的值是1”。

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

类比:我的名字是“卢克”,但这与我不一样,我是谁。这只是一个词。在某些时候我可以改变我的名字并不是不可能的,还有许多其他人与我的名字相同。但在我的朋友圈(在我的名字空间,如果你愿意)的背景下,“卢克”这个词意味着我。在一个幻想的Clojure-land中,我可能是一个为你带来价值的变种。

但为什么?

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

首先,并非所有符号都与vars绑定。在本地上下文中,例如函数参数或let绑定,代码中的符号引用的值实际上根本不是var - 它只是一个本地绑定,它将被优化掉并在它到达时转换为原始字节码编译器。

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

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

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

答案 1 :(得分:10)

在仔细考虑这个问题后,我可以想到区分符号和变量的几个原因,或者正如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,您也可以明确请求每次调用派生函数时都应查找的当前值。 (大概是以小小的性能损失为代价。)

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

答案 2 :(得分:6)

(ns a)

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


(ns b)

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

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

答案 3 :(得分:6)

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

当我第一次回答这个问题时,过了一段时间我想出了两个可能的原因:动态地“重新定义”以及dynamic scope的相关概念。然而,以下使我确信这些都不是这种双重间接的原因:

=> (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

答案 4 :(得分:5)

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

作为一个具体的例子,符号可以在创建它们引用的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

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

答案 5 :(得分:1)

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

答案 6 :(得分:1)

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

  

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

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

我怀疑这个设施很少提供真正的好处,如果有的话,与重新考虑问题不需要特定于线程的动态绑定相比,但是如果你需要它就会存在。

答案 7 :(得分:1)

奇怪的是,无人机会提到这一点,但即使这个var间接肯定有多个原因,一个重要的原因是在运行时开发时在repl中更改引用的可能性。因此,您可以在修改时看到正在运行的程序中已更改的效果,这允许具有即时反馈的开发风格(或实时编码等)。

这位Guy比我更好地解释:https://www.youtube.com/watch?v=8NUI07y1SlQ(在这个问题发布后差不多两年)。他还讨论了一些性能影响,并给出了一个示例,其中这个额外的间接成本约为10%。考虑到你的回归,这并不是那么糟糕。虽然最大的惩罚来自于额外的堆使用和长时间的Clojure启动时间,这仍然是我认为的一个大问题。