【问题标题】:Understanding Implementation of call-with-continuation理解调用的实现
【发布时间】:2021-07-01 20:25:28
【问题描述】:

我正在尝试理解用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

【问题讨论】:

    标签: python scheme lisp continuations callcc


    【解决方案1】:

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

    您真正希望能够在任何语言中实现的目标是能够轻松地从某些上下文中逃脱,回到给定点。这显然是异常处理的基础,但它比这更普遍。假设您有一些搜索程序:

    (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 示例代码的作用:

    1. 它会创建一个异常,ball
    2. 它创建了一个过程throw,在ball 中存储一个值,然后将其提升;
    3. 然后它调用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))
    

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

    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. 现在您调用((lambda (x) 3) (lambda (x) 3)),它返回3

    最后

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

    我不打算解释。

    【讨论】:

    • @tbh -- 哇,非常有帮助,非常感谢您的文章。你能在 python 函数中添加一点点让它“工作”吗?例如,不是在字符串(或其他)中搜索诸如字母之类的通用搜索,而是为了测试示例并使用它?
    • 你的回答比我的好很多!不清楚这家伙是否在问“什么是呼叫/抄送?”或“这如何实现 call/cc?”
    • @FrankYellin 是 ball 应该是 bail
    • @carl.hiass:不,它是用来做球的! Lisp 有(CL 仍然有)称为throwcatch 的构造,它们允许您动态地将某些东西扔给堆栈的更上层的捕手,而不会将其视为错误。我相当肯定,在早期的 Scheme 中,即使语义发生了变化,这些名称仍然被使用。你扔和接的东西是……一个球。
    • @carl.hiass:是的,你的例子看起来不错。是的,在 Python 版本中,“延续”对象只能向上转义:在 Scheme 中,它在任何地方都有效。
    【解决方案2】:

    你明白什么是延续吗? callcc(proc) 说调用函数 proc 时使用一个名为“延续”的参数。如果稍后在代码中的某个地方,您使用参数调用此延续,它会将调用延续的任何值返回给调用 callcc 的任何人。

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

    callcc 的真正实现实际上可以做很多这个实现不能做的事情。延续超过堆栈。但这是一个好的开始。

    【讨论】:

    • 谢谢。但是在实践中如何使用呢?例如,如果我的程序是 def square(x): return x*x ?
    • 或者,也许我的例子没有意义,你可以展示一个带有延续的示例函数以及如何使用它。
    • (对所有 cmets 感到抱歉)但我用我目前的理解更新了这个问题,以防万一。谢谢!
    【解决方案3】:

    其他问题更正确,但我在 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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-04-03
      • 2013-02-17
      • 2019-03-22
      • 2015-11-30
      • 2014-11-18
      • 2016-04-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多