SETF 既不终止也不报告错误

SETF neither terminates nor reports an error

我是 Common Lisp 初学者,遇到了这段代码:

(let ((foo (list 42)))
  (setf (rest foo) foo))

REPL 在尝试执行时似乎永远循环。

什么是 FOO

FOO 最初是一个新列表,(42)。在 Lisp 中,列表由 cons cells 表示,可变内存块包含每个 CARCDR 槽。 另一种打印方式是 (42 . NIL),其中 CARCDR 位于点的两侧。这也可以如下图:

  car  cdr
------------
| 42 | NIL |
------------
     ^
     |
    FOO

当您调用 SETF with the (rest foo) place and the foo value, you are saying that you want the cdr cell of FOO to hold the value FOO. In fact, this occurrence of SETF is likely to macroexpand into a call to RPLACD.

------------
| 42 | FOO |
------------
     ^
     |
    FOO

为什么 REPL 会一直循环?

"REPL"(打印)的 "P" 部分尝试打印您的圆形结构。这是因为 SETF 的值是从正在评估的表单返回的值,而 SETF 返回的值是其第二个参数的值,即 FOO。想象一下你想用一个朴素的算法写一个cons cell X:

1. PRINT "("
2. PRINT the CAR of X
3. PRINT " . "
4. PRINT the CDR of X
5. PRINT ")"

但是,对于 foo,第 4 步将递归打印相同的结构,并且永远不会终止。

你能做什么?

首先尝试将 *PRINT-CIRCLE* 设置为 T:

(setf *print-circle* t)

现在,您的 REPL 应该很高兴:

CL-USER> (let ((foo (list 42)))
           (setf (rest foo) foo))
#1=(42 . #1#)

"Sharpsign Equal-Sign" 表示法允许 reader 将表单的一部分影响到 (reader) 变量,如 #1=...,然后再使用它,例如#1#。这使得在读取或打印期间表示数据之间的循环交叉引用成为可能。在这里,我们可以看到变量 #1# 表示一个 cons-cell,其中 CAR 是 42 而 CDR#1# 本身。