2012年6月26日 星期二

Continuation

call/cc 語義

(define (f return)
  (return 2)
  3)
 
(display (f (lambda (x) x))) ; displays 3
 
(display (call-with-current-continuation f)) ; displays 2
Calling f with a regular function argument first applies this function to the value 2, then returns 3. However, when f is passed to call/cc (as in the last line of the example), applying the parameter (the continuation) to 2 forces execution of the program to jump to the point where call/cc was called, and causes call/cc to return the value 2. This is then printed by the display function.
这个很难理解,而且几乎找不到解说资料。
1.什么是current-constitution?
The current continuation at any point in the executionof a program is an abstraction of therest of theprogram.
current-constitution就是运行到目前,剩余的程序的抽象。
2.怎么去call?
就是用call/cc这个过程来调用。
        call/cc参数必须为一个单参数的过程pro,call/cc 构造一个具体的“目前继续”的代表传递给pro,而“目前继续”由过程kont来表示。每一次kont被传递一个值,它返回该值给call/cc的目前继续,这个值本质上就变为call/cc的值。如果pro没有引用kont,pro的返回值变成call/cc的值。
被翻译成汉语了还是这么难以理解啊!
大概的意思就是:call/cc会以一个单参数的过程(设为pro)为参数,pro的参数(设为kont)会夺取pro的目前继续,那pro没有“目前继续”怎么办?放心,call/cc会为其构造一个(不过当然是山寨货了),而kont有了pro的”目前继续“也变成了一个过程(procedure)。每当kont被传递一个值,它会把她送给call/cc。如果没有传递值给kont,pro会把返回值送给call/cc。
说的再简单点的话:
       kont想出人头地,抢了大哥pro的东西”current-constitution“,自己地位提高了,大哥却很伤心,kont虽然觉得对不起大哥,但是地位的诱惑还是很大的,不愿意把东西还给大哥。call/cc看到了,自己造了一个山寨货给pro,kont为感谢call/cc凡是自己收到的东西(地位提高了嘛,自然就有人1414),都送给call/cc,pro为了感谢call/cc,凡是别人送的东西(当然和kont的东西相比不值什么钱)也都送给call/cc。这么看来call/cc真是好奸啊!

