今回は Scheme の「例外 (exception) 処理」について説明します。一般に、例外はエラー処理で使われる機能です。「例外=エラー処理」と考えてもらってもかまいません。最近は例外処理を持っているプログラミング言語が多くなりました。
Scheme の場合、昔の仕様書 (R5RS) には例外処理の規定がありませんでした。このため、例外処理は Scheme のライブラリ (SRFI-34 など) で実装されていたのですが、R6RS と R7RS-samll では例外処理が規定されています。今回は例外処理の基本的な使い方を簡単に説明します。
たとえば、1 から 100 までの 2 乗と 3 乗の値をファイルに書き込む処理を考えてみましょう。例外処理のないプログラミング言語、たとえばC言語でプログラムする場合、ファイルをオープンする処理、データを書き込む処理、ファイルをクローズする処理でエラーが発生していないかチェックする必要があります。ところが、Scheme でプログラムすると次のようになります。
リスト : データの出力 (import (scheme base) (scheme write) (scheme file)) (call-with-output-file "output.txt" (lambda (oport) (do ((n 1 (+ n 1))) ((> n 100)) (display n oport) (display ", " oport) (display (* n n) oport) (display ", " oport) (display (* n n n) oport) (display "\n" oport))))
エラーをチェックする処理がありません。これは例外処理が働いて、エラーが発生したらプログラムの実行が中断されるからです。例外処理のおかげで、プログラムをすっきりと書くことができます。
なお、エラーが発生したことを「例外が発生した」とか「例外が送出された」という場合もあります。本稿でもエラーのことを例外と記述することにします。
ところで、例外が発生するたびに実行を中断するのではなく、致命的な例外でなければプログラムの実行を継続したい場合もあるでしょう。このような場合にこそ、例外処理が役に立つのです。
Scheme で発生した例外を捕まえる場合、guard を使うと簡単です。guard の構文を下図に示します。
(guard (variable clause1 clause2 ... clauseN) body) 図 : guard の構文
guard は body の S 式を評価して、その結果を返します。Gauche の場合、body に複数の S 式を記述することができます。body を評価しているときにエラーが送出されると、エラーオブジェクト (エラーを表す値) を変数 vairable に束縛して、節 clause を順番にチェックします。clause は cond の節と同じ形式です。
guard は cond と同様に条件部 test が真を返す節を選択します。そして、その節の S 式を順番に評価して、最後の S 式の評価結果を返します。2 はまだ説明してませんが、通常の cond でも使用できる形式です。test が真の場合、その結果を関数 proc に渡して評価します。3 は cond と同じ形式です。
簡単な使用例を示します。
gosh[r7rs.user]> (guard (exc (else exc)) 1 2 3) 3 gosh[r7rs.user]> (guard (exc (else exc)) (/ 2 0)) #<error "attempt to calculate a divisio">
最初の例はエラーが送出されないので、最後に評価した 3 が返されます。次の例は 0 で除算したのでエラーが送出され、それを guard で捕捉しています。変数 exc にはエラーオブジェクトがセットされ、その節の S 式が評価されます。この場合は変数 exc にセットされたエラーオブジェクトを返しているだけです。
例外の送出は関数 error を使うと簡単です。
error message obj ...
error は引数のエラーメッセージ message と残りの引数 obj ... (イリタント) を格納したエラーオブジェクトを生成して例外を送出します。エラーオブジェクトは述語 error-object? で判定することができます。また、エラーメッセージとイリタントは以下の関数で取得することができます。
error-object? obj error-object-message err-obj error-object-irritants err-obj
簡単な使用例を示しましょう。
gosh[r7rs.user]> (guard (exc ((error-object? exc) exc)) (error "oops!" 1 2 3)) #<error "oops! 1 2 3"> gosh[r7rs.user]> (guard (exc ((error-object? exc) (error-object-message exc))) (error "oops!" 1 2 3)) "oops!" gosh[r7rs.user]> (guard (exc ((error-object? exc) (error-object-irritants exc))) (error "oops!" 1 2 3)) (1 2 3) gosh[r7rs.user]> (guard (exc ((error-object? exc) (error-object-irritants exc))) (error "oops!")) ()
関数 raise はエラーオブジェクトを生成しないで、引数の obj を使って例外を送出します。
raise obj
引数 obj は何でもかまいません。簡単な例を示します。
gosh[r7rs.user]> (guard (exc (else exc)) (raise 1)) 1 gosh[r7rs.user]> (guard (exc (else exc)) (raise 'foo)) foo gosh[r7rs.user]> (guard (exc (else exc)) (raise "oops")) "oops"
自分で特別な例外を表すレコード型を定義して、それを raise で送出することもできます。
gosh[r7rs.user]> (define-record-type Myerror (make-myerror message args) myerror? (message get-message) (args get-args)) get-args gosh[r7rs.user]> (guard (exc ((myerror? exc) exc)) (raise (make-myerror "oops" 123))) #<Myerror 0x7f10498c0e80> gosh[r7rs.user]> (guard (exc ((myerror? exc) (get-message exc))) (raise (make-myerror "oops" 123))) "oops" gosh[r7rs.user]> (guard (exc ((myerror? exc) (get-args exc))) (raise (make-myerror "oops" 123))) 123
file-error? obj read-error? obj
file-error? と read-error? はエラー型を判定する述語です。ファイルをオープンできないときやライブラリ (scheme file) の関数 delete-file でファイルを削除できないとき、捕捉したエラーオブジェクトに file-error? を適用すると #t を返します。関数 read でデータを読み込むときに例外が送出された場合、捕捉したエラーオブジェクトに read-error? を適用すると #t を返します。
簡単な使用例を示しましょう。
gosh[r7rs.user]> (open-input-file "test3.txt") *** SYSTEM-ERROR: couldn't open input file: "test3.txt": ・・・省略・・・ ・・・省略・・・ gosh[r7rs.user]> (guard (exc ((file-error? exc) exc)) (open-input-file "test3.txt")) #<system-error "couldn't open input file: "tes"> gosh[r7rs.user]> (define iport (open-input-string "(a b c")) iport gosh[r7rs.user]> (read iport) *** READ-ERROR: Read error at "(input string port)":line 1: ・・・省略・・・ Stack Trace: ・・・省略・・・ gosh[r7rs.user]> (define iport (open-input-string "(a b c")) iport gosh[r7rs.user]> (guard (exc ((read-error? exc) exc)) (read iport)) #<read-error "Read error at "(input string p">
ファイル test3.txt が存在しない場合、open-input-file で例外が送出されますが、そのエラーオブジェクトは file-error? で判定することができます。文字列ポートから read でデータを読み込みますが、リストの右カッコがないため例外が送出されます。この場合は read-error? で判定することができます。
例外は guard だけではなく with-exception-handler でも捕捉することができます。
with-exception-handler handler thunk
引数 handler は引数を一つ受け取る関数で、thunk は引数を受け取らない関数です。with-execption-handler は thunk を評価して、その結果を返します。thunk を評価しているときに例外が送出されると handler に制御が移ります。このとき、handler にはエラーオブジェクトまたは raise の引数が渡されます。
例外を捕捉したとき、guard は節 clause の評価結果を返しますが、with-exception-handler は handler の評価が終了してもその結果を返しません。さらに上位の例外処理に制御が移るのです。次の例を見てください。
gosh[r7rs.user]> (with-exception-handler (lambda (x) (display x) (newline)) (lambda () (error "oops!"))) #<error "oops!"> *** ERROR: oops! Stack Trace: ・・・省略・・・
エラーオブジェクトを表示したあと、上位の例外処理がないので例外は捕捉されず、REPL でエラーが表示されます。この連鎖を止めるには、継続を使って with-exception-handler から脱出する必要があります。
gosh[r7rs.user]> (call/cc (lambda (cont) (with-exception-handler (lambda (x) (display x) (newline) (cont #t)) (lambda () (error "oops!"))))) #<error "oops!"> #t
上の例では、継続 cont で with-exception-handler から脱出しているので、REPL でエラーは表示されずに、継続 cont の返り値 #f が表示されます。
関数 raise-continuable を使うと、例外を送出したあと handler の返り値を使って実行を再開することができます。ただし、使い方が少々難しいようなので、ここでは簡単な例を示すだけにとどめます。詳細は R7RS-small や Gauche のマニュアルをお読みください。
gosh[r7rs.user]> (with-exception-handler (lambda (x) (if (string? x) (string->number x) (error "oops!"))) (lambda () (+ 10 (raise-continuable "123")))) 133 gosh[r7rs.user]> (with-exception-handler (lambda (x) (if (string? x) (string->number x) (error "oops!"))) (lambda () (+ 10 (raise-continuable 'foo)))) *** ERROR: oops! Stack Trace: ・・・省略・・・