天天看點

Racket程式設計指南——10 異常與控制

10 異常與控制

Racket提供了一組特别豐富的控制操作——不僅是用于提高和捕捉異常的操作,還包括抓取和恢複計算部分的操作。

    10.1 異常
    10.2 提示和中止
    10.3 延續

10.1 異常

每當發生運作時錯誤時,就會引發異常(exception)。除非捕獲異常,然後通過列印與異常相關聯的消息來處理,然後從計算中逃逸。

> (/ 1 0)
/: division by zero
> (car 17)
car: contract violation
  expected: pair?
  given: 17

若要捕獲異常,請使用with-handlers表:

(with-handlers ([predicate-expr handler-expr] ...)
  body ...+)

在處理器中的每個predicate-expr确定一種異常,它由with-handlers表捕獲,代表異常的值傳遞給處理器程式由handler-expr生成。handler-expr的結果即with-handlers表達式的結果。

例如,零做除數錯誤建立了exn:fail:contract:divide-by-zero結構類型:

> (with-handlers ([exn:fail:contract:divide-by-zero?
                   (lambda (exn) +inf.0)])
    (/ 1 0))
+inf.0
> (with-handlers ([exn:fail:contract:divide-by-zero?
                   (lambda (exn) +inf.0)])
    (car 17))
car: contract violation
  expected: pair?
  given: 17

error函數是引起異常的一種方法。它打包一個錯誤資訊和其它資訊進入exn:fail結構:

