理解 call-with-continuation 的实现

问题描述

我正在尝试理解用 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 作为参数值调用 throw(edure?)?评论“仅转义”是什么意思?


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

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 从词法最内层函数之外的函数返回(对于 {{1},没有像 nonlocal 这样的东西) }s)。但是您可以使用异常来模拟它,因为异常具有标识。因此,如果您抛出异常,那么您可以将其包装在一个函数中,该函数只会引发传递到您的代码中的异常。调用此函数只会引发该异常(不是同一类之一:该实际对象),并在其中存储一个值。然后你建立一个 return 块,它检查它刚刚捕获的异常是否是刚刚创建的异常,如果它是同一个对象,它返回它知道藏在那里的值。如果不是,它只是重新加注。

所以这是一个hack,因为如果你有很多这样的东西嵌套,很多处理程序会查看它并拒绝它,直到它找到它所属的那个。但为此目的,这是一个可以接受的黑客攻击。具体来说,这意味着您可以将一个函数传递给另一个函数,如果该函数调用它,则会从您创建它的地方返回一个值,并放弃任何中间计算。

这个习语就像一个非常结构化的 GOTO 用法:您可以进行非本地控制转移,但只能转移到函数调用链中“高于”您的一点(众所周知,调用堆栈总是向下增长:这是因为建造在拉伸下稳定的结构比压缩容易得多,而且结构故障也不会损坏故障上方的堆栈部分)。

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

  1. 它创建了一个异常,try ... except:;
  2. 它创建了一个过程 ball,它在 throw 中存储一个值,然后提升它;
  3. 然后它用这个 ball 过程作为它的参数调用 proc,(在它确实返回的情况下将调用的值返回到 throw),包裹在一个小 { {1}} 块,用于检查向上传递的特定异常,如果找到,则返回存储在其中的值 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) ... 来放弃。


当然,这只是延续让您在 Scheme 中所做的事情的一半。因为一旦你得到了这个将从某处返回的过程对象,那么,它就是一个过程:它是一个可以返回的一流对象,如果你愿意,可以稍后再调用。在我们的假设语言中,这应该做什么:

search_with_escape

好吧,在我们假设的语言(如您所见,是 Lisp-2)中,这是一个运行时错误,因为控制通过 escape 形式从 (let ((c (block foo (lambda (v) (return-from foo v))))) (funcall foo 3)) 变得无效,所以虽然我有这个程序,但它不再有用了。

但这太可怕了,对吧?我怎么知道我不能调用这个东西?我是否需要一些特殊的“可以在这里调用”谓词?为什么它不能做正确的事情?好吧,Scheme 的人正在摸着他们的燕麦,他们做到了,所以 Scheme 等价物确实有效:

block

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

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

你需要把这些运行时错误变成这样:

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

最后

((lambda (x) 3) (lambda (x) 3))

我不打算解释。

,

你明白什么是延续吗? 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