Continuation 和高級流程控制
發表于:2007-05-25來源:作者:點擊數:
標簽:
流程控制通常非常簡單:包括序列化、選擇和迭代等過程。很多一直在使用這些基本控制結構的程序員都曾經經歷過一段困難的時間來確定哪種流程控制是必需的。本文將簡要介紹有關 continuation 的內容,并向您展示如何用最新的方法來考慮流程控制的問題。 很多程
流程控制通常非常簡單:包括序列化、選擇和迭代等過程。很多一直在使用這些基本控制結構的程序員都曾經經歷過一段困難的時間來確定哪種流程控制是必需的。本文將簡要介紹有關 continuation 的內容,并向您展示如何用最新的方法來考慮流程控制的問題。
很多程序員在首次接觸編程之后,并不會太多考慮有關流程控制的問題,因為大部分編程語言都只有幾個簡單的流程控制結構。然而,流程控制的內容實際上比大部分主流編程語言中提供的更為豐富。有很多并不被大多數人所知的專用語言都具有高級且有用的流程控制結構。
高級流程控制的例子
我們開始了解流程控制的最好方法是查看幾個使用不同語言的高級流程控制結構的例子。然后可以將這些知識歸納為一個適用于高級流程控制情況的框架。
大規模退出
第一種高級流程控制技術您可能已經聽說過:大規模退出(non-local exit)。大規模退出有幾種類型,每種類型都可以劃分成兩類:結構化的和非結構化的。非結構化大規模退出(Unstructured non-local exit) 就是計算機科學老師警告您不要這樣做的一種情況,例如可怕的 goto
語句。實際上,如果得到廣泛而正確地使用,非結構化的大規模退出可能會非常有用。例如,在流程控制非常復雜的程序中,它們可以提高程序的可讀性。如果復雜的流程控制不能非常自然地嵌套,那么強行使用嵌套結構會使整個程序的可讀性變差,而不是變好。有關使用 goto
語句的優缺點的更多信息,請參考本文后面 參考資料 部分給出的鏈接。
對于結構化的大規模退出(structured non-local exit),您可能熟悉最流行的一種類型:異常。如果您使用 C、Fortran 和 Cobol 的時間已經超過了 20 年,而且一直沒有再使用過其他語言,那么請參閱下面對異常的一段簡介。
異常 (exception) 是在代碼中觸發錯誤或對錯誤進行局部化的一種方法。通常當錯誤發生時,我們希望能夠對這個錯誤進行處理。除非我們明確下達繼續操作的指示,否則其余的工作不會繼續進行。例如,我們可以看一下下面使用 Java™ 語言編寫的這個簡單的數據庫代碼:
清單 1. 簡單的數據庫代碼的例子
//NOTE -- this code will not compile, but that's on purpose.
import java.sql.*;
...
public static int testdbcode(Connection conn) {
//Prepare the statement
PreparedStatement s = conn.prepareStatement(
"select id from test_table where name = ?");
//Set the parameters
s.setString(1, "fred");
//Run the query
ResultSet rs = s.executeQuery()
//Move to the first result row
rs.next();
//Get the answer (first result parameter)
int id = rs.getInt(1);
return id;
}
...
|
這段代碼的問題是沒有錯誤處理,在處理數據庫或其他外部實體時,每個地方都可能產生錯誤。例如,如果在準備查詢時產生了錯誤,那么設置參數、運行查詢以及檢索結果就都沒什么用了。這個查詢在碰到第一個錯誤之后就毫無意義了。因此在 Java 語言中,提供了一些異常,這讓我們可以封裝一段代碼,這樣在碰到第一個錯誤時就會跳到這段代碼上來。要在 Java 語言中實現這種功能,我們可以按照下面的方式重新編寫這段代碼:
清單 2. 采用異常處理的簡單數據庫函數
import java.sql.*;
...
public static int testdbcode(Connection conn) {
try {
//Prepare the statement
PreparedStatement s = conn.prepareStatement(
"select id from test_table where name = ?");
//Set the parameters
s.setString(1, "fred");
//Run the query
ResultSet rs = s.executeQuery()
//Move to the first result row
rs.next();
//Get the answer (first result parameter)
int id = rs.getInt(1);
return id;
} catch (Exception e) {
//Put error handling code here
return -1;
}
}
...
|
try
中的代碼塊會一直執行完,除非有一條語句出現了錯誤。如果觸發 了某個錯誤,就會跳過 try
中的代碼塊,執行 catch
中的代碼塊,其中變量 e
中保存了異常的信息。在 Java 中,被觸發的錯誤都是完整的類,因此任何信息都可以放入異常中。實際上,我們可以創建多個 catch
代碼塊,每個代碼塊分別用來處理一種特定的異常類。
在上面的代碼例子中,我們處理的是系統生成的異常。不過我們也可以創建應用程序級的異常,并對其執行同樣的處理。應用程序可以在任意時刻使用 throw
關鍵字觸發異常。例如,我們可以說如果上面的代碼中 ID 值為 1,那么就可以認為這是一個應用程序級的異常。我們可以使用下面的方式:
清單 3. 觸發應用程序級異常的簡單數據庫的例子
import java.sql.*;
...
public static int testdbcode(Connection conn) {
try {
//Prepare the statement
PreparedStatement s = conn.prepareStatement(
"select id from test_table where name = ?");
//Set the parameters
s.setString(1, "fred");
//Run the query
ResultSet rs = s.executeQuery()
//Move to the first result row
rs.next();
//Get the answer (first result parameter)
int id = rs.getInt(1);
//Check for application exception
if(id == 0) {
//Signal the exception
//Assume that InvalidUserIDException is defined elsewhere
throw new InvalidUserIDException();
}
return id;
} catch (InvalidUserIDException e) {
//Specialized error handling here
return -1;
} catch (Exception e) {
//Handle all other errors here
return -1;
}
}
...
|
另外,處理異常的代碼沒有理由只能放在函數本身中。try
和 catch
語句也可以放到任何包含函數(containing function)中。異常處理機制會展開堆棧,直到它到達一個合適的異常處理程序,此時程序就會執行異常處理代碼。這種執行結構化大規模退出的能力可以極大地簡化代碼的編寫,以及對代碼的維護工作,因為在某些情況下,錯誤處理與實際實現功能的代碼是完全分隔開的。當然,這一切都非常簡單。異常處理還有幾個主要的功能,這已經超出了本文介紹的范圍,不過 參考資料 中的部分內容可以為您提供這方面的幫助。
生成函數
另外一種高級流程控制結構是生成器。生成器是一些用來生成一組值的函數,這些值會在調用這些函數時一起返回。生成器可以將當前位置加入當前函數的 “書簽”,從而對編程進行簡化。
例如,假設我們希望得到這樣一個函數:每次調用該函數時它都會返回從 1 開始的一個數字序列,并且每次調用這個函數時,這個序列都會不斷地增加。盡管采用閉包(closure)或全局變量都很難實現該函數,但是使用生成器卻很容易實現它。下面是一個 Python 生成器實現的例子:
清單 4. 簡單的 Python 生成器程序
#This is our generator function
def sequence(start):
current = start
while true:
yield current
current = current + 1
generator_func = sequence(1) #Create a new sequence generator starting at 1
print generator_func.next() #prints 1
print generator_func.next() #prints 2
print generator_func.next() #prints 3
|
正如您可以看到的那樣,這個生成器每次被調用時都會回到上次退出這個函數時的狀態,然后繼續執行,直到碰到 yield
語句。這種 “書簽” 和 “從書簽中重用” 的特性在大部分語言中都不是標準特性,但是它卻非常有用,可以讓復雜邏輯的可讀性更強,并且更容易編程。
邏輯編程
另外一種高級流程控制是邏輯編程,它在諸如 Prolog 之類的編程語言中得到了廣泛的使用。在 Prolog 中,會為計算機提供一組定義,它可以 “魔術般地” 為您解答查詢和設置值。例如,請查看下面的 Prolog 程序(大寫字母表示變量):
清單 5. Prolog 中簡單的邏輯程序
likes(john,sports). %this asserts that john likes sports
likes(john,food). %this asserts that john likes food
likes(amy,X) :- likes(john,X). %amy likes X if john likes X
likes(brad,food). %brad likes food
likes(brad,television). %brad likes television
%query for a value that both amy and brad like, and write it out.
?- likes(amy,X), likes(brad,X), write(X).
|
其工作原理是 Prolog 會創建一個 john 和 brad 所喜歡東西的列表。它還給出了一條規則:凡是 amy 喜歡的東西 john 都喜歡。然后當我們執行查詢時,它首先會尋找 “amy 喜歡什么” 這個問題的答案。這個問題的答案是 “john 喜歡的任何東西”。然后遍歷 john 所喜歡的東西的列表,并得出第一個匹配答案,即 sports。然后展開下一個問題:brad 也必須喜歡 amy 所喜歡的東西(這是通過表達式中的 X
來表示的)。然而,sports 并不在 brad 的愛好列表里面。因此,Prolog 就會繼續回溯(backtrack),并從 amy 的愛好列表中再為 X
尋找一個匹配項。下一個值是 food。然后繼續檢查 food 是否也在 brad 的愛好列表中。答案是肯定的,因此可以繼續執行下一個步驟:打印出為 X
找到的值。
這種編程就稱為邏輯編程,因為它允許以邏輯關系的方式表達自己的目標,并讓計算機來執行所有的調研工作,為這些邏輯關系找到正確的答案。對于我們的目的來說,最重要的就是所建立的惟一一種流程控制方法:回溯。這意味著在計算的任何地點,如果在對應的變量中沒有找到合適的值,或者一組變量之間的某個特定關系不正確,那么程序就可以進行回溯并分配一個新值。這種編程方法簡化了問題集的數量,在智能領域更是如此。


