引号和列表有什么区别?

64

我知道你可以使用'(也称为quote)来创建一个列表,我经常使用它,就像这样:

> (car '(1 2 3))
1

但它并不总是像我期望的那样运行。例如,我尝试创建一个函数列表,就像这样,但它没有起作用:

> (define math-fns '(+ - * /))
> (map (lambda (fn) (fn 1)) math-fns)
application: not a procedure;
  expected a procedure that can be applied to arguments
  given: '+

当我使用list时,它工作正常:

> (define math-fns (list + - * /))
> (map (lambda (fn) (fn 1)) math-fns)
'(1 -1 1 1)

为什么?我原以为 ' 只是一个方便的简写,那么为什么它的行为不同呢?


3
(供参考:我创建了这个作为一种标准的重复目标,以解决这种混淆。我经常看到这些问题出现。) - Alexis King
一个仍然可以使用引用的方法: (define math-fns (map (lambda (s) (lambda args (eval (s . args) (environment '(rnrs))))) '(+ - * /))) - bipll
2
作为额外说明:您也不能在使用引用创建的列表上执行 set-car!set-cdr! 操作。 - mihai
2个回答

143

TL;DR: 它们是不同的,当有疑问时请使用 list

一个经验法则:每当需要求值参数时,请使用 listquote "分发" 其参数,因此 '(+ 1 2) 等价于 (list '+ '1 '2)。你将得到列表中的符号而非函数。


listquote 的深入解析

在Scheme和Racket中,quotelist完全不同的事物,但由于两者都可以用于生成列表,因此混淆是常见且可以理解的。它们之间有一个非常重要的区别:list 是一个普通的函数,而 quote (即使没有特殊的 ' 语法)也是一个特殊形式。也就是说,list 可以在普通的Scheme中实现,但 quote 不能。

list 函数

list 函数实际上是两者中更简单的一个,因此我们从这里开始。它是一个接受任意数量参数的函数,并将这些参数收集到一个列表中。

> (list 1 2 3)
(1 2 3)

上面的示例可能会让人感到困惑,因为结果是以可以引用的s表达式形式打印出来的,而在这种情况下,两种语法是等价的。但是如果我们稍微复杂一些,你会发现它是不同的:

> (list 1 (+ 1 1) (+ 1 1 1))
(1 2 3)
> '(1 (+ 1 1) (+ 1 1 1))
(1 (+ 1 1) (+ 1 1 1))

quote示例中发生了什么?稍后我们将讨论这个问题,但首先看一下list。它只是一个普通的函数,因此它遵循标准的Scheme评估语义:在它们传递给函数之前,它会对它的每个参数进行求值。这意味着像(+ 1 1)这样的表达式将在被收集到列表之前被简化为2

当向列表函数提供变量时,也可以看到这种行为:

> (define x 42)
> (list x)
(42)
> '(x)
(x)

使用list时,x会在传递给list之前被求值。而使用quote则更加复杂。

最后,由于list只是一个函数,它可以像其他任何函数一样使用,包括以高阶方式使用。例如,它可以被传递给map函数,并且将正常工作:

> (map list '(1 2 3) '(4 5 6))
((1 4) (2 5) (3 6))

quote表单

引用(Quotation)是Lisps中的一种特殊形式,不同于listquote表单很特殊,部分原因是它获取了一个特殊的读取器缩写('),但即使没有这个缩写也是很特殊的。与list不同,quote不是一个函数,因此它不需要像函数一样运行——它有自己的规则。

Lisp源代码的简要讨论

在Lisp中(包括Scheme和Racket),所有代码实际上都由普通数据结构组成。例如,请考虑以下表达式:

(+ 1 2)

这个表达式实际上是一个列表,并且它有三个元素:

  • +符号
  • 数字1
  • 数字2

所有这些值都是程序员可以创建的普通值。创建1值非常容易,因为它评估为自身:只需键入1即可。但符号和列表更难:默认情况下,源代码中的符号执行变量查找!也就是说,符号不是自我评估的:

> 1
1
> a
a: undefined
  cannot reference undefined identifier

事实证明,符号基本上只是字符串,实际上我们可以在它们之间进行转换:

> (string->symbol "a")
a

列表比符号更为强大,因为默认情况下,在源代码中,列表会调用一个函数!执行(+ 1 2)时,会查找列表中的第一个元素,即+符号,并查找与之关联的函数,然后用列表中的其余元素调用该函数。

不过有时候您可能想禁用这种“特殊”行为。您可能希望只获取列表或获取符号而不进行求值。为此,可以使用quote

引用的含义

考虑到所有这些,quote的作用就很明显了:它仅“关闭”了封装表达式的特殊求值行为。例如,考虑对符号进行quote

> (quote a)
a

同样地,考虑引用一个列表:quote

> (quote (a b c))
(a b c)
无论您传递给quote什么,它都会始终、永远地将其原样返回。没有更多,也没有更少。这意味着如果您传递一个列表,那么其中任何子表达式都不会被评估——请不要期望它们会被评估!如果您需要任何类型的计算,请使用list
现在,你可能会问:如果你给quote引用除符号或列表之外的其他东西会发生什么?好吧,答案是......没什么!你只是拿到了原样返回的结果。
> (quote 1)
1
> (quote "abcd")
"abcd"

这是有道理的,因为quote仍然只会输出你所给出的内容。这就是为什么像数字和字符串这样的“字面量”有时在Lisp术语中被称为“自引用”的原因。

还有一件事:如果您对包含quote的表达式进行quote操作会发生什么?也就是说,如果您“双引号引用”?

> (quote (quote 3))
'3

发生了什么事?好的,记住'实际上只是quote的直接缩写,所以没有任何特殊之处!实际上,如果你的Scheme有一种在打印时禁用缩写的方法,它会像这样:

> (quote (quote 3))
(quote 3)

不要被 quote 欺骗:就像 (quote (+ 1)) 一样,这里的结果只是一个普通的旧列表。实际上,我们可以从列表中获取第一个元素:你能猜出它会是什么吗?

> (car (quote (quote 3)))
quote

如果你猜测的是3,那么你是错误的。记住,quote会禁用所有求值,而包含quote符号的表达式仍然只是一个普通的列表。在REPL中玩弄它,直到你感到舒适。

> (quote (quote (quote 3)))
''3
(quote (1 2 (quote 3)))
(1 2 '3)

引言非常简单,但由于其往往违反传统评估模型的理解而显得非常复杂。事实上,它之所以令人困惑是因为它非常简单:没有特殊情况,也没有规则。它只是精确地返回您给定的内容,就像所述的那样(因此被称为“引言”)。


附录A:准引言

因此,如果引言完全禁用了评估,那么它有何用处呢?除了制作提前知道的字符串、符号或数字列表之外,没有太多用处。幸运的是,准引言的概念提供了一种打破引言并返回普通评估的方法。

基础知识非常简单:不要使用quote,而要使用quasiquote。通常,这在每个方面都与quote完全相同:

> (quasiquote 3)
3
> (quasiquote x)
x
> (quasiquote ((a b) (c d)))
((a b) (c d))

quasiquote之所以特别,是因为它识别一个特殊的符号unquote。在列表中出现unquote的地方,它将被其中包含的任意表达式所替换:

> (quasiquote (1 2 (+ 1 2)))
(1 2 (+ 1 2))
> (quasiquote (1 2 (unquote (+ 1 2))))
(1 2 3)

这使您可以使用quasiquote构建一种具有“空洞”的模板,以便填充unquote。这意味着实际上可以在引用的列表中包含变量的值:

> (define x 42)
> (quasiquote (x is: (unquote x)))
(x is: 42)
当然,使用quasiquoteunquote相当冗长,因此它们有自己的缩写,就像'一样。具体来说,quasiquote`(反引号),而unquote,(逗号)。有了这些缩写,上面的示例就更易于理解了。
> `(x is: ,x)
(x is: 42)

最后一个要点:使用相当复杂的宏,Racket实际上也可以实现quasiquote。它会扩展成对listcons和当然是quote的使用。


附录B:在Scheme中实现listquote

由于“剩余参数”语法的工作方式,实现list非常简单。这是你需要的全部内容:

(define (list . args)
  args)

就是这样!

相比之下,quote则更加困难,事实上它是不可能的!尽管禁用表达式求值的想法听起来很像宏,但一个天真的尝试会揭示出问题:

(define fake-quote
  (syntax-rules ()
    ((_ arg) arg)))
我们只是获取arg并将其拆分,但这样做行不通。为什么呢?因为我们的宏的结果将被评估,所以一切都是徒劳。我们可能能够扩展到类似于quote的东西,通过扩展到(list ...)并递归地引用元素,像这样:
(define impostor-quote
  (syntax-rules ()
    ((_ (a . b)) (cons (impostor-quote a) (impostor-quote b)))
    ((_ (e ...)) (list (impostor-quote e) ...))
    ((_ x)       x)))

不幸的是,如果没有过程宏,我们就无法处理没有quote的符号。使用syntax-case可以更接近,但即便如此,我们只能模拟quote的行为,而不能复制它。


附录 C:Racket 打印约定

在 Racket 中运行本答案中的示例时,您可能会发现它们的输出结果与预期不同。通常情况下,它们可能会以前导'打印,例如这个例子:

> (list 1 2 3)
'(1 2 3)

这是因为Racket默认情况下会尽可能地将结果打印为表达式。也就是说,您应该能够在REPL中输入结果并获得相同的值。我个人认为这种行为很好,但在试图理解引用时可能会令人困惑,因此如果您想关闭它,请调用(print-as-expression #f),或在DrRacket语言菜单中更改打印样式为“write”。


2
非常好的答案,我已经点赞了。但是我不同意DrRacket的默认打印行为。我认为它有三个问题:1)它会让语言学习者产生困惑,就像这个问题和其他一些问题(例如什么是Racket中的'(撇号)?)清楚地显示的那样;2)它会产生无意义的结果(使用(list 1 (λ(x)(+ x 1)) 3)系统会打印出'(1 #<procedure> 3),这是一个准表达式!);3)它与所有其他Scheme实现不同。 - Renzo
3
我对此有不同的看法。也许如果它不是默认设置会更好。当然,我对默认开启的原因知之甚少,所以无法发表评论,但我绝对理解您的观点,并且在某种程度上同意它。 - Alexis King
在“嵌套引用”部分,我们是否应该提供一个嵌套列表的示例?这应该展示'(1 2 '(3))(list 1 2 (list 3))之间的区别,前者可能是错误的,而后者则是正确的。 - Alex Knauth
老实说,我现在正在阅读《Reasoned Schemer》的第一章,但它却突然出现了这样一句话:"因为`(,x)是(cons x '())的简写",而没有任何上下文。这是我能找到的唯一提供一些背景信息的东西了。 - Alper
这比我能找到的任何其他东西都要好。谢谢。 - Alper
但是你为什么要引用任何东西呢? - Alper

6
你看到的行为是Scheme未将符号视为函数的结果。
表达式'(+ - * /)产生一个值,该值是符号列表。这仅仅是因为(+ - * /)本身就是一个符号列表,我们只是引用它来抑制求值,以便将它作为一个字面量值获取。
表达式(list + - * /)生成一个函数列表。这是因为它是一个函数调用。符号表达式list+-*/被求值。它们都是表示函数的变量,因此缩减为这些函数。然后调用list函数,并返回这四个剩余函数的列表。
在ANSI Common Lisp中,将符号作为函数调用可以正常工作:
[1]> (mapcar (lambda (f) (funcall f 1)) '(+ - * /))
(1 -1 1 1)

如果一个符号被用在需要函数的地方,且该符号有顶层函数绑定,那么该符号将被替换为该函数,这样就可以正常运行。实际上,在Common Lisp中,符号是可调用的函数对象。

如果你想像'(+ - * /)一样使用 list 来生成一个符号列表,你需要分别对它们进行引号处理,以抑制它们的求值:

(list '+ '- '* '/)

回到Scheme世界,你会发现如果你对此进行map,它会以与原始引用列表相同的方式失败。原因也是一样的:试图将符号对象用作函数。
你看到的错误消息是误导性的。
expected a procedure that can be applied to arguments
given: '+

这里显示的 '+ 就是 (quote +)。但这不是应用程序所提供的;它只提供了 +,问题在于该方言中符号对象 + 无法用作函数。

这里发生的情况是,诊断消息以“表达式输出”模式打印了 + 符号,这是 Racket 的一个功能,我猜你正在使用它。

在“表达式输出”模式下,对象将使用语法进行打印,必须读取并计算才能生成类似的对象。请参阅 StackOverflow 问题“为什么 Racket 解释器在列表之前加上撇号?


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