此答案使用 Common Lisp 作为示例,但在 Scheme 中并没有根本不同。另请注意,如果您想了解如何实际实现打印图表,可以在 Common Lisp 中使用sdraw.lisp(例如使用 CCL 或 SBCL 程序)。
您首先必须清楚要打印的结果,当源包含 cons/list/append 操作时,这可能会很困难。此外,由于源代码也是一个 cons-cells 树,因此您必须注意不要将源代码与评估后获得的值混在一起。
所以这一切都从正确评估表单开始。
评估cal
让我们首先评估表达式。下面,我还提到直接从输入表达式中绘制框,但 IMO 它有助于详细说明该中间步骤。
在对所有表达式进行递归计算后,Scheme 和 Common Lisp 中的结果是相同的:
((((1 2) 3) (4 5)) 6 7)
在Common Lisp中,您可以通过以下方式要求系统跟踪计算。首先,要知道您无法跟踪 list 等标准函数。所以让我们用简单的包装器将它们隐藏在自定义包中:
(defpackage :so
(:use :cl)
(:shadow #:list #:cons #:append))
(in-package :so)
(defun list (&rest args) (apply #'cl:list args))
(defun cons (&rest args) (apply #'cl:cons args))
(defun append (&rest args) (apply #'cl:append args))
然后,在 REPL 中,转到该包:
CL-USER> (in-package :so)
#<PACKAGE "SO">
要求追踪这些功能:
SO> (trace list append cons) ;; the shadowing ones
(LIST CONS APPEND)
现在,您可以直接输入cal 的值,但这次使用的符号是我们要求跟踪的符号。
SO> (list (append (list (cons (list 1 2) (cons 3 '())))
(list (cons 4 (cons 5 '()))))
6
7)
环境然后评估表单并打印每个函数的调用方式以及它返回的结果。
0: (SO::LIST 1 2)
0: LIST returned (1 2)
0: (SO::CONS 3 NIL)
0: CONS returned (3)
0: (SO::CONS (1 2) (3))
0: CONS returned ((1 2) 3)
0: (SO::LIST ((1 2) 3))
0: LIST returned (((1 2) 3))
0: (SO::CONS 5 NIL)
0: CONS returned (5)
0: (SO::CONS 4 (5))
0: CONS returned (4 5)
0: (SO::LIST (4 5))
0: LIST returned ((4 5))
0: (SO::APPEND (((1 2) 3)) ((4 5)))
0: APPEND returned (((1 2) 3) (4 5))
0: (SO::LIST (((1 2) 3) (4 5)) 6 7)
0: LIST returned ((((1 2) 3) (4 5)) 6 7)
((((1 2) 3) (4 5)) 6 7)
可视化为 cons 单元格
将列表视为 cons-cells 链会很有帮助,即将 (a b c) 转换为 (a . (b . (c . nil)))。让我们定义一个辅助函数:
(defun consprint (x)
(if (consp x)
(format nil
"(~a . ~a)"
(consprint (car x))
(consprint (cdr x)))
(prin1-to-string x)))
结果如下:
SO> (consprint '((((1 2) 3) (4 5)) 6 7))
"((((1 . (2 . NIL)) . (3 . NIL)) . ((4 . (5 . NIL)) . NIL)) . (6 . (7 . NIL)))"
绘制:一种术语重写方法
使用递归、自下而上的方法来绘制它。
定义。:这里我将 leaf 定义为在其 CAR 和 CDR 槽中都有原子的 cons-cell:例如(0 . NIL) 和 (X . Y) 都是叶子,但不是 ((0 . 1) . 2)。请注意,这包括不正确的列表,当我用符号替换子项时,我依靠这些列表来解释绘图方法。
((((1 . (2 . NIL)) . (3 . NIL)) . ((4 . (5 . NIL)) . NIL)) . (6 . (7 . NIL)))
^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^
上面我在所有叶子上加了下划线:你可以很容易地画出这些盒子,并用符号(A、B、...)标记它们。
下面我将原始单元格替换为其相关框的名称,并再次在新叶子下划线:
((((1 . A) . B) . ((4 . C) . NIL)) . (6 . D))
^^^^^^^ ^^^^^^^ ^^^^^^^
然后,当有一个符号代表一个盒子时,画一个指向那个盒子的箭头。例如,您定义了一个名为 E 的框,对应于 (1 . A),因此您绘制 [1/x] 并将 x 连接到框 A。
您获得:
(((E . B) . (F . NIL)) . G)
现在,考虑(E . B):它的汽车是一个符号,因此您需要绘制的框没有任何价值,但是从 CAR 插槽指向E 单元格的向外箭头(就像它的 CDR 指向B)。
你重复这个过程直到终止。剩下的就是视觉布局,通常情况下,只是原子链表的盒子是水平布局的。
直接从表达式中绘制
大概原始练习希望您直接从原始表达式中绘制方框。可以像上面那样做,从叶表达式向上工作,并用表示现有框的符号替换它们的值。
- 一个 cons 直接映射到一个框。
- 列表只是对 cons 的重复应用,您可以通过绘制与元素一样多的框来快速完成。
- 实际上,append 会复制参数中除最后一个列表之外的所有列表,但在绘制时,您可以“改变”现有框。对于每个现有的框,继续跟踪 CDR,直到到达 cdr 中没有箭头的框,然后将该框链接到参数中的下一个框,从而将不同的框链接在一起。
绘制实际的纯函数式 append 来看看结构共享和垃圾回收是如何工作的可能会很有趣。