作者:lnssm | 来源:互联网 | 2023-09-13 18:06
我正在尝试通过《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 中很常见的第三种方法。我将其称为嵌套 defuns。
同样的问题将被解决为:
(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
知道这AUX我想看到的。
最重要的是,这种遗漏真的让我很感兴趣。特别是因为我真的很喜欢这本书。我想我可能错过了一些东西。
1 - 为什么没有提到这种方法?这种带有嵌套 defun 的“球拍风格”在 Common Lisp 中被认为是糟糕的风格吗?
2 - 我是否遗漏了一些重要的细节(例如,这种方法可能是难以找到错误或产生性能问题的根源)?
3 - 这种遗漏是否有一些历史原因?
回答
是的,有充分的理由。在 Racket 中,我们有define
在内部定义上下文中,define
表单引入了本地绑定;请参阅内部定义。A 顶层,id
在评估后创建顶层绑定expr
因此,正如您所说, adefine
在局部上下文(例如函数体)中定义了一个局部函数,可以访问封闭变量并且仅在该函数期间存在。
现在将其与 Common Lisp 的进行比较 defun
在全局环境中定义一个名为function-name的新函数。
因此,无论 adefun
出现在哪里,它总是在全局范围内定义一个名称,不能访问局部变量,并且名称在全局范围内可用。因此,您对嵌套的建议defun
实际上等同defun
于在顶级定义 the (从名称在顶级可用的意义上说,并且在局部变量不可访问的意义上),只是名称不存在直到您至少调用了一次原始函数,坦率地说,这是相当不直观的行为。
顺便说一下,这种labels
方法是你想要的。在 Common Lisp 中,如果我们想要局部辅助函数,我们使用flet
(对于非递归函数)或labels
(对于递归函数)。
至于为什么会这样,Common Lisp 始终试图强制执行一个非常明确的变量范围。在任何函数中,局部变量都是用(let ...)
和引入的,并且只存在于块内部,局部函数是用(flet ...)
和引入的(labels ...)
。Racket 具有类似的构造,但也允许使用更类似于 Scheme 的范式(毕竟 Racket 是一种 Scheme 方言),define
用于为当前范围的其余部分定义局部变量,类似于您在更多命令式语言中的做法。
回答
不要写嵌套defuns
.
编写嵌套的 defuns 通常是一个错误。defun
(和大多数其他defsomething
运算符)被认为在顶级使用。顶级通常意味着作为最顶层的表达式或仅嵌套在progn
or 中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
宏的文档。它可能支持跟踪本地函数,但有一个特殊的语法来这样做。