微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

在顶层使用标签与 helper-and-main 与 Common Lisp 中的嵌套 defun 之间的比较哪个最好?

如何解决在顶层使用标签与 helper-and-main 与 Common Lisp 中的嵌套 defun 之间的比较哪个最好?

我正在尝试通过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 是如何工作得这么好,因为我使用了一堆不同的辅助函数,并且所有这些函数都具有相同的名称,在球拍样式下被称为 auxtrace 知道哪个我想看。

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

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 具有类似的结构,但也允许使用 definecurrent 范围的其余部分定义局部变量的更类似于 Scheme 的范式(毕竟 Racket 是一种 Scheme 方言),类似了解如何使用更多命令式语言来实现。

,

不要编写嵌套的 defuns

编写嵌套的 defuns 通常是一个错误。 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
...

糟糕!全局函数 BAR 在每次调用 FOOBAZ 时都会被覆盖。

嵌套函数

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。