[我不确定这个答案是否比另一个更有用:我在另一个答案之前开始了它,然后分心了。]
您真正希望能够在任何语言中实现的目标是能够轻松地从某些上下文中逃脱,回到给定点。这显然是异常处理的基础,但它比这更普遍。假设您有一些搜索程序:
(define (search-thing thing)
(if (thing-is-interesting? thing)
<return from search routine>
(search-children (thing-children thing)))
(define (search-children children)
... (search-thing ...) ...)
有时你可以很自然地表达这一点,这样当你找到你刚刚返回的东西时,它就会一直向上渗透。有时这要困难得多。所以你想要的是某种方式能够说“这是程序中的一个地方,这里有一台将返回那个地方的小机器”。所以用一些假设的语言:
(block here
...
(return-from here ...)
...)
这里block 构造建立了一个位置,return-from 从一个块返回。
好吧,如果你想返回的块在词法上对你来说是不可见的,你会怎么做?您可以将return-from 包装在一个函数中:
(block here
...
(my-search-function (lambda (v) (return-from here v)) ...
...)
这足以完成这个“逃到给定点”的事情:如果你在块的动态范围内调用这个过程,它会立即从块中返回它的参数。请注意,它没有做的是以某种方式在调用堆栈中搜索以寻找正确的返回位置:它只是直接进入块并从中返回一个值。
嗯,一个更自然的方法,也许,就是去掉所有这些做块的事情,直接进入过程的事情:只需要一个过程,它接受一个过程作为参数,然后用我上面做的这个逃生程序来调用它。这就是call/cc 的含义:
(call/cc (lambda (escape)
(my-search-function escape ...))
现在如果my-search-function或其调用的任何函数调用escape,那么它将立即从call/cc 表单返回其参数。
Python 没有真正像这样的构造(免责声明:我可能错了,因为我正在用更有趣的东西替换我三年前知道的 Python)。 Python 中的return 总是从词汇最里面的函数返回:你不能说return-from 从词汇最里面的函数之外的函数返回(对于returns,没有像nonlocal 这样的函数)。但是您可以使用异常来模拟它,因为异常具有标识。因此,如果您发生异常,则可以将其包装在一个函数中,该函数仅引发该异常,该异常将传递到您的代码中。调用此函数只会引发该异常(不是同一类之一:该实际对象),并在其中存储一个值。然后你建立一个try ... except: 块,它检查它刚刚捕获的异常是否是刚刚创建的异常,如果它是同一个对象,它返回它知道存储在那里的值。如果不是,它只是重新加注。
所以这是一个 hack,因为如果你有很多嵌套的东西,很多处理程序会查看它并拒绝它,直到它找到它所属的那个。但为此目的,这是一个可以接受的黑客攻击。特别是这意味着您可以将一个函数传递给另一个函数,如果它调用它,它将从您创建它的位置返回一个值并放弃任何中间计算。
这个习语就像 GOTO 的一种非常结构化的用法:您可以进行非本地控制转移,但只能转移到函数调用链中“上方”的一点(众所周知,调用堆栈总是向下增长:这是因为建造在受拉下稳定的结构比在受压下更容易,而且结构失效也不会损坏失效上方的堆叠部分)。
这正是 Python 示例代码的作用:
- 它会创建一个异常,
ball;
- 它创建了一个过程
throw,在ball 中存储一个值,然后将其提升;
- 然后它调用
proc 并以这个throw 过程作为它的参数,(在它返回的情况下将调用的值返回到proc),包裹在一个小try: ... except: ... 块中,用于检查这个特定的异常向上通过它,如果它发现它返回值throw 隐藏在其中。
所以你可以使用这个,例如,像这样:
def search(thing):
callcc(lambda escape: search_with_escape(escape, thing))
def search_with_escape(escape, thing):
...
if all_done_now:
escape(result)
...
这里search_with_escape实现了一些精细的搜索过程,可以通过调用escape来放弃。
当然,这只是Scheme 中延续功能的一半。因为一旦你得到了这个从某个地方返回的过程对象,那么,它就是一个过程:它是一个一流的对象,你可以返回它,然后如果你想调用它。用我们假设的语言应该怎么做:
(let ((c (block foo (lambda (v) (return-from foo v)))))
(funcall foo 3))
嗯,在我们假设的语言(如您所见,它是 Lisp-2)中,这是一个运行时错误,因为当控制通过 block 形式传递出去时,return-from 变得无效,所以虽然我有这个程序,但它不再有任何用处。
但这很可怕,对吧?我怎么知道我不能调用这个东西?我需要一些特殊的“可以在这里调用它”谓词吗?为什么它不能做正确的事?好吧,Scheme 的人正在感受他们的燕麦,他们做到了,因此 Scheme 等效项确实有效:
(let ((c (call/cc (lambda (cc) cc))))
(c 3))
好吧,当我说“确实有效”时,它仍然是运行时错误,但原因完全不同: 允许您调用我称之为“转义过程”的东西,它会尽职尽责地从生成它的表单返回一个值,无论它在哪里。所以:
-
(call/cc (lambda (cc) cc)) 只是返回延续对象;
-
(let ((c ...)) ...) 绑定到c;
-
(c 3) 调用 ...
- ...从
call/cc返回(再次)3,其中...
- ...将
c绑定到3;
- 现在您尝试调用
(c 3)这是一个错误。
你需要把这些运行时错误变成这样:
(let ((c (call/cc (lambda (cc) cc))))
(c (lambda (x) 3)))
-
(call/cc ...) 像以前一样返回一个延续对象;
-
(let ... ...) 绑定到c;
-
(c (lambda (x) 3) 调用 ...
- ...从
call/cc返回(lambda (x) 3),其中...
- ...将
c绑定到(lambda (x) 3);
- 现在您调用
((lambda (x) 3) (lambda (x) 3)),它返回3。
最后
(let ((c (call/cc (lambda (cc) cc))))
(c c))
我不打算解释。