1 前言
我最一開始聽到 CPS 變換這個詞是在王垠的博客里 (請求不要噴我),就是那篇他第一次宣傳他的40行代碼的文章。 我當時什么都看不懂,所以沒太注意,不過我也正在學程序語言方面的東西, 不久我就在 EOPL 和 The Little Schemer 里面又見到了 CPS。 我有點不服氣,知道了 CPS 不過就是這么個東西, 於是我也開始想自己重造王垠40行代碼,然后我很驚訝地也花了 剛好一個星期寫了出來,而且還基本上跟他的一模一樣......
(注意,我不是引戰)
畢竟可能每個人有自己的思考方式,我在這里只是分享一下我自己的思路, 我寫出這個 CPS 程序的經歷。當然為了顯得我稍微強一點的樣子, 我把中間許多非常蠢的錯誤都省略了。 其實我也不知道我怎么就把這段代碼寫出來了。
我寫出這個程序以后,又去看了那篇經典論文 Representing Control, 這里有一個更友好一點的版本 How to compile with continuations, 發現我的思路和他的完全不同,我反倒覺得我的思路很清楚, 他的思路我要繞個彎才能看懂,雖然實質上是一樣的。
這篇文章不涉及什么是 CPS 變換,CPS 變換入門請參考 The Little Schemer, 我直接就開始寫 cps 函數了,我們的目標就是王垠的那40行代碼, 我也把我自己的程序的變量名什么的都改成了跟那段代碼一樣的格式,方便對照, 當然也有一些地方不一樣,懶得改了。
我采用 Racket 語言,就是因為用起來方便一些。就這樣了。
2 簡單的CPS變換
其實一個正確的CPS變換程序,只要學過一點點寫解釋器的人都會寫, 所以我就不細講了,只是提供一個回憶,你如果要看下去的話最好是這個會自己寫。 我們先只考慮λ-calculus的語法,就只有3個分支,變量,λ,函數調用。 先把最簡單,沒有經過優化的程序寫出來。
主要的函數是cps1,它有兩個參數expr和ctx, expr就是一個s-expression, ctx是當前的context,是一個symbol或'(λ...), 比如(cps1'(fx)'(λ(x)x))的值為 '(fx(λ(x)x))。所以(cpsexpr)函數 就是(cps1expr'id)或(cps1expr'(λ(x)x)), 我暫且用前者。
(define (cps expr) (define (atom? x) (not (pair? x))) (define n -1) (define (fv) (set! n (add1 n)) (string->symbol (string-append "v" (number->string n)))) (define (cps1 expr ctx) ....) (cps1 expr 'id))
(define (cps1 expr ctx) (match expr [(? atom? expr) ....] [`(λ(,x) ,body) ....] [`(,rator ,rand) ....]))
(cps1 'x 'k) ==> '(k x) (cps1 'x '(λ(x) ....)) ==> '((λ(x) ....) x)
[(? atom? expr) `(,ctx ,expr)]
(cps1 '(λ(x) x) 'id) ==> '(id (λ(x k) (k x)))
[`(λ(,x) ,body) `(,ctx (λ(,x k) ,(cps1 body 'k)))]
(cps1 '((f a) (g b)) 'id) ==> '(f a (λ(v0) (g b (λ(v1) (v0 v1 id)))))
(cps1 '(f x) 'id) ==> '((λ(v0) ((λ(v1) (v0 v1 id)) x)) f) <=> '(let ([v0 f]) (let ([v1 x]) (v0 v1 id)))
[`(,rator ,rand) (define v-rator (fv)) (define v-rand (fv)) (cps1 rator `(λ (,v-rator) ,(cps1 rand `(λ (,v-rand) (,v-rator ,v-rand ,ctx)))))]
(cps '((f a) (g b))) ==> '((λ (v4) ((λ (v5) (v4 v5 (λ (v0) ((λ (v2) ((λ (v3) (v2 v3 (λ (v1) (v0 v1 id)))) b)) g)))) a)) f)
不過算作是個很好的開頭吧。
3 最簡CPS輸出
其實下面才開始真正的任務,上面一節只是因為, 市面上的程序都是分好幾個函數,我要把它們合在一起。
上面的程序的問題就在於,當ctx是'(λ(v)...v...), 而且expr是一個'x之類時,輸出應該為 '...x...而不是'((λ(v)...v...)x), 照λ-calculus的術語說就是產生了一個beta-redex。
我們來觀察一下現在我們的CPS程序的3個分支產生的ctx
case 1:如果是一個atom,就產生`(,ctx,expr),這時ctx在函數的位置。
case 2:如果是λ表達式,ctx也在函數的位置。
case 3:但如果是函數調用,這時ctx在參數的位置((vfvxctx))
很顯然,在參數位置時ctx是不可能被化簡的,因為結果必須是 (vfvxk/id)或(vfvx(λ(v?)???)) 的形式。而在函數位置時是有可能化簡的,當它是λ函數的時候。
;; 原來的輸出 '((λ(v0) (f v0)) x) ;; 現在變成 ((λ(v) `(f ,v)) 'x) ==> '(f x)
第二,如果ctx是'k/id, 就改成(λ(out)`(k/id,out))
因為總共就兩種情況:ctx在函數位置和參數位置。 我們不妨把cps1函數的ctx參數改成兩個, 一個叫ctx-f在函數位置,一個叫ctx-a在參數位置。
;; ctx-f : symbol -> s-exp ;; ctx-a : s-exp (define (cps1 expr ctx-f ctx-a) (match expr ....)) (cps1 expr (λ(out) out) 'id)
(λ(out)out)就是id這個函數。因為原來的slideshow 'id可以看成是`(λ(v?)v?),所以化簡后 就變成了(λ(out)out)
[(? atom?) (ctx-f expr)] [`(λ(,x) ,body) (ctx-f `(λ(,x k) ,(cps1 body (λ(out) `(k ,out)) 'k)))]
[`(,rator ,rand) (define v-rator (fv)) (define v-rand (fv)) (cps1 rator (λ(out-rator) (cps1 rand (λ(out-rand) `(,out-rator ,out-rand ,ctx-a)) `(λ(,v-rand) (,out-rator ,v-rand ,ctx-a)))) `(λ(,v-rator) ,(cps1 rand (λ(out-rand) `(,v-rator ,out-rand ,ctx-a)) `(λ(,v-rand) (,v-rator ,v-rand ,ctx-a)))))]
這段代碼應該也不難理解,只是分別討論了函數和參數分別處於函數位置和參數位置的情況。 大致思路就是,首先,cps1要根據rator和rand 是否為一個atom來決定如何輸出,其次,我們不願意在遞歸進去之前就判斷一次, 遞歸進去之后又要matchexpr(開頭提到的那篇論文的方法就有這個問題)。 所以我們把現在的狀態分成了兩個參數,也一起遞歸進去。
它有唯一一個但很好修復的缺陷,就是v-rator和v-rand 定義地太早了,所以有時候會出現vn不連續的情況, 如果不嫌麻煩的話可以在每次第一次出現v-?的地方再 (let([v-?(fv)])....),當然這個代碼看起來就...... 另外,做出了下面一道習題后也會很好修復這個缺陷。
甚至還有一個寫法,就是利用多返回值,再返回一個布爾值表示當前的選擇, 這個方法看起來會有些麻煩。就不提了。
(cps '(f x)) ==> (cps1 'f ctx-f ctx-a) ==> (ctx-f 'f) ==> (cps1 'x .... ....)where[out-rator='f] ==> `(,out-rator ,out-rand id)where[out-rator='f out-rand='x] ==> '(f x id)
(cps '((f a) b)) ==> (cps1 '(f a) ctx-f ctx-a) ==> `(f a ,ctx-a)where[ctx-a='(λ(v0) (v0 b id))] ==> '(f a (λ(v0) (v0 b id)))
現在可以來看一下這段代碼對我們有什么啟發。
所謂的continuation-passing style多用一個參數k來告訴 我們要調用的函數當前的狀態是什么,就是這個函數運行完了以后 應該干什么。但這里的cps函數也有一個參數ctx, 它也表示一個狀態,它表示的是現在的狀態,讓更深層遞歸的函數能得知一些外部信息。
很多時候我們發現就傳一個死的數據(比如第一個版本里的ctx) 是不夠的,不但遞歸進去的函數需要這個數據,而這個數據也要隨着當前的情況而變化。 在簡單的情況下我們可以傳好幾個參數,或者一個對象進去,里面的函數 選擇性地使用這些數據。但是在支持高階函數的語言里很多時候方便很多, 因為我們可以傳一個函數進去。
這種模式更廣泛的應用之處在於ctx的參數不是一個用來選擇的符號, 而是一個連續數值或對象的時候。我一下子想不出實際的例子, 有了我會補充。
習題:請擴展這個程序以支持多參數的λ和函數調用。
令我驚訝的是,支持多參數就不用分4類討論了! 因為只要分兩類討論,依次遍歷整個列表就可以, 不用區分函數和參數,所以代碼反倒簡單多了。 推薦做一下這個習題。
4 簡化 cps1 函數
這一節,我們把ctx-f和ctx-a合並成一個ctx
觀察所有產生的ctx-f/a參數,總結一下總共有這些:
1. λ(out)`(k,out)
2. 'k
3. λ(out)....`(....,out....)
4. `(λ(,vn)....(....,vn....))
如果要只傳一個參數的話,我們會發現,由2可以推出1, 因為我們只要給它包一個λ就可以了。 由3可以推出4,如果3是ctx,4就是 `(λ(,vn),(ctxvn))
問題就在於,13是一個形式的,24是一個形式的, 我們要選擇的就是只傳13還是只傳24.
我們發現,13是兩個固定的值,而24里面是有一堆省略號的, 也就是說,如果采用一點類似作弊的策略,從1也可以推出2, 只要判斷ctx是否等於(λ(out)`(k,out))。 但是無論如何也不可能從任意的4推出3(當然你如果使用eval 的話,我就沒話說了,按理來說是可以的,你可以自己嘗試一下, 成功了記得偷偷告訴我一聲。
於是,我們決定采用13型的ctx。
(define (cps1 expr ctx) (match expr [(? atom? expr) (ctx expr)] [`(λ(,x) ,body) (ctx `(λ(,x k) ,(cps1 body ctx1)))] [`(,rator ,rand) (cps1 rator (λ (out-rator) (cps1 rand (λ (out-rand) `(,out-rator ,out-rand ,(ctx-f->a ctx))))))]))
你現在可以自己隨意試驗這個程序了。
下面我們對它進行一些擴展,先增加多參數的λ和函數調用, 然后是原生的幾個函數(比如+,-,zero?等), 最后添加if語句。
5 多參數和原生函數
都已經到這一步了,支持多參數其實很簡單。
[(? atom? expr) (ctx expr)]
[`(λ ,args ,body) (ctx `(λ(,@args k) ,(cps1 body ctx1)))]
[_ ; else : expr = ‘(,rator . ,rands) (let recur ([exprs expr] [acc '()]) (if (null? exprs) `(,@acc ,(ctx-f->a ctx)) (cps1 (car exprs) (λ(v) (recur (cdr exprs) `(,@acc ,v))))))]
( cps1 ( car exprs ) ( λ ( v ) .... ) )
然后省略號要填的是,遞歸遍歷 ( cdr exprs ),所以結構必須是這樣的,
(let recur ([exprs expr]) (cps1 (car exprs) ; when exprs is not null (λ (v) (recur (cdr exprs)) (process-v))))
(cps '(+ x y)) ;; instead of (+ x y id) ==> '(+ x y) (cps '(+ (f x) y)) ==> '(f x (λ(v0) (+ v0 y))) (cps '(+ (* x y) z)) ==> '(+ (* x y) z) ;; when used as higher order procedure (cps '(((λ(m) +) n) ; returns + x y)) ==> '((λ(m k) (k +)) n (λ(v0) (v0 x y id)))
[_ (let recur ([exprs expr] [acc '()]) (if (null? exprs) (if (trivial? (car acc)) .... `(,@acc ,(ctx-f->a ctx))) (cps1 (car exprs) (λ(v) (recur (cdr exprs) `(,@acc ,v))))))]
6 if語句
(cps '(λ(x) (if a b (f c)))) ==> '(λ(x k) (if a (k b) (f c k)))
[`(if ,test ,conseq ,alt) (cps1 test (λ(t) `(if ,t ,(cps1 conseq ctx) ,(cps1 alt ctx))))]
(cps '(λ(x) (f (if a b c)))) ==> '(λ (x k) (if a (f b k) (f c k)))
(cps '(λ(x) (f (g (h (if a b c)))))) ==> '(λ (x k) (if a (h b (λ (v0) (g v0 (λ (v1) (f v1 k))))) (h c (λ (v2) (g v2 (λ (v3) (f v3 k))))))) (cps '(λ(x) (if (if a b c) d e))) ==> '(λ (x k) (if a (if b (k d) (k e)) (if c (k d) (k e))))
(cps '(λ(x) (f (if a b c)))) ==> '(λ(x k) (let ([k (λ(v0) (f v0 k))]) (if a (k b) (k c)))) (cps '(λ(x) (if (if a b c) d e))) ==> '(λ(x k) (let ([k (λ(v0) (if v0 (k d) (k e)))]) (if a (k b) (k c))))
[`(if ,test ,conseq ,alt) (define (if-body ctx) (cps1 test (λ(t) `(if ,t ,(cps1 conseq ctx) ,(cps1 alt ctx))))) (if (ctx1? ctx) (if-body ctx) `(let ([k ,(ctx-f->a ctx)]) ,(if-body ctx1)))]
(cps '(if a b c)) ==> '(let ([k (λ(v0) v0)]) (if a (k b) (k c)))
(define (cps expr) .... (cps1 expr id))
(define (ctx-f->a ctx) (cond [(ctx1? ctx) 'k] [(id? ctx) 'id] [else (define v (fv)) `(λ(,v) ,(ctx v))])) ;; 話說這里用 case 語句會更舒服一點的...
7 總結
這么多代碼看下來,其實你會發現,就只有幾個關鍵點, 只要想到了,其實也沒有多難。我自己想這個程序的時候,手頭上沒有電腦, 我是寫在紙上的(好痛苦啊),但是放到電腦上測試,一次性就全是對的, 畢竟不是很大的工程,也沒有各種復雜的角角落落需要考慮,思路還是很簡單的。
這個程序還有升級空間,就是 begin 和 set! 語句,提示一下, 東西越來越復雜的時候,可能不得不回歸到第3節中的方式,把各種 ctx 拆開, 否則處理 set! 的時候會產生一堆嵌套的 begin 語句。 另外,如果是Common Lisp里的那種有返回值的賦值語句,處理起來會簡單一些, 因為可以簡單地看作一個表達式。
最后就隨便說說,其實這段代碼也沒有特別的高級,只是自己寫出來了,那就開心一下就好。 代碼里倒是有幾個挺特別的想法值得學習。
沒得寫了,就打個廣告吧,本文章用 scribble 生成, 不過用它只是因為,它是唯一一個支持racket代碼高亮的......
文筆不好請見諒,有任何錯誤或寫的不好的地方歡迎指出。
(define (cps expr) (define (atom? x) (not (pair? x))) (define n -1) (define (fv) (set! n (add1 n)) (string->symbol (string-append "v" (number->string n)))) (define ctx1 (λ(out) `(k ,out))) (define (ctx1? ctx) (eq? ctx ctx1)) (define (ctx-f->a ctx) (if (ctx1? ctx) 'k (let ([v (fv)]) `(λ(,v) ,(ctx v))))) (define (trivial? x) (memq x '(zero? add1 sub1 + - * /))) (define id (λ(x) x)) (define (id? x) (eq? x id)) (define (cps1 expr ctx) (match expr [(? atom?) (ctx expr)] [`(if ,test ,conseq ,alt) (define (if-body ctx) (cps1 test (λ(t) `(if ,t ,(cps1 conseq ctx) ,(cps1 alt ctx))))) (if (or (ctx1? ctx) (id? ctx)) (if-body ctx) `(let ([k ,(ctx-f->a ctx)]) ,(if-body ctx1)))] [`(λ ,args ,body) (ctx `(λ(,@args k) ,(cps1 body ctx1)))] [_ (let recur ([exprs expr] [acc '()]) (if (null? exprs) (if (trivial? (car acc)) (ctx acc) `(,@acc ,(ctx-f->a ctx))) (cps1 (car exprs) (λ(v) (recur (cdr exprs) `(,@acc ,v))))))])) (cps1 expr id))