error-kit解説

clojure.contrib.error-kit*1を使うと、Common Lispのconditionに似たエラー処理ができるようになる。
Clojureのエラー処理では、Javaのtry/throw/catchをそのまま使うが、error-kitのwith-handle/raise/handle/bind-continue/continue*2を使う。

bind-continueとcontinueにより、処理を再開*3させることができるため、より柔軟なエラー処理が可能である。

まずはただの例外のように使ってみる

Javaのtry/throw/catchは、error-kitではwith-handler/raise/handleに対応する。
厳密にはいろいろ違う部分が多いが、使う用途は同じ。

  • throw -> raise
  • try -> with-handler
  • catch -> handle
(ns errkit-sample
  (:use
    [clojure.contrib.error-kit]))

(deferror not-even-error [] [x]
  {:msg (str "not even numner: " x)})

(defn half [x]
  (if (zero? (mod x 2))
    (quot x 2)
    (raise not-even-error x))) ;ここで上げたerrorが・・・↓

(defn half-numbers [xs]
  (map half xs))

(defn main []
  (with-handler
    (reduce + (half-numbers [2 4 6 8 9 10]))
    (handle not-even-error [x]                ;ここでhandleされる。
      (println "Not Even: " x))))
実行結果
=> (main)
Not Even:  9
nil

普通の例外のような処理だ。

再開をしてみる

ここで、再開(continue)を使ってみよう。
handleの値としてcontinueを返すと、もう一度スタックの奥の(bind-continueした位置)に戻って再開できる。

(ns errkit-sample
  (:use
    [clojure.contrib.error-kit]))

(deferror not-even-error [] [x]
  {:msg (str "not even numner: " x)})

(defn half [x]
  (with-handler                     ;(A)
    (if (zero? (mod x 2))
      (quot x 2)
      (raise not-even-error x))     ;(1)エラーが上げられ・・・↓
    (bind-continue use-value [x]    ;(4)ここから再開。
      x)))                          ;(5)with-handler(A)全体の値が0になって返る。

(defn half-numbers [xs]
  (map half xs))

(defn main []
  (with-handler
    (reduce + (half-numbers [2 4 6 8 9 10]))
    (handle not-even-error [x]               ;(2)ここでhandleされる。
      (println "Not Even: " x)
      (continue use-value 0))))        ;(3)ここで引数0を渡して再開・・・↑
実行結果
=> (main)
Not Even:  9
15

例外とerror-kitの仕組みの違い

例外(try/throw/catch)では、catchした時点でスタックが巻き戻っている。throwした時点のスタックの情報は、Throwableオブジェクトの中にしか残っていない。
これに対して、error-kitのhandleが呼び出された時点では、スタックは巻き戻っていない。
with-handlerはあくまでもスレッドローカルのリストにhandleやbind-continueの中身を関数として登録しているだけで、raiseやcontinueはそれを呼び出している。

再開処理で変わるエラー設計

上記の例をtry/throw/catchで作るには、エラー発生時に例外を投げる場合と値を置き換える場合の両方を実装するには下記のいずれかの方法をとる必要がある。

  • (A)呼び出し元からフラグ変数を渡す。
  • (B)別個の関数として定義する。
  • (C)関数の外でフラグ変数をdefし、bindingで囲う。

(A)や(B)の方法では、half-numbersのような中間の関数にまで影響する。中間の関数にパラメータを追加したり、二つに分割したりしなければならない。(C)の方法ではエラーの定義が関数内に閉じず、別個のグローバルなシンボルを使う必要があるため、リファレンスマニュアルが汚れてわかりにくくなる。

error-kitのように再開処理が抽象化されていれば、関数のエラー処理の定義の仕方は下記のように変わる。

例外処理での関数のエラー仕様
  • XXXException: XXXがおきたときにthrowされる。
    • パラメータ
      • XXXException/getAaaで、例外発生時のAaaの値を得ることができる。
error-kitでの関数のエラー仕様
  • xxx-error: xxxが起きた時にraiseされる。
    • パラメータ
      • 第1パラメータには、エラーraise時のaaaの値が代入されている。
    • 再開
      • ハンドラ内で(continue vvv a b c)を呼び出すことにより、UUUの値をWWWに置き換えることができる。

いつ使うべきか

再開を使わないのならtry/throw/catchで特に問題はない。tryは3文字だが、with-handlerは12文字もある:-p

try/throw/catchとerror-kitの相互の変換も考慮されており、エラーがハンドルされなかった時にはException型*4が投げられるため、再開が必要な場所のみで使えばいいと思う。

*1:公式にはEXPERIMENTALとあり、APIは今後変わるかもしれないので注意。

*2:ここでいうcontinueは、call/ccのcontinuationとは何の関係も無い。

*3:continueを「再開」と訳すのはあまりよくないかもしれないが、他に良い訳語が思いつかなかった。ちなみに、Practical CommonLispでは「再起動」と言っている。

*4:例外の型はdeferrorの:unhandledでカスタマイズできる。