已经有一个公认的答案,但我认为应该对原始代码中的问题进行更多解释。 mapcan 将一个函数应用于列表的每个元素,以生成一堆破坏性地连接在一起的列表。如果你破坏性地将一个列表与其自身连接起来,你会得到一个循环列表。例如,
(let ((x (list 1 2 3)))
(nconc x x))
;=> (1 2 3 1 2 3 1 2 3 ...)
现在,如果您有多个连接,则无法完成,因为要将某些内容连接到列表的末尾需要走到列表的末尾。所以
(let ((x (list 1 2 3)))
(nconc (nconc x x) x))
; ----------- (a)
; --------------------- (b)
(a) 终止,并返回列表 (1 2 3 1 2 3 1 2 3 ...),但 (b) 无法终止,因为我们无法到达 (1 2 3 1 2 3 ...) 的末尾以便将内容添加到末尾。
现在剩下的问题是为什么
(defun foo (a b)
(mapcan #'(lambda (x) a) b))
(foo (list 1 2 3) '(a b))
导致冻结。由于(a b) 中只有两个元素,这相当于:
(let ((x (list 1 2 3)))
(nconc x x))
这应该终止并返回一个无限列表(1 2 3 1 2 3 1 2 3 ...)。事实上,确实如此。问题是 REPL 中的列表打印会挂起。例如,在 SBCL 中:
CL-USER> (let ((x (list 1 2 3)))
(nconc x x))
; <I manually stopped this, because it hung.
CL-USER> (let ((x (list 1 2 3)))
(nconc x x) ; terminates
nil) ; return nil, which is easy to print
NIL
如果将*print-circle* 设置为true,则可以看到第一个表单的结果:
CL-USER> (setf *print-circle* t)
T
CL-USER> (let ((x (list 1 2 3)))
(nconc x x))
#1=(1 2 3 . #1#) ; special notation for reading and
; writing circular structures
调整代码以消除问题行为的最简单方式(即最少的更改)是在 lambda 函数中使用 copy-list:
(defun foo (a b)
(mapcan #'(lambda (x)
(copy-list a))
b))
与(reduce 'append (mapcar ...) :from-end t) 解决方案相比,这还有一个优势,因为它不必分配结果的中间列表。