Common Lisp/高階主題/條件系統
Common Lisp 擁有一個高階的條件系統。條件系統允許程式處理異常情況,即程式設計師定義的程式正常執行之外的情況。異常情況的一個常見例子是錯誤,但是 Common Lisp 條件系統涵蓋的範圍遠不止錯誤處理。
條件系統可以分為三個部分:發出或引發條件、處理條件以及提供條件恢復。幾乎所有現代程式語言都提供前兩種協議,但很少有語言提供最後一種(或區分最後兩種)。這種最後一種協議,提供重啟或程式恢復的方法,在某種程度上是 Common Lisp 條件處理最重要的方面。
重啟是一種從異常情況中恢復的方法。異常情況經常發生,通常是錯誤。如果你一直在用這本書的 REPL 玩,毫無疑問你至少進入過一次偵錯程式會話。當引發嚴重條件並且 Lisp 系統別無選擇只能詢問你該怎麼辦時,就會呼叫偵錯程式。它會給你一個可以從這種條件中恢復的方式的選項列表。這些選項就是重啟。通常偵錯程式提供的唯一重啟是返回到頂層 REPL,但有時你被允許繼續或重試計算。此外,你可以定義其他重啟。
例如,如果你試圖從檔案中讀取資料,可能會出現很多問題:檔案可能不存在,你可能沒有足夠的許可權讀取它,或者檔案中的資料可能已損壞。這些事件通常被認為是異常情況,並且每種情況都可以用多種方法處理。如果檔案不存在,你可能想要指定另一個檔名;如果許可權不足,你可能想要指定另一個檔名或修改檔案許可權使其可讀;如果資料已損壞,你可能想要指定一個新的檔名,嘗試以有意義的方式解釋損壞的資料,甚至嘗試修復檔案並重新讀取。
當你實現重啟時,你的工作是識別這些可能的恢復機制並將它們作為條件系統的重啟放置到位。為了完成我們的示例,假設你正在讀取一個包含多行檔案,每行都包含一個 (x,y) 座標列表,它們只是作為由空格分隔的數字對列出。例如,該檔案可能看起來像這樣
0 0 100 150 50 30
30 20 65 65 10 20
0 100 150 50 30 0
作為第一步,我們可以這樣編寫讀取該檔案的函式
(defun read-points-file (filename)
(iter (for line in-file filename using #'read-line)
(collecting
(iter (for val in-stream (make-string-input-stream line))
(collect val) ))))
現在,我們看看如果我們在不存在的檔案上呼叫該函式會發生什麼(在 SBCL 中)
(read-points-file #p"does-not-exist.data")
error opening #P"does-not-exist.data":
No such file or directory
[Condition of type SB-INT:SIMPLE-FILE-ERROR]
Restarts:
0: [RETRY] Retry SLIME REPL evaluation request.
1: [ABORT] Return to SLIME's top level.
2: [TERMINATE-THREAD] Terminate this thread (#<THREAD "repl-thread" RUNNING {BA80BC1}>)
Backtrace:
0: (SB-IMPL::SIMPLE-FILE-PERROR "error opening ~S" #P"does-not-exist.data" 2)
1: ((LABELS SB-IMPL::VANILLA-OPEN-ERROR))
2: (OPEN #P"does-not-exist.data")[:EXTERNAL]
3: (READ-POINTS-FILE #P"does-not-exist.data")
...etc...
我們看到 SBCL 已經引發了一個錯誤,並且由於它不知道該怎麼做,所以它詢問使用者。它提供了一個重啟列表:RETRY、ABORT 和 TERMINATE-THREAD。如果你建立了一個名為does-not-exist.data的檔案,那麼你可能會使用 RETRY 重啟。如果檔案存在但由於許可權問題而無法讀取,我們會得到幾乎相同的結果,但錯誤訊息許可權被拒絕代替。同樣,你可以修復檔案並呼叫 RETRY 重啟。
如果無法開啟檔案進行讀取,似乎使用者可能輸入了不正確的檔名。考慮到這一點,讓我們提供一個重啟來更改我們嘗試開啟的檔名。我們可以使用以下形式restart-case或更通用的restart-bind.
(defun prompt-for-new-file ()
(list (prompt "Input new file name: ")) )
(defun read-points-file (filename)
(restart-case
(iter (for line in-file filename using #'read-line)
(collecting
(iter (for val in-stream (make-string-input-stream line))
(collect val) )))
(try-different-file (filename)
:interactive prompt-for-new-file
(read-points-file filename) )))
現在,如果我們因為無法讀取檔案而進入偵錯程式,我們會得到一個額外的選項:TRY-DIFFERENT-FILE。
(read-points-file #p"does-not-exist.data")
error opening #P"does-not-exist.data":
No such file or directory
[Condition of type SB-INT:SIMPLE-FILE-ERROR]
Restarts:
0: [TRY-DIFFERENT-FILE] TRY-DIFFERENT-FILE
1: [RETRY] Retry SLIME REPL evaluation request.
2: [ABORT] Return to SLIME's top level.
3: [TERMINATE-THREAD] Terminate this thread (#<THREAD "repl-thread" RUNNING {BA80BC1}>)
Backtrace:
0: (SB-IMPL::SIMPLE-FILE-PERROR "error opening ~S" #P"does-not-exist.data" 2)
1: ((LABELS SB-IMPL::VANILLA-OPEN-ERROR))
2: (OPEN #P"does-not-exist.data")[:EXTERNAL]
3: (READ-POINTS-FILE #P"does-not-exist.data")
...etc...
Input new file name: #p"does-exist.data"
==> ((0 0 100 150 50 30) (30 20 65 65 10 20) (0 100 150 50 30 0))
形式restart-case在重啟生效的環境中執行其第一個引數。這意味著指定的重啟 TRY-DIFFERENT-FILE 可以隨時在該第一個形式執行期間被呼叫。在我們的示例中,在形式執行期間呼叫了偵錯程式,這意味著重啟包含在它可以採取的操作列表中。
條件用於描述程式設計師不想在主程式流中處理的情況。我們還沒有在我們的示例中定義任何條件,但是,一個條件被髮出。這個條件是一個錯誤,open在嘗試開啟它無法開啟的檔案時引發。該錯誤的型別為sb-int:simple-file-error,它內置於 Lisp 系統中。
你可以使用define-condition宏定義你自己的條件,它與defclass宏非常相似。
處理程式的作用是將條件與重啟繫結在一起。這意味著如果該條件被引發,該重啟將自動被選中,這意味著我們不會被轉儲到偵錯程式中。
例項化處理程式的形式是handler-case和更通用的handler-bind.
Common Lisp 還提供了一套錯誤處理機制,這些機制模仿了大多數其他語言中發現的行為。