續體

在計算機科學中，續體（continuation，也譯作計算續體、續延、延續性），是對計算機程式的控制狀態的一種抽象表示。續體實化了程式控制狀態。可以理解為，續體是一種資料結構，它表示在程序執行中給定點上的計算過程，所建立的資料結構可以被程式語言訪問，而不是被執行時環境所隱藏掉。這對實現程式語言的某些控制機制，比如例外處理、協程、生成器非常有用。

“當前續體” 從執行程式碼的角度看來，是可以從程式執行的當前點匯出的續體。續體還被用來提及 “頭等續體”，它是一種構造，賦予程式語言儲存在任何點上的執行狀態的能力，並在程式中後來的點上可能多次返回到這一點。

歷史

Gerald J. Sussman 和 Guy L. Steele Jr. 在 1976 年論文《Lambda: The Ultimate Imperative》中，認定 Alonzo Church 在 1941 年著作《The Calculi of Lambda-Conversion》裡，已經清晰的理解了 “續體” 的使用，這是用純 λ 演算徹底完成所有事情即邱奇編碼的唯一方式，並舉出其中有序對定義所對應的 Scheme 表示為例

M, N means (LAMBDA (A) (A M N))

21 means (LAMBDA (A) (A (LAMBDA (B C) B)))

22 means (LAMBDA (A) (A (LAMBDA (B C) C)))

where 21 e.g. selects the first element of a pair. (Note that these functions are isomorphic to CONS, CAR, and CDR!) ”

：

(define (cons m n)

(lambda (a) (a m n)))

(define (car a)

(a (lambda (b c) b)))

(define (cdr a)

(a (lambda (b c) c)))

在 1993 年的論文《The Discoveries of Continuations》中給出了發現續體的完整歷史。Reynolds 認為續體的最早描述由 Adriaan van Wijngaarden 在 1964 年 9 月作出。Van Wijngaarden 在奧地利的維也納附近巴登召開的關於 “形式語言描述語言” 的工作會議上，在關於 ALGOL 60 預處理器的的論文《Recursive Definition of Syntax and Semantics》中，倡導透過將真正（proper）過程變換成續體傳遞風格而消除標籤和 goto 語句，但是他沒有使用 “續體” 這個名字。

Christopher Strachey、Christopher P. Wadsworth 和，在指稱語義領域的工作中突出了術語 “續體”，此間廣泛利用續體來允許使用函數語言程式設計語義來分析順序程式。

Steve Russell 為他給 IBM 704 的 LISP 實現的一個使用者，發明了續體來解決問題，但是他沒有為其命名。

頭等續體

頭等續體對程式語言而言是能完全控制指令執行次序的能力。它們可以用來跳轉到產生對當前函式呼叫的那個函式，或者跳轉到此前已經退出了的函式。人們可以認為頭等體儲存了程式執行狀態，注意到真正的頭等續體不同於是很重要的，它不儲存程式資料，只儲存執行上下文。這經常採用 “續體三明治” 譬喻來說明：

假想你正站在廚房冰箱之前，想要一個三明治。你就在這裡將一個續體放入口袋裡。接著在從冰箱裡拿出火雞肉和麵包自己做了一個三明治，然後坐到桌案前。你啟用了口袋裡的續體，這時發現自己再次站到了冰箱之前，想要一個三明治。幸運的是，在桌案上就有一個三明治，而用於做三明治的材料都沒有了。你可以吃三明治了。

在這個譬喻中，三明治是一部份程式資料，比如在分配堆上一個物件，並非去呼叫 “製做三明治” 例程並等它返回，這裡呼叫 “以當前續體制做三明治” 例程，它建立一個三明治並在已脫離執行的續體所儲存的地方繼續執行。

Scheme 是支援續體的第一個完全的生產系統，它最初提供了源自 Maclisp 的用於例外處理的命名 catch/throw，後來提供了頭等續體運算元（簡寫為 call/cc）。

Bruce Duba 將 callcc 介入到了 SML/NJ：

val callcc : ('a cont -> 'a) -> 'a，這裡的 'a cont 是接受型別為 'a 的引數的續體的型別；callcc f 應用 f 到當前續體，如果 f 以引數 x 呼叫這個續體，則如同 (callcc f) 返回 x 作為結果。

程式語言支援

很多程式語言以各種名義體現出頭等續體，特別是：

