Lisp/基本からさらに一歩進んで/Condition System

Common Lisp は非常に発達したコンディションシステムを備えています。このコンディションシステムでは、例外的状態、あるいは、プログラマに定義されたプログラムの通常の処理から外れた状況の処理を可能とします。例外的状況の一般的な例は例えばエラーです。しかし Common Lisp のコンディションシステムではエラーハンドリング以上のものを含んでいます。

コンディションシステムは3つの部分に分けることが出来、その3つは通知、あるいは連絡コンディション、ハンドラーコンディション、そして備えられているそれらのコンディションからの回復方法です。ほとんど全ての最近のプログラミング言語では、最初の二つのプロトコルは提供されています。しかし最後のものを提供しているものはほとんどありません。(あるいは最後の2つは区別されます。)この最後のプロトコルが再起動か、あるいはプログラムの回復の手段を準備します。これは Common Lisp のコンディションハンドリングの最も重要な側面のうちの一つです。

再起動 編集

再起動は例外的な状態から復帰するための手段です。例外的状況はしばしばエラーとしてですが、いつでも起こります。もし REPL でこの本に沿って操作していたのなら、間違いなく少なくとも一回はデバッガセッションに陥ったはずです。デバッガは重大な状態が起こったときや、 Lisp システムがほかに頼る人もいないので、あなたに何をすべきか尋ねるために呼び出されます。デバッガはその状況から回復するためのいくつかの選択肢のリストを与えてくれます。これらの選択肢が複数の種類の再起動で、しばしばデバッガの提供する再起動だけが REPL のトップレベルに戻るものですが、時には実行をそのまま続けたり、あるいは計算を再試行したりするのも可能となります。加えて、そのほかの再起動をあなた自身で定義することも出来ます。

例えば、ファイルからデータを読み込もうとしているとします。そこには相当数の間違いを犯しえます。例えば、ファイルそのものが存在しないかもしれません。あるいはあなたはファイルの読み込みのために十分な権限を持っていないかもしれません。あるいはファイルの中身のデータが破損しているかもしれません。これらの出来事はしばしば例外状況として考えられるものです。そしてそれぞれの状況にはいくつもの方法で対処しえます。ファイルが存在しない、と言うケースでは、他のファイルの名前を指定したくなるでしょうし、ファイルの読み込みのための十分な権限がない場合では、他のファイルの名前を指定するか、読み込み可能なようにファイルの権限を変更したいと考えるでしょう。データが破損しているケースでは、新しいファイルの名前を指定したくなるか、破損したデータを意味のある方法で処理したくなるでしょう。あるいはファイルの修復と再読み込みさえ試みるかもしれません。

再起動を利用する際のあなたの役目は、利用可能な復帰メカニズムとコンディションシステムの再起動の適切さを確認することです。例題を充実させるために、あなたは何行ものデータが入ったファイルを読んでいるとしましょう。それぞれの行にはスペースで区切られた (x, y) 座標の list が含まれているとします。例えばそのファイルは以下のようなものになるでしょう。

0 0 100 150 50 30
30 20 65 65 10 20
0 100 150 50 30 0

まずは、このファイルを読みこむ関数を書き出してみましょう。以下のようなものです。

