(cons '(叄 . 續延) 《爲本身寫本-Guile-書》)

(car 《爲本身寫本-Guile-書》)

在本書前言中,我宣稱本書的主題是用 Guile 實現一個文式編程工具。接下來,第一章中講述瞭如何編寫這個文式編程工具的命令行界面,第二章表面上是講述 Guile 的 I/O 機制,實際上在最後講述瞭如何利用 Guile 的 I/O 機制對文式編程元文檔進行初步解析,然而本章所講的東西——續延(Continuation)卻與本書主題無必要的關係。原本應該承接第二章的主題寫下去的,可是這一章卻沒這樣作,結果形成了讀者(我)對後續章節能回到主題上來這樣一種期待,我有意無心之間就在現實中創造了一個續延。若是不知道我說什麼,能夠簡單的將這些理解爲:本章與本書的主題無關,閱讀時能夠跳過去。之後須要了,能夠再回到這裏。回到這裏以後,可能又會致使後續章節發生變化。html

setjmp/longjmp

在講 Guile 的續延以前,先回顧一下 C 語言標準庫提供的 setjmplongjmp 這兩個函數。看下面的示例:git

#include <setjmp.h>
#include <stdio.h>

jmp_buf env;

void
foo(void) {
        printf("Entering foo!\n");
        longjmp(env, 1984);
        printf("Exiting foo!\n");
}

int
main(void) {
        int i = setjmp(env);
        if (i == 0) {
                foo();
        } else {
                printf("The result of foo: %d\n", i);
        }
}

程序輸出結果爲:github

Entering foo!
The result of foo: 1984

這個程序的控制流以下圖所示,編程

setjmp/longjmp 的控制流

第一次知道 setjmplongjmp 的存在時,對於已經具有不少 C 語言編程經驗的我而言,依然以爲很神奇——居然有辦法從一個函數的內部直接跳轉到另外一個函數的內部。咱們此時此刻在 foo 函數裏所做出的決定,居然對已經發生了的事件產生了不可逃避的影響!segmentfault

call/cc

如下 Guile 代碼與上一節的 C 代碼近乎等效:函數

(define cc 'current-continuation)

(define (foo)
  (display "Entering foo!\n")
  (cc 1984)
  (display "Exiting foo!\n"))

(let ((i (call/cc (lambda (k)
                    (set! cc k)
                    (k 0)))))
  (cond  ((= i 0) (foo))
         (else (begin
                 (display "The result of foo: ")
                 (display i)
                 (newline)))))

上述 Guile 代碼與上一節 C 代碼的對應關係以下:工具

  • call/cc 相似於 setjmp
  • 全局變量 (cc 1984) 相似於全局變量 envlongjmp 的『合體』——longjmp(env, 1984)

續延

在下面的這行 C 代碼中,ui

int i = setjmp(env);

int i = 是一個續延,可將它寫爲 int i = [],表示這個賦值過程在等待所賦之值的到來。setjmp 函數第一次被執行後的返回值是 0,這表示當前的續延 int i = [] 調用了 setjmp 函數,獲得了值 0,使得它的計算過程達到終點。spa

後來,在 foo 函數中執行了 longjmp(env, 1984),致使程序的執行點又跳到上一行代碼中的 setjmp(env) 位置,將 longjmp 的參數值 1984 傳遞給 setjmp 函數,而後第二次執行 setjmp 函數,讓它返回 1984,因而就完成了對 i 的第二次賦值。能夠將這個過程想象爲,咱們將 int i = [] 這個續延保存到了 env 這個全局變量中,而後在其餘地方能夠經過 longjmp 讓這個續延再次獲得所賦之值。命令行

將一個計算過程當中的某個計算單元『抽走』,這就製造了一個續延。不管什麼時候,只要從新補上缺失的計算單元,這個計算過程會基於所填補的計算單元產生相應的結果。這沒有什麼高深莫測的東西,在生活中咱們常常運用續延這種技巧。譬如,考試時,遇到不會作的題目,能夠暫時跳過去——大不了不掙這些題目的分,等把後面的題目都完成了,再回頭跟它們慢慢死磕。

續延在等候它所缺失的計算單元,這種行爲相似於函數們在等候參數值的傳入。若是向續延提供了它所缺失的計算單元,續延就會將這個計算單元映射爲續延所對應的計算過程的最終計算結果。若是向函數提供了參數值,函數會將這些參數值映射會函數的返回值。因此,在行爲上續延與函數是等價的,因此可將其視爲一種另類的函數。

簡單的說,續延就是在表達式上挖了個洞,讓它變成了一種相似函數的東西。

call-with-current-continuation

call/cccall-with-current-continuation 的簡寫,意思是『用當前的續延來調用』。來調用什麼?一個匿名函數:

(lambda (k)
  (set! cc k)
  (k 0))

這個匿名函數的形參 k 是一個續延。call/cc 會捕捉當前的續延,將它做爲參數傳遞給這個匿名函數,即調用這個匿名函數。

對於上一節的 Guile 代碼而言,call/cc 捕捉的當前續延是:

(let ((i []))
  (cond  ((= i 0) (foo))
         (else (begin
                 (display "The result of foo: ")
                 (display i)
                 (newline)))))

假設這個續延爲 i-賦值續延,它會被 call/cc 做爲參數傳遞給上述的匿名函數:

((lambda (k)
  (set! cc k)
  (k 0)) i-賦值續延)

這個匿名函數接受這個續延後,會執行如下兩個運算過程:

(set! cc i-賦值續延)
(i-賦值續延 0)

第一個運算過程是用全局變量 cc 記錄這個續延。第二個計算過程是以參數值 0 『調用』這個續延——參數值 0 剛好填補了 i-賦值續延 所缺失的計算單元,結果 i 被綁定到 0 上,使得續延變爲:

(let ((i 0))
  (cond  ((= i 0) (foo))
         (else (begin
                 (display "The result of foo: ")
                 (display i)
                 (newline)))))

接下來,cond 的第一個謂詞 (= 1 0) 的結果爲真,因而進入 foo 函數的計算過程,結果會遇到 (cc 1984)。因爲在 call/cc 語句中,已將 i-賦值續延 記錄於 cc。所以 (cc 1984) 本質上就是用 1984 來填補 i-賦值續延 所缺失的計算單元,將其變爲:

(let ((i 1984))
  (cond  ((= i 0) (foo))
         (else (begin
                 (display "The result of foo: ")
                 (display i)
                 (newline)))))

如今 i 的值就變成了 1984 了,所以接下來 cond 的第一個謂詞的結果爲假,從而進入 else 分支。最終獲得如下結果:

Entering foo!
The result of foo: 1984

注意,在 foo 函數中,當 (cc 1984) 語句被執行時,本質上它會將當前的程序環境切換到 i-賦值續延 環境,所以位於它後面的 (display "Exiting foo!\n") 語句不會有運行機會。

它有什麼用?

十三年前,王垠寫過一篇文章『二叉樹匹配問題』,較爲詳細的詮釋了《Teach Yourself Scheme in Fixnum Days》這本書的第十三章中的一個續延示例。能夠結合這兩份文檔瞭解一下續延的應用場合。這個二叉樹匹配問題是基於續延構造了一個二叉樹結點生成器來解決的。很坦誠的說,這兩份文檔所講的東西,目前我也只是似懂非懂。也許只有在真正須要使用續延的時候,方能真正知道怎麼運用它。我以爲在現實中續延真正有用的地方就在於實現協程。不過《Teach Yourself Scheme in Fixnum Days》第十四章、十五章對續延有着更有趣的應用——非肯定性運算與引擎。

(cdr 《爲本身寫本-Guile-書》)
相關文章
相關標籤/搜索