[PLT] 柯里化的前生今世(八):尾調(diào)用與CPS

關(guān)于

本文是系列文章中的第八篇,
上一篇中,我們介紹了continuation的概念,還介紹了Lisp中威力強大的call/cc,它提供了first-class continuation,最后我們用call/cc實現(xiàn)了python中的generator和yield。

call/cc賦予了我們很強的表達能力,Lisp中的異常處理機制也很人性化。
例如,Common Lisp: Condition_system
由于call/cc可以捕捉到異常處的continuation,
我們就可以手動調(diào)用這個continuation,
讓程序從錯誤的位置以給定狀態(tài)重新開始執(zhí)行,
甚至結(jié)合REPL還可以詢問用戶,讓用戶輸入這個狀態(tài)。

其他語言的try/catch是無法做到這一點的,
我們拿到錯誤時,出現(xiàn)錯誤的那個環(huán)境已經(jīng)被丟棄了,無法恢復(fù),
那么除了提示用戶程序崩潰了就沒有別的辦法了。

call/cc這么強大,更堅定了我們實現(xiàn)它的想法,
本文就從實現(xiàn)的角度來看call/cc。

尾調(diào)用

In computer science, a tail call is a subroutine call performed as the final action of a procedure.

如果在某個函數(shù)的末尾調(diào)用了另一個函數(shù),這個調(diào)用就稱為尾調(diào)用。
我們舉個例子吧,

(define (f a)
  (display a)
  (g 2))

(define (g b)
  (display b))

(f 1)

我們看到,函數(shù)f的末尾調(diào)用了函數(shù)g(g 2)。

尾調(diào)用有什么好處呢?
一個基本的事實是,如果gf的尾調(diào)用,g就可以不返回到f中,
而直接返回到f該返回的地方。

因為gf的尾調(diào)用,g后面沒有其他調(diào)用了,
(g 2)調(diào)用結(jié)束后就可以不必返回到f的函數(shù)體中了,而是直接返回到(f 1)處。
因此,調(diào)用g的時候,調(diào)用??梢圆辉黾?,而是直接廢棄f的調(diào)用環(huán)境即可。

注意,我們上面提到的是『不必返回到f的函數(shù)體中』,
因為不是每個語言都可以做到這一點,
這個語言特性,稱為尾調(diào)用優(yōu)化(tail call optimization)。

調(diào)用棧和調(diào)用圖

調(diào)用棧對我們來說是一個耳熟能詳?shù)拿~,
可是我們有沒有考慮過,為什么調(diào)用構(gòu)成了一個『棧』呢?
有這么多的數(shù)據(jù)結(jié)構(gòu),為什么不是一個隊列,不是一個樹,不是一個圖呢?

是因為函數(shù)的調(diào)用和返回機制,恰好可以用幀(frame)的壓棧和彈棧來描述。
可是,尾調(diào)用優(yōu)化,開始動搖了這一點,
為了能返回到調(diào)用者該返回的地方,調(diào)用棧有的時候可能會彈出兩次,或者彈出更多次。

進一步,我們再來看call/cc的場景,它使得程序可以直接跳轉(zhuǎn)到之前的某個狀態(tài),
根本上改變了壓棧彈棧的規(guī)則,跳過去以后,以全新的狀態(tài)重新開始執(zhí)行。
然而,發(fā)生跳轉(zhuǎn)時的狀態(tài)還不能丟棄,因為有可能再跳回來。
因此,call/cc讓調(diào)用不再構(gòu)成一個棧,而是構(gòu)成了一個調(diào)用圖。

CPS

