隨手扎
[Common Lisp] 狀況(condition)處理快速筆記
Common Lisp的狀況系統(Condition System)遠比其他語言的錯誤處理來的強大,卻也不那麼容易理解。LAG也學習了不下三次,在重新翻閱「實用Common Lisp」之後,有更深程度的理解。今天先來快速的寫下重點筆記。
狀況系統(Condition System)
狀況系統遠比錯誤系統來的更通用,他能夠很輕易實現錯誤處理系統的Try-Catch,更可以處理許多複雜情況。
狀態系統主要分成三個層次:
- 狀況引發、發出信號(Signal)
- 重啓案例(Restart Cases)
- 狀況處理(Handle Condition)
分別對應於低中高的分成架構。在狀況引發後,提供幾個可選的重啓選項,教有高層次的程式邏輯決定處理方式。
模擬C++類似的Try-Catch
絕大多數程式語言提供的錯誤處理以Try-Catch、Try-Exception。ECMAScript的錯誤處理起初也就真的是針對錯誤(Error)處理。
儘管預期外(Exception)與錯誤(Error)概念上有所區別。 但是,在多數程式語言中,錯誤處理由呼叫的程式階段進行,其中子階段的堆疊狀態已經展開、釋放,處理完錯誤後,不易回到錯誤發生階段。來看看Common Lisp怎麼處理這種情況。
簡單的Try-Catch
Common Lisp也有Try-Throw-Catch。可以很簡單的實現Try-Catch的邏輯。不過Throw丟出的是一個符號,相較於狀態,沒有類別系統。
我們先建立一個會出錯的函式,使丟出一個訊息div-by-zero
。
注意:在throw之後的程式片段都是多於的。在丟出throw之後不會執行,與C++的錯誤處理相同。
(defun div (a b)
(format t "~& in div(a b) function ~%")
(if (/= b 0)
(/ a b)
(throw 'div-by-zero 0))
(format t "~& Finish div(a b) and return (/ a b) ~%")
(/ a b))
接著一個呼叫,並處理該錯誤的程式:
(defun run-div (a b)
(format t "~& in run-div(a b) function ~%")
(catch 'div-by-zero
(format t "~& run-div result: ~A ~%" (div a b))
(format t "~& After catch ~%")))
如果呼叫(run-div 1 2)
,所有format
會被正確執行並輸出。但是如果呼叫(run-div 1 0)
,將會得到以下結果:
in run-div(a b) function
in div(a b) function
不過實際上catch
仍然收到了thrwo丟出的返回值 (0
),其可以當成catch body
程式碼的預設返回值供更上層
程式使用。相比許多程式語言 try-catch 作為控制結構的敘述語句有很大的不同。
使用狀況系統
同樣的例子,不過這次要先定義狀況 (通常實做版本已經有一個相關狀況division-by-zero
可以使用) :
(define-condition div-by-zero (error)
((message :initarg :message :type string :initform "Can not div by zero." :accessor message))
(:report "Can Not Div By Zero"))
類似的div
函式:
(defun div (a b)
(format t "~& in div(a b) function ~%")
(if (/= b 0)
(/ a b)
(error 'div-by-zero :message "(div a b) <= b can not be zero. "))
(format t "~& Finish div(a b) and return (/ a b) ~%")
(/ a b))
現在要很簡單的處理div-by-zero
的狀況:
(defun run-div (a b)
(format t "~& in run-div(a b) function ~%")
(handler-case (progn (format t "~& run-div result: ~A ~%" (div a b))
(format t "~& After catch ~%"))
(div-by-zero (c) (format t "~& Happend div-by-zero: ~A ~%" (message c)))))
handler-case
可以處理更為複雜的情況,而不僅僅是返回預設值而已。另外,載配合上unwind-protect
,可以輕易實現Try-Catch-Finlly的語句結構。
(unwind-protect (run-div 1 0)
(format t "~& Finally Clean ~%"))
error之外
除了使用(error 'div-by-zero :message "Message")
引發狀況外,也可以簡單的使用(error "Message")
引發Simple-Error
,並且Common Lisp還提供以下方式:
type | decribe |
---|---|
error | 引發錯誤狀態,並進入除錯器。 |
warn | 引發警告狀態,顯示警告(warning類別) ,並繼續執行程式。 |
cerror | 可繼續錯誤。引發錯誤,並進入除錯器選擇 |
break | 引發中斷,並進入除錯器。並且無法被任何handler處理。 |
這些都有更底層引發信號的函式:(signal condition)
。
重啓案例
到目前為止,狀態系統與錯誤系統沒有什麼太大的不同。不過接著就不太一樣了。
作為底層的程式,div
會比高層次的程式清楚如何處理狀態。像是回傳值(use-value)
、修改值(store-value)
、直接繼續程式(continue)
等等。接著,該讓div
提供幾個回覆(重啓)案例,供更高層次的程式選擇策略。
我們來提供回傳值(use-value)
、修改值(store-value)
等重啓方案。 不過要注意的是,因為隨後有在div
之中執行一次(/ a b)
,故回傳值的策略仍然會在其他地方引發錯誤(基於範例原因才保留無需的程式碼) 。可以透過將restart-case
包覆更大範圍的程式以解決相關錯誤,實現類似Try-Catch的邏輯。不過同樣基於範例原因,修改為以下程式片段:
(defun div (a b)
(format t "~& in div(a b) function ~%")
(if (/= b 0)
(/ a b)
(restart-case (error 'div-by-zero :message "(div a b) <= b can not be zero. ")
(use-value (other-value)
:report "Use Other Value Replace B Variable."
:interactive (lambda nil
(format t "Input Other Value to Return: ")
(finish-output)
(multiple-value-list (read)))
other-value)
(store-value (new-value)
:report "Set New Value to B Variable"
:interactive (lambda nil
(format t "Input New Value to set B: ")
(finish-output)
(multiple-value-list (read)))
(setf b new-value))))
(format t "~& Finish div(a b) and return (/ a b) ~%")
(/ a b))
同樣來改造run-div
。試著使用store-value
的重啓方案,並繼續子程序。為了回復到子程序,必須使用更低階的handler-bind
來處理狀況。
(defun run-div (a b)
(format t "~& in run-div(a b) function ~%")
(handler-bind
((div-by-zero (lambda (condition)
(warn "~&Happend Condition `DIV-BY-ZERO, and Use restart-case `store-value` if existed.
~&Condition Message: ~A.~%" (message condition))
(let ((restart (find-restart 'store-value)))
(invoke-restart restart 20)))))
(format t "~& run-div result: ~A ~%" (div a b))
(format t "~& After catch ~%")))
如此以來,所有format
都會被正確輸出處理。
Case vs Bind
就像上面看到的, handler 有handler-case
和handler-bind
, restart 也有restart-case
和restart-bind
。基本上,case
是bind
的更高階用法。在 sbcl 透過macroexpand-1
展開(restart-case (div 1 0) (case1 (v) v) (case2 (v) v))
和(handler-case (div 1 0) (div-by-zero () 0))
可能會分別得到以下結果:
(BLOCK #:BLOCK614
(LET ((#:G615 NIL))
(DECLARE (IGNORABLE #:G615))
(TAGBODY
(RESTART-BIND ((CASE1
(LAMBDA (SB-IMPL::TEMP)
(SETQ #:G615 SB-IMPL::TEMP)
(LOCALLY
(DECLARE (OPTIMIZE (SAFETY 0)))
(GO #:TAG616))))
(CASE2
(LAMBDA (SB-IMPL::TEMP)
(SETQ #:G615 SB-IMPL::TEMP)
(LOCALLY
(DECLARE (OPTIMIZE (SAFETY 0)))
(GO #:TAG617)))))
(RETURN-FROM #:BLOCK614 (DIV 1 0)))
#:TAG616
(RETURN-FROM #:BLOCK614 (FUNCALL (LAMBDA (V) (PROGN V)) #:G615))
#:TAG617
(RETURN-FROM #:BLOCK614 (FUNCALL (LAMBDA (V) (PROGN V)) #:G615)))))
(SB-INT:DX-FLET ((#:FORM-FUN-622 ()
(PROGN (DIV 1 0)))
(#:FUN619 ()
(PROGN 0)))
(DECLARE (OPTIMIZE (SB-C::CHECK-TAG-EXISTENCE 0)))
(BLOCK #:BLOCK620
(SB-INT:DX-LET ((#:CELL621 (CONS :CONDITION NIL)))
(DECLARE (IGNORABLE #:CELL621))
(TAGBODY
(SB-IMPL::%HANDLER-BIND
((DIV-BY-ZERO
(LAMBDA (SB-IMPL::TEMP)
(DECLARE (IGNORE SB-IMPL::TEMP))
(GO #:TAG618))))
(RETURN-FROM #:BLOCK620 (#:FORM-FUN-622)))
#:TAG618
(RETURN-FROM #:BLOCK620 (#:FUN619))))))
可以看到分別再調用了restart-bind
和handler-bind
。並且存在著block
區塊與return-from
區塊。這也是為什麼handler-bind
能夠回到子程序,而hander-case
不行,因為後者返回了控制權,就如同C++中的Try-Catch一樣。