> (error "crash!")
crash!
> (with-handlers ([exn:fail? (lambda (exn) 'air-bag)])
    (error "crash!"))
'air-bag

exn:fail:contract:divide-by-zero和exn:fail結構類型是exn結構類型的子類型。核心表和核心函數引起的異常總是建立exn的或其子類的一個執行個體,但異常不必通過結構表示。raise函數允許你建立任何值作為異常:

> (raise 2)
uncaught exception: 2
> (with-handlers ([(lambda (v) (equal? v 2)) (lambda (v) 'two)])
    (raise 2))
'two
> (with-handlers ([(lambda (v) (equal? v 2)) (lambda (v) 'two)])
    (/ 1 0))
/: division by zero

在一個with-handlers表裡的多個predicate-expr讓你在不同的途徑處理各種不同的異常。判斷按順序進行嘗試,如果沒有比對,則将異常傳播到封閉上下文中。

> (define (always-fail n)
    (with-handlers ([even? (lambda (v) 'even)]
                    [positive? (lambda (v) 'positive)])
      (raise n)))
> (always-fail 2)
'even
> (always-fail 3)
'positive
> (always-fail -3)
uncaught exception: -3
> (with-handlers ([negative? (lambda (v) 'negative)])
   (always-fail -3))
'negative

使用(lambda (v) #t)作為判斷捕獲所有異常,當然:

> (with-handlers ([(lambda (v) #t) (lambda (v) 'oops)])
    (car 17))
'oops

然而,捕獲所有異常通常是個壞主意。如果使用者在一個終端視窗鍵入Ctl-C或者在DrRacket點選停止按鈕(Stop)中斷計算,那麼通常exn:break異常不會被捕獲。僅僅會抓取具有代表性的錯誤,使用exn:fail?作為判斷:

> (with-handlers ([exn:fail? (lambda (v) 'oops)])
    (car 17))
'oops
> (with-handlers ([exn:fail? (lambda (v) 'oops)])
    (break-thread (current-thread)) ; simulate Ctl-C
    (car 17))
user break

10.2 提示和中止

當一個異常被引發時,控制将從一個任意深度的求值上下文逃逸到異常被捕獲的位置——或者如果沒有捕捉到異常,那麼所有的出路都會消失:

> (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (/ 1 0)))))))
/: division by zero

但如果控制逃逸“所有的出路”,為什麼REPL在一個錯誤被列印之後能夠繼續運作?你可能會認為這是因為REPL把每一個互動封裝進了with-handlers表裡,它抓取了所有的異常,但這确實不是原因。

實際的原因是,REPL用一個提示(prompt)封裝了互動,有效地用一個逃逸位置标記求值上下文。如果一個異常沒有被捕獲,那麼關于異常的資訊被列印,然後求值中止(aborts)到最近的封閉提示。更确切地說,每個提示有提示标簽(prompt tag),并有指定的預設提示标簽(default prompt tag),未捕獲的異常處理程式用來中止。

call-with-continuation-prompt函數用一個給定的提示标簽設定提示,然後在提示符下對一個給定的铛(thunk)求值。default-continuation-prompt-tag函數傳回預設提示标記。abort-current-continuation函數轉義到具有給定提示标簽的最近的封閉提示符。

> (define (escape v)
    (abort-current-continuation
     (default-continuation-prompt-tag)
     (lambda () v)))
> (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (escape 0)))))))
> (+ 1
     (call-with-continuation-prompt
      (lambda ()
        (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (escape 0))))))))
      (default-continuation-prompt-tag)))
1

在上面的escape中,值v被封裝在一個過程中,該過程在轉義到封閉提示符後被調用。

提示(prompts)和中止(aborts)看起來非常像異常處理和引發。事實上,提示和中止本質上是一種更原始的異常形式,與with-handlers和raise都是按提示執行和中止。更原始形式的權力與操作符名稱中的“延續(continuation)”一詞有關,我們将在下一節中讨論。

10.3 延續

延續(continuation)是一個值,該值封裝了表達式的求值上下文。call-with-composable-continuation函數從目前函數調用和運作到最近的外圍提示捕獲目前延續(current continuation)。(記住,每個REPL互動都是隐含地封裝在一個提示中。)

例如,在下面内容裡

(+ 1 (+ 1 (+ 1 0)))

在求值0的位置,表達式上下文包含三個嵌套的加法表達式。我們可以通過更改0來擷取上下文,然後在傳回0之前擷取延續:

> (define saved-k #f)
> (define (save-it!)
    (call-with-composable-continuation
     (lambda (k) ; k is the captured continuation
       (set! saved-k k)
       0)))
> (+ 1 (+ 1 (+ 1 (save-it!))))
3

儲存在save-k中的延續封裝程式上下文(+ 1 (+ 1 (+ 1 ?))),?代表插入結果值的位置——因為在save-it!被調用時這是表達式上下文。延續被封裝進而其行為類似于函數(lambda (v) (+ 1 (+ 1 (+ 1 v)))):

> (saved-k 0)
3
> (saved-k 10)
13
> (saved-k (saved-k 0))
6

通過call-with-composable-continuation捕獲的延續是動态确定的,沒有文法。例如,用

> (define (sum n)
    (if (zero? n)
        (save-it!)
        (+ n (sum (sub1 n)))))
> (sum 5)
15

在saved-k裡延續成為(lambda (x) (+ 5 (+ 4 (+ 3 (+ 2 (+ 1 x)))))):

> (saved-k 0)
15
> (saved-k 10)
25

在Racket(或Scheme)中較傳統的延續運算符是call-with-current-continuation,它通常縮寫為call/cc。這是像call-with-composable-continuation,但應用捕獲的延續在還原儲存的延續前首先中止(對于目前提示)。此外,Scheme系統傳統上支援程式啟動時的單個提示符,而不是通過call-with-continuation-prompt允許新提示。在Racket中延續有時被稱為分隔的延續(delimited continuations),因為一個程式可以引入新定義的提示,并且作為call-with-composable-continuation捕獲的延續有時被稱為組合的延續(composable continuations),因為他們沒有一個内置的中止。

作為一個延續是多麼有用的例子,請參見《 更多:用Racket進行系統程式設計(More: Systems Programming with Racket)》。對于具體的控制操作符,它有比這裡描述的原語更恰當的名字,請參見racket/control部分。

繼續閱讀