1. 程式人生 > >scheme心得(1) continuation與陰陽謎題

scheme心得(1) continuation與陰陽謎題

摘要:簡要介紹了scheme語言中continuation的用法。解釋了陰陽謎題程式的執行過程與結果。

Scheme是一種lisp方言,個人比較常用的執行環境是MIT-GNU Scheme。

今天談一談Continuation,是scheme的一種特性。一個有趣而神祕的應用是陰陽謎題。它將連續不斷的打印出一列字串。@*@**@***。。。這段小程式碼充分體現了continuation的魅力。

(define call/cc call-with-current-continuation)
(let* ((yin ( (lambda (foo) (display "@") foo) (call/cc (lambda (bar) bar))))
       (yang ( (lambda (foo) (display "*") foo) (call/cc (lambda (bar) bar)))))
  (yin yang))

continuation本質就是control context的抽象,能夠實現函式式語言裡原本沒有的跳轉結構,類似break, return, goto等等。continuation就是一個單引數的過程,它等待輸入一個值,然後繼續其餘的運算。或者也可以把這個continuation儲存在一個變數裡,當這個變數被呼叫時,相當於返回到定義continuation的那個點重新開始計算。

continuation語法結構是:

(前面的程式碼... call-with-current-continuation (lambda(k) body) ...後面的程式碼)

這就是一個過程,過程名為call-with-current-continuation(通常可以define為call/cc),call/cc過程的引數是一個有單引數的lambda表示式,引數名可以為k,也可以為return或其他。這個call/cc表示式,有一個返回值,這個返回值將與前後的程式碼一起,構成完整的程式。看上去與正常的scheme程式碼沒有太大區別。但程式在計算call/cc語句的時候,給lambda(k)的形參k傳遞的實參是當前的continuation。也就是說,像是在此處建立了一個座標,當前的continuation可以儲存下來,並作為實參傳遞給了(lambda(k) body)。body對於引數k可以有兩種操作,也就是call/cc的兩種用法。

第一種用法,body裡面有k x這樣的語句。剛才說過,call/cc處有一個座標(continuation),此時整個程式的狀態就是,就差call/cc( lambda(k) body)語句返回一個值,就能完成全部的計算了。那麼k x 的意思就是,把x的值傳給剛才定義的continuation並完成整個程式。

第二種用法,body裡面有set! c=k這樣的語句。c是一個call/cc之前就定義了的變數。把c賦值為k的結果就是,這個continuation被暫時儲存到變數c了。call/cc語句執行完畢返回並完成後續的計算不提,那如果後面有c x這個表示式,程式控制流又回到了call/cc之前的座標,continuation等待的值現在是x,將x的值代替call/cc語句,可完成整個程式的計算。

看一些例子:

(call/cc
  (lambda(k)
    (* 5 4)))
(call/cc
  (lambda(k)
    (* 5 (k 4)))))
(+ 2
  (call/cc
    (lambda(k)
      (* 5 (k 4))))))
(call/cc
  (lambda(k)
   (for-each(lambda(x)
                (if (negative? x)
                    (k x)))
            '(1 2 0 -1 -2))
  #t))

這4個例子都是body裡面k x的用法。當body中出現k x的語句時,x的值被送到continuation,完成計算。

第一個例子,返回20。第二個例子,返回4,因為有(k 4)。第三個例子,返回6,因為call/cc語句返回的就是(k 4)中的4,整個程式相當於(+ 2 4),故返回6。第四個例子,返回-1,因為(k x)在x為負數的時候返回,第一個負數就是-1,相當於(k -1)。

再看一個複雜的例子。

(define 2-dynamic-binding
  (let ((var 1)
        (inside-continuation))
   (write-line '() )
   (write-line var)                            ;1, print "1"
   (call/cc                                                              ;outside continuation begin
     (lambda (k1)
       (fluid-let ((var 2))
       (write-line var)                        ;2, print "2"
       (set! var 3)
       (call/cc                                                            ;inside continuation begin
         (lambda(k2)                       
           (set! inside-continuation k2)       
           (k1 #t)))                           ;3, return to k1            ;inside end
       (write-line var)                        ;6, print "3"
       (set! inside-continuation #f))))        ;7, set value           ;outside end
   (write-line var)                            ;4, print "1" 
                                               ;8, print "4"
   (if inside-continuation                     ;when run here for first time, inside is k2, var is 3
                                               ;when run here for second time, inside is #f, var is 4
     (begin
       (set! var 4)
       (inside-continuation #f)))))            ;5, return to k2

這個例子兩種用法都用到了。有編號的註釋表明程式的流程順序。程式裡面有兩次call/cc,而且是巢狀使用。第一個call/cc在lambda(k1) body中用到了k1 #t,是直接把#t返回到第一個call/cc的continuation,程式跳轉到註釋4的位置並繼續執行。第二個call/cc用到了set! inside-continuation k2,保留了continuation以備後面呼叫。

分析其執行流程。從註釋1到註釋2,列印2次var好理解。進入call/cc語句後,在inside的call/cc語句中將當時的continuation儲存到inside-continuation變數。執行到註釋3,有(k1 #f),跳出outside的call/cc語句,來到註釋4的位置。body外部var的值是1,故列印1,然後進入if語句。註釋5處,執行(inside-continuation #f),相當於k2的continuation,也就是跳轉到了註釋6的位置繼續。因為此continuation儲存的環境裡var是3,列印3,再將inside-continuation賦值為#f。最後一次離開call/cc語句,因為外部的環境裡在註釋5之前已將var賦值為4,故最後在註釋8處列印4,並在if語句前結束。

現在來看陰陽謎題就不難了。

yin賦值(display "@") (call/cc (lambda(k1) k1) ),輸出一個“@”並將continuation儲存在變數yin,此continuation記為k1,yin=k1。

yang賦值(display "*")(call/cc (lambda(k2) k2) ),輸出一個“*”並將continuation儲存在yang,此continuation記為k2,yang=k2。

(yin yang)就是(k1 k2)。此時,程式第一次展開為@ * (k1 k2)。上面說過continuation就是接收一個引數然後繼續運算的過程,那麼k2作為引數,賦值給yin變數,然後繼續k1的計算。則定義新continuation記為k3,賦值yang=k3,後面是計算新的(yin yang),即(k2 k3)。

此時程式展開為@ * @ * (k2 k3)。k2是前面定義過的continuation,(k2 k3)相當於輸出*並將yang賦值為k3,k2的環境下yin是k1,則程式展開為@ * @ * *(k1 k3)。

(k1 k3)是輸出一個@並賦值yin=k3,再輸出*並定義新k4,賦值yang=k4,再繼續計算(yin yang),即相當於(k3 k4)。

此時程式展開為@ * @ * * @* (k3 k4)。

同理繼續展開為@ * @ * * @ * *(k2 k4)。

繼續@ * @ * * @ * * *(k1 k4)。

之後是@ * @ * * @ * * *@ * (k4 k5)。

然後是@ * @ * * @ * * *@ * * (k3 k5)。

@ * @ * * @ * * *@ * * * (k2 k5)。

@ * @ * * @ * * *@ * * * * (k1 k5)。

@ * @ * * @ * * *@ * * * * @ *(k5 k6)。

如此繼續。@ * @ * * @ * * *@ * * * * @ * * * * * @ * * * * * *。。。。。。

原程式中最後的語句(yin yang)可以改為(yin yin)、(yang yang)、(yang yin),效果也很有趣。

文中的例子摘抄自《the scheme program language》和《MIT/GNU scheme reference manual》。