为什么可以在准引用列表末尾的非列表上使用取消引用拼接?

问题描述

准引用列表 `(1,@2 3) 无效,因为 2 不是列表。但是,`(1 2,@3) 是有效的,并且会返回一个点列表:(1 2 . 3)。我在 Common Lisp 和 Scheme 中观察到了这个结果。为什么可以在准引用列表的末尾对非列表使用 unquote-splicing?为什么结果是点状列表?

解决方法

表达式 `(1 2,@3) 在 Scheme 或 Common Lisp 中均无效。


方案

在 R6RS 方案中(对于 R5RS 也是如此),没有为在带有 unquote-splicing 的非列表上操作指定行为。 R6RS 方案标准要求 (11.17 Quasiquotation):

如果一个 (unquote-splicing ...) 形式出现在一个 ,则 必须计算为列表....


普通 Lisp

Common Lisp HyperSpec 首先说 (2.4.6 Backquote):

如果逗号后面紧跟着一个at-sign,那么形式at-sign 之后被评估以产生 objectslist。 这些对象然后被“拼接”到模板中。

在子表达式 ,@3 中,3 不计算为列表。这似乎是一个非常有力的论据,即该表达式无效。即使 3 在拼接之前神奇地放在一个列表中,这也不会导致一个点列表。 HyperSpec 继续提供反引号语法的正式摘要。感兴趣的部分是:

  • `(x1 x2 x3 ... xn . atom) 可以解释为意思

    (append [ x1] [ x2] [ x3] ... [ xn] (引用原子))

    其中括号用于表示 xj 的变换如下:

    -- [form] 被解释为 (list `form),其中包含一个必须被进一步解释的反引号形式。

    -- [,form] 被解释为(列表形式)。

    -- [,@form] 被解释为表单。

所以在 Common Lisp 中,相当于 `(1 2,@3 . nil) 的原始表达式可以解释为:

(append (list `1) (list `2) 3 (quote nil))

但是,这不是对 append 的有效调用,它需要所有参数的正确列表,除了最后一个。因此,似乎不支持原始表达式有效的想法。

事实上,它在 Scheme 和 Common Lisp 中都适用于 OP,这可能归结为不同实现中反引号宏的相似定义。这些定义似乎都期望 ,@ 后面的形式将评估为列表;如果不是这种情况,根据标准,不能依赖观察到的行为(点列表的产生)。也就是说,我测试了 Chez Scheme、Guile Scheme、MIT Scheme、SBCL、CCL 和 CLisp:它们都表现出 OP 报告的相同行为。


一个有趣的案例

我还针对 an implementation of the backquote macro by Guy Steele and published in CLTL2 进行了测试。这个案例更有趣。 CLTL2 中的这种反引号实现旨在探索反引号表达式的行为,并具有可选的代码简化阶段。这里$对应反引号,%@对应,@。不进行代码简化,将原表达式展开的结果为:

CL-USER> (setf *bq-simplify* nil)
NIL
CL-USER> (try '("$(1 2 %@3)"))
`(1 2,@3) = (APPEND (LIST '1) (LIST '2) 3 'NIL)

这对应于上面阅读 HyperSpec 中的描述所预期的表达式。但请注意,此表达式不会编译:

CL-USER> (append (list 1) (list 2) 3 nil)

The value
  3
is not of type
  LIST
[Condition of type TYPE-ERROR]

但是,当打开代码简化时:

CL-USER> (setf *bq-simplify* t)
T
CL-USER> (try '("$(1 2 %@3)"))
`(1 2,@3) = (LIST* '1 '2 3)

这个“简化”的表达式是有效的,并且计算为一个点列表:

CL-USER> (list* 1 2 3)
(1 2 . 3)

我的结论是 ,@ 后面的表达式必须是一个符合 Common Lisp 标准的列表,但一些常见的实现要么做某种形式的代码简化,类似于 CLTL2 中所示的内容或以其他方式扩展反引号形式,使其看起来非列表形式可以跟在 ,@ 之后。不要依赖这个,因为很难说它什么时候行不通。

,

适当的列表

其他答案已经详细说明了这一部分,让我快速改写一下:列表要么是正确的列表,要么是不正确的列表;不正确的列表要么是循环列表,要么是点列表。

特别是,点状列表是一个非循环列表,它的最后一个 cons 单元格有一个非列表 cdr 槽。

某些功能只有在给定适当的列表时才能工作,而其他功能则没有这样的限制,通常是因为检查此属性不是没有成本的。 编写 (cons 1 2) 会创建这样一个列表。现在,如果拼接语法扩展为 (cons x y),其中 y 是一个非列表,你也会有一个点列表。

切片

2.4.6 Backquote 章指出(强调我的):

如果逗号后面紧跟一个 at 符号,则at 符号后面的形式 将被评估以生成对象列表

我没有看到其他说明表明拼接非列表值可能是 conforming program 的一部分,即使当拼接发生在列表末尾时实际上会导致不正确的列表。

该部分还指出:

`((,a b),c,@d)

将被解释为好像是

(append (list (append (list a) (list 'b) 'nil)) (list c) d 'nil)

但它也可以被合理地解释为以下任何一种:

(append (list (append (list a) (list 'b))) (list c) d)
(append (list (append (list a) '(b))) (list c) d)
(list* (cons a '(b)) c d)
(list* (cons a (list 'b)) c d)
(append (list (cons a '(b))) (list c) d)
(list* (cons a '(b)) c (copy-list d))

扩展为对 list*append 等标准函数的调用使这种极端情况自然会产生不正确的列表,但请注意,第一个和最后一个示例不允许拼接最后一个元素。例如:

(list* (cons a '(b)) c (copy-list d))

d 不是列表时,上面的表单会报错,因为 COPY-LIST 只适用于列表(正确的或带点的,但在这里它会被赋予一个非列表值,例如一个数字) .

因此我的理解是这个表达:

`(1 2,@3)

实际上在 Common Lisp 中是无效的,并且碰巧只是偶然起作用。编写它可能会使您面临可移植性问题。

结论

为什么可以在准引用列表的末尾对非列表使用取消引用拼接?

这在您的实现中是偶然发生的,因为反引号、取消引号和拼接被重写/扩展为列表构建函数,这些函数可能会在这种极端情况下生成点列表,而不会发出错误信号。

为什么结果是点状列表?

因为该代码可能被解释为对 append 的调用,其中最后一项是非列表值,从而产生一个点列表。在表单中间拼接时它不起作用,因为 append 期望除最后一个参数之外的所有参数都具有正确的列表。

奖金

今天我了解了 ,.,它类似于 ,@,但可以使用 nconc 而不是 append

,

要构造一个带有 quasiquoting 的 3 个变量的列表,可以这样写

`(,x,y,z)

这可以脱糖

(cons x (cons y (cons z nil)))

如果 z 是一个列表,我们想将它的内容拼接到结果列表中怎么办?然后我们没有为它创建一个新的 cons,而是将它放在尾部。当我们写

`(,@z)

这种脱糖

(cons x (cons y z))

如果 z 碰巧实际上不是一个列表,那么结果是一个合法的值,虽然不是一个正确的列表。例如,如您所见,(1 2 . 3)。如果您将脱糖形式显式编写为 (cons 1 (cons 2 3)),您将看到相同的结果。如果这也困扰着您,您可能需要调查一般不正确列表的概念,或点对符号。

,

'(1 2 3) 实际上是 (cons 1 (cons 2 (cons 3 nil)))

因此,'(1 2 3) 中的最后一个元素不是“非列表”,而是 (cons 3 nil),即 (list 3),因此可以拼接。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...