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

Why is it possible to use unquote-splicing on a non-list at the end of a quasiquoted list?

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

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

因此,'(1 2 3)中的最后一个元素不是'non-list',而是(cons 3 nil)(list 3),因此可以拼接。

要用准引用构造一个包含 3 个变量的列表,可以这样写

`(,x ,y ,z)

这个可以脱糖到

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

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

`(,x ,y ,@z)

这个脱糖到

(cons x (cons y z))

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

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


方案

在 R6RS 方案中(对于 R5RS 也是类似的),未指定使用 unquote-splicing 在非列表上操作的行为。 R6RS 方案标准要求 (11.17 Quasiquotation):

If an (unquote-splicing <expression> ...) form appears inside a <qq template>, then the <expression>s must evaluate to lists....


普通 Lisp

Common Lisp HyperSpec 首先说 (2.4.6 Backquote):

If a comma is immediately followed by an at-sign, then the form following the at-sign is evaluated to produce a list of objects. These objects are then "spliced" into place in the template.

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

  • `(x1 x2 x3 ... xn . atom) may be interpreted to mean

    (append [ x1] [ x2] [ x3] ... [ xn] (quote atom))

    where the brackets are used to indicate a transformation of an xj as follows:

    -- [form] is interpreted as (list `form), which contains a backquoted form that must then be further interpreted.

    -- [,form] is interpreted as (list form).

    -- [,@form] is interpreted as 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 槽。

一些函数只有在给定适当的列表时才能工作,其他函数没有这样的限制,通常是因为检查这个适当的ty 不是没有成本的。 写作 (cons 1 2) 创建这样一个列表。现在,如果拼接语法扩展为 (cons x y),其中 y 是一个非列表,您也会有一个虚线列表。

切片

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

If a comma is immediately followed by an at-sign, then the form following the at-sign is evaluated to produce a list of objects.

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

该部分还指出:

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

will be interpreted as if it were

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

but it could also be legitimately interpreted to mean any of the following:

(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 等标准函数的调用使极端情况自然产生不正确的列表,但请注意第一个和最后一个示例 not 允许拼接最后一个元素。例如:

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

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

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

`(1 2 ,@3)

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

结论

Why is it possible to use unquote-splicing for non-lists at the end of a quasiquoted list?

它在您的实现中是偶然发生的,因为反引号、反引号和拼接 rewritten/expanded 作为列表构建函数可能会在极端情况下生成点列表,而不会发出错误信号。

Why is the result a dotted list?

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

奖金

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