在這些復(fù)雜場景中,為了能顯式的表示執(zhí)行過程,
將程序轉(zhuǎn)化為CPS(continuation passing style)是一種常用的辦法,
CPS是一種程序的書寫風(fēng)格,經(jīng)常作為編譯器的一種中間表示。(IR

; 調(diào)用風(fēng)格
(define (f x)
  (+ (g x) 1))

(define (g x)
  (* x 2))

(f 1)

; CPS
(define (f x cont)
  (g x (lambda (v)
         (cont (+ v 1)))))

(define (g x cont)
  (cont (* x 2)))

(f 1 display)

我們發(fā)現(xiàn)寫成CPS之后,每個函數(shù)多了一個cont參數(shù),
用來表示該函數(shù)調(diào)用表達式的continuation,
我們調(diào)用一個函數(shù),就應(yīng)該把它相應(yīng)的continuation顯式的傳給它。
例如,我們在f中調(diào)用了g,那么我們就將(g x)的continuation傳給了g,即(lambda (v) (cont (+ v 1)))

除此之外,我們還發(fā)現(xiàn),CPS是一個尾調(diào)用形式,
因此程序的執(zhí)行就變成了continuation的不斷變換生長。

開始動手術(shù)

為了實現(xiàn)call/cc,首先我們要把解釋器改造成CPS形式,
然后再將continuation拿出來包裝一下,提供給用戶使用。

我們先進行第一步改造,CPS,
回憶一下,為了實現(xiàn)詞法作用域,我們給解釋器中每個函數(shù)末尾加上了參數(shù)env,用于表示被求值表達式的環(huán)境。這次也相似,我們給每個函數(shù)加上了新的參數(shù)cont,用于表示被求值表達式的continuation,這樣我們就可以將解釋器改造成CPS形式了。

下一步改造我們要實現(xiàn)call/cc了,它直接使用了這些包含cont參數(shù)的函數(shù),限于篇幅,CPS形式的解釋器我們就略過了,這里我們只是先看一下handle-decision-tree的樣子吧,

(define (handle-decision-tree tree exp env cont)
  (if (null? tree)
      (error 'handle-decision-tree "failed to make decision")
      (let* ((head (car tree))
             (predicator (car head))
             (decision (cadr head)))
        
        (predicator exp env 
                    (lambda (predicate-result)
                      (if predicate-result
                          (if (not (list? decision))
                              (decision exp env cont)
                              (handle-decision-tree decision exp env cont))
                          (handle-decision-tree (cdr tree) exp env cont)))))))

實現(xiàn)call/cc

將解釋器轉(zhuǎn)換成CPS之后,我們就可以將cont進行包裝了,
下面的實現(xiàn)中,我們將cont包裝成了一個內(nèi)部的數(shù)據(jù)結(jié)構(gòu)continuation。
(和閉包一樣,continuation從實現(xiàn)的角度來看也是一個數(shù)據(jù)結(jié)構(gòu)

然后,把這個數(shù)據(jù)結(jié)構(gòu)提供給用戶,就可以讓用戶代碼實現(xiàn)自定義跳轉(zhuǎn)了。
為了實現(xiàn)這一點,我們在解釋器中判斷是否調(diào)用了continuation,來做相應(yīng)的處理。
handle-decision-tree增加了兩個分支,is-continuation?is-continuation-call?。

#lang racket

; tool

(struct closure 
  (param body env))

(struct continuation 
  (cont))

(define (create-frame)
  (make-hash))

(define (extend-frame frame key value)
  (hash-set! frame key value))

(define (extend-env env frame)
  (cons frame env))

(define (get-symbol-value env key)
  (let lookup-env
    ((env env))
    (if (null? env)
        (error 'get-symbol-value "failed to find symbol")
        (let ((head-frame (car env)))
          (if (hash-has-key? head-frame key)
              (hash-ref head-frame key '())
              (lookup-env (cdr env)))))))

(define (handle-decision-tree tree exp env cont)
  (if (null? tree)
      (error 'handle-decision-tree "failed to make decision")
      (let* ((head (car tree))
             (predicator (car head))
             (decision (cadr head)))
        
        (predicator exp env 
                    (lambda (predicate-result)
                      (if predicate-result
                          (if (not (list? decision))
                              (decision exp env cont)
                              (handle-decision-tree decision exp env cont))
                          (handle-decision-tree (cdr tree) exp env cont)))))))

; env & cont

(define *env* `(,(create-frame)))

(define *cont* (lambda (v)
                 (display v)))

; main

(define (eval-exp exp env cont)
  (handle-decision-tree 
   `((,is-symbol? ,eval-symbol)
     (,is-self-eval-exp? ,eval-self-eval-exp)
     (,is-continuation? ,eval-continuation)
     (,is-list?
      ((,is-lambda? ,eval-lambda)
       (,is-call/cc? ,eval-call/cc)
       (,is-continuation-call? ,eval-continuation-call)
       (,is-function-call-list? ,eval-function-call-list))))
   exp env cont))

(define (is-symbol? exp env cont)
  (display "is-symbol?\n")
  (cont (symbol? exp)))

(define (eval-symbol exp env cont)
  (display "eval-symbol\n")
  (cont (get-symbol-value env exp)))

(define (is-self-eval-exp? exp env cont)
  (display "is-self-eval-exp?\n")
  (cont (number? exp)))

(define (eval-self-eval-exp exp env cont)
  (display "eval-self-eval-exp\n")
  (cont exp))

(define (is-continuation? exp env cont)
  (display "is-continuation?\n")
  (cont (continuation? exp)))

(define (eval-continuation exp env cont)
  (display "eval-continuation\n")
  (cont exp))

(define (is-list? exp env cont)
  (display "is-list?\n")
  (cont (list? exp)))

(define (is-lambda? exp env cont)
  (display "is-lambda?\n")
  (cont (eq? (car exp) 'lambda)))

(define (eval-lambda exp env cont)
  (display "eval-lambda\n")
  (let ((param (caadr exp))
        (body (caddr exp)))
    (cont (closure param body env))))

(define (is-call/cc? exp env cont)
  (display "is-call/cc?\n")
  (cont (eq? (car exp) 'call/cc)))

(define (eval-call/cc exp env cont)
  (display "eval-call/cc\n")
  (let ((fn (cadr exp))
        (data-cont (continuation cont)))
    (eval-function-call-list `(,fn ,data-cont) env cont)))

(define (is-continuation-call? exp env cont)
  (display "is-continuation-call?\n")
  (eval-exp (car exp) env
            (lambda (value)
              (cont (continuation? value)))))

(define (eval-continuation-call exp env cont)
  (display "eval-continuation-call\n")
  (eval-exp (car exp) env
            (lambda (data-cont)
              (let ((wrapped-cont (continuation-cont data-cont)))
                (eval-exp (cadr exp) env
                          (lambda (arg)
                            (wrapped-cont arg)))))))

(define (is-function-call-list? exp env cont)
  (display "is-function-call-list?\n")
  (cont #t))

(define (eval-function-call-list exp env cont)
  (display "eval-function-call-list\n")
  (eval-exp (car exp) env
            (lambda (clos)
              (eval-exp (cadr exp) env
                        (lambda (arg)
                          (let ((body (closure-body clos))
                                (lexical-env (closure-env clos))
                                (param (closure-param clos))
                                
                                (frame (create-frame)))
                            
                            (extend-frame frame param arg)
                            
                            (let ((executing-env (extend-env lexical-env frame)))
                              (eval-exp body executing-env cont))))))))

測試

(eval-exp '1 *env* *cont*)

(display "\n\n")
(eval-exp '(lambda (x) x) 
          *env* *cont*)

(display "\n\n")
(eval-exp '((lambda (x) x) 
            1) 
          *env* *cont*)

(display "\n\n")
(eval-exp '((lambda (x)
              ((lambda (y) x)
               2))
            1) 
          *env* *cont*)

(display "\n\n")
(eval-exp '((lambda (x)
              ((lambda (f)
                 ((lambda (x)
                    (f 3))
                  2))
               (lambda (z) x)))
            1)
          *env* *cont*)

(display "\n\n")
(eval-exp '(call/cc (lambda (k)
                      1))
          *env* *cont*)

(display "\n\n")
(eval-exp '(call/cc (lambda (k)
                      (k 2)))
          *env* *cont*)

要點分析

(1)eval-call/cc時會創(chuàng)建一個continuation,
然后用這個continuation作為參數(shù)調(diào)用call/cc的參數(shù)。
call/cc的參數(shù),就是后面的(lambda (k) 1),因此k就是這個continuation

; (call/cc (lambda (k) 1))

(define (eval-call/cc exp env cont)
  (display "eval-call/cc\n")
  (let ((fn (cadr exp))
        (data-cont (continuation cont)))
    (eval-function-call-list `(,fn ,data-cont) env cont)))

(2)eval-continuation-call會解開continuation的包裝,得到內(nèi)部包含的cont,
然后用這個cont作為參數(shù)求值表達式,
這樣就實現(xiàn)了,表達式求值完以后跳轉(zhuǎn)到產(chǎn)生cont位置的效果。

(define (eval-continuation-call exp env cont)
  (display "eval-continuation-call\n")
  (eval-exp (car exp) env
            (lambda (data-cont)
              (let ((wrapped-cont (continuation-cont data-cont)))
                (eval-exp (cadr exp) env
                          (lambda (arg)
                            (wrapped-cont arg)))))))

(3)(call/cc ...)表達式中,如果k沒有被調(diào)用,那么(call/cc ...)的值,就是call/cc參數(shù)函數(shù)的返回值,即(call/cc (lambda (k) 1)) = 1。
這一點看起來很難實現(xiàn),實則不然。

我們只需要巧妙的指定(lambda (k) 1)的continuation,
讓它就是(call/cc (lambda (k) 1))的continuation即可。
這一點體現(xiàn)在eval-call/cc中,我們直接將cont原封不動的傳給了eval-function-call-list

(define (eval-call/cc exp env cont)
   ...
    (eval-function-call-list `(,fn ,data-cont) env cont)))

下文

Lisp語言真是博大精深,寫到這里我們甚至還沒有提及它最重要的語言特性——宏,
Lisp宏提供了一種元編程的手段,同像性讓Lisp元編程異常強大,
然而,把宏說清楚也頗費筆墨,因此,我打算在適當?shù)臅r候單獨討論它。

本系列標題為『柯里化的前生今世』,意在通過柯里化引入種種有趣的概念,
目前為止,我們討論了高階函數(shù),閉包,continuation,這些可以看做『柯里化的前生』,
我們不但理解了這些概念,還實現(xiàn)了它們,算是小有收獲吧。

使用Racket也有一段日子了,對它也逐漸從陌生到熟悉,
可是偏執(zhí)卻容易讓人誤入歧途,錯過其他風(fēng)景,
下文我們將開啟新的旅程了,Let's go !

參考

continuation passing style
Compiling with Continuations
An Introduction to Scheme and its Implementation

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容