確実に後処理を行なう


Tags: R6RS, 例外処理, 制御構造, マクロ

ファイルを開いた場合やネットワークに接続した場合、例外が発生した場合でもその後処理を確実に行ないたいことがある。そのような場合、 with-exception-handler を使い例外ハンドラの内部でも後処理を行なうようにしておけばよい。

(let ((p (open-file-input-port "foo")))
  (with-exception-handler
      (lambda (e)
        (cond ((non-continuable-violation? e)
               (close-port p)
               (raise e))
              (else
               (raise-continuable e))))
    (lambda ()
      (let-values ((v (some-procedure p)))
        (close-port p)
        (apply values v)))))

これは定型的な処理なのでマクロ化しておくとよい。

(define-syntax unwind-protect
  ((_ body cleanup ...)
   (with-exception-handler
       (lambda (e)
         (cond ((non-continuable-violation? e)
                cleanup
                ...
                (raise e))
               (else
                (raise-continuable e))))
     (lambda ()
       (let-values ((v body))
         cleanup
         ...
         (apply values v))))))

すると次のように書ける。

(let ((p (open-file-input-port "foo")))
  (unwind-protect
      (begin (some-procedure p))
    (close-port p)))

ただし、ポートに関する処理について言えば call-with-port を使った方がよい。

(call-with-port (open-file-input-port "foo")
  (lambda (p)
    (some-procedure p)))

またちなみに、 raise によって起動される例外ハンドラは raise を呼び出した場所での動的存続期間で起動されるため、 dynamic-wind によって例外に対応することはできない。

;; XXX: うまく動かない
(let ((p (open-file-input-port "foo")))
  (dynamic-wind
    (lambda () #f)
    (lambda () (some-procedure p))
    (lambda () (close-port p))))

dynamic-windで後始末をすることのもうひとつの問題点は、 call/ccdynamic-windの組み合わせは dynamic-windの中に再び戻ってくるような用途にも使われるということだ (例えばユーザレベルのコルーチンやスレッドなど)。dynamic-windの after-thunkで不可逆的な後始末をしてしまうと、再びbody-thunkに 戻って処理を続行することが出来なくなってしまう。 dynamic-windのbefore/after thunkの処理は動的環境の一貫性を 保つこと(例えばbody thunkにおけるactive exception handlerのすげ替えなど)に 限定し、後始末などは上位の機構であるwith-exception-handlerやguardに 任せるのが良い。