Common Lisp：cl-cont，還可以使用定製宏
C# / VB.NET：async/await
Factor：callcc0 和 callcc1
Haskell：在 Control.Monad.Cont 中的續體單子
Haxe：haxe-continuation
Icon / Unicon：suspend，coexpression：create 與 @運算元
Java：Lightwolf 和 javaflow
Kotlin：Continuation
Rhino (JavaScript 引擎)：Continuation
Parrot：Continuation PMC，對所有控制流程使用
Perl：Coro 和 Continuity
：call (exp ) 和 continue (aContinuation, anyValue)
Python：PyPy 的_continuation.continulet
Racket：（通常簡寫為 call/cc）
Ruby：callcc
Scala：scala.util.continuations 提供了 shift/reset
Scheme：（通常簡寫為 call/cc）
Smalltalk：Continuation currentDo:，在多數現代 Smalltalk 環境中續體不需要額外的 VM 支援就能實現
新澤西 Standard ML：SMLofNJ.Cont.callcc

在支援閉包和適當尾呼叫的任何程式語言中，都可能以續體傳遞風格書寫程式並手工實現 call/cc。在續體傳遞風格下，call/cc 成為了可以用 lambda 表示式書寫的一個簡單函式。需要支援適當尾呼叫，因為在續體傳遞風格下沒有函式會返回，所有呼叫都是尾呼叫。

續體傳遞風格

}}

續體被用於計算模型，包括 λ 演算 Thus, in a safe schema, we prohibit the value returned by an application or conditional from being passed on to another function as an argument. It follows that such a value can never be evaluated and must eventually propagate to the top level to become the final result of the whole evaluation. One can then show:Lemma 4.2: Let f be a safe lambda-function. Then fcnd (f) = fcnr (f).We are now in a position to state the main theorem in precise terms.Theorem I: Let f be any lambda function. We can effectively find a lambda-function f' such that fcnd (f') = fcnr (f') = fcnr (f) for all interpretations.Proof (sketch): By assumption, f = (λx1…xn.p) for some schema p with free variables x1, …, xn. Using well-known techniques, we can first find a schema p1 such that …… .We now define a function Φ to translate p1 into a safe form p2, and p2 will be such that fcnr ((λx1…xn.p)) = fcnd ((λx1…xn.(p2 (λx.x)))). We then take f' = (λx1…xn.(p2 (λx.x))). ……Thus, our idea is to modify the schema so that for any sub-lambda-function g, instead of g passing its result back to the caller, the caller is passed to g as an additional functional argument, g then applies the new argument to the result it used to return, thereby avoiding the necessity of returning immediately. ……In the definition of Φ, we also use the auxiliary function Ψ which adds the new argument to a lambda-function and translates its body. ……Examples:⑴ Φx = (λf.(f x)), x∈.⑵ Φ(x y) = (λf.((λf.(f x)) (λh.((λf.(f y)) (λx.(h f x)))))), x,y∈.⑶ Φ(λx.a) = (λf.(f (λgx.((λf.(f a)) g)))), x∈, a∈.⑷ Ψ(λx.a) = (λgx.((λf.(f a)) g)), x∈, a∈.}}

、指稱語義、演員模型

We shall assume that the reader is familiar with advanced pattern matching languages such as SNOBOL4, CONVERT, PLANNER-71, QA4, and POPLER.

We shall use (% A M%) to indicate sending the message M to the actor A. We shall use s1 s2 … sn to denote the finite squence s1, s2, … sn. ……

Identifiers can be created by the prefix operator =. ……

An expression (=> pattern body) is an abbreviation for (receive (message pattern) body) where receive is a more general actor that is capable of bindinq elements of the action in addition to the message.

Evaluating

(%(=> pattern body) the-message%), i.e. sending

(=> pattern body) the-message,

will attempt to match the-message against pattern, If the-message is not of the form specified by pattern, then the actor is NOT-APPLICABLE to the-message. If the-message matches pattern, the body is evaluated.……

Conceptually at least a new actor is created every time a message is sent. Consider sending to a target T a message M and a continuation C.

(send T

(message M

continuation C))

The transmission (% T M%) is an abbreviation for the above where C is defaulted to be the caller. If target T is the following:

(receive

(message the-body

continuation =the-continuation)

the-body)

then the the-body is evaluated in an environment where the-message is bound to M and the-continuation is bound to C.……

