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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么Clojure区分符号和变量?相关的知识,希望对你有一定的参考价值。

我已经看过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,或动态...),但这些属性不能简单地应用于符号本身吗?

答案

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

符号是名称

与大多数编程语言不同,Clojure区分事物和事物的名称。在大多数语言中,如果我说像var x = 1这样的话,那么说“x是1”或“x的值是1”是正确和完整的。但是在Clojure中,如果我说(def x 1),我做了两件事:我创建了一个Var(一个值保持实体),并且我用符号x命名它。说“x的值是1”并不能说明Clojure中的整个故事。更准确(尽管很麻烦)的陈述是“由符号x命名的var的值是1”。

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

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

但为什么?

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

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

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

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

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

另一答案

在仔细考虑这个问题之后,我可以想到区分符号和变量的几个原因,或者正如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中,列表/矢量/等。都是不可变的,所以你需要某种方式明确地引用“可变的东西”。

另一答案
(ns a)

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


(ns b)

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

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

另一答案

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

当我第一次回答这个问题时,过了一段时间我想出了两个可能的原因:飞行中的“重新定义”以及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

另一答案

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

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

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

另一答案

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

另一答案

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

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

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

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

另一答案

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

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

以上是关于为什么Clojure区分符号和变量?的主要内容,如果未能解决你的问题,请参考以下文章

025_为什么JavaScript变量以美元符号开头?

类型提示存储为clojure中的元数据?

PHP的基础语法简单总结

Java编程中如何区分常量和变量?

如何在 Javadoc 中使用 @ 和 符号格式化代码片段?

自定义变量