|
回頁首 |
|
continuation :流程控制工具
到目前為止,我們已經了解了 3 種高級流程控制:大規模退出、生成函數和回溯。它們的共同點是什么呢?基本上來說,它們都有一些程序堆棧和指令指針機制。在大規模退出中,包含退出代碼段的堆棧幀被帖上書簽,這就是退出處理的代碼塊。當我們調用這個大規模退出代碼塊時,堆棧就會展開到書簽引用的地方,指令指針也被移動到處理程序代碼塊所在的地方。在生成函數中,包含生成器的變量包含了一個到生成函數的堆棧幀的指針,以及一個上次離開生成函數地方的指針。在回溯中,書簽被保持在上次進行賦值的地方,如果計算失敗并且需要進行回溯,流程控制就返回到這個位置。
我們可以調用這些書簽的 “連續點(continue point)” —— 如果這個高級流程控制結構被調用,程序就會從此處繼續執行?;蛘吒_切地說,它們都稱為 continuation(連續性)。實際上,所有這些控制結構都可以使用一個單一的流程控制函數實現:call-with-current-continuation
。
call-with-current-continuation
是 Scheme 編程語言中的一個函數,它利用了當前的堆棧幀和指令指針,并將其封裝到一個可調用的實體(continuation)中,并使用這個 continuation 作為惟一的參數調用另外一個函數。continuation 是一個可調用實體,它只可以接收一個參數,這個參數然后會返回到創建這個 continuation 的地方。這聽起來可能會令人感到困惑 —— 實際上它的確是比較繞口。讓我們看幾個例子來感受一下它實際上是如何工作的。
首先,下面是使用 continuation 的一個簡單例子
清單 6. Continuation 的例子
(display
;;Calling the continuation will return the parameter as the return value to
;;the call-with-current-continuation function. This is the "bookmarked" location
(call-with-current-continuation
;;Continuation variables are often written with a "k" or "kont" to remind
;;yourself that it is a continuation. In this example, "kont" will be
;;the continuation. It is a function, that, when called, will return its
;;parameter to the bookmarked location.
(lambda (kont)
;;For this example, we will simply run the continuation. This returns
;;the number 5 to the "bookmarked" location
(kont 5))))
(newline)
|
上面的程序會簡單地顯示數字 5。注意使用一個參數調用 continuation 會返回使用書簽引用的位置處的值?,F在,讓我們來看一個稍微復雜點的例子。在這種情況中,我們將使用 continuation 作為一種提前退出的手段。這個例子是故意這樣設計的,不過它可以充分展示上述方面。對于這個程序而言,我們將對一個列表中的每個數字求平方值。然而,如果在這個列表中存在任何 非數字的值,這個程序不會返回一個列表,而是只返回符號 *error*
。
清單 7. 使用 continuation 從錯誤條件中提前退出
(define a '(1 2 3 4 5))
(define b '(1 b 2 e f))
(define (square-list the-list)
;;Bookmark here
(call-with-current-continuation
;;early-exit-k will return us to our bookmark
(lambda (early-exit-k)
;;run the procedure
(map
(lambda (x)
;;Check to see if it is a number
(if (number? x)
;;yes it is, perform the multiplication
(* x x)
;;no it isn't, exit _now_ with the value *error*
(early-exit-k '*error*)))
the-list))))
;;This will square the list
(display (square-list a))
(newline)
;;This will give the error
(display (square-list b))
(newline)
|
希望上面這個例子可以就如何使用 continuation 來實現異常為您提供一些提示。
下面這個例子展示了 continuation 另外一個不常見的屬性:它們可以具有無限的范圍(unlimited extent)。這意味著它與異常有所不同,continuation 可以在產生代碼塊的范圍之外激活。當您為某個 continuation 標記一個書簽時,只要這個 continuation 值是活動的,它就會強制堆棧幀也保持活動狀態。因此,即使已經從創建這個 continuation 的代碼塊中返回,在調用這個 continuation 時,依然會恢復前面的活動堆棧幀并從此處繼續執行。
下面的例子將 continuation 保存到一個全局變量中,然后在激活這個 continuation 之前,重新激活原來的堆棧幀。
清單 8. 使用 continuation 重新激活一個堆棧幀
;;Global variable for the continuation
(define k-global #f)
;;We are using let* instead of let so that we can guarantee
;;the evaluation order
(let* (
(my-number 3)
(k
;;set bookmark here
(call-with-current-continuation
;;The continuation will be stored in "kont"
(lambda (kont)
;;return the continuation
kont))))
;;We are using "my-number" to show that the old stack
;;frame is being saved. When we revisit the continuation
;;the second time, the value will remain changed.
(display "The value of my-number is: ")
(display my-number)
(newline)
(set! my-number (+ my-number 1))
;;Save the continuation in a global variable.
(set! k-global k))
;;Is "kontinuation" a continuation?
(if (procedure? k-global)
(begin
(display "This is the first run, k-global is a continuation")
(newline)
;;Send "4" to the continuation which goes to the bookmark
;;which will assign it to "k"
(k-global 4))
(begin
;;This oclearcase/" target="_blank" >ccurs on the second run
(display "This is the second run, k-global is not a continuation, it is ")
(display k-global)
(newline)))
|
利用這些特性,我們就可以創建一些有用的步驟和宏來模擬各種其他特性。
使用 continuation 實現異常
現在來看一下異常是什么樣子:
清單 9. 異常的例子
try {
//Code here which might generate an exception
} catch(SQLException e) { //catch a specific exception
//Error handling code
} catch(Exception e) { //catch a general exception
//Error handling code
}
//remaining code here
|
因此,我們需要做的事情主要是創建一個宏,它可以建立:
因此,這個宏展開的結果 看起來必須如下所示:
清單 10. 假設的異常宏的期望展開結果
;;This establishes the throw function as globally available, but displays an
;;error message if used without being in a try block.
(define throw (lambda (x) (display "No try clause is active!") (newline)))
(let* (
;Save the old containing try block
(old-throw throw)
;we are saving the results in retval because we still have to clean up after
;ourselves before we can exit
(retval (call-with-current-continuation
;The exception will use this continuation to get back to the
;remaining code
(lambda (k-exit-to-remaining-code)
(let (
;This defines the exit handler
(error-handler
(lambda (exception)
(k-exit-to-remaining-code
;;error-handling code here
))))
;This sets our error handler to be the official "throw" function
(set! throw error-handler)
;;Regular code here
;;You can throw an exception by doing:
(throw 'test-exception))))))
;Reinstate old try block
(set! throw old-throw)
;Return the result
retval)
|
這會建立一些嵌套,從而使 throw
總是引用里面的 try
代碼塊?,F在我們已經知道了自己希望代碼是什么樣子,接下來就可以編寫一個宏來實現這個基礎設施的所有設置了。
清單 11. 生成異常代碼的宏
;;This establishes the throw function
(define throw (lambda (x) (display "No try clause is active!") (newline)))
;;establishes syntax for a "try" block
(define-syntax try
(lambda (x)
(syntax-case x (catch)
(
(try expression (catch exception exception-handler-expression))
(syntax
(let* (
(old-throw throw)
(retval
(call-with-current-continuation
(lambda (k-exit)
(let (
(error-handler
(lambda (exception)
(k-exit exception-handler-expression))))
(set! throw error-handler)
expression)))))
(set! throw old-throw)
retval))))))
;;Short test suite
;Function that throws an error
(define (test-function)
(throw 'ERROR))
;Function that does not throw an error
(define (test-function-2)
(display "No error is generated.")
(newline))
;Test out our try block
(try (test-function) (catch e (begin (display "Exception! e is: ") (display e) (newline))))
MILY: Andale Mono, Lucida Console, Monaco, fixed, monospace" twffan="done">|-------- XML error: The previous line is longer than the max of 90 characters ---------|
(try (test-function-2) (catch e (begin (display "Exception! e is: ") (display e) (newline))))
|-------- XML error: The previous line is longer than the max of 90 characters ---------|
|
盡管我們已經解決了大部分問題,但仍然有一些問題存在。問題來自于混合 continuation。例如,除了將 continuation 用于異常之外,如果還將它們用于其他類型的早期退出邏輯(early exit logic),那么您將遇到一個問題。仔細查看以下代碼:
清單 12. continuation 的不良交互
;;Uses previously defined try/catch macro
(try
(begin
(call-with-current-continuation
(lambda (kont)
(try
(kont 'value) ;;jumps out of the continuation, but also skips resetting
;;the active continuation
(catch e (begin
(display "Interior exception handler. Exception e is: ")
(display e)
(newline))))))
;;Because we didn't exit out through the try block in a normal fashion, this will
;;actually send us _back_ to the interior catch block the first time it is called!
(throw 'ERROR))
(catch e (begin
(display "Exterior exception handler. Exception e is: ")
(display e)
(newline))))
|
正如您可以看到的一樣,這種自由跳轉的能力可能會在保留書簽時帶來一些難題。為了減少這些問題,Scheme 采用了一個特殊的過程 dynamic-wind
。dynamic-wind
可以檢測出何時有一個 continuation 跳過了某個給定的堆棧幀。我們可以在 continuation 來回跳轉時重置堆棧。dynamic-wind
使用了 3 個變量,每個變量都是一個不帶任何參數的過程。第一個變量是每次進入堆棧幀時都需要運行的一個過程。第二個變量是真正運行的過程。最后一個變量是每次離開堆棧幀時都需要運行的過程。下面這個例子給出了關于 dynamic-wind
如何工作的一個簡短跟蹤:
清單 13. 使用 dynamic-wind 的例子
(let (
(k-var (call-with-current-continuation
(lambda (kont)
(dynamic-wind
(lambda () (display "Entering frame") (newline))
(lambda ()
(begin
(display "Running")
(newline)
(call-with-current-continuation
(lambda (inner-kont)
;;Exit across the dynamic-wind boundary,
;;saving the current continuation
(kont inner-kont)))))
(lambda () (display "Leaving frame") (newline)))))))
(if (procedure? k-var)
(k-var 'the-value)
(begin
(display k-var)
(newline))))
|
首先,它創建一個外部 continuation。然后進入這個堆棧幀,調用 “進入” 過程。然后運行一個過程,在 dynamic-wind
內部生成一個 continuation。這個 continuation 之后會通過原來的 continuation 返回。然而,由于它穿過了 dynamic-wind
線,因此會執行 “離開” 過程。然后再次執行這個內部 continuation,這會將控制權再次通過 dynamic-wind
傳遞,此時會再次調用 “進入” 過程。然后返回 dynamic-wind
,再次調用 “離開” 過程。
盡管這個調用序列會令人有些困擾,但是如果您將 dynamic-wind
看作是一個對長遠的 continuation 的一個 “警戒線”,那么意義非常明確了。如果流程控制想要通過這個 “警戒線”(不管是通過 continuation 還是通過普通的流程控制),則必須執行適當的過程(根據方向的不同,分別是 “進入” 或 “離開” 過程)來完成清理工作。
使用這種方法,我們可以對 try
宏中的某些問題進行預防??梢允褂?dynamic-wind
來重置代碼執行哪個 try
/catch
代碼塊。代碼如下:
清單 14. 使用 dynamic-wind 來改進 try/catch
;;This establishes the throw function as globally available, but displays an
;;error message if used without being in a try block.
(define throw (lambda (x) (display "No try clause is active!") (newline)))
;;establishes syntax for a "try" block
(define-syntax try
(lambda (x)
(syntax-case x (catch)
(
(try expression (catch exception exception-handler-expression))
(syntax
;;Exit point using continuation k-exit
(call-with-current-continuation
(lambda (k-exit)
(let (
;;These are the two exception handlers, the old and the
;;new. Dynamic-wind sets them up and tears them down
;;upon entering and exiting from scope
(old-throw throw)
(error-handler
(lambda (exception)
(k-exit exception-handler-expression))))
(dynamic-wind
;;Entering scope -- set exception handler
(lambda () (set! throw error-handler))
;;Actual processing
(lambda () expression)
;;Exitting scope -- set exception handler to old value
(lambda () (set! throw old-throw)))))))))))
|
這個版本要簡單多了,它可以滿足原來的測試用例和使用 continuation 的測試用例的需求。同樣,如果您認為增加一個 dynamic-wind
控件是受到了欺騙,那么 Kent Dybvig 已經展示,dynamic-wind
也可以使用 call-with-current-continuation
來實現,相信這可以消除您的疑慮。
雖然并沒有涉及 try
/catch
可能會產生的非預期性行為的所有方面,但是這對于大部分情況來說這應該已經足夠了。下一節將重新審視可能發生的一些問題。
使用 continuation 的生成器
正如前面介紹的一樣,生成器是一種流程控制形式。Python 是實現生成器的最常用的一種語言。下面讓我們考慮一下生成器是如何工作的,以及如何使用 continuation 來實現生成器。
首先要創建一個生成器。這必須通過一個 continuation 或閉包來節省堆棧幀的使用。然后,我們需要返回一個值,并使用書簽標記當前位置在函數中的位置。這還意味著您必須已經標記了要返回的位置。
因此,我們的生成器有兩個書簽,它們可以使用 continuation 來實現。第一個書簽是一個變量,但是在首次創建這個生成器時建立的。其中保存了生成器函數的當前位置。然后當我們運行這個生成器時,還會有一個 continuation,它是在調用函數中的返回點。就在返回之前,我們還需要創建當前位置的一個 continuation,并將其保存下來供下次調用使用。
現在來看一下我們希望的 Python 風格的 Scheme 接口是什么樣子:
清單 15. 使用 Python 風格的生成器的例子
(define-syntax generator
(syntax-rules ()
(
(generator (yieldfunc) body ...)
(let (
(placeholder #f) ;;placeholder in this function
(return-proc #f) ;;how to get out
(finished #f)) ;;whether or not it is finished
;;this is the generator entrance
(lambda ()
(call-with-current-continuation
(lambda (tmp-return-proc)
;;save the way out in "return-proc"
(set! return-proc tmp-return-proc)
(let (
;;"yieldfunc" resets the placeholder and returns the value
(yieldfunc
(lambda (x)
(call-with-current-continuation
(lambda (current-placeholder)
;;new placeholder
(set! placeholder current-placeholder)
;;return value
(return-proc x))))))
;;If the generator is done, return a special value
(if finished
'generator-finished
;;If this is the first time, there will not be a
;;placeholder established, so we just run the body.
;;If there is a placeholder, we resume to that point
(if placeholder
(placeholder 'unspecified)
(let (
(final-value (begin body ...)))
(set! finished #t)
(return-proc final-value))))))))))))
(define sequence-generator
;;Initial parameters
(lambda (start end increment)
;;"yield" will be used to generate a single value
(generator (yield)
;;Main function body
(let loop ((curval start))
(if (eqv? curval end)
curval
(begin
;;yield the value
(yield curval)
;;continue on
(loop (+ curval increment))))))))
;;Example usage
(define my-sequence (sequence-generator 1 3 1))
(display (my-sequence))(newline)
(display (my-sequence))(newline)
(display (my-sequence))(newline)
(display (my-sequence))(newline)
|
這段代碼理解起來有點困難。如果我們添加了查詢生成器是否擁有更多值或其他狀態的功能,情況就會變得更加復雜。不過要注意,這里有兩個生成器范圍的函數。一個是返回到調用程序的過程,另外一個是執行生成器的當前位置。返回過程保存在一個生成器范圍的變量中,這看起來可能會有些奇怪,但是這種安排是必需的,只有這樣才能使 continuation 調用完成之后處于活動狀態的是正確版本的變量。否則,在第一次調用之后,就會恢復成在創建 continuation 時處于活動狀態的版本了。
正如前面介紹的一樣,在基于異常的 continuation 情況中,問題可能會很多。通常,如果我們在啟動一個生成器時有一個 try
代碼塊,在運行生成器是也有一個 try
代碼塊,并且在生成函數中觸發了一個異常,那么就會遇到應該運行哪個 catch
代碼塊的難題?在我使用的實現中,會使用第一個 catch
代碼塊。這是實現這種功能最直觀的方法嗎?這要取決于具體的情況。然而,這些 continuation 到 continuation 的交互都可能存在問題,因為究竟哪些操作才是適當的并不十分明確。
使用 continuation 進行回溯
最后,讓我們來看一下回溯的問題?;厮莺瘮档慕涌谑且粋€ amb
函數。這個函數可以接收一個值列表。對于每個值來說,amb
都會設置一個回溯書簽。如果這個列表中的當前值不能解答問題(這可以通過調用 amb:fail
函數看出來),那么程序就會回溯到最后一個書簽,并嘗試新的值。如果參數不是 true,那么 amb:assert
函數就會觸發 amb:fail
。清單 16 給出了所使用的一些函數:
清單 16. 在 Scheme 中使用回溯
(let* (
(a (amb 1 2 3 4 5 6 7))
(b (amb 5 6 7 8 9 10)))
(amb:assert (> a b))
(display "a is ") (display a) (newline)
(display "b is ") (display b) (newline))
|
當首次運行這個函數時,它會為 a
選擇 1
,為 b
選擇 5
。由于 a
小于 b
,因此就會失敗并回溯到上一次標記的回溯位置。在這里,是對 b
進行賦值的地方。然后嘗試 b
的每個值。如果每個都不成功,那么會繼續回溯到對 a
進行賦值的地方。然后使用 b
的每個值來嘗試 2
是否能成功。依此類推,繼續執行,直到找到 a
的一個值大于 b
為止,此時就可以繼續執行后面的操作了。
清單 17 給出了具體的實現:
清單 17. 回溯的實現
;AMB definition
(define amb:fail '*)
(define amb:initialize-fail
(lambda x
(set! amb:fail
(if (null? x)
(lambda () (error "amb tree exhausted!"))
(car x)))))
(amb:initialize-fail)
(define amb
(lambda alternatives
(letrec (
(amb-internal
;;"sk" returns the value (success continuation),
;;"alts is the list of values
(lambda (sk alts)
;;fail if there are no alternatives
(if (null? alts)
(prev-amb-fail)
(begin
(call/cc
;;"fk" is where to go when an
;;alternative fails (failure continuation)
(lambda (fk)
;;set the failure function to come back here
(set! amb:fail
(lambda ()
(set! amb:fail prev-amb-fail)
(fk 'fail)))
;;return the first alternative
(sk (car alts))))
;;We get here after a failure, so we
;;run the function again using the rest
;;of the list
(amb-internal sk (cdr alts))))))
;;this is the previous failure function
(prev-amb-fail amb:fail))
(call/cc
;;bookmark the way to the assignment into "sk"
(lambda (sk)
;;go through each alternative
(amb-internal sk alternatives)
;;After going through them, note that we have a failure
(prev-amb-fail))))))
;;Calling "amb" without arguments will cause a failure. This function
;;just makes it a little more obvious what is supposed to be happening.
(define amb:assert-failure
(lambda () (amb)))
(define amb:assert
(lambda (pred)
(if (not pred) (amb:assert-failure))))
|
在閱讀這段代碼時,要記得 “失敗 continuation” 就是回溯返回到值列表的方法,“成功 continuation” 才是函數返回下一個值到正常的程序流程中的方法。


|
回頁首 |
|
Continuation:從流程控制中希望獲得的東西
正如我們可以看到的一樣,我們可以使用 continuation 來實現所有的高級流程控制語句。只需要使用幾條語句,就可以將 continuation 構建成異常、生成器、回溯以及其他類型的高級流程控制。但是本文只不過是觸及了它的表面。使用 continuation,我們還可以實現很多功能,例如將 Web 應用程序轉換成更為傳統的流程控制結構,以及實現用戶級的線程。不幸的是,很多語言都沒有實現 continuation,因此這些語言的用戶都無法使用很多流程控制特性。如果某種語言只有 continuation,那么它可以嘗試實現其他高級流程控制特性。
原文轉自:http://www.kjueaiud.com
老湿亚洲永久精品ww47香蕉图片_日韩欧美中文字幕北美法律_国产AV永久无码天堂影院_久久婷婷综合色丁香五月
|