Many actors who are executing in parallel can share the same continuation. They can all send a message “return” to the same continuation. ”

和程序演算。這些模型仰仗於程式設計者或語義工程師書寫數學函式時採用 “續體傳遞風格”（continuation-passing style，簡寫為 CPS）。這意味著每個函式都消費一個表示有關於這個函式呼叫的餘下計算的函式。要返回一個值，這個函式以此返回值呼叫這個續體函式；要中止這個計算，它返回一個值。以續體傳遞風格書寫程式的函數語言程式設計者，獲得了以任意方式操縱控制流程的表達能力。代價是他們必須手工維護控制和續體的不變條件，這通常是高度複雜的任務。

以續體傳遞風格（CPS）書寫的函式接受一個額外的實際引數：顯式的續體，它是有一個實際引數的函式。當 CPS 函式已經計算出來其結果值的時候，它透過以這個值作為實際引數呼叫續體函式來 “返回” 它。這意味著在呼叫 CPS 函式的時候，呼叫者函式需要提供一個過程接受這個子例程的 “返回” 值。以這種形式表達程式碼使得在直接風格中隱含的一些事物顯露出來。這包括了：過程返回變成了對續體的呼叫，中間值都有了給定名字，實際引數求值的次序變為顯見，而尾呼叫成為簡單的將傳遞給這個呼叫者的續體不修改的傳遞給被呼叫過程。

CPS 的關鍵在於：所有函式都接受稱為其續體的一個額外實際引數，並且在函式呼叫中的所有實際引數必須要麼是一個變數，要麼是一個 λ 表示式而非更復雜的表示式。這具有將表示式 “從內至外” 翻轉的效果，由於表示式最內部份必須首先求值，因此 CPS 使得求值次序以及控制流程變得明確了。下面是在 Scheme 語言中僅使用其基本形式的幾個例子，對比了直接風格程式碼和對應的 CPS 程式碼，這裡約定了將續體函式表示為名為 k 的形式引數：

注意在 CPS 版本的程式碼中，使用的函式原語（functional primitive）如這裡的 *&、+&、-&、=& 和 sqrt & 自身也是 CPS 而非直接風格，所以要使得上述例子在 Scheme 系統中執行，就必須寫出這些函式原語的 CPS 版本，例如 =& 定義為：

(define (=& x y k)

(k (= x y)))

更通用的方式是寫一個轉換例程：

(define (cps-prim f)

(lambda args

(let ((r (reverse args)))

((car r) (apply f (reverse (cdr r)))))))

(define =& (cps-prim =))

(define & (cps-prim ))

(define +& (cps-prim +))

(define -& (cps-prim -))

(define sqrt& (cps-prim sqrt))

要在直接風格函式中呼叫 CPS 函式，必須提供接收 CPS 函式計算結果的一個續體。上述例子在所需 CPS 函式原語均已提供的條件下，可以呼叫為：

(pyth& 3 4 (lambda (x) x))

(factorial& 4 (lambda (x) x))

一般的程式設計經常不採用 CPS 函式原語，比如將前面的階乘函式寫為：

(define (factorial& n k)

(if (= n 0)

(k 1)

(factorial&

(- n 1)

(lambda (acc) (k (* n acc))))))

以當前續體呼叫

}}

Scheme 程式語言包括了控制流程運算元（簡寫為 call/cc），Scheme 程式可以透過它操縱控制流程。在計算機科學中，使這種型別的隱含程式狀態可見為物件，術語上叫做實化。在 Scheme 中，續體物件是頭等值並被表示為函式，具有作為其唯一的運算。Scheme 在應用續體和函式二者之間不做語法上的區分。

在 Scheme 中，出現在一個表示式中的 (call/cc f)，接受一個函式 f 作為它的唯一實際引數，並把它應用到這個表示式的當前續體上。當一個續體物件被應用於一個實際引數的時候，現存的續體被消除，而應用的續體在其所在位置上被恢復，所以程式流程將在續體被捕獲的那一點上繼續，而 “續體的實際引數” 則變成 call/cc 呼叫的 “返回值”。call/cc 建立的續體可以被多於一次呼叫，甚至可以從這個 call/cc 應用的動態時效範圍（extent）的外部來呼叫。

