我的目标是一个可以无限期地再次召唤自己的功能 遇到错误。 我正在描述基于Common Lisp HyperSpec尝试的不同方法,并且如果有人能够揭示原因的秘密,我将不胜感激 他们像他们一样行事。 我使用SBCL 1.3.8启用了尾部调用优化,并验证它在简单的尾递归函数上正常工作。
使用我尝试的第一种方法,m0被调用两次。一旦作为原始呼叫的结果,一次作为清除形式的一部分在展开保护中。 在第二个正文中遇到错误后,它不会正确执行清理表单。
我希望函数能够一遍又一遍地调用自身,并且遇到堆栈溢出或者SBCL能够将调用识别为尾调用并对其进行优化。
(defun m0 ()
(unwind-protect
(progn
(write-line "body")
(error "error"))
(write-line "cleanup")
(m0)))
(m0)
对结果感到好奇,我调查了它是否是嵌入式展开保护的一般情况,似乎是。以下程序显示相同的行为:
(unwind-protect
(progn
(write-line "body 0")
(error "error 0"))
(unwind-protect
(progn
(write-line "body 1")
(error "error 1"))
(write-line "body 2")
(error "error 2"))))
这种行为是否与内部展开保护的退出程度有关? 有没有办法让它工作,尤其是支持尾部呼叫消除的方式? 为什么unwind-protect不能任意嵌套?
我尝试的第二种方法遇到堆栈溢出。这并不像第一种方法的结果那么令人惊讶,但是如果不知道条件系统的内部细节,我会期望函数是尾递归的,因此我希望SBCL能够优化尾调用。
(define-condition m-error () nil)
(defun m1 ()
(handler-case
(progn (write-line "body")
(error 'm-error))
(m-error ()
(progn (write-line "cleanup")
(m1)))))
(m1)
是否有一种方法可以稍微修改函数以确保消除尾部调用?
由于达到为运行时环境定义的最大错误深度而引发错误。 我原本预计这将大致等于处理程序案例解决方案。由于handler-case和handler-bind的行为不同,在执行清理表单之前堆栈没有解开,但我仍然希望m的调用被识别为尾调用并在宏伟的计划。
(defun m2 ()
(handler-bind
((m-error #'(lambda (c)
(progn (write-line "cleanup")
(m2)))))
(write-line "body")
(error 'm-error)))
(m2)
与m1相关的问题也适用于此。
我想知道为什么这些案例不能像我预期的那样工作,基于文档。 freenode上#lisp的人也对这种行为感到困惑。
如果没有办法解决这些例子的问题,那么我会很感激指向一些可以实现此行为的构造,而不会将控制权返回到更高级别。
答案 0 :(得分:3)
首先,不能保证这是可能的:CL语言根本没有被指定为尾递归,因此完全取决于它们是否优化尾调用以及何时优化尾调用做什么,关于什么是尾部位置。
其次,你的第一个unwind-protect
实现可能不会按照你的想法做到,你的第三个也不行。在第三个实现的情况下,你的处理程序无法处理错误,这实际上意味着没有希望代码是尾递归的,因为处理程序必须保留在堆栈上,直到它正常返回或处理错误,它确实如此。
handler-bind
实施我认为handler-bind
未被广泛理解,这里是你的第三个实现的一个版本,可能有一个尾递归的机会:处理程序 处理错误,并且然后它跳转到recurses的代码。
(define-condition m-error ()
())
(defun m4 ()
(let* ((errored nil)
(result
(block escape
(handler-bind ((m-error
#'(lambda (c)
(declare (ignorable c))
(setf errored t)
(return-from escape nil))))
(error 'm-error)))))
(if (not errored)
result
(m4))))
但是,在我没有立即访问的实现(LW和CCL)中,这很容易编译为对m4
的尾调用(两个实现都会优化尾调用)。
我也试过这个解决方案的一个更可怕但更明确的版本:
(defun m5 ()
(tagbody
(return-from m5
(handler-bind ((m-error
#'(lambda (c)
(declare (ignorable c))
(go recurse))))
(error 'm-error)))
recurse
(m5)))
我无法暗示将m5
的递归调用编译为尾调用。可能要理解他们为什么不需要查看汇编程序。
unwind-protect
实施我不清楚这个可以工作。特别要记住
unwind-protect
评估 protected-form ,并保证在unwind-protect
退出之前执行 cleanup-forms ,无论它是正常终止还是由中止某种控制转移。
(来自CLHS。)
所以任何看起来像
的代码(defun m6 ()
(unwind-protect
...any form...
(m6)))
将以递归方式调用自己无论发生什么。特别是当你在 ...任何形式...... 中的任何错误之后退出调试器时,几乎肯定会这样做,如果 no 错误,肯定会这样做 ...任何形式...... ,只要它终止,并且当你退出Lisp实现本身时,它可能会尝试调用它自己。事实上,这个功能可能会使重新获得控制变得相当困难:即使通过中断评估,它终止或者很容易迫使它这样做也不是很明显。
以下内容让您有更多机会逃脱:
(defun m7 ()
(let ((errored nil))
(unwind-protect
(handler-case
(error 'm-error)
(m-error ()
(setf errored t)))
(when errored
(m7)))))
真正的程序员(正确地称为REAL PROGRAMMERS
)当然会编写以下版本,这样可以避免担心所有这些时髦的“尾递归”无意义:
(defun m8 ()
(tagbody
loop
(return-from m8
(handler-bind ((m-error
#'(lambda (c)
(declare (ignorable c))
(go loop))))
(error 'm-error)))))
(除非他们将其写在UPPERCASE
)。