了解继续调用的实现



我试图理解用python代码编写的方案过程:

def callcc(proc):
"Call proc with current continuation; escape only"
ball = RuntimeWarning("Sorry, can't continue this continuation any longer.")
def throw(retval): ball.retval = retval; raise ball
try:
return proc(throw)
except RuntimeWarning as w:
if w is ball: return ball.retval
else: raise w

它来自本教程:http://norvig.com/lispy2.html。

以上是如何工作的?ball是什么意思,为什么proc(edure?)会用throw作为其参数值来调用?评论"仅转义"是什么意思?


顺便说一下,这是我目前(可能是被误导的)对延续的理解,因为它适用于 python,这类似于传递一个带有 yield 的函数:

def c(func, *args, **kwargs):
# func must be a coroutine
return func(*args, **kwargs)
def inc(x=0):
while True:
yield x
x += 1
>>> ct=c(inc, 3)
>>> next(ct)
3
>>> next(ct)
4

[我不确定这个答案是否比另一个更有用:我在另一个之前开始了它,然后分心了。

你真正希望能够在任何语言中实现的事情是能够无痛地从某些上下文中逃脱到给定点。 这显然是异常处理的基础,但它比这更通用。 假设您有一些搜索过程:

(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:块,用于检查它刚刚捕获的异常是否是刚刚创建的异常,如果它是同一个对象,它返回它知道藏在那里的值。 如果不是,它只是重新提出它。

所以这是一个黑客,因为如果你有很多嵌套的东西,很多处理程序都会查看它并拒绝它,直到它找到它所属的那个。 但对于这个目的来说,这是一个可以接受的黑客。 特别是,这意味着您可以将一个函数传递到另一个函数中,如果它调用它,它将从您创建它的位置返回一个值并放弃任何中间计算。

这个成语就像GOTO的一个非常结构化的用法:你可以进行非本地的控制权转移,但只能在函数调用链中"高于"你的一点(众所周知,调用堆栈总是向下增长:这是因为构建在张力下稳定的结构比压缩更容易, 结构故障也不会损坏故障上方的堆栈部分)。

这正是 Python 示例代码所做的:

  1. 它创建一个异常,ball;
  2. 它创建一个过程throw该过程,该过程将值存储在ball中,然后引发它;
  3. 然后,它使用此throw过程作为其参数调用proc(在它确实返回的情况下返回对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变得无效,所以尽管我有这个过程,但它不再有任何用处。

但这太可怕了,对吧? 我怎么知道我不能叫这个东西? 我是否需要一些特殊的"可以在这里调用它"谓词? 为什么它不能做正确的事情? 好吧,计划的人正在感受他们的燕麦,他们这样做是为了让计划等价物确实有效:

(let ((c (call/cc (lambda (cc) cc))))
(c 3))

好吧,当我说"确实有效"时,它仍然是一个运行时错误,但出于完全不同的原因:你可以调用我称之为"转义过程"的东西,它会尽职尽责地从制作它的形式中返回一个值,无论它在哪里。 所以:

  1. (call/cc (lambda (cc) cc))只返回继续对象;
  2. (let ((c ...)) ...)将其绑定到c;
  3. (c 3)调用的延续...
  4. 。从call/cc返回(再次)3,其中...
  5. 。将c绑定到 3;
  6. 现在您尝试调用(c 3)这是一个错误。

这些运行时错误需要将其变成这样的东西:

(let ((c (call/cc (lambda (cc) cc))))
(c (lambda (x) 3)))
  1. (call/cc ...)像以前一样返回一个延续对象;
  2. (let ... ...)将其绑定到c;
  3. (c (lambda (x) 3)调用的延续...
  4. 。从call/cc返回(lambda (x) 3),其中...
  5. 。将c绑定到(lambda (x) 3);
  6. 现在你调用返回3((lambda (x) 3) (lambda (x) 3)).

最后

(let ((c (call/cc (lambda (cc) cc))))
(c c))

我不会试图解释。

你明白什么是延续吗?callcc(proc)说用一个叫做"延续"的参数来调用函数proc。 如果在代码后面的某个地方,你用参数调用这个延续,它会将调用延续的任何值返回给调用callcc的人。

throw是延续。 当您使用参数调用延续时,它会引发异常,然后弹出堆栈,直到找到对创建它的callcc的精确调用。 然后返回一个值。

callcc的真正实现实际上可以做很多这个实现不能做的事情。 延续比堆栈寿命长。 但这是一个良好的开端。

其他问题更正确,但我正在发布一个可用于测试的 python 工作示例:

def callcc(function):
bail = RuntimeWarning("My custom bail.")
def escape_function(retval): 
bail.retval = retval; # adding our functions return value into the exception itself
raise bail
try:
# this will call the function and the escape function RAISES bail 
# so it'll never return
return function(escape_function)
except RuntimeWarning as w:
if w is bail: 
retval = bail.retval
print("About to return value of %s..." % retval)
return retval
else: 
raise w
def countdown(n):
# the function we are passing to callcc is `countdown_with_escape`
# countdown_with_escape will later be called by callcc with the 'throw' as the escape function
return callcc(lambda escape_function: countdown_with_escape(escape_function, n))

def countdown_with_escape(escape_function, n):
while True:
print (n)
if n == 9:
escape_function(n) # this passes '9' as the retval to the escape function
n -= 1

并运行它:

x = countdown(20)
print ('Done with value: %s' % x)
20
19
18
17
16
15
14
13
12
11
10
9
About to return value of 9...
Done with value: 9

最新更新