例如在表示式 ((call/cc f) g) 中，在概念上給出 f 所應用到的當前續體的方式，是透過將 (call/cc f) 替代為以 lambda 抽象繫結的一個變數比如叫做 cc，故而它的當前續體是 (lambda (cc) (cc g))。對它應用函式 f，得到最終結果 (f (lambda (cc) (cc g)))。而在表示式 (g (call/cc f)) 中，子表示式的 (call/cc f) 的續體是 (lambda (cc) (g cc))，故而整個表示式等價於 (f (lambda (cc) (g cc)))。

5.1 立即返回

下面的例子中，使用 call/cc 來模擬 C 風格語言中的 return 語句：

(define (f return)

(return 2)

3)

(f (lambda (x) x))

=> 3

(call/cc f)

=> 2

首先以正規函式實際引數 (lambda (x) x) 呼叫 f，它將繫結到形式引數 return 上的這個函式應用於值 2，接著執行下一個表示式返回 3。但是，在 f 被傳遞給 call/cc 的時候，將綁定了續體的形式引數應用於 2，將程式執行強制為跳轉到呼叫 call/cc 那一點上，並導致 call/cc 返回值 2。這個結果接著被頂層續體用 display 函式打印出來。

5.2 生成器

在下面的生成器程式碼中，call/cc 被使用了兩處：一處用於生成立即返回續體，而另一處用於遍歷一個成員列表的迭代在暫停時儲存恢復位置：

(define (generator-factory lst)

(define (control-state return)

(for-each

(lambda (element)

(set! return (call/cc (lambda (resume-here)

(set! control-state resume-here)

(return element)))))

lst)

(return 'fell-off-the-end))

(define (generator)

(call/cc control-state))

generator)

上述程式碼的簡單用例：

(define generate-digit

(generator-factory '(0 1 2)))

(define (display-two-digits)

(display (generate-digit)) (newline)

(display (generate-digit)) (newline))

(display-two-digits) ;; 分兩行列印 0 和 1

(display-two-digits) ;; 分兩行列印 2 和 fell-off-the-end

在定義變數 generate-digit 之時，將其定義為函式 generator-factory 應用於列表 '(0 1 2) 上而得到的一個閉包 generator。這個 generator 被定義為 (call/cc control-state)。

在第一次執行 (generate-digit) 之時，執行 (call/cc control-state)，進而執行 (control-state return)，它將用於立即返回的續體繫結到形式引數 return 之上；然後開始遍歷列表的元素進行迭代的 (for-each (lambda (element) ……) lst)，在求值 (set! return (……)) 的第二個實際引數之時，進行每一次迭代步驟 (call/cc (lambda (resume-here) ……))，其中的 (set! control-state resume-here)，將繫結到變數 resume-here 上的當前續體，重新繫結到函式名字 control-state 之上用於以後恢復執行，最後執行 (return element) 返回當前列表元素。

在下一次執行 (generate-digit) 之時，(call/cc control-state) 將 control-state 所繫結的續體應用當前續體上，從而在迭代的 (set! return (call/cc ……)) 之處恢復執行，它將傳入的立即返回續體繫結到變數 return 上，此後繼續這一次的迭代步驟。在遍歷了列表的元素之後迭代結束，最終執行 (return 'fell-off-the-end)，返回一個約定的常量。

5.3 協程

call/cc 還可以表達其他複雜的原始運算。下面的程式碼使用續體達成協作式多工的使用者級執行緒，亦稱為協程：

(define ready-list ')

(define (fork fn arg)

(call/cc (lambda (return)

(set! ready-list (cons return ready-list))

(fn arg)

(let ((cont (car ready-list)))

(set! ready-list (cdr ready-list))

(cont #f)))))

(define (yield)

(call/cc (lambda (return)

(let ((cont (car ready-list)))

(set! ready-list (append (cdr ready-list*) (list return)))

(cont #f)))))

(define (schedule)

(let loop

(if (not (null? ready-list)) (begin

(call/cc (lambda (return)

(let ((cont (car ready-list)))

(set! ready-list (append (cdr ready-list*) (list return)))

(cont #f))))

(loop)))))

上述程式碼的簡單用例：

(import (srfi 28))

(define (do-stuff-n-print str)

(let loop ((n 0))

(if (

其輸出為：

This is AAA 0

Hello from BBB 0

This is AAA 1

Hello from BBB 1

This is AAA 2

Hello from BBB 2