在顶层使用标签与 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 运算符)被认为在顶层使用。顶级通常意味着作为最顶层的表达式或仅嵌套在 progneval-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
...

糟糕!每次调用 FOOBAZ.

时,全局函数 BAR 都会被覆盖

嵌套函数

使用 FLETLABELS 定义局部函数。

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)