报价单和清单有什么区别?

What is the difference between quote and list?

我知道您可以使用 '(又名 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)

为什么?我以为 ' 只是一个方便的 shorthand,那么为什么行为不同?

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

一条经验法则:只要你想对参数求值就使用listquote 对其参数进行“分配”,因此 '(+ 1 2) 类似于 (list '+ '1 '2)。您最终会在列表中得到一个符号,而不是一个函数。


in-depth 查看 listquote

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

list函数

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

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

上面的例子可能会造成混淆,因为结果打印为 quoteable s-expression,这是真的,在这种情况下,两种语法是等价的。但是如果我们稍微复杂一点,你会发现它是不同的:

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

对于 listx 在传递给 list 之前先求值。使用 quote,事情就更复杂了。

最后,因为 list 只是一个函数,所以它可以像任何其他函数一样使用,包括 higher-order 方式。例如,它可以传递给 map 函数,它会正常工作:

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

quote 形式

list 不同,引用是 Lisp 的一个特殊部分。 quote 形式的特殊部分是因为它有一个特殊的 reader 缩写,',但即使没有它,它也是 特殊的。与 list 不同,quote 不是 函数,因此它不需要表现得像一个函数——它有自己的规则。

简单讨论 Lisp 源代码

在Scheme和Racket派生出来的Lisp中,所有的代码其实都是由普通的数据结构组成的。例如,考虑以下表达式:

(+ 1 2)

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

  • + 符号
  • 人数1
  • 人数2

所有这些值都是普通值,可以由程序员创建。创建 1 值真的很容易,因为它会自行计算:您只需键入 1。但是符号和列表更难:默认情况下,源代码中的符号执行变量查找!即符号不是self-evaluating:

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

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

> (string->symbol "a")
a

列表比符号做的更多,因为默认情况下,源代码中的列表 调用一个函数!(+ 1 2) 查看列表中的第一个元素,+ 符号,查找与其关联的函数,并使用列表中的其余元素调用它。

不过,有时您可能希望禁用此“特殊”行为。您可能只想获取列表或获取符号而不对其进行评估。为此,您可以使用 quote.

引用的含义

考虑到所有这些,quote 的作用就很明显了:它只是“关闭”了它所包装的表达式的特殊求值行为。例如,考虑 quoteing 一个符号:

> (quote a)
a

类似地,考虑 quote 创建一个列表:

> (quote (a b c))
(a b c)

不管你给什么quote,它总是,总是吐给你。不多也不少。这意味着如果你给它一个列表,none 的子表达式将被评估——不要期望它们是!如果您需要任何类型的评估,请使用 list.

现在,有人可能会问:如果 quote 除了符号或列表之外的东西会怎样?好吧,答案是……没什么!你拿回来就行了。

> (quote 1)
1
> (quote "abcd")
"abcd"

这是有道理的,因为 quote 仍然只是吐出你给它的东西。这就是为什么像数字和字符串这样的“文字”在 Lisp 中有时被称为“self-quoting”。

还有一件事:如果你 quote 一个 e包含 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)

报价非常简单,但由于它往往会违背我们对传统评估模型的理解,所以它可能会变得非常复杂。事实上,它令人困惑 因为 它是多么简单:没有特殊情况,没有规则。它只是 returns 正是你给它的,完全按照说明(因此得名“报价单”)。


附录 A:准引用

那么如果引用完全禁止求值,它有什么用呢?好吧,除了列出提前已知的字符串、符号或数字之外,没有太多。幸运的是,quasiquotation 的概念提供了一种突破引用并返回普通评估的方法。

基础非常简单:不要使用 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)

最后一点:quasiquote 实际上 可以 在 Racket 中使用一个相当复杂的宏来实现,而且确实如此。它扩展到 listcons 的用法,当然还有 quote.


附录 B:在 Scheme

中实现 listquote

实现 list 非常简单,因为“剩余参数”语法的工作原理。这就是您所需要的:

(define (list . args)
  args)

就是这样!

相比之下,quote要难得多——事实上,这是不可能的!这似乎是完全可行的,因为禁用评估的想法听起来很像宏。然而,天真的尝试揭示了问题所在:

(define fake-quote
  (syntax-rules ()
    ((_ arg) arg)))

我们只是拿 arg 然后把它吐出来...但这不起作用。为什么不?好吧,我们的宏的结果将被评估,所以一切都是徒劳的。我们可以通过扩展到 (list ...) 并递归引用元素来扩展到类似于 quote 的东西,像这样:

(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 中尝试此答案中的示例时,您可能会发现它们没有按预期打印。通常,它们可能会以 ' 开头打印,例如在本例中:

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

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

您看到的行为是 Scheme 未将符号视为函数的结果。

表达式'(+ - * /) 产生一个值,它是一个符号列表。这仅仅是因为 (+ - * /) 一个符号列表,我们只是引用它来抑制求值,以便从字面上获取该对象作为值。

表达式(list + - * /) 生成一个函数列表。这是因为它是一个函数调用。计算符号表达式 list+-*/。它们都是表示函数的变量,因此被简化为那些函数。然后调用 list 函数,returns 一个包含其余四个函数的列表。

在 ANSI Common Lisp 中,调用符号作为函数有效:

[1]> (mapcar (lambda (f) (funcall f 1)) '(+ - * /))
(1 -1 1 1)

当在需要函数的地方使用符号时,符号的 top-level 函数绑定被替换,如果它有一个,一切都很酷。实际上,符号是 Common Lisp 中的 function-callable 个对象。

如果您想使用 list 生成符号列表,就像 '(+ - * /) 一样,您必须单独引用它们以抑制它们的计算:

(list '+ '- '* '/)

回到 Scheme 世界,你会发现如果你 map 超过这个,它会像原来的引用列表一样失败。原因是一样的:试图将符号对象用作函数。

显示给您的错误消息具有误导性:

expected a procedure that can be applied to arguments
given: '+

此处显示的 '+(quote +)。但这不是申请的内容;它只是 +,问题是符号对象 + 在该方言中不能用作函数。

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

在“打印为表达式”模式下,对象使用必须阅读并评估 的语法来打印以生成类似的对象。请参阅 Whosebug 问题“