(defun read-points-file (filename)
  (iter (for line in-file filename using #'read-line)
        (collecting
         (iter (for val in-stream (make-string-input-stream line))
               (collect val) ))))

では次に、関数でファイルを呼び出したときにファイルがなかった場合にどうなるかを見てみましょう(この例では SBCL を使用しています。)

(read-points-file #p"does-not-exist.data")

error opening #P"does-not-exist.data":
  No such file or directory
   [Condition of type SB-INT:SIMPLE-FILE-ERROR]

Restarts:
 0: [RETRY] Retry SLIME REPL evaluation request.
 1: [ABORT] Return to SLIME's top level.
 2: [TERMINATE-THREAD] Terminate this thread (#<THREAD "repl-thread" RUNNING {BA80BC1}>)

Backtrace:
  0: (SB-IMPL::SIMPLE-FILE-PERROR "error opening ~S" #P"does-not-exist.data" 2)
  1: ((LABELS SB-IMPL::VANILLA-OPEN-ERROR))
  2: (OPEN #P"does-not-exist.data")[:EXTERNAL]
  3: (READ-POINTS-FILE #P"does-not-exist.data")

...etc...

SBCL がエラーを吐き出すのを見てみました。処理系はこの後どうすればいいかわからないため、ユーザーにどうするかを尋ねています。最初のものは再起動の手段のリストです。 RETRY, ABORT, そして TERMINATE-THREAD です。もしこのあと does-not-exist.data というファイルを作成したら RETRY で再起動するのが良いでしょう。もしファイルが存在していても権限のためファイルが読めない、というのでしたら、ほとんど同じ結果ですが、 "Again" というメッセージの代わりに "Permission denied" というエラーメッセージを受け取るでしょう。ファイルを修正し、 RETRY で再起動を行うことが出来ます。

ユーザーが間違ったファイル名を入力することもあるかもしれませんから、そうした場合に読み込みのためにファイルが開けないのは賢明なことです。こうした点を考慮して、開こうとするファイルの名前を変更して再起動する方法を提供するようにしてみましょう。これは restart-case フォームかあるいは、より一般的な restart-bind で実現できます。

(defun prompt-for-new-file ()
  (list (prompt "Input new file name: ")) )

(defun read-points-file (filename)
  (restart-case
      (iter (for line in-file filename using #'read-line)
            (collecting
             (iter (for val in-stream (make-string-input-stream line))
                   (collect val) )))
    (try-different-file (filename)
      :interactive prompt-for-new-file
      (read-points-file filename) )))

このようにファイルが読み込めないときのためにデバッガを入れると TRY-DIFFERENT-FILE という特別なオプションを得ることになります。

(read-points-file #p"does-not-exist.data")

error opening #P"does-not-exist.data":
  No such file or directory
   [Condition of type SB-INT:SIMPLE-FILE-ERROR]

Restarts:
 0: [TRY-DIFFERENT-FILE] TRY-DIFFERENT-FILE
 1: [RETRY] Retry SLIME REPL evaluation request.
 2: [ABORT] Return to SLIME's top level.
 3: [TERMINATE-THREAD] Terminate this thread (#<THREAD "repl-thread" RUNNING {BA80BC1}>)

Backtrace:
  0: (SB-IMPL::SIMPLE-FILE-PERROR "error opening ~S" #P"does-not-exist.data" 2)
  1: ((LABELS SB-IMPL::VANILLA-OPEN-ERROR))
  2: (OPEN #P"does-not-exist.data")[:EXTERNAL]
  3: (READ-POINTS-FILE #P"does-not-exist.data")
...etc...

Input new file name: #p"does-exist.data"

==> ((0 0 100 150 50 30) (30 20 65 65 10 20) (0 100 150 50 30 0))

restart-case フォームは最初の引数を再起動が有効な環境で実行します。これは TRY-DIFFERENT-FILE のような特別な再起動が最初のフォームの実行時にはいつでも呼び出せることを意味します。今回の例ではフォームの実行時にデバッガが呼び出されます。これは再起動がとりうる動作のリストに含まれているということです。

コンディション 編集

コンディションは、プログラマがメインプログラムの流れの制御をしたくない状況の説明に利用されます。上の小さな例題ではコンディションを定義はしませんでしたが、コンディションは示されています。このコンディションはファイルを開こうとして出来なかったときに立ち上がるエラーのことです。このエラーは sb-int:simple-file-error という型で Lisp システムに組み込まれているものです。

あなた自身が自分のコンディションを define-condition マクロによって定義することが出来ます。 define-condition マクロは defclass マクロと非常によく似ています。

ハンドラー 編集

ハンドラーはコンディションを再起動する試みのシンプルな目的にかなうものです。これはつまり、もしそのコンディションが起動すると、特定の再起動が自動的に選ばれ、我々がデバッガにダンプ出力せずに、再起動が呼び出されるということを意味します。

ハンドラーのインスタンスを生成するフォームは handler-case と、より一般的な handler-bind です。

ありふれたエラーハンドリング 編集

Common Lisp では、ほとんどのほかの言語にも見られるような挙動を真似して作られたエラーハンドリングメカニズムのセットを提供しています。