Clojure:没有cons单元格

15

我听说Clojure不像大多数Lisp语言一样具有cons单元格

这是否意味着Clojure列表不以空列表结尾?

有人能解释一下这到底是什么意思吗?


Lisps使用cons在列表前构建(扩展)列表,但是(从我在Rich Hickey的YouTube视频中看到的)Clojure使用conj在数组后面构建其支持列表 - Will Ness
4个回答

27
Lisp提供了一个原始的cons数据结构及其标记法。
请参见约翰·麦卡锡, 《符号表达式的递归函数及其机器计算》第一部分,1960年,第三章,符号表达式的递归函数
该章节介绍了:
Symbolic expressions由原子和使用点符号表示的符号表达式对组成:( a . b ) 。 使用列表符号简写某些符号表达式:(a b c) 。 使用原子符号nil终止列表。 基本函数包括:carcdrconseqatom。 还有其他几个函数:ffsubstequalnullcadrcaddrnullappendamongpairassocsublisapplyeval等。
早在Lisp语言中,就添加了用于改变cons单元的函数:rplaca(表示替换car)和rplacd(表示替换cdr)。请参阅1962年John McCarthy等人的LISP 1.5程序员手册。这些函数允许我们编写破坏性函数,并允许我们创建基于循环cons的数据结构,例如循环列表。

通用Lisp

通常Lisp方言实现大部分功能。通用Lisp也不例外,该功能在其标准中描述:Conses。下面是使用上述函数的示例:

; pair two lists into a list of cons cells.
; the function pair is called pairlis in Common Lisp.
CL-USER 17 > (pairlis '(john mary eva) '(34 29 40))
((EVA . 40) (MARY . 29) (JOHN . 34))

