递归调用错误

时间:2016-08-30 04:12:47

标签: recursion lisp common-lisp tail-recursion

我的目标是一个可以无限期地再次召唤自己的功能 遇到错误。 我正在描述基于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的人也对这种行为感到困惑。

如果没有办法解决这些例子的问题,那么我会很感激指向一些可以实现此行为的构造,而不会将控制权返回到更高级别。

1 个答案:

答案 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)。