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》。