【问题标题】:Why does Clojure distinguish between symbols and vars?为什么 Clojure 区分符号和变量?
【发布时间】:2012-07-24 14:43:56
【问题描述】:

我已经看到this question,但它并不能解释我在想什么。

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

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

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

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

【问题讨论】:

  • 并非所有符号都被评估为 var。例如符号String 计算为一个类。
  • 还要注意,符号在编译时解析,而变量在运行时存在。
  • @AlexTaggart,很好——我想知道这是否是正确的答案。
  • @amalloy,这可能是真的 - 但它也可以很容易地成为另一种方式。这并不能解释 为什么 Clojure 的设计者选择与以前的 Lisps 决裂。
  • @amalloy 对于编译时与运行时的注释,其背后的想法是符号的存在是为了灵活和可读的编译时操作,而变量的存在是为了运行时速度?还是我只是误解了?

标签: clojure lisp symbols


【解决方案1】:

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

【讨论】:

  • 我的问题不是什么 Clojure 和CL 的“符号”思想有什么区别,而是为什么 Clojure 是这样设计的。这种设计能实现什么,如果将符号和变量组合成一个类型,这是不可能的?
【解决方案2】:
(ns a)

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


(ns b)

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

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

【讨论】:

  • 所以符号本质上是变量的每个命名空间哈希表的键? +1 用于解释这一点。不过,我的问题是: 这种设计有什么好处?
【解决方案3】:

我从您的帖子中推测出以下问题(告诉我我是否偏离了基础):
为什么将符号映射到它们的基础值有两个间接级别? p>

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

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

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

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

【讨论】:

  • 这里和 CL 没有区别:CL-USER> (defvar my-code `(foo 1 2)) MY-CODE CL-USER> my-code (FOO 1 2) CL-USER> (评估我的代码);在: FOO 1 ; (Foo 1 2) ; ;抓住风格警告:;未定义函数:FOO; ;编译单元完成;未定义函数:;福;捕获 1 个样式警告条件;评估在 # 上中止。 CL-USER> (defun foo (a b) (+ a b)) FOO CL-USER> (eval my-code) 3
【解决方案5】:

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

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

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

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

【讨论】:

  • 感谢您的意见,但这仍然不能回答问题。为什么?因为附加到 Var 的每个线程的数据允许它在每个线程的基础上被重新绑定,理论上 可以 直接附加到一个符号。
【解决方案6】:

在对这个问题进行了深思熟虑之后,我可以想到区分符号和变量的几个原因,或者正如 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”的变量的任何引用。它只是像现在一样捕获 value。如果您稍后重新定义my-func,则更改不会“传播”到使用my-func 的值定义的其他实体。

即使在这种情况下,通过使用#'my-func,您也可以显式请求在每次调用派生函数时查找my-func 的当前值。 (大概是以性能下降为代价的。)

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

【讨论】:

    【解决方案7】:

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

    符号就是名称

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

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

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

    但是为什么呢?

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

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

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

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

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

    【讨论】:

    • 非常好。我对是否接受这个或我自己的答案存在分歧。
    • 这是一个很好的答案,但对我来说并没有解释为什么 Clojure 选择了与 Common Lisp 不同的路线。
    • If extend the earlier example and say (def y x), I haven't created a new var, I've just given my existing var a second name. -- 这是不正确的; x 和 y 是两个不同的变量,而不是一个有两个名称的变量。
    • @phyzome 这在技术上是正确的,但我认为这实际上没有任何区别。这对用户是不可见的。
    • @JAtkin 如果他们使用 alter-var-rootbinding 或其他 var 操作,它不是不可见的。它们是完全独立的变量。
    【解决方案8】:

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

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

    【讨论】:

      猜你喜欢
      • 2012-02-25
      • 2010-10-11
      • 2011-04-05
      • 1970-01-01
      • 2014-09-16
      • 2012-07-24
      • 2011-03-20
      • 2011-03-05
      • 2011-04-08
      相关资源
      最近更新 更多