; find a cons cell in a list of cons cells,
; based on the content of the car of those cons cells
CL-USER 18 > (assoc 'eva (pairlis '(john mary eva)
                                  '(34 29 40)))
(EVA . 40)

; create a tree out of cons cells and atoms
CL-USER 19 > (cons (cons 10 20) (cons 30 40))
((10 . 20) 30 . 40)

; a cons cell is not an atom
CL-USER 20 > (atom (cons 1 2))
NIL

; a cons cell is not nil
CL-USER 21 > (null (cons 1 2))
NIL

; substitute an item with a new one in a tree
CL-USER 22 > (subst 30                          ; new
                    'bar                        ; old
                    '((10 . 20) . (bar . 40)))  ; tree
((10 . 20) 30 . 40)   ; also written as  ((10 . 20) . (30 . 40))

; substitute several items in a tree, using an assoc list
; to describe the substitutions
CL-USER 23 > (sublis '((a . 10) (d . 40))      ; substitutions
                     '((a . b) . (c . d)))     ; tree
((10 . B) C . 40)

列表是符号表达式的一种特殊情况。它们通常不带点写成:

CL-USER 24 > '(a . (b . nil))
(A B)

Common Lisp也支持Lisp 1.5的可变操作rplacarplacd

CL-USER 25 > (let ((c (cons 0 1)))              ; create a cons
               (print c)                        ; print it
               (print (rplaca c 'foo))          ; replace the car
               (print (rplacd c 'bar))          ; replace the cdr
               (print (eq c (rplaca c 'baz)))   ; identical ?
               (values))
(0 . 1)      ; the cons cell
(FOO . 1)    ; car replaced
(FOO . BAR)  ; cdr replaced
T            ; still the same object

Emacs Lisp

Emacs Lisp也实现了上述功能:

ELISP> (sublis '((a . 10) (d . 40))                                             
               '((a . b) . (c . d)))
((10 . b) c . 40)

Clojure

Clojure不支持John McCarthy所描述的这些符号表达式。它没有cons单元,没有点表示法,并且不提供上述接口。例如,在Clojure中,atom表示完全不同的东西。cons不创建cons单元。列表不是由cons单元组成的。

在Clojure中,点只是另一个符号:

user=> (count '(1 . 2))
3

有一个原始函数用于构建列表

user=> (list 1 2 3)
(1 2 3)

结果应该是一个列表:
user=> (list? (list 1 2 3))
true

有一个名为cons的函数:
user=> (cons 0 (list 1 2 3))
(0 1 2 3)

不知何故这不是一个列表:

user=> (list? (cons 0 (list 1 2 3)))
false

基本上,Clojure使用不同的数据结构(-> 序列, 逻辑列表),具有自己的命名和语义。即使名称类似于Lisp名称,也不要期望它们执行相同的操作。

Scheme

编程语言Scheme也提供类似上述的cons单元。它缺少一些函数,但是可以很容易地实现它们。例如,sublis在Scheme中可以像这样实现(见initdr.scm):

(define (sublis alist tree)
  (if (pair? tree)
      (cons (sublis alist (car tree))
            (sublis alist (cdr tree)))
      (if (assv tree alist)
          (cdr (assv tree alist))
          tree)))

7
根据 clojure.org 上的此页面

consfirstrest 操作序列抽象,而不是具体的 cons 单元。

Clojure 列表不以空列表结尾,也不是传统的 cons 单元。它们是实现序列的数据结构。这篇关于编写抽象代码的文章解释了 Clojure 对“seqable”结构(包括列表)的处理方式:

一般来说,编写抽象代码可以让您使用各种不同的数据结构库函数,而不用管这些数据结构的实现方式如何。

因此,Clojure 列表类似于 cons 单元,因为它们实现了 consfirstrest,但这仅表示它们共享相同的接口。它们的底层实现不同,但它们都是“seqable”的。


7
  • Clojure有一个cons结构:clojure.lang.Cons
  • 它用于cons调用的结果。
  • ......仅此而已:既不是列表,也不是向量,也不是任何类型的懒惰序列。
  • 它也不能用于一般对象的对(pair):尾部/rest/cdr是一个序列,而不是一个Object
  • 如果你在列表、向量或懒惰序列上使用cons,你将得到一个Cons
  • 但正如其他回答所明确说明的那样,没有处理Cons的函数。它们都处理一般序列。

另外一个用途:对一个不确定的序列(既不是向量、列表、集合、字典),进行conj操作会产生一个Cons


1
在Common Lisp中,列表是一系列cons单元的序列。每个cons单元有两个插槽或指针,称为“car”和“cdr”。car指向(或持有)任何东西。cdr通常指向另一个cons单元或nilnil表示列表的末尾。Clojure通过其列表提供了大致相同的功能,但底层表示不同。它确实有一个名为Cons的数据类型,但并非所有列表或给定列表的所有部分都是由Cons构建的。(如果您还没有阅读jmargolisvt的答案,请现在阅读。)[编辑:其他答案表明,我关于Clojure中列表和Cons之间关系的说法是不正确的。在“列表”的非正式意义上,人们可能会觉得它是正确的-或者不是。]
此外,请注意,部分原因是由于序列抽象思想,在Clojure中,列表本身比Common Lisp或Scheme中要少得多。但是,其他类型的序列非常常见。
值得一提的是,在Clojure中,您不能假设打印时看起来像列表的东西实际上是一个列表。例如,它可能是一个惰性序列,而不被视为列表。
以下是一些使用列表的潜在信息丰富的Clojure示例:
user=> (def foo (list 1))
#'user/foo
user=> foo
(1)
user=> (class foo)
clojure.lang.PersistentList
user=> (def bar (cons 2 foo))
#'user/bar
user=> bar
(2 1)
user=> (class bar)
clojure.lang.Cons

(即使 class 返回不同的数据类型,foo 和 bar 都被视为列表。)
user=> (next bar)
(1)
user=> (rest bar)
(1)
user=> (class (next bar))
clojure.lang.PersistentList
user=> (class (rest bar))
clojure.lang.PersistentList
user=> (next foo)
nil
user=> (rest foo)
()
user=> (= nil ())
false
user=> (rest ())
()
user=> (rest nil)
()
user=> (next ())
nil
user=> (next nil)
nil

在Common Lisp中,您可以将一个对象连接到除列表或nil之外的另一个对象上。结果是一个“点对列表”(1 . 2),它是一个单一的cons单元,其中cdr指针指向除了另一个cons单元或nil之外的其他内容,就像在正常列表中一样。让我们在Clojure中尝试一下:
user=> (cons 1 2)
IllegalArgumentException Don't know how to create ISeq from: java.lang.Long  clojure.lang.RT.seqFrom (RT.java:528)

顺便说一下,与Common Lisp相比(其中nil= ()= false),还有一个显著的区别:

user=> (= nil false)
false
user=> (= () false)
false

然而,虽然nil不是false,但你可以像使用false一样使用它:

user=> (if nil "nil works like true" "nil works like false")
"nil works like false"

然而,您无法使用空列表执行此操作:

user=> (if () "() works like true" "() works like false")
"() works like true"

尽管存在这些例子,总的来说,Clojure比Common Lisp要简单和更优雅,个人认为。即使像我一样也喜欢Common Lisp的人们,必须承认Common Lisp既不简单也不优雅。它有自己的美感。

1
这有点令人困惑,因为Common Lisp实际上有一个“序列”数据类型。例如,空列表和cons单元是“序列”类型。像字符串这样的向量也是如此。 - Rainer Joswig
@RainerJoswig,尽管我非常喜爱CL,但我知道你比我更懂它。在CL中,sequence类型比list更广泛吗?所以我所称呼的list实际上是一个sequence?(那会让我感到困惑!)例如,向量也是sequence吗?因此,也许序列概念在两种语言中的作用是类似的? - Mars
标准CL中的序列是对列表和各种向量的抽象。有许多操作适用于序列,包括列表和向量。 - Rainer Joswig
3
闭包是如此简单而优雅...以至于(cons 1 2)会因为某些Java级别的错误而崩溃,并指向Java代码中的行号。太棒了! - Kaz

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