问题描述
我正在尝试通过Common Lisp:符号计算的温和介绍这本书来学习 Common Lisp。此外,我正在使用 SBCL、Emacs 和 Slime。
在第 8 章的进阶部分,作者介绍了 labels
特殊函数。实际上,他将在顶层(主函数和辅助函数)上定义事物与在函数内使用 label
表达式进行对比。
例如,这将是使用顶级方法的带有尾调用的 reverse
列表函数:
(defun reverse-top-level-helper (xs-left accu)
(cond ((null xs-left) accu)
(t (reverse-top-level-helper (cdr xs-left)
(cons (car xs-left)
accu)))))
(defun reverse-top-level-main (xs)
(reverse-top-level-helper xs nil))
另一方面,下面的代码将使用 labels
执行相同的操作:
(defun reverse-labels (xs)
(labels ((aux-label (xs-left accu)
(cond ((null xs-left) accu)
(t (aux-label (cdr xs-left)
(cons (car xs-left) accu))))))
(aux-label xs nil)))
因此,标签方法避免了人们将顶层的辅助函数弄乱的机会。与顶级方法不同的是,标签方法可以访问主函数的局部变量。
不幸的是,根据作者的说法,在大多数 lisp 实现中,没有办法跟踪标签表达式中的函数。这似乎是我的情况,因为我是从 REPL 中得到的:
CL-USER> (trace aux-label)
WARNING: COMMON-LISP-USER::AUX-LABEL is undefined,not tracing.
NIL
让我感兴趣的一点是,作者没有没有展示在 Racket 中很常见的第三种方法。我称之为嵌套定义。
同样的问题将被解决:
(defun reverse-racket-style (xs)
(defun aux (xs-left accu)
(cond ((null xs-left) accu)
(t (aux (cdr xs-left) (cons (car xs-left) accu)))))
(aux xs nil))
在这种方法中,辅助函数可以从主函数访问局部变量。它也可以通过 REPL 进行跟踪。
我一整天都在使用它。所以我知道它可以在一个文件中使用它的许多功能。实际上,我什至不知道 trace 是如何工作得这么好,因为我使用了一堆不同的辅助函数,并且所有这些函数都具有相同的名称,在球拍样式下被称为 aux
。 trace
知道哪个我想看。
最重要的是,这个遗漏真的让我很感兴趣。特别是因为我真的很喜欢这本书。我想我可能遗漏了一些东西。
1 - 为什么没有提到这种方法?这种带有嵌套 defun 的“球拍风格”在 Common Lisp 中被认为是糟糕的风格吗?
2 - 我是否遗漏了一些重要的细节(例如,这种方法可能是难以发现错误或产生性能问题的根源)?
3 - 这种遗漏是否有历史原因?
解决方法
是的,有充分的理由。在 Racket 中,我们有 define
在 internal-definition context 中,define
形式引入了本地绑定;见Internal Definitions。顶级,id
的顶级绑定是在评估 expr
因此,正如您所说,本地上下文(例如函数体)中的 define
定义了一个本地函数,可以访问封闭变量并且仅在该函数期间存在。
现在将其与 Common Lisp 的 defun
在 function 中定义一个名为 function-name 的新 global environment。
因此,无论 defun
出现在何处,它总是在全局范围内定义一个名称,无法访问局部变量,并且名称在全局范围内可用。因此,您对嵌套 defun
的建议实际上等同于在顶级定义 defun
(从某种意义上说,名称在顶级可用,并且局部变量不可访问),除非您至少调用原始函数一次,否则该名称不存在,坦率地说,这是相当不直观的行为。
顺便说一下,labels
方法正是您想要的。在 Common Lisp 中,如果我们想要局部辅助函数,我们使用 flet
(对于非递归函数)或 labels
(对于递归函数)。
至于为什么会这样,Common Lisp 始终试图强制执行一个非常明确的变量范围。在任何函数中,局部变量都是用(let ...)
引入的,并且只存在于块内部,局部函数是用(flet ...)
和(labels ...)
引入的。 Racket 具有类似的结构,但也允许使用 define
为 current 范围的其余部分定义局部变量的更类似于 Scheme 的范式(毕竟 Racket 是一种 Scheme 方言),类似了解如何使用更多命令式语言来实现。
不要编写嵌套的 defuns
。
编写嵌套的 defuns 通常是一个错误。 defun
(和大多数其他 defsomething
运算符)被认为在顶级使用。顶级通常意味着作为最顶层的表达式或仅嵌套在 progn
或 eval-when
中。然后文件编译器会识别这些宏。
作为嵌套函数,编译器无法识别 defun
。调用外部函数将在每次调用时和全局定义内部函数。
示例:
(defun foo ()
(defun bar ()
20))
(defun baz ()
(defun bar ()
30))
现在做:
(bar) ; -> error undefined function BAR
(foo)
(bar) ; -> 20
(baz)
(bar) ; -> 30
(foo)
(bar) ; -> 20
(baz)
(bar) ; -> 30
...
糟糕!全局函数 BAR
在每次调用 FOO
和 BAZ
时都会被覆盖。
嵌套函数
使用 FLET
和 LABELS
定义局部函数。
DEFUN
不不定义局部词法函数。它定义了以符号为名称的全局函数。
CL-USER 77 > (defun one ()
(defun two ()
40))
ONE
CL-USER 78 > (fboundp 'two)
NIL
CL-USER 79 > (one)
TWO
CL-USER 80 > (fboundp 'two)
T
跟踪本地函数
(trace aux-label)
以上通常不是跟踪本地函数的方式。该语法跟踪全局函数。
要跟踪本地函数,请参阅您的 Lisp 实现手册以获取 trace
宏的文档。它可能支持跟踪本地函数,但有一个特殊的语法来这样做。
如果您需要跟踪,labels
使用起来会很烦人。这是一个定义 labels*
的小助手宏,它是 labels
的变体,用于跟踪被调用函数的执行情况。
这是打印跟踪的函数:
(defun depth-trace (io depth name args)
(let ((*standard-output* *trace-output*))
(fresh-line)
(dotimes (i depth) (princ (case io (:in "| ") (:out ": "))))
(format t "~a. ~a ~s~%" depth name args)))
宏使用 alexandria:with-gensyms
,定义了一个局部特殊的 depth 变量,用于跟踪递归级别,并向定义的函数添加副作用以打印跟踪。
(defmacro labels* ((&rest bindings) &body body)
(alexandria:with-gensyms ($depth $result $args)
(loop
with special = `(declare (special,$depth))
for (name args . fn-body) in bindings
collect `(,name (&rest,$args),special
(depth-trace :in,$depth ',name,$args)
(let ((,$result
(multiple-value-list
(let ((,$depth (1+,$depth))),special
(apply (lambda (,@args),@fn-body),$args)))))
(depth-trace :out,$result)
(values-list,$result)))
into labels-bindings
finally
(return
`(let ((,$depth 0)),special
(labels,labels-bindings,@body))))))
例如:
(labels* ((a (b) (if (> b 0) (* 2 (a (- b 2))) (- b))))
(a 11))
打印:
0. A (11)
| 1. A (9)
| | 2. A (7)
| | | 3. A (5)
| | | | 4. A (3)
| | | | | 5. A (1)
| | | | | | 6. A (-1)
: : : : : : 6. A (1)
: : : : : 5. A (2)
: : : : 4. A (4)
: : : 3. A (8)
: : 2. A (16)
: 1. A (32)
0. A (64)
它也适用于相互递归的函数:
(labels* ((a (x) (* x (b x)))
(b (y) (+ y (c y)))
(c (z) (if (> z 0) (* 2 z) (a (- z)))))
(a -5))
跟踪是:
0. A (-5)
| 1. B (-5)
| | 2. C (-5)
| | | 3. A (5)
| | | | 4. B (5)
| | | | | 5. C (5)
: : : : : 5. C (10)
: : : : 4. B (15)
: : : 3. A (75)
: : 2. C (75)
: 1. B (70)
0. A (-350)