在顶层使用标签与 helper-and-main 与 Common Lisp 中的嵌套 defun 之间的比较。哪个最好?
A comparison between using labels vs helper-and-main at the top level vs nested defun's in Common Lisp. Which is the best?
我正在尝试通过 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 中很常见的第三种方法。我将其命名为 nested 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追踪到。
我已经用了一整天了。所以我知道它在一个文件中工作,其中包含许多使用它的函数。实际上,考虑到我使用了一堆不同的辅助函数,并且它们都具有相同的名称,在 racket 样式下被称为 aux
,我什至不知道 trace 是如何工作得这么好的。 trace
知道 我想看哪个 辅助。
最重要的是,这个遗漏真的让我很感兴趣。特别是因为我真的很喜欢这本书。我想我可能遗漏了什么。
1 - 为什么没有提到这种方法?这种带有嵌套 defun 的“球拍风格”在 Common Lisp 中被认为是糟糕的风格吗?
2 - 我是否遗漏了一些重要的细节(例如,这种方法可能是难以发现错误或产生性能问题的根源)?
3 - 这种遗漏是否有一些历史原因?
是的,有充分的理由。在球拍中,我们有 define
In an internal-definition context, a define
form introduces a local binding; see Internal Definitions. A the top level, the top-level binding for id
is created after evaluating expr
因此,正如您所说,局部上下文(例如函数体)中的 define
定义了一个局部函数,可以访问封闭变量并且仅在该函数期间存在。
现在将其与 Common Lisp 的 defun
进行比较
Defines a new function named function-name in the global environment.
因此,无论 defun
出现在哪里,它 总是 在全局范围内定义一个名称,无法访问局部变量并且名称成为全球可用。因此,您对嵌套 defun
的建议实际上等同于在顶层定义 defun
(从某种意义上说,该名称在顶层可用,并且从某种意义上说,局部变量不可访问),除了在您至少调用一次原始函数之前该名称不存在,坦率地说,这是相当不直观的行为。
顺便说一句,labels
方法正是您想要的。在 Common Lisp 中,如果我们需要局部辅助函数,我们使用 flet
(对于非递归函数)或 labels
(对于递归函数)。
至于为什么会这样,Common Lisp 一直试图强制执行一个非常清晰的变量范围。在任何函数中,局部变量都是用 (let ...)
引入的,并且只存在于块内部,局部函数是用 (flet ...)
和 (labels ...)
引入的。 Racket 具有类似的构造,但也允许使用 define
为剩余的 current 定义局部变量的更像 Scheme 的范例(毕竟 Racket 是 Scheme 方言)范围,类似于您在更多命令式语言中的做法。
不要写嵌套defuns
.
写嵌套的 defun 通常是一个错误。 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
...
糟糕!每次调用 FOO
和 BAZ
.
时,全局函数 BAR
都会被覆盖
嵌套函数
使用 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 ,$depth ',name ,$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)
我正在尝试通过 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 中很常见的第三种方法。我将其命名为 nested 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追踪到。
我已经用了一整天了。所以我知道它在一个文件中工作,其中包含许多使用它的函数。实际上,考虑到我使用了一堆不同的辅助函数,并且它们都具有相同的名称,在 racket 样式下被称为 aux
,我什至不知道 trace 是如何工作得这么好的。 trace
知道 我想看哪个 辅助。
最重要的是,这个遗漏真的让我很感兴趣。特别是因为我真的很喜欢这本书。我想我可能遗漏了什么。
1 - 为什么没有提到这种方法?这种带有嵌套 defun 的“球拍风格”在 Common Lisp 中被认为是糟糕的风格吗?
2 - 我是否遗漏了一些重要的细节(例如,这种方法可能是难以发现错误或产生性能问题的根源)?
3 - 这种遗漏是否有一些历史原因?
是的,有充分的理由。在球拍中,我们有 define
In an internal-definition context, a
define
form introduces a local binding; see Internal Definitions. A the top level, the top-level binding forid
is created after evaluatingexpr
因此,正如您所说,局部上下文(例如函数体)中的 define
定义了一个局部函数,可以访问封闭变量并且仅在该函数期间存在。
现在将其与 Common Lisp 的 defun
Defines a new function named function-name in the global environment.
因此,无论 defun
出现在哪里,它 总是 在全局范围内定义一个名称,无法访问局部变量并且名称成为全球可用。因此,您对嵌套 defun
的建议实际上等同于在顶层定义 defun
(从某种意义上说,该名称在顶层可用,并且从某种意义上说,局部变量不可访问),除了在您至少调用一次原始函数之前该名称不存在,坦率地说,这是相当不直观的行为。
顺便说一句,labels
方法正是您想要的。在 Common Lisp 中,如果我们需要局部辅助函数,我们使用 flet
(对于非递归函数)或 labels
(对于递归函数)。
至于为什么会这样,Common Lisp 一直试图强制执行一个非常清晰的变量范围。在任何函数中,局部变量都是用 (let ...)
引入的,并且只存在于块内部,局部函数是用 (flet ...)
和 (labels ...)
引入的。 Racket 具有类似的构造,但也允许使用 define
为剩余的 current 定义局部变量的更像 Scheme 的范例(毕竟 Racket 是 Scheme 方言)范围,类似于您在更多命令式语言中的做法。
不要写嵌套defuns
.
写嵌套的 defun 通常是一个错误。 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
...
糟糕!每次调用 FOO
和 BAZ
.
BAR
都会被覆盖
嵌套函数
使用 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 ,$depth ',name ,$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)