4

我正在尝试通过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 追踪。

我一整天都在使用它。所以我知道它可以在具有许多使用它的功能的文件中工作。实际上,考虑到我使用了一堆不同的辅助函数并且它们都具有相同的名称,aux在球拍风格下被调用,我什至不知道 trace 是如何工作的。trace知道我想看哪个辅助。

最重要的是,这个遗漏真的让我很感兴趣。特别是因为我真的很喜欢这本书。我想我可能遗漏了一些东西。

1 - 为什么没有提到这种方法?这种带有嵌套 defun 的“球拍风格”在 Common Lisp 中被认为是不好的风格吗?

2 - 我是否遗漏了一些重要的细节(例如,这种方法可能是难以发现错误或产生性能问题的根源)?

3 - 这种遗漏是否有一些历史原因?

4

3 回答 3

7

是的,有充分的理由。在球拍中,我们有define

内部定义上下文中define表单引入了本地绑定;请参阅内部定义。A 顶级,顶级绑定id是在评估后创建的expr

因此,正如您所说, adefine在局部上下文(例如函数体)中定义了一个局部函数,可以访问封闭变量并且仅在该函数期间存在。

现在将其与 Common Lisp 进行比较defun

全局环境中定义一个名为function-name的新函数

因此,无论 adefun出现在哪里,它总是在全局范围内定义一个名称,不能访问局部变量并且具有全局可用的名称。因此,您对嵌套的建议defun实际上等同于defun在顶层定义(从某种意义上说,该名称在顶层可用并且在某种意义上局部变量不可访问),除了该名称不存在直到您至少调用了一次原始函数,坦率地说,这是相当不直观的行为。

顺便说一句,labels方法是你想要的。在 Common Lisp 中,如果我们想要局部辅助函数,我们使用flet(for non-recursive functions) 或labels(for recursive functions)。

至于为什么会这样,Common Lisp 总是试图强制执行一个非常清晰的变量范围。在任何函数中,局部变量都是用 和 引入的,(let ...)并且只存在于块内部,而局部函数是用(flet ...)和引入的(labels ...)。Racket 具有类似的结构,但也允许使用更类似于 Scheme 的范例(毕竟 Racket 是一种 Scheme 方言)define用于为当前范围的其余部分定义局部变量,类似于您在更命令式语言中的操作方式。

于 2021-06-02T21:52:22.790 回答
6

不要写嵌套defuns

编写嵌套的 defuns 通常是一个错误。defun(和大多数其他defsomething运算符)被认为是在顶层使用的。顶级通常意味着作为最顶层的表达式或仅嵌套在prognor中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

嵌套函数

使用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)

上面通常不是跟踪本地函数的方式。该语法跟踪全局函数。

trace要跟踪本地函数,请查阅 Lisp 实现手册以获取宏的文档。它可能支持跟踪本地函数,但这样做有一个特殊的语法。

于 2021-06-02T23:26:49.140 回答
3

如果需要跟踪,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,定义了一个局部特殊深度变量来跟踪递归级别,并向定义的函数添加副作用以打印跟踪。

(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)
于 2021-06-03T07:53:04.993 回答