美文网首页
call with current continuation

call with current continuation

作者: 青蛙跳井 | 来源:发表于2014-05-05 21:23 被阅读0次

    翻译自:http://community.schemewiki.org/?call-with-current-continuation

    call-with-current-continuation

    call-with-current-continuation 简介

    Scheme 提供了一个非常强大的控制抽象化叫做 call-with-current-continuation。不仅这个名字令人生畏,连很多对其语义的解释亦是如此。这篇介绍尽量做到平缓易懂。C 程序员也可以阅读这篇 call-with-current-continuation-for-C-programmers

    为了理解这篇文章,你应当对你使用的高阶函数(higher order procedure)清楚掌握,比如 map 和 for-each,lambda 的使用,传递函数给其它函数,以及从函数返回函数。

    首先,这篇简介使用缩写来代替这个冗长的名字:call/cc。这些许不再那么吓人并且输入更加方便。一些实现已经提供了一个别名。如果没有提供,为了运行这篇教程里的示例代码可以简单的定义一个:

    (define call/cc call-with-current-continuation)
    

    Exit Continuations:让我离开这里

    一个最简单的 call/cc 应用就是提供一个简单的方法来离开任何嵌套的计算。有些语言中,比如 Java 和 C,体现为 break,continue 和 return 等关键字。然而现在我有个问题。使用普通的递归,多数需要使用这些语言的这些关键字的问题(比如遍历二维数组)使用递归的方式来解决在Scheme中是十分平常的。所以现在你得容忍我举几个牵强的例子。

    让我们以在一个 list 中查找一个条目为例。当然我们可以手动在这个 list 上递归,一旦我们找到了就不再递归了。但是,有 for-each 这个函数来遍历一个 list。出于未知原因,但是我们必须使用它。如果我们像下面这样写,它将会十分强大:

    ;;; Return the first element in LIST for which WANTED? returns a true 
    ;;; value. 
    ;;; NOTE: This is not working Scheme code! 
    (define (search wanted? lst) 
      (for-each (lambda (element) 
                  (if (wanted? element) 
                      (return element))) 
                lst) 
      #f) 
    

    我们使用了一个叫做 return 的函数,希望从 search 中返回,并且使用当作其参数的 element 的值作为 search 的返回值。如果 for-each 执行完了,我们也没找到被查找的元素,那么返回 #f。

    遗憾的是,这样的在Scheme中并不存在。但是,call/cc 可以拯救一切!通过它,我们就可以得到这样的函数。现在,我们该如何使用 call/cc?该函数以另一个函数作为参数,接着作为参数的函数被调用,调用过程中被传入一个单一的参数:该参数正是我们上面想要的 return 函数!当 return 函数被调用时,对 call/cc 的调用立刻返回。特别地,call/cc 返回传递给 return 函数的参数。在实际的例子中,上面的代码可以写成可以真正运行的 Scheme 代码:

    ;;; Return the first element in LST for which WANTED? returns a true 
    ;;; value. 
    (define (search wanted? lst) 
      (call/cc 
       (lambda (return) 
         (for-each (lambda (element) 
                     (if (wanted? element) 
                         (return element))) 
                   lst) 
         #f)))
    

    这里我们可以看到函数 return 是如何被引入到表达式 for-each 的作用域中并被我们调用。当我们找到了想要的元素时,立刻将它传给 return 函数。当以上情况发生时,call/cc 表达式立即返回这个值。其它代码不再执行。整个表达式终止。你可以把这个想象成一种 goto —— 你只是跳出了这些代码和 call/cc 中的调用并且从中返回。

    这里有个需要注意的地方。Dijkstra 的著名论文《GOTO陈述有害论》(GOTO Statement Considered Harmful) 在这里也是恰当的:call/cc 能使程序难以理解。应当谨慎限制的使用以及尽量较好地抽象出来,以便它无法干涉对程序的正常理解。

    现在我发出这个警告,然后我们继续。你的大脑已经相当乱套了吗?你还没看见 call/cc 能做到的很多事情呢。

    既然 return 仅仅是个普通的变量,我们当然可以到处传递它。再一次把例子延伸一点以便聚焦眼前的问题,比如说我们传递的不是一个断言(predicate) wanted?,而是一个函数 treat 接受两个参数:一个元素,一个如果需要这个元素就被调用的函数。

    ;;; Call LIKE-IT with a custom argument when we like ELEMENT. 
    (define (treat element like-it) 
      (if (good-element? element) 
          (like-it 'fnord)))
    
    ;;; Call TREAT with every element in the LIST and a procedure to call 
    ;;; when TREAT likes this element. 
    (define (search treat lst) 
      (call/cc 
       (lambda (return) 
         (for-each (lambda (element) 
                     (treat element return)) 
                   lst) 
         #f)))
    

    如你所见,我们把从 call/cc 获得的 return 函数传递给 treat。然后 treat 做进一步的处理,如果想要这个元素,就以希望的值作为参数来调用 return 函数。正如我们所知道,一旦调用 return 函数,它的参数就会立刻被生成这个 return 函数的 call/cc 返回。在 search 中,我们有效地从 call/cc 调用的 treat 函数中跳出。

    这里与上面 return 的使用没有任何区别,但是你可以看到,它在程序中的任何位置都生效。这个特性赋予了这种操作一个特殊的名字:非本地退出(non-local exit)。我们没有在本地退出代码的地方,而是在一个完全不同的地方或位置退出。

    Full Continuations:回来!

    目前为止我们只看到了 call/cc 普通的用法。目前为止我们所做的全部就是从一个深度嵌套的调用体系——非常深的函数调用中离开,我们能够使用 return 函数来甩开这些全部的调用回到上层,退出。这叫做 exit continuation。此时,作为 call/cc 的一部分,单词 continuation 又一次出现了。Exit continuation 是一种特殊的 continuation,但是 call/cc 会创建常规的。

    Continuation 是一个用来继续(continue)的工具。上面我们使用的 return 是一个 continuation,用来返回到创建它的地方。我们继续了 call/cc 位置的计算。

    当 call/cc 产生的 continuation 跳出了传递给 call/cc 的函数的内部的时候会发生什么呢?比如说,下面这个例子:

    (define return #f)
    
    (+ 1 (call/cc 
          (lambda (cont) 
            (set! return cont) 
            1)))
    

    这个求值程序将输出 2,因为 call/cc 表达式的主体正常退出,返回了 1。然后与 1 相加,产出 2。但是!现在我们有一个叫做 return 的变量,它被绑定到了 call/cc 产生的 continuation 上。如果我们传递一个参数,比如 22,来调用它,会发生什么呢?

    > (return 22)
    23
    

    现在这里发生了什么呢?函数 return 与它上面做的事情相同:传递给 return 的 22 被用来当做调用 call/cc 的返回值。然后这个返回值与 1 相加,产出 23。这就是我们得到的。我们从来没有从调用 return 的地方返回,而是在上面的另外一个位置返回了。

    这里不同的是我们从外界重新进入了这个计算过程,而不只是仅仅离开它。这是一个超大的脑筋急转弯。千万要理解它!

    Coroutines:出去,进来,出去...

    假设我们有个函数执行一项耗时的任务。我们不想完全阻塞住系统,所以我们把这个任务分割成小步骤。当我们完成其中一个步骤时,我们调用一个函数,让系统执行任意一个现在想要执行的任务。拧劲儿的地方来了:这个函数接受一个函数,具体来说是个 continuation,被用来调用以继续这个耗时的计算:

    (define (hefty-computation do-other-stuff) 
      (let loop ((n 5)) 
        (display "Hefty computation: ") 
        (display n) 
        (newline) 
        (set! do-other-stuff (call/cc do-other-stuff)) 
        (display "Hefty computation (b)")  
        (newline) 
        (set! do-other-stuff (call/cc do-other-stuff)) 
        (display "Hefty computation (c)") 
        (newline) 
        (set! do-other-stuff (call/cc do-other-stuff)) 
        (if (> n 0) 
            (loop (- n 1))))) 
    

    当 do-other-stuff 调用 call/cc 传递给它当做参数的函数的时候,计算将会在这里继续,循环(loop)将接着递归,下一步被执行。当然,除了这种大型计算,所有其它的计算都是多余的。既然是多余的,它应当做相同的事情,点到为止,可能的话就让系统处理其它的事物:

    ;; notionally displays a clock 
    (define (superfluous-computation do-other-stuff) 
      (let loop () 
        (for-each (lambda (graphic) 
                    (display graphic) 
                    (newline) 
                    (set! do-other-stuff (call/cc do-other-stuff))) 
                  '("Straight up." "Quarter after." "Half past."  "Quarter til.")) 
        (loop))) 
    

    如果你照着下面输入就会出现这些:

    > (hefty-computation superfluous-computation) 
    Hefty computation: 5 
    Straight up. 
    Hefty computation (b) 
    Quarter after. 
    Hefty computation (c) 
    Half past. 
    Hefty computation: 4 
    Quarter til. 
    . 
    . 
    . 
    

    现在这里发生了什么呢?这个大型计算执行一步,就把控制权交给其它多余的计算。同样执行一步之后就把控制权还给大型计算。现在当然又是执行一步之后向其它多余计算交出控制权。然后……你发现了这个模式。这两个函数相互调用,在这两个之间传递控制权。当然,这里没有限制为两个函数,而是能够在任意数量函数之间实现。像这样在它们之间传递控制权的函数叫做协程(Coroutine),因此实际上它们是并行的。

    我希望现在你对协程有了一个基础的认识,并且能够告诉其他对此畏惧的人说这一点也不吓人。但是让我重复我先前的警告:Continuation 能够被轻易的滥用。不要过度使用它。在任何地方你都不是必须使用 continuation 的,真的!


    你还可以参考 R5RS 中的 call-with-current-continuation

    相关文章

      网友评论

          本文标题:call with current continuation

          本文链接:https://www.haomeiwen.com/subject/tpyrtttx.html