Condition System #1 handler-case と handler-bind の違い

${XYZZY}/lisp/handler.l 参照。

handler-bind

(handler-bind ((CONDITION-TYPE 'HANDLER-FN))
  FORM*)

FORM* を実行中に CONDITION-TYPE 型のコンディションが投げられると、HANDLER-FN に引数として投げられたコンディションを与えて呼び出す。
handler-bind がやることはそんだけで、エラーから回復とかさせるのは tagbody と go だとか block と return-from だとかで制御を飛ばす必要がある。飛ばさなかったら HANDLER-FN の戻り値は捨てられて、コンディションがさらに上(?)へ飛んでいく

最初の式は展開するとこうなる。

(let ((si:*condition-handlers*
          (cons (list (cons 'CONDITION-TYPE 'HANDLER-FN))
                system:*condition-handlers*)))
  FORM*)

要するに si:*condition-handlers* に指定された CONDITION-TYPE と HANDLER-FN の組み合わせを追加して FORM* を評価する。

FORM* を評価中にエラーを投げると、その場で si:*condition-handlers* の中から投げられたエラーが typep な CONDITION-TYPE を探して、組み合わされた HANDLER-FN にそのエラーを与えて呼び出す。つまりエラーを投げる関数がやってる事はこんな感じ。

;;; この関数はあくまでイメージです
(defun error (datum &rest args)
  (let ((condition
            ;ホントは引数によって変わる
            (apply #'make-condition datum args)))
    (funcall (cdr
              ;; ホントは si:*condition-handlers* はフラットなリストじゃ
              ;; ないので単なる find ではない
              (find condition si:*condition-handlers*
                    :test (lambda (condition type/fn)
                            (typep condition (car type/fn)))))
             condition)))

だもんで、HANDLER-FN はエラーが投げられたときのダイナミック環境の中で呼び出される。

TODO: もうちょっとかみ砕いてリファレンスに追加

handler-case

(handler-case
      EXPRESSION
  (CONDITION-TYPE (ARG)
    FORM*))

大雑把にいうとこんなコードに展開される。

(block :handler-case
  (let ((signaled-condition))
    (tagbody
      (handler-bind ((CONDITION-TYPE (lambda (condition)
                                       (setq signaled-condition condition)
                                       (go :handler))))
        (return-from :handler-case
          EXPRESSION))
      :handler
      (return-from :handler-case
        (let ((ARG signaled-condition))
          FORM*)))))

EXPRESSION で CONDITION-TYPE のエラーが投げられたときに何が起きるかというと、handler-bind が指定してる HANDLER-FN(lambda 式の関数)にそのエラーを渡して呼び出す。この関数は (go :handler) で制御を飛ばす。
んで :handler から実行は進んで let 式の中で FORM* を実行した結果を return-from で :handler-case、つまり handler-case 式の戻り値として返す。

両者の違い

まず handler-bind はエラーの伝搬を止めたりしないが、handler-case は止めてくれる。

それと handler-case でエラー処理をする FORM* は handler-bind の HANDLER-FN とは別物。handler-case の FORM* はエラーが投げられたときのダイナミック環境から go で脱出してから実行されるので、その環境を使うことができない。
このせいで再起動を使うには handler-bind を使わないといけない。くっそう。