不要嵌套 defun(使用 flet 或标签),sbcl REPL 不会报告所有情况(警告)

do not nest defun (use flet or labels), sbcl REPL does not report all conditions (warnings)

在 SBCL 1.3.1 上执行的测试

我在函数 xxx 中定义了函数 xx。 (最初 xx 是递归的,并且从 xxx 闭包中使用了不变量。但是递归在这里并不重要, xx 只是 returns txxx 只调用 xx。因此 xxx 预计也会 return t

xxx 在函数 call-xxx 中被调用了两次。要显示此问题(条件的问题),必须调用两次。虽然调用之间没有共享状态,但为什么这很重要很好奇。

call-xxx 作为 #'call-xxx 传递以在 handler-case 内应用。它不需要任何参数,所以它只是应用于 nil。 handler-case 把它拉出来说它抛出一个条件。

xxxxx 之外定义时,没有条件,并且 t 的预期结果是 returned。这在下面显示的代码的第二部分中显示,其中我们有 wwwwwcall-www 等。此外,当 handler-case 未使用时,没有REPL 报告的异常。

[这个例子是从测试框架中删除的,因此当 call-xxx 被认为抛出异常时,测试失败。然而,当测试手动运行时(请参阅 run-call-xx),它会毫无例外地通过,从而产生矛盾并使调试明显失败的测试变得困难。]

造成这种情况的原因是什么?条件处理程序调用是否应该不同?这是 SBCL 错误吗?

代码如下:

(defun test (test-function)
  (handler-case (apply test-function '()) (condition () ':exception))
  ;;;  (apply test-function '()) ;; returns t, no exception
  )

;;---------------------------------------------------------------
;; throws exception, but shouldn't (?)
;;
  (defun xxx ()
    (defun xx () t) ; note comments, should be a flet or labels form
    (xx))

  (defun call-xxx ()
    (xxx)  ;; #'xxx must be called twice for the exception to appear
    (xxx)
    t)

  ;; call-xxx throws exception when run from test, but shouldn't
  (defun run-test-call-xxx ()
    (test #'call-xxx))

  ;; no problem here, call-xxx returns t when called directly
  (defun run-call-xxx ()
    (call-xxx))

;;--------------------------------------------------------
;; works fine  
;;  pulled out the nested definition of #'ww from #'www
;;
  (defun ww () t)

  (defun www ()
    (ww))

  (defun call-www ()
    (www) 
    (www)
    t)

  (defun run-test-call-www ()
    (test #'call-www))

这里是 运行:

§sbcl> sbcl
This is SBCL 1.3.1.debian, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.
* (load "src/test-xxx")

T
* (run-test-call-xxx)

:EXCEPTION
* (run-test-call-www)

T
* 

added 这里是重新定义的测试函数,用于在没有处理程序的情况下进行回复。我这样做是为了查看 REPL 报告的内容。

(defun test (test-function)

  ;;; -> this does not get handled
  ;;; (handler-case (apply test-function '()) (serious-condition () ':exception))

  ;;; -> this triggers the handler
  ;;; (handler-case (apply test-function '()) (condition () ':exception))

  (apply test-function '()))

这就是 REPL 中发生的事情:

§sbcl> sbcl
This is SBCL 1.3.1.debian, an implementation of ANSI Common Lisp....
distribution for more information.
* (load "src/test-handler.lisp")

T
* (run-test-call-xxx)

T
* 

如您所见,REPL 没有打印警告。

注意注释,defun 只是顶层,所以使用 flet 处理闭包中的递归。 defun 内部函数将在每个条目上定义,尽管发出警告,但不会进入 REPL(尽管处理程序案例确实看到它,这就是这里发生的事情)

(defun f (s too-big-for-stack-invariant)
    (defun r (s) ;; note comments, should be a labels form, not defun
        ;; changes s and checks for termination
        ;; makes use of the very large invariant data
        ...
        r(s))
   ;; some stuff making use of r(s)
   )

代码的两个问题:

    Common Lisp 中的
  1. defun 定义了 top-level 定义。即使它出现在另一个函数内部(这种方式与 Scheme 的 define 不同)。因此,每次调用 xxx 时都会重新定义函数 xx
  2. 条件 condition 甚至可以捕获某些东西 "not worth mentioning"(参见 handler-case 上的 CLHS)。由于 xx 的重新定义会产生警告,因此它会捕获它。如果您不这样做,handler-case 警告将显示在 REPL 中,但会产生正确的结果。

我认为这里的问题是无论如何你都在拦截任何条件。在这种情况下,SBCL 发出关于 xx 被重新定义为 defun 的条件(不是错误,而是警告,因为它可能是无意的)。

将代码更改为:

(defun test (test-function)
  (handler-case (apply test-function '())
    (condition (x)
      (print x)
      ':exception)))

将显示 #<SB-KERNEL:REDEFINITION-WITH-DEFUN {1002BE3E93}>

合乎逻辑的问题是,你不应该声明能够处理你不知道的情况:你应该处理你知道的情况,让其他人由知道的人来处理。条件可以由适当的处理程序通过让程序继续运行来解决运行(就像这里的情况一样)。

这是条件和异常之间的主要区别:仅当处理程序认为它是正确的事情时才执行展开。用 C++ 代替,一旦抛出异常,当异常被处理时,所有都已经在较低的调用堆栈级别丢失。