例外その1

10章のうち、後半の例外について。

まず、他の言語との概念の違いについて。Schemeには例外に関係する用語として以下のようなものがある。

  • 例外:例外的な事象のこと。
  • コンディション:例外が発生した時に作成される、例外の状況を記述するためのオブジェクト。

Scheme以外の言語ではこれらを区別しないこともあり、まとめて例外ということもある。

guard構文

guard構文とは、例外が発生したときに投げられたコンディションを捕捉し、その例外に対処するコードを書くしくみ。他の言語ではtryブロック、catchブロックといったところだろうか。使い方はこう。

gosh> (guard (e
              (else e))
        (car 1))
#<error "pair required, but got 1">

tryブロック内に相当するところが(car 1)。つまり、例外が発生するかもしれない処理。carにはペアを渡さないといけないので、ここでは必ず例外が発生する。で、例外が発生しコンディションが投げられたら、そのコンディションで名前eを束縛し、elseを評価する。elseはcondでのelseと同じ働きをし、常に真となる値なので、guard構文の値としてはeとなる。もう一つguardの使用例を挙げると、

gosh> (guard (e
              (#f "expected not to be evaluated.")
              (#f "may not be evaluated.")
              (#f "supposed not to be evaluated.")
              (else "must to be evaluated."))
        (car 1))
"must to be evaluated."

まず、(car 1)で例外が発生しコンディションが作成され、eを束縛し、以降の式をcondと同じ要領で評価していって、elseのあとの"must to be evaluated."がguard式全体の値となる。

さて、elseだけ書いておけば全ての種類のコンディションを捕捉できる。しかし、発生した例外応じて事後処理は変わってくるのが普通だろう。どんな例外が発生したのかはコンディションの型で分かるので、以下のように型を調べて、自分が捕捉したいコンディションのみを捕まえられる。

gosh> (guard (e
              ((<io-error> e) "io error")
              ((<read-error> e) "read error")
              (error "other error"))
        (read))
)             ; ←read式での入力。コッカだけを入力し、意図的に例外を発生させる。
"read error"  ; ←guard式の値

readは、入力された文字列を元に構文解析するプリミティブな手続き。構文解析に失敗するとという型のコンディションが投げられる(のかな?詳しくは知らん)。はコンディションの型であると同時に、引数で指定されたオブジェクトがそのコンディション型を持つかどうかの述語としても利用できるらしい。これを利用して、型を調べて自分が対処したい例外にのみ対処することができる。ここではreadできなかった例外(コンディションが投げられた時)および、入出力に関する例外(コンディションが投げられた時)に対処している。どちらでもない例外はelseで処理している。

こんな感じかな?

try { read(); }
catch(IoError e) { return "io error"; }
catch(ReadError e) { return "read error"; }
catch { return "other error"; }

文字列を返すとか意味分からんけど。

raise手続き

すべての例外を捕捉してしまうと、意図しない例外も握りつぶしてしまうのでよろしくない。例外の後処理のみして例外が発生した旨を呼び出し元に伝搬させたい場合、つまり、以下のようなことをしたい場合にはどうすればいいだろうか?

try { DoSomething(); }
catch (SomeException e)
{
  PostOperation();
  throw;
}

補足したコンディションをそのまま投げるにはraise手続きを使える。ってことで、次のようにかける(かも)。

(guard (e
        ((<some-error> e) (post-operation) (raise e)))
  (do-something))

非常に手続き型言語臭いコードだなぁ、おい。

ちなみにC#と対比しているけど、筆者であるyagieyは

try { DoSomething(); }
catch (SomeException e) { throw; }

try { DoSomething(); }
catch (SomeException e) { throw e; }

の違いをまだ理解していません。はい。

error手続き

raise手続きは引数として指定されたコンディションを投げて、現在の処理を中断するけど、error手続きを使えばコンディションの作成とraise手続きの実行をまとめてやってくれる。こんな感じ。

gosh> ((lambda (lis)
         (if (pair? lis)
             lis
             (error "the procedure needs a pair, but got: " lis)))
       1)
*** ERROR: the procedure needs a pair, but got:  1
Stack Trace:
_______________________________________

本に倣って引数を2個渡しているけど、詳しい仕様は分からない。ドキュメントを読むべし。さて、errorで作成されるコンディションの型は何だろう?

gosh> (guard (e
              (else e))
        (error "hoge"))
#<error "hoge">

ということで、という型らしい。

本によると、error手続きは投げるコンディションの型を指定できる形式もあるらしい。...けどキーワードとか使い方がよくわからん。自分で調べろっちゅうことかな。

今のところ、分からないこと

例外ハンドラ

何度か「例外ハンドラ」という言葉が出てきたけど、具体的にどれのことだろう?

(guard (e
        (<テスト1> <式1> ...)
        (<テスト2> <式2> ...)
        (<テスト3> <式3> ...)
        ...)
  <本体> ...)

(e (<テスト1> <式1> ...) (<テスト2> <式2> ...) (<テスト3> <式3> ...) ...)がハンドラ?
(<テスト1> <式1> ...)がハンドラ?<式1> ... がハンドラ?