例子:
(+    2
        (call/cc 
          (lambda (kont) 
            (+    3    (kont   5))))
kont收到礼物了,所以会返回:
(+   2     5)
  => 7

(+   2
      (call/cc
       (lambda  (kont)
        (+3    5))))
看来还有人恋旧情,pro收到礼物:
(+   2      8)
  => 10

用法一:退出循环
定义一个函数list-product,该函数接受一个列表,返回列表中所有元素的积。
法一:
(define   list-product
       (lambda   (s)
          (let     recur   ((s    s)) 
              (if     (null?  s)   1
                   (*     (car     s)   (recur     (cdr    s)))))))
这样定义可行,但是如果有一个列表有几百个元素,而第二个元素就是零会怎么样?
结果这个程序会一个劲的傻算,浪费大家时间,还有我的电费。
用call/cc会怎么样呢?
(define      list-product
          (lambda     (s)
             (call/cc
                (lambda   (exit)
                    (let     recur     ((s    s))
                        (if    (null?   s) 1)
                            (if    (=    (car   s)  0)  (exit    0)
                                     (*      (car    s)    (recur   (cdr    s)))))))))
程序突然变长了不少,这个就是代价吧! 我们只用分析很色部分,其余的和上边的那个一样。
红色部分:如果在某一步(程序是从左到右一步一步算)发现出现了0,那个2B kont——现在是exit,就会收到礼物0,他立马返回该值给call/cc,而程序的结果就是call/cc的返回值kont送的0。

用法二:树的匹配
定义一个谓词same-fringe?匹配列表的展开是否相同
如:(same-fringe? '(1 (2 3)) '((1 2) 3))=>#t(same-fringe? '(1 2 3 4) '(1 (3 2) 4 ))=>#f
定义:
(define   same-fringe?
      (lambda   (tree1   tree2)
            (let   loop     ((ftree1   (flatten   tree1))
                              (ftree2     (flatten  tree2)))
                  (cond     ((and    (null?   ftree1)  (null?   ftree2))  #t)
                                ((or   (null?      ftree1)  (null?   ftree2))   #f)
                                ((eqv?   (car   ftree1)   (car    ftree2))
                                     (loop  (cdr  ftree1)    (cdr   ftree2))
                                  (else   #f))))) 
其中函数flatten的作用为将tree展开,使之只含最外面一对括号。
例如:(1   (2   3)  4)展开为(1    2   3   4)
faltten的定义:
(define   flatten
       (lambda   (tree)
           (cond     ((null?    tree)   '())
                         ((pair?     (car   tree))
                           (append     (flatten     (car     tree))
                                             (flatten     (cdr    tree))))
                           (else
                               (cons    (car    tree)
                                            (flatten      (cdr     tree)))))))
我们先看一下这个faltten的定义是不是能满足我们的要求。
1.如果要展开的列表为空,返回空表。
2.如果第一个元素是一个pair,我们就把它看作是列表吧,因为列表也是pair。
就是说这种情况:((2    4)    6),它会把剩余的展开附加到(2   4)的展开,结果就为:(2  4  6).
3.如果第一个元素不为pair,会把第一个元素之外的剩余部分的展开附加到第一个元素后。
这样:(2  (4   5))   =>  (2   4   5)
flatten确实能达到我们的要求。
再来看same-fringe?
它会先用flatten把两棵树整个展开,然后从左到有依次比较各个元素。
and部分:如果最后两棵树的元素都为空了,那么结果为真。
or部分:如果最后其中一个为空而另一个不为空(不可能两者都为空,因为or字句在and字句后),那么结果为假。
eqv?部分:如果这一步两者相同,就进入下一步循环。
否则(else部分):就为假。
在这里我们终于说明same-fringe?可以达到我们的目地。
但是肯定是有问题的呀,我们的call/cc还没出马呢,举一些极端情形下的例子,问题就出来了。
假设:两颗树都很长很长,比如几千个元素,而它们的展开第二个元素就不同。那我们就没必要把他们全堵展开。


函數調用與continuation

一般函數調用方法都是使用堆棧,用Activation record或Stack frame來記錄從最頂層函數到當前函數的所有context。

一個frame/record就是函數的局部上下文信息,包括所有局部變量的值和SP, PC指針的值(通過靜態分析,某些局部變量的信息是不必保存的,特殊的如尾調用的情況則不需要任何stack frame。不過,邏輯上,我們認爲所有信息都被保存了)。

函數的調用前往往伴隨著一些push來保存context信息,函數退出時則是取消當前的record/frame,恢複上一個調用者的record/frame。

象pascal這樣的支持嵌套函數的,則需要一個額外的指針來保存父函數的frame地址。不過,無論如何,在任何時候,系統保存的就是一個後入先出的堆棧,一個函數一旦退出,它的frame就被刪除了。

Continuation則是另一種函數調用方式。它不采用堆棧來保存上下文,而是把這些信息保存在continuation record中。這些continuation record和堆棧的activation record的區別在于,它不采用後入先出的線性方式,所有record被組成一棵樹(或者圖),從一個函數調用另一個函數就等于給當前節點生成一個子節點,然後把系統寄存器移動到這個子節點。一個函數的退出等于從當前節點退回到父節點。

這些節點的刪除是由garbage collection來管理。如果沒有引用這個record,則它就是可以被刪除的。

這樣的調用方式和堆棧方式相比的好處在哪裏呢?

最大的好處就是,它可以讓從任意一個節點跳到另一個節點。而不必遵循堆棧方式的一層一層的return方式。比如說,在當前的函數內,只要有一個其它函數的節點信息,完全可以選擇return到那個函數,而不是循規蹈矩地返回到自己的調用者。也可以在一個函數的任何位置儲存自己的上下文信息,然後,在以後某個適當的時刻,從其它的任何一個函數裏面返回到自己現在的位置。

Scheme語言有一個CallCC (call with current continuation)的機制,也就是說:
取得當前的continuation,傳遞給要call的這個函數,這個函數可以選擇在適當的時候直接return到當前的continuation。

經典的應用有:exception,back-tracking算法, coroutine等。

應用continuation對付exception是很明顯的,只要給可能抛出異常的函數一個外面try的地方的continuation record,這個函數就可以在需要的時候直接返回到try語句的地方。

  Exception-handling也可以利用continuation。c++等語言普遍采用的是遇到exception就直接中止當前函數的策略,但是,還有一種策略是允許resume,也就是說,出現了異常之後,有可能異常處理模塊修複了錯誤發生的地方然後選擇恢複執行被異常中斷了的代碼。被異常中斷的代碼可以取得當前的continuation,傳遞給異常處理模塊,這樣當resume的時候可以直接跳到出現異常的地方。

  Back-tracking算法也可以用類似的方法,在某些地方保存當前的continuation,然後以後就可以從其它的函數跳回當前的語句。

  Coroutine是一個並行計算的模型。它讓兩個進程可以交替執行。典型的coroutine的應用例子如consumer-producer。比如說:

  Code:Producer ( c ):

  Loop:

  Produce;

  CallCC c

  Consumer( p ):

  Loop:

  Consume;

  Callcc p

  這兩個線程接受對方的continuation,在需要交出控制的時候,就把自己的continuation傳遞給對方。如此,兩者就可以交替執行,而不需要返回。

  Continuation機制的優化始終不是一個trivial的問題,實際上采取continuation的語言不多。而且,continuation調用方式依賴垃圾收集,也不是c/c++這類中低級的語言所願意采用的。

  不過,continuation的思想仍然是有其用武之地的。有一種設計的風格叫做continuation-passing-style。它的基本思想是:當需要返回某些數據的時候,不是直接把它當作函數的返回值,而是接受一個叫做continuation的參數,這個參數就是一個call-back函數, 它接受這個數據,並做需要做的事情。

  舉個例子:

  Code:X = f();

  Print x;

  把它變成continuation-passing-style, 則是:

  f(print);

  F()函數不再返回x, 而是接受一個函數,然後把本來要返回的x傳遞給這個函數。

  這個例子也許看上去有點莫名其妙:爲什麽這麽做呢?對Haskell這樣的語言,一個原因是:

  當函數根據不同的輸入可能返回不同類型的值時,用返回值的話就必須設計一個額外的數據結構來處理這種不同的可能性。比如:

一個函數f(int)的返回值可能是一個int, 兩個float或者三個complex,那麽,我們可以這樣設計我們的函數f.

  Code:F:: Int - (Int-a) - (Float-Float-a) - (Complex-Complex-Complex-a) - a

這個函數接受一個整形參數,三個continuation回調用來處理三種不同的返回情況,最後返回這三個回調所返回的類型。

另一個原因:對模擬imperative風格的monad,可以在函數中間迅速返回(類似于C裏面的return或者throw)

對于C++,我想,除了處理不同返回類型的問題,另一個應用可以是避免返回值的不必要拷貝。雖然c++現在有NRV優化,但是這個優化本身就很含混,各個編譯器對NRV的實現也不同。C++中的拷貝構造很多時候是具有副作用的,作爲程序員,不知道自己寫的的副作用到底是否被執行了,被執行了幾次,總不是一個舒服事。

而continuation-passing-style,不依賴于任何偏僻的語言特性,也不會引入任何的模棱兩可,也許可以作爲一個設計時的選擇。舉個例子, 對于字符串的拼接,如果使用continuation-passing-style如下:

  Code:Template<class F

  Void concat(const string& s1, const string& s2, F ret){

  String s(s1);

  S.append(s2);

  ret(s);//此處,本來應該是return(s),但是我們把它變成ret(s)。

  }

  我們就可以很安心地說,我們此處沒有引入任何不必要的拷貝,不論什麽編譯器。

當然,continuation style的問題是,它不如直接返回值直觀,類型系統也無法保證你確實調用了ret(s)。而且,它需要一個function object,c++又不支持lamda,定義很多trivial的functor也會讓程序變得很難看。

  利弊如何,還要自己權衡。

Scheme 是一門神奇的編程語言,它不僅是世界上第一個完整支持閉包(closure)的語言,也是世界上第一個提供continuation 的語言。你可以看到wiki 上幾個關於Continuation 的條目全部用Scheme 作為示例語言。如無特指,本文以及接下來的兩篇文章中凡是提到continuation 的地方,均是指Scheme 中的continuation。

During the evaluation of a Scheme expression, the implementation must keep track of two things: (1) what to evaluate and (2) what to do with the value. ... We call "what to do with the value" the continuation of a computation.

Continuation 就是一個表達式被求值之後,接下來要做的事情。描述很簡單,但是Scheme continuation 的用法比較奇葩,導致我在學習continuation 的過程中被幾個問題困擾了很久,不過沒關係,哥現在終於搞清楚了。寫下這篇文章不是為了說明continuation 是什麼,而是想能幫助其他人更好地理解這個東西。下面我們通過幾個小實驗全面了解continuation 的各個特性,在看每一個例子的時候,希望你先思考一下“這段代碼的結果是什麼?”、或者“為什麼是這個結果?”,然後再看我的解說。

I. 非本地退出 Non-local exit
非本地退出是初學continuation 很好的例子,因為它邏輯簡單,並且體現了continuation 的三個特性,:

continuation as first-class,簡單地說就是continuation 也可以視為一等公民,可以當做參數被傳遞和返回;

continuation is represented by procedure,也就是說可以視continuation 為過程,可以調用它,本來也應該如此,因為continuation 表示的正是“將來要做的事情;

假設call/cc 捕捉了當前的continuation,並綁定到lambda 的參數cc,那麼在lambda 函數體內,一旦cc 被直接或間接的作為過程調用,那麼call/cc 會立即返回,並且提供給cc 的參數即為call/cc 的返回值。

請看下面這段程序:

(define (test element cc)
  (if (zero? element)
      (cc 'found-zero) ; non-local exit
      (void))) 

(define (search-zero test lst) 
  (call/cc 
   (lambda (return) 
     (for-each (lambda (element)
                 (test element return)
                 (printf "~a~%" element)) ; print
               lst)
     #f)))

(search-zero test '(-3 -2 -1 0 1 2 3))

運行結果:
-3
-2
-1
'found-zero

函數search-zero 遍歷表中所有的元素,每一次都把當前的continuation 取出來然後傳給test,一旦遇到0,就立即終止並返回'found-zero。本來for-each 應當遍歷了整個表,但是test發現0的時候,就把'found-zero 傳給cc,並調用之,由此search-zero 在這時直接返回,而不會執行下剩下的迭代。


Scheme <wbr>Continuation <wbr>三部曲(一)鈥斺斏钊肜斫 <wbr>Continuation
我把函數的一次次求值畫成一段段的箭頭,當test 遇到0的時候,它利用cc 執行了一次非本地退出,跳到了cc 定義的continuation,怎麼知道這個continuation 是什麼呢?分析一下它的值被用來做什麼了。由於search-zero 的整個函數體都被call/cc 包起來了,所以call/cc 的返回值剛好就是search-zero的返回值,而這個值接下來被用來執行的操作就是輸出到屏幕——這就是cc 的內容,test 利用cc 令search-zero 中的call/cc 返回'found-zero,隨後這個結果被輸出到了屏幕上。

實際上還沒完,R5RS描述道,“ The continuation represents an entire (default) future for the computation”。 continuation 不僅保存了這個表達式的值被求出來後拿去做了什麼,還保存著這個值被求出來後,對後序表達式的求值操作。 sea​​rch-zero 是在頂層被調用的,所以cc 還包括“提示下一個輸入,執行輸入的表達式,如此永不停歇”。也就是說, cc 中包含著一個無限的操作—— 一時難以理解是吧,但事實就是如此,哈哈哈。

再看一個生成器,它接收一個列表作為參數,每次被調用的時候就返回一個元素,有點像python 的yield:

(define (generate-one-element-at-a-time lst)
  ;; Hand the next item from a-list to "return" or an end-of-list marker
  (define (control-state return)
    (for-each 
     (lambda (element)
       (call/cc
        (lambda (resume-here)
          ;; Grab the current continuation
          (set! control-state resume-here) ;; !!!
          (return element))))
     lst)
    (return 'end))

  (define (generator)
    (call/cc control-state)) 

  ;; Return the generator 
  generator)

(define generate-digit
  (generate-one-element-at-a-time '(0 1 2)))

> (generate-digit)
0
> (generate-digit)
1
> (generate-digit)
2
> (generate-digit)
'end
> (generate-digit)
'end


又是一个非本地退出的例子,值得注意的是 control-state,这个函数第一次被调用的时候它还是个函数,但是看注释“!!!”的这一行,经过第一次调用后, control-state 就把自己改成了一个 continuation,随后借助 generator 传进来的 return,完成一次非本地退出(在 control-state 里退出generator)。第一次看这个可能有点晕,因为这里有两个 call/cc,而且还是嵌套的。这两个 call/cc 各司其职,generator 里的 call/cc 是为了非本地退出,第二个 call/cc 则是为了记录遍历列表的状态——这样下一次调用 generator 的时候,通过 control-state(它是个 continuation),就能直接从上次修改 continuation 的地方继续运行,从而跳了回去,达到生成器的效果。


通过在 call/cc 内 apply continuation,我们可以在任意时刻从函数中跳出来;通过修改中途获取的 continuation,我们还可以跳回去。



II. 非引用透明性


我们知道,引用透明性(Referential Transparent)是纯函数式语言的重要特征,但通过 call/cc,我们能定义出一个不具有引用透明性的函数:
(define (get-cc) (call/cc (lambda (c) c)))
看出 get-cc 做了什么吗?它捕捉到当前的 continuation 然后返回,显然这个函数的返回值受到上下文——准确地说是下文,的影响,所以它其实很不透明= = 但正是它的不透明性能帮助我们更好地了解 continuation。来看下面这段代码:

> (define x (get-cc))
> x
#< continuation>
> (x 10)
> x
10
> (number? x)
#t
> ((get-cc) 10)
. . a application: not a procedure;
 expected a procedure that can be applied to arguments
  given: 10
  arguments...:
   10


你明白上面发生了什么吗? x 明明是个 continuation,怎么变成10了??为什么 (get-cc) define 成 x可以被调用,却不能被直接调用???


我曾经被这几个“奇怪的现象”困扰了好一阵子。但是想清楚 get-cc 的不透明性及其缘由就明白了。实际上,经过 define 之后, x 获得了一个 continuation,这个 continuation 是什么呢?……是获取一个值,然后绑定到 x —— 恰好就是 define 所做的事情。所以再以10为参数调用 x 的时候, continuation 把10绑定到 x,x 又被绑定了一次,就变成数字10了。但是直接对 (get-cc) apply 10的时候,却提示错误,这是因为这个 (get-cc) 接下来被用来当成函数调用了,所以给它传一个10之后,这个10就被当做函数执行,显然这是不对的。get-cc 是个神奇的函数,就是获取它这个位置上的 continuation, (get-cc) 自己被用来做了什么事,它返回的 continuation 就对别人做同样的事,以彼之道,还施彼身。如果你感到有点晕,别急着往下看,先消化一下。


接下来这个表达式,你能看出它的返回值是什么吗?(let ([x (get-cc)]) (x (lambda (unused) "我擦")))



也许你能马上猜到结果,但是不一定能马上明白。我们仍然从 (get-cc) 开始分析。我们看到 (get-cc)被用来绑定到 x上,随后以 (lambda (unused) "我擦")) 为参数调用 x。现在开始还施彼身。于是,接下来(lambda (unused) "我擦")) 也被绑定到 x 上,然后用参数 (lambda (unused) "我擦")) 调用新的 x,显然这个参数没有用上,所以 x 直接返回了“我擦”。真是我擦啊……


这是最后一个关于 get-cc 的例子。
(((get-cc) (lambda (x) x)) "我又擦")


你应该马上就能猜到,这个表达式返回了“我又擦”,but why?


沿用刚才的思路,先分析 (get-cc) 自己被用来做了什么?它被当成函数,以 (lambda (x) x) 为参数进行调用,调用的返回值又被当成函数,以“我又擦”为参数执行调用。好,(get-cc) 的参数是 (lambda (x) x)——一个恒等函数,于是 (lambda (x) x) 被当成了函数,以 (lambda (x) x)(没错,就是它自己)为参数执行调用,返回值当然还是它自己,又一个恒等函数;最后,这个恒等函数接收参数“我又擦”,当然也就返回“我又擦”。


(get-cc) 的非引用透明性来源于它的语义,它总是捕捉当前的 continuation 并返回之。可以这么理解call/cc,它可以出现在任何一个本应是表达式的地方(它占了表达式的位置)。凡是表达式都要求值,并且还要求它的后续表达式的值,程序员通过call/cc,可以在该表达式出现的地方捕捉(catch)到该表达式的后续操作。被捕捉到的后续操作即为 continuation,某种程度上说,它就像个时光机,调用捕捉到的 continuation 可以回到过去。但是注意调用 continuation 和非本地退出的区别,后者是在 call/cc 的函数体内(直接或间接)调用捕捉到的 cc,这是 continuation 的特殊用法,它能立即退出,而且可以在非本地退出;而前者是在相应 continuation 的 call/cc 之外调用,它的作用就是重复后续操作。

III. 移花接木
这一部分只讲解一个例子,不过这个例子更绕~ V神跟我说,monad 就是给一个函数打很多个孔,然后灵活地组合,我觉得 continuation 也是给函数打孔,而且不同函数打的孔还可以对接起来。

生产者-消费者问题是检验多任务机制的经典问题,我们可以用 continuation 模拟这个过程,即生成-消费-再生产-再消费-... (define dish #f) (define (put! fruit) (set! dish fruit)) (define (get!) (let ([fruit dish]) (set! dish #f) fruit)) (define (consumer do-other-stuff) (let loop () (when dish (printf "C: get a ~a~%" (get!)) (set! do-other-stuff (call/cc do-other-stuff)) (loop)))) (define (producer do-other-stuff) (for-each (lambda (fruit) (put! fruit) (printf "P: put a ~a~%" fruit) (set! do-other-stuff (call/cc do-other-stuff))) '("apple" "strawberry" "peach" "grape"))) (producer consumer)

producer 通过 for-each “生产”了几个水果,每生成一个就放进盘子里,并通过 call/cc 把控制权交给 consumer,consumer 先看看盘子里有没有水果,如果有就取。运行结果不难预科: > (producer consumer) P: put a apple C: get a apple P: put a strawberry C: get a strawberry P: put a peach C: get a peach P: put a grape C: get a grape

实际上这段程序有两个过程交替运行,我们知道多任务控制流的一个关键就是,保存每个任务的上下文,让它切出去再返回的时候能接着执行,就像没有发生过切换一样。这个任务,continuation 完全胜任。How does it works?

producer 往盘子里放水果之后,通过 call/cc 调用 consumer,它的 continuation 就传进 consumer了(consumer 的 do-other-stuff 就是 producer 的 continuation), consumer 取出水果之后,就能通过这个 continuation 回到 producer,同时它也把自己的 continuation 传给了 producer。两个函数通过互传 continuation 的方式实现切换和恢复。

IV. 模拟多任务


最后,我们看看如何用 continuation 模拟多任务。刚才的例子已经模拟了一个很简单的多任务过程,即producer 和 conmuser 两个过程“同时”运行,但那是两个任务都知道对方的存在从而实现精确的同步。如果是几个独立的进程,如何并发执行呢? (define lwp-list '()) (define lwp (lambda (thunk) (set! lwp-list (append lwp-list (list thunk))))) (define start (lambda () (let ([p (car lwp-list)]) (set! lwp-list (cdr lwp-list)) (p)))) (define pause (lambda () (call/cc (lambda (k) (lwp (lambda () (k #f))) (start))))) (lwp (lambda () (let f () (pause) (display "h") (f)))) (lwp (lambda () (let f () (pause) (display "e") (f)))) (lwp (lambda () (let f () (pause) (display "y") (f)))) (lwp (lambda () (let f () (pause) (display "!") (f)))) (lwp (lambda () (let f () (pause) (newline) (f))))



lwp 即 ligth-weight process, lwp-list 是进程表,不难看出 lwp 和 start 的作用分别是添加一进程到进程表、和从进程表里取出一个进程并执行,理解这段程序的关键在于 pause。


pause 是这些进程真正的调度者,它不仅有副作用,而且也不具有引用透明性,所以难以理解,我们分析一下。(lwp (lambda () (k #f))) 把 pause 的 continuation 添加到进程表尾,以备将来恢复。但注意,call/cc 能取出一个函数的全部未来,当然也包括这个函数执行完毕后的后续操作——每一个 lwp 调用pause 之后的操作是“输出一个字符,然后再递归调用自己”,这些操作当然也在 pause 的 continuation 里面——
oh no


你看出来了吗?这些进程第一次被执行时不输出任何信息,因为它们到达 (pause) 的时候,pause 把剩余操作打包成 continuation 放到进程表尾,然后就执行下一个进程了。最后的执行结果就是不停的打印 "hey!": > (start) hey! hey! hey! hey! hey!

V. 总结一下


如果充分理解的上面所有的例子,那也就相当程度地掌握了 continuation。说了那么多,continuation 有什么用呢?这篇文章举的例子已展示了一些使用 continuation 的模式,如非本地退出、保存上下文等。在第三篇文章中我们还会看到如何用 continuation 消除非尾递归。continuation 为程序员提供了一种强大、灵活、近乎随心所欲的控制流(Tom 言:时光机),但是它太锋利,而且很多时候还要涉及一些带副作用的操作,另外想必你也发现带 call/cc 的代码非常难读= =,综上所述,除非你已经非常非常熟悉 continuation, 否则不建议轻易使用。其实我仍然有一些小细节没有讲,有些是连自己也没明白透,有些是特意留着让你自己去发现。


下一篇文章,我们将领略 continuation 的终极奥义,真·阴阳谜题,敬请期待。

沒有留言:

張貼留言