概要
ANSI Common Lispの第9章「コンディション」を説明します。Common Lispのコンディションシステムは非常に発達した例外処理機構として設計されています。一般的な言語に見られる
throw
- catch
のようなシンプルな例外処理だけでなく、複雑なフローコントロールも可能であり、デバッガなどのCommon Lispの基本システムともよく統合されています。
コンディションシステムの全貌を体系的に説明することはとても難しいので、このページではコンディションシステムの仕様と実践を様々な観点からサンプル中心に示します。
REPLとコンディション
Common Lispが極めて「動的な言語」として利用可能であるのは、REPL ( Read-Eval-Print Loop )とデバッガを備えている点です。実はこれらの機能の基盤としてコンディションシステムが使われており、安全にREPLを使うことができるようにしているのです。試しに、Common Lisp処理系で以下のように打ち込んでみます。
(+ 1 "2")
; *** - +: "2" is not a number
; The following restarts are available:
; USE-VALUE :R1 Input a value to be used instead.
; ABORT :R2 Abort main loop
この式では
+
という加算関数に1
と"2"
という2つの引数を渡して評価しようとしていますが、"2"
は数ではなく、加算できないため、エラーになっています。
関数の設計と例外への対応
「エラー」と述べましたが、これは仕様に定められた動作です。ANSI Common Lisp仕様の+
関数の項には以下のように定められています。
- Description:
- Returns the sum of numbers, performing any necessary type conversions in the process. If no numbers are supplied, 0 is returned.
- Exceptional Situations:
- Might signal type-error if some argument is not a number. Might signal arithmetic-error.
+
の場合は「引数が数でない場合にtype-error
が通知される可能性があること、arithmetic-error
の可能性もあること」が例外への対応として記述されています。つまり、"2"
を引数として与えた時に発生したエラーは、+
関数に組み込まれた例外処理だったのです。
このように関数は本来の目的に加え、例外的な状況に対処するような方法を備えていなければなりません。関数の内部で対処できればそれは本体仕様になりますが、関数本体で対処できず、関数の呼出し元に異常を知らせるだけでも十分な対応です。つまり、安全な関数は以下の機能を持つことが要求されます。
- 関数本体の処理が記述されていること。想定される入力(引数やデータの読み込み)に対して、正しい出力(返り値やプリント)を行うことができるような計算のプロセス。
- 例外的状況の発生を通知すること。想定されていない入力(型異常、ファイル不存在、通信切断など)が発生し、そのままでは処理を継続できない状態に陥った場合に、そのことを適切に関数の呼出し元に伝えること。
- 例外的状況への対処方法を提示すること。単純に処理を中断してプログラムを終了するしか方法がない場合は「終了する」という対処を提示し、それ以外の回復方法がある場合は、それを呼出し元に提示して選んでもらうこと。
最後の砦: デバッガ
関数の内部で発生した異常事態(例外的状況)を関数の呼出し元に伝え、可能であれば対処方法も合わせて提示することが安全な関数の設計に重要であることを述べましたが、「呼出し元」とは何でしょうか。例えば、以下の式の場合、外側の式が内側の式を「呼んでいる」ことになります。
(* (+ 1 2) 3)
; => 9
Common Lispのプログラムは関数呼び出しの連鎖として設計されます。そのため、関数の実行中には必ず呼出し元が存在します。関数の呼出し元は、呼出した関数の計算結果を待っている状態であり、計算結果(返り値)を得ると残りの計算を続けます。このように、ある関数を呼出し中(評価中)の時、残っている計算を「継続(continuation)」と言います。
しかし、関数には呼出し元が常に存在することと、その呼出し元の関数が通知された異常事態(例外的状況)にうまく対処できることは別の問題です。これから呼び出す関数において異常が発生する可能性があることを想定していない呼出し元は、結局自分では例外的状況に対処できず、その関数自身もまた異常事態に陥ることになります。つまり、例外は遡及します。
このように例外が呼出し元を遡及していく時、最後の砦になるのが「デバッガ」です。例外にうまく対処できない場合は、最終手段としてデバッガが立ち上がり、プログラムの予期せぬ終了や暴走を防ぎます。デバッガは最後の砦として異常事態の中身をユーザーにレポートし、対処方法が存在する場合はそれを提示します。存在しない場合はプログラムの終了(トップレベルへの復帰)を選択肢として提示します。
もう一度、加算関数のエラーの例を掲載します。
(+ 1 "2")
; *** - +: "2" is not a number
; The following restarts are available:
; USE-VALUE :R1 Input a value to be used instead.
; ABORT :R2 Abort main loop
これは、
+
関数内部の異常を制御できる関数が存在しないため、デバッガが最後の砦として立ち上がったことを示します。(今回は+
関数がトップレベルから呼ばれているため、そもそも呼出し元もREPL自身です。)
このメッセージは4行ですが、以下のようなことを説明しています。
-
例外の内容の表示:
+
関数において、"2"
は数ではない、という異常を知らせてくれます。 -
対処方法の提示: 2つの対処が可能であることを提示してくれます。
-
:R1
:use-value
により、代わりに使う値を入力するという対処方法 -
:R2
:abort
により、メインループ(トップレベル)に戻るという対処方法
-
+
関数で単に型異常を発生させただけであるにも関わらず、Common Lispのデバッガは様々な働きをしてくれます。デバッガは例外をハンドリングする最後の砦であることを覚えておいてください。
回復: use-value
とabort
次は、「対処方法の提示」で示されている2つの方法に注目してください。このような「対処方法」はANSI Common Lispで Restart と呼ばれ、標準化されています。再起動や回復などと訳される場合もありますが、訳語はどのようなものでも構いません。
ANSI Common Lispの標準仕様で定められている Restart は5種類ありますが、いずれも同名の関数としても定義されており、使いやすくなっています。ここでは
+
の例で提示されている2種類を説明します。
- Restart:
abort
- トップレベルにまで戻るという対処方法です。マルチプロセスなどの場合は、プロセスをKILLすることなども含まれます。基本的に全てのコードが
abort
機能付で実行されるので、プログラムのどのような場所からでもabort
で戻ることができます。 - Restart:
use-value
- 異常が発生した値を新しい値で置き換えて、異常が発生した場所に回復するという対処方法です。新しい値は1度しか使うことができません。デバッガで対話的にこの対処方法を選択すると新しい値を入力することができます。
use-value
からです。
(+ 1 "2")
; *** - +: "2" is not a number
; The following restarts are available:
; USE-VALUE :R1 Input a value to be used instead.
; ABORT :R2 Abort main loop
:r1 ;; ここで :r1(use-value) を選択している
; Use instead> 2 ;; 代わりの新しい値として 2 を入力した
; => 3 ;; 正しい値である 3 を得ることができた
use-value
が新しい値の入力を受け付け、その値を使って計算を続けていることが分かると思います。では次はabort
です。
(+ 1 "2")
; *** - +: "2" is not a number
; The following restarts are available:
; USE-VALUE :R1 Input a value to be used instead.
; ABORT :R2 Abort main loop
:r2 ;; ここで :r2(abort) を選択している
;; トップレベルに戻る(返り値はない)
abort
を使うと残りの計算をすっ飛ばしてトップレベルにまで戻っています。このように残りの計算をすっ飛ばすことを「継続の破棄」と言います。abort
は継続の破棄を行う Restart です。例外の捕捉(ハンドリング)
さて、最も単純なREPLでの処理においても様々な例外対処機構があることが分かったと思いますが、すでに説明した通り、デバッガは「最後の砦」ですから、そこに到るまでに異常事態へ対処できればベストです。異常事態に対処するには、まずはその異常事態発生通知を受け取らなければなりません。部下から報告を受ける時、対処方法を考える前に部下の話を聞くのが先です。例外も同じように、どのような異常なのかを適切に聞き分けなればなりません。
このように、異常事態発生通知を聞くことを「ハンドリング」すると言います。ANSI Common Lispでは3つの「ハンドラー」(
handler-case
, handler-bind
, ignore-errors
)が定められており、このハンドラーで例外をハンドリングします。ハンドリングはthrow
スペシャルオペレータに対するcatch
スペシャルオペレータに似ていることから、「捕捉」するとも表現されます。
handler-case
マクロ
最も手軽なハンドラーはhandler-case
マクロです。ここでは、handler-case
を使って、型異常の際はエラーの通知ではなくnil
を返すという関数を定義してみます。
(defun my-add (x y)
(handler-case (+ x y)
(type-error (condition)
(declare (ignore condition))
nil)))
; => MY-ADD
(my-add 1 2)
; => 3
(my-add 1 "2")
; => NIL
下の実行例ではエラーが発生せず、デバッガも起動していないことに注目してください。静かに
NIL
を返すだけです。
handler-case
マクロはcase
マクロと同じように記述しますが、構文は以下のようになります。
(handler-case expression
(typespec1 (var) declaration1* form1*)
(typespec2 (var) declaration2* form2*)
:
:
(:no-error (vars*) declaration-last form-last*))
expression の部分に評価する式を記述します。これは1つだけなので、複数式の場合は
progn
で括ります。
typespec は型指定子です。
typecase
マクロと同様に型でマッチングされますが、マッチングされるのは expression の評価結果ではなく、その評価過程で発生した例外の型です。
var には発生した例外を束縛する変数名を記述します。
declaration には各種宣言を記述します。今回のように捕捉した例外を使わない場合は
declare
シンボルでignore
しなければコンパイル時に警告が表示されます。(ただし、省略形を使うこともできます。本節最後の参考を参照してください。)
form は例外発生時の処理です。今回は
type-error
という型でマッチングさせて、nil
を返すので、nil
を form にしていますが、こちらは暗黙的にprogn
されるので複数式をそのまま記述することができます。
最後に
:no-error
という型はcase
マクロのotherwise
シンボルと同様に、例外が発生しなかった場合の処理を記述します。
handler-case
マクロで例外を捕捉した場合、さらに上の関数には例外の発生通知が伝播しなくなります。つまり、handler-case
は例外の遡及を止めます。
「例外」とは何か: コンディション
ところで、これまで「例外」や「異常事態」と述べてきた状態は何者なのでしょうか。ANSI Common Lispでは「例外」はクラスを用いて実装されます。つまり、「例外」とは
condition
クラスを継承して定義されたサブクラスのインスタンスです。ANSI Common Lispでは「例外」という言葉は Exceptional Situation 、つまり例外的状況を指す言葉として用いられ、その状況において通知され、捕捉(ハンドリング)される実体は「コンディション」と呼びます。
前節の
handler-case
マクロは捕捉したコンディションを利用することができます。my-add
関数を以下のように再定義すると、例外が発生した時にそのコンディションそのものを返すことができます。
(defun my-add (x y)
(handler-case (+ x y)
(type-error (condition)
condition)))
; => MY-ADD
(my-add 1 "2")
; => #<SIMPLE-TYPE-ERROR #x00000002003C0B91>
ここでは
simple-type-error
クラスのインスタンスであることが分かります(処理系によって異なる可能性がありますが、type-error
かそのサブクラスであることは確実です)。
コンディションはインスタンスなので、アクセッサでスロットにアクセスすることができます。
type-error
クラスは以下の2つのアクセッサを標準で用意しています。
- Function:
type-error-datum
datum
スロットを読み取ります。このスロットには例外を発生させた問題のデータが入っています。- Function:
type-error-expected-type
expected-type
スロットを読み取ります。このスロットには期待される正しい型の情報が入っています。
my-add
関数を以下のように再定義してみます。例外が発生した時は、コンディションの中にある上の2つのスロットの値を多値として返します。
(defun my-add (x y)
(handler-case (+ x y)
(type-error (condition)
(values (type-error-datum condition)
(type-error-expected-type condition)))))
; => MY-ADD
(my-add 1 "2")
; => "2"
; NUMBER
+
関数を使用した場合のエラーメッセージは以下の通りでしたから、コンディションのスロットの値を使ってメッセージを表示していることになります。
(+ 1 "2")
; *** - +: "2" is not a number
simple-condition
のスロット
simple-type-error
クラスはtype-error
クラスとsimple-condition
クラスを継承しています。simple-condition
クラスには2つのスロットがあり、以下のアクセッサで読み取ることができます。
- Function:
simple-condition-format-control
format-control
スロットを読み取ります。このスロットにはメッセージを表示するためのフォーマット文字列が入っています。- Function:
simple-condition-format-arguments
format-arguments
スロットを読み取ります。このスロットにはメッセージの表示に必要な引数が入っています。
"~S: ~S is not a number"
でしたから、エラーメッセージは具体的に以下のように表示していることになります。
(defun my-add (x y)
(handler-case (+ x y)
(type-error (condition)
(let ((args (simple-condition-format-arguments condition)))
(format t (simple-condition-format-control condition)
(first args)
(second args))))))
; => MY-ADD
(my-add 1 "2")
; +: "2" is not a number
; => NIL
+
は異常事態が発生した関数のシンボル、"2"
は異常事態が発生したデータです。このように見ると、CLISPではexpected-type
のスロットに入っているNUMBER
ではなく、format-control
スロットの"is not a number"
という文章がそのまま使われていることが分かります。
+
関数は仕様ではtype-error
を通知することが求められていますが、そのサブクラスであるsimple-type-error
までは求められていないので、このようなスロットが使えるかどうかは処理系依存ですので、参考程度に覚えておいてください。
参考: handler-case
の省略形
handler-case
マクロはとても便利なオペレータで、例外的状況が発生するとコンディションを捕捉し、適切な処理を施して脱出してくれます。上の例では、type-error
が発生した際にnil
を返すという処理をしています。
前掲のサンプルではハンドラーの中で束縛したコンディションを
ignore
を使って破棄していますが、コンディションを使わないことは結構よくあります。また、例外発生時に返り値としてnil
を返すこともよくあります。
そのため、
handler-case
には2つの省略スタイルが認められています。
- コンディションの束縛を記述しない省略形。
- ハンドリング処理を記述しない省略形。
type-error
発生時にnil
を返すmy-add
関数は以下のように定義することができます。
(defun my-add (x y)
(handler-case (+ x y)
(type-error ())))
; => MY-ADD
(my-add 1 "2")
; => NIL
こちらの方がシンプルですので、省略形を使える時は積極的に利用してください。
デバッガを超えて(Restartの起動)
さて、今までに見てきたことを一旦総括すると、以下のようになります。- Common Lispの関数には例外的状況の通知が組み込まれていて、コンディションシステムをそのまま使うことができる。
- 例外の通知は伝播(遡及)する。最後まで遡及した場合、「最後の砦」であるデバッガが受け止める。
- コンディションの実体はクラスのインスタンス。スロットにもアクセスできる。
-
handler-case
マクロを使うとデバッガよりも手前でコンディションを捕捉(ハンドリング)することができる。 -
handler-case
マクロは例外の遡及を止める。つまり、ハンドリングされるとそのハンドリング処理の結果が全体の結果となる。
handler-case
マクロは非常に便利なのですが、デバッガよりも手前でハンドリングしたいという要求以上に多様なフロー制御をしたい場合は使うことができません。それはどのような場合かというと、一番最初に示した Restart を使う場合です。
デバッガはエラーの表示と対処方法の提示を行い、
use-value
やabort
などを選択すれば対処方法を対話的に変更することができます。特にuse-value
のような処理は、例外が発生した箇所に制御を戻すという処理ですが、handler-case
マクロにおけるハンドリング処理の結果は最終的な評価結果として返り値になってしまうため、元の箇所に戻して処理を継続することはできません。つまり、handler-case
マクロは、「例外が発生する直前時点での継続」を破棄して、選択された「ハンドリング処理」という新しい継続を構築して計算するということを意味します。
Common Lispは人工衛星や軍事戦略システム、航空機運行管理システムなどの「ミッションクリティカル」な分野で使われているため、エラー(異常事態)が発生したからといって、「もう、さようなら」とプログラムを終了するわけにはいかないことがあります。何らかの対処方法を提示して、何とか困難を切り抜けようとするための手段が Restart です。
handler-case
マクロはそのような目的で使うことができないのです。
この節では Restart の起動について、デバッガで選択するという処理を超えた部分を説明します。
invoke-debugger
関数
Restart (対処方法)の利用は3つのステップに分けられます。
- Restart そのものを確立し、アクティブな状態にしておく。
- 異常事態が発生した時に、利用可能な Restart を提示する。
- 選択された Restart を評価し、異常事態に対処する。
invoke-debugger
です。
例えば、今までに何度も使ってきた
my-add
関数で、+
関数のエラー通知(コンディション)をhandler-case
マクロで捕捉しつつ、そのコンディションに基づいてデバッガを起動するには以下のように定義します。
(defun my-add (x y)
(handler-case (+ x y)
(type-error (condition)
(invoke-debugger condition))))
; => MY-ADD
(my-add 1 "2")
; *** - +: "2" is not a number
; The following restarts are available:
; ABORT :R1 Abort main loop
明示的にデバッガを起動することができました。
継続を破棄しないhandler-bind
マクロ
しかし、Restart の選択肢が少ないことに注目してください。REPLで直接エラーを発生させた時はuse-value
という Restart が提示され、それを選択すると実際に処理を継続することができたのですが、今回は単に処理を終えるabort
という Restart しか提示されていません。
これが、前節で述べた「例外が発生する直前時点での継続」が破棄されているという状況です。エラーの原因である
"2"
に到る直前の継続とは、以下のような計算であると考えられます。
(lambda (x) (+ 1 x))
"2"
の部分がx
に変わっていますが、このx
に正しい型(すなわち数)のデータが来ればこの継続が適切に計算されるはずでした。use-value
という Restart は"2"
に変わる新しい値をこの継続の引数として適用する、という対処方法であると言えます。
このように、例外が発生する直前時点での継続を破棄せず、Restart の処理においてその継続を使いたいという場合は
handler-bind
マクロを使います。
実は
handler-case
マクロはhandler-bind
マクロをベースに実装されているのですが、ハンドリングの処理を行いやすいようにフローの制御も自動で行うようになっています。フローの制御とは何度も説明している通り、以下のような流れです。
- 例外が発生した場合、その時点での継続は破棄する。
- 型がマッチするハンドリング処理にジャンプ。
-
ハンドリング処理の結果を
handler-case
全体の返り値としてreturn-from
する。
handler-bind
マクロはもっとシンプルで、継続を破棄したり、自動でreturn-from
による脱出を行ったりすることはありません。必要なフロー制御は自分で行う必要がある反面、「動的環境」を伴って処理を継続することができます。動的環境を伴うということは、例外が発生した時の状況がハンドリング処理においても続けられている(参照可能である)、ということを意味します。ハンドリング処理においては Restart を起動することで、残っていた継続を再び計算することができるのです。
handler-bind
マクロでデバッガを明示的に起動するようなmy-add
関数は以下のような定義になります。
(defun my-add (x y)
(handler-bind
((type-error
#'(lambda (condition)
(invoke-debugger condition))))
(+ x y)))
; => MY-ADD
(my-add 1 "2")
; *** - +: "2" is not a number
; The following restarts are available:
; USE-VALUE :R1 Input a value to be used instead.
; ABORT :R2 Abort main loop
:r1
; Use instead> 2
; => 3
REPLに直接打ち込んだ時と同じように、
use-value
という Restart が使用できることが分かります。
ここでは明示的にデバッガを起動し、その中で Restart を使用していますが、Restart を使う場合は
handler-case
ではなくhandler-bind
を使うようにしましょう。逆に Restart を使わない場合は、handler-case
の方が簡潔に記述できます。
なお、
handler-bind
マクロの構文は極めてシンプルです。
(handler-bind
((type1 handler-function1)
(type2 handler-function2)
:
:
(type-last handler-function-last))
forms)
forms は暗黙的に
progn
されるため、複数の式をそのまま記述します。handler-function はdefun
で定義された関数か、lambda
による無名関数で、いずれも関数の実体が必要です。handler-bind
には「例外が発生した場合にはハンドリング処理関数を評価する」ということ以上のフロー制御が組み込まれていないため、通常は handler-function の中でフロー制御を行います。そして、その代表的な制御はこの節で使ったinvoke-debugger
関数ではなく、invoke-restart
関数です。
invoke-restart
関数
Common Lispにおいてデバッガは最後の砦であり、invoke-debugger
関数を使わなくても自動的に起動します。そのため、デバッガそのものをオリジナルに実装する場合を除き、あまり使われることはありません。
handler-bind
との組み合わせで威力を発揮するのは、invoke-restart
関数です。この関数を使えば、デバッガを起動せずに Restart を直接起動することができます。
先にサンプルを示しましょう。
(defun my-add (x y)
(handler-bind
((type-error
#'(lambda (condition)
(let ((v (type-error-datum condition)))
(if (typep v 'string)
(invoke-restart 'use-value (read-from-string v))
(invoke-debugger condition))))))
(+ x y)))
; => MY-ADD
(my-add 1 2) ;; 数の場合は普通に計算(例外処理は無関係)
; => 3
(my-add 1 "2") ;; 文字列の場合は数に変換して Restart
; => 3
(my-add 1 'two) ;; 数でも文字列でもない場合はデバッガを起動
; *** - +: TWO is not a number
; The following restarts are available:
; USE-VALUE :R1 Input a value to be used instead.
; ABORT :R2 Abort main loop
:r1 ;; use-value を選択して Restart
; Use instead> 2 ;; 'two の代わりに 2 を使うので入力
; => 3 ;; 正しく計算できた
invoke-restart
関数は第1引数にアクティブな Restart を識別できるシンボルを取ります。すでに説明した通り、handler-bind
マクロは例外が発生した時の動的環境をハンドリング処理関数でも使うことができるので、use-value
という Restart を指定しています。
今までの流れで
"2"
がエラーとなることを想定していたので、エラーとなったデータが文字列の場合は、一度read-from-string
関数で数に変換してからuse-value
( Restart )を起動しています。より一層安全性を確保するなら、read-from-string
したデータをそのままuse-value
に渡すのではなく、そのデータが数であることをtypep
で確認してから渡した方がいいかもしれません。(例えば"two"
というデータを想定してみてください。)
もちろん、エラーとなる原因のデータは文字列であるとは限らないので、
if
スペシャルオペレータの else 部分ではデバッガを起動しています。すると今まで通りデバッガが起動し、利用可能な再起動を提示してくれるので、use-value
を選んで新しい値を入力すれば正しい計算結果を得ることができます。
しかし、実はANSI Common Lispのコンディションシステムはもう1歩自動化できます。どこだと思いますか。
invoke-restart-interactively
関数
答えはエラーの原因が文字列ではない場合の「起動したデバッガでuse-value
を選択する」という動作です。
この
my-add
関数は、何としても加算処理を完遂させたいと考えています。計算が最後まで終わっていないのに途中でabort
するという選択肢は極力避けたいとします。そのため、以下のような仕様を想定します。
- 引数が数の場合: 普通に加算する
-
引数が文字列の場合:
-
文字列を
read-from-string
すると数になる場合: 自動的にread-from-string
して加算する
-
文字列を
- 上記以外の場合: 代わりの値を入力してもらい、加算する
use-value
であることは分かると思いますが、デバッガの中でuse-value
を選択することをユーザーに期待するのではなく、例外のハンドリングの中で自動的に選択するようにするのです。これがもう1歩の自動化です。
invoke-restart
関数は Restart に引数を与えてそのまま起動する関数ですが、Restart に与える引数を対話的に入力してもらうことができる関数がinvoke-restart-interactively
です。
この仕様に合わせたサンプルを以下に示します。
(defun my-add (x y)
(handler-bind
((type-error
#'(lambda (condition)
(let ((v (type-error-datum condition)))
(when (typep v 'string)
(setq v (read-from-string v)))
(if (typep v 'number)
(invoke-restart 'use-value v)
(invoke-restart-interactively 'use-value))))))
(+ x y)))
; => MY-ADD
(my-add 1 2) ;; 引数が数の場合: そのまま加算
; => 3
(my-add 1 "2") ;; 引数が文字列で数に変換できる場合: 変換して加算
; => 3
(my-add 1 "two") ;; 引数が文字列だが数に変換できない場合:
; Use instead> 2 ;; 代わりの値を入力
; => 3 ;; 代わりの値で加算
(my-add 1 'two) ;; その他のデータ型の場合も同じ
; Use instead> 2
; => 3
invoke-restart-interactively
関数を使うことで、デバッガを起動させずに対話的な入力環境を利用することができることが分かると思います。ここでは Use instead としか表示されないので多少不親切ですが、それでもかなり「例外的状況に強い」関数になったと思います。
invoke-restart
関数やinvoke-restart-interactiveluy
関数を用いて Restart を自在に操ることで、デバッガに依存せずに例外的状況へ対処することができます。ANSI Common Lispのコンディションシステムが他言語の例外処理機構と根本的に異なるのも、この Restart が独立して操作可能になっているからです。ANSI Common Lispのミッションクリティカルな分野への執念とも言えるような粘り強さを感じることができるのではないかと思います。
ちなみに、ANSI Common Lisp標準の
+
関数は可変長引数ですが、ここで定義したオリジナルのmy-add
関数も簡単に可変長に対応することができます。reduce
という高階関数を使い、my-add
をラップするようなmy+
関数を定義します。すると、すぐに可変長引数にも対応します。
(defun my+ (&rest args)
(reduce #'my-add args))
; => MY+
(my+ 1 "2" 3 "4" 5 "6" 7 "8" 9)
; => 45
例外の通知(シグナリング)
ここまでの節で扱ったトピックは、大きくまとめると以下の4つです。- コンディションの概要( Condition )
- 例外の補足( Handling )
- 回復方法の起動( Invoke Restarts )
- デバッガの利用( Debugging )
- 例外の通知( Signaling )
- 回復方法の確立( Establish Restarts )
- 例外の定義( Define Condition )
様々な通知関数
ANSI Common Lispでは例外を通知する関数が4つ定められています。- Function:
error
- エラーを通知します。ハンドリングされない場合は、自動的に
invoke-debugger
関数を使ってデバッガを起動するため、処理が中断します。通知されるコンディションは標準ではsimple-error
クラスのインスタンスです。 - Function:
warn
- 警告を通知します。ハンドリングされない場合は、コンディションの内容を表示します。返り値は
nil
です。通知されるコンディションは標準ではsimple-warning
クラスのインスタンスです。 - Function:
signal
- シグナルを通知します。ハンドリングされない場合でも、特に特別な介入動作は行いませんので、ハンドリングすることを前提に使われます。返り値は
nil
です。通知されるコンディションは標準ではsimple-condition
クラスのインスタンスです。 - Function:
cerror
continue
(Restart)付でエラーを通知します。continue
は標準のRestartの一つで、エラーが発生した場所に単純に復帰します。
動作 | error | warn | signal | cerror |
---|---|---|---|---|
デバッガの起動 | Yes | No | No | Yes |
内容の表示 | Yes | Yes | No | Yes |
返り値 | None | nil | nil | nil |
continue の有無 | No | No | No | Yes |
標準のcondition | simple-error | simple-warning | simple-condition | simple-error |
error
には明示的な返り値がありませんが、cerror
はnil
が返り値となります。
通知関数で通知できるもの
通知関数は前節で説明したような違いがありますが、構文はどれも同じで、第1引数に通知するデータ、第2引数以降は可変長引数で引数に対する引数になります(cerror
のみ異なりますので、次節で説明します)。これらのパターンは3種類あります。
-
データがシンボルの場合
- シンボルに対応するコンディションを作成し、可変長引数は作成時の引数として適用されます。
-
データが文字列の場合
-
標準のコンディションを作成し、
format-control
スロットに第1引数を、format-arguments
スロットに可変長引数をセットします。
-
標準のコンディションを作成し、
-
データがコンディションの場合
- 新しいコンディションは作成せず、コンディションそのものを通知します。この場合は可変長引数(第2引数以降)を指定することはできません。
'double-float
型ではない場合に警告を表示します。
(defun check-double-float (v)
(unless (typep v 'double-float)
(warn 'simple-warning
:format-control "~a is not the type of double-float."
:format-arguments (list v))))
; => CHECK-DOUBLE-FLOAT
(check-double-float 1.0)
; WARNING: 1.0 is not the type of double-float.
; => NIL
'simple-warning
というシンボルで同名のクラスを基礎とするコンディションのインスタンスを作ることができます。残りの部分はwarn
関数のキーワード引数ではなく、可変長引数としてまとめてリストにされ、コンディション作成時に使われます。
次は文字列です。エラーや警告のメッセージを作成するなら、文字列が便利です。
(defun check-double-float (v)
(unless (typep v 'double-float)
(warn "~a is not the type of double-float." v)))
; => CHECK-DOUBLE-FLOAT
(check-double-float 1.0)
; WARNING: 1.0 is not the type of double-float.
; => NIL
第2引数以降は可変長引数の機能で自動的にリストになりますので、リストとして渡さず引数として渡します。
最後はコンディションそのものです。
(defun check-double-float (v)
(unless (typep v 'double-float)
(warn (make-condition
'simple-warning
:format-control "~a is not the type of double-float."
:format-arguments (list v)))))
; => CHECK-DOUBLE-FLOAT
(check-double-float 1.0)
; WARNING: 1.0 is not the type of double-float.
; => NIL
make-condition
関数はコンディションを作成します。CLOS (Common Lisp Object System)におけるmake-instance
総称関数と同じ構文ですが、こちらはコンディションの作成専用です。シンボルや文字列の場合は、自動的にmake-condition
関数を適用していることになります。
ということで、この節の冒頭では「通知するデータは3種類」と述べたのですが、実は通知されるものは全てコンディションなのです。シンボルや文字列はコンディションの作り方が違うだけで、通知される実体はコンディションです。以下のように使い分けてください。
- 通知したいコンディションが標準のものでいい場合は、文字列を使用してください。
- 通知したいコンディションが標準のものではないが、スロットの初期化は必要ない場合は、シンボルを使用してください。
-
通知したいコンディションが標準のものではなく、スロットの初期化も行う場合は、
make-condition
関数を使用してコンディションそのものを渡してください。
cerror
関数とcontinue
error
, warn
, signal
の各通知関数はデバッガの起動やメッセージの表示が違うので分かりやすいと思いますが、cerror
関数に付いているcontinue
は説明が必要でしょう。
continue
は標準で用意されている Restart の一つで、単純にエラー発生場所にフロー制御を戻すというものです。
- Restart:
continue
- コンディションが通知された場所に制御を戻すという対処方法です。コンディションが通知されたのに何もせずに制御を戻すことはあまりないので、このRestartを使う場合は制御を戻す前に何らかの副作用的処理を行うことが一般的です。
continue
を使うと関数の処理本体とエラーハンドリングの間を行き来することができるので、例えばFizzBuzz問題を型とコンディションシステムで解くこともできます。
;;;; サンプル: FizzBuzz Problem Solving with Type and Condition System
(defun mod3p (v) (zerop (mod v 3)))
(defun mod5p (v) (zerop (mod v 5)))
(deftype mod3 () `(and integer (satisfies mod3p)))
(deftype mod5 () `(and integer (satisfies mod5p)))
(deftype mod15 () `(and (satisfies mod3p) (satisfies mod5p)))
(defun check-fizzbuzz (v)
(typecase v
(mod15 (cerror "" "FizzBuzz"))
(mod3 (cerror "" "Fizz"))
(mod5 (cerror "" "Buzz"))
(otherwise (cerror "" (write-to-string v)))))
(defun fizzbuzz (n)
(handler-bind
((simple-condition
#'(lambda (c)
(format t "~a "
(simple-condition-format-control c))
(invoke-restart 'continue))))
(loop for i from 1 to n
do (check-fizzbuzz i))))
最初のブロックでは型の定義をしています。
mod3p
は3で割り切れるかどうかを判定する述語関数で、mod5p
は5の場合の述語関数です。これらの述語関数を満たすことを条件にmod3
とmod5
という型を定義しています。mod15
は両方の型を同時に満たすような場合として定義できます。
check-fizzbuzz
関数は引数の型を調べ、対応するエラーを発生させます。このページのここまでの範囲ではまだ独自のコンディションを定義する方法と Restart を確立する方法を示していないので、cerror
関数を使って「continue
付のsimple-error
クラスのコンディション」を通知しています。どのように区別するかというと、format-control
スロットの値で使い分けています。
cerror
関数は残りの3つのコンディション通知関数よりも必須引数が一つ多いです。最初の引数はcontinue
の説明に使うフォーマット文字列ですが、今回はデバッガでcontinue
を選択することはないので使いません。また、第2引数は前節で説明したようにシンボル・文字列・コンディションのいずれかのデータになるので、数をそのまま渡すことはできず、write-to-string
関数を使って文字列に変換してから渡しています。
実際のFizzBuzz関数は
fizzbuzz
です。この関数は1
から引数の値まで繰り返しを行いながらcheck-fizzbuzz
関数を呼び出しますが、handler-bind
マクロで包んでいるので、check-fizzbuzz
を評価している最中に発生したコンディションをハンドリングすることができます。cerror
関数で通知しているので渡されてくるコンディションはsimple-error
クラスのインスタンスですが、デバッガの起動やメッセージの表示は行わないので、本来的にはどのようなコンディションでも構いません。ただ、format-control
スロットを使っているので、simple-condition
型でマッチさせています。(simple-error
もsimple-warning
もsimple-condition
のサブクラスとして定義されているので、format-control
スロットやformat-arguments
スロットにアクセスすることができます。)
continue
を起動しているのはinvoke-restart
関数です。ここで、制御フローが元に戻り、処理を継続します。回復方法の確立
Restart はとても便利で堅牢性の高い例外処理を可能にしますが、これまでに使ってきた Restart は全て既存のものでした。既存のものとは、ANSI Common Lispに定められており、可能であれば回復手段を提供すべき、となっているようなもので、abort
, use-value
, continue
の3種類を使いました。
標準の Restart は他に2種類定められています。
- Restart:
muffle-warning
- 警告の内容を無視するという対処方法です。警告を通知する
warn
関数を発した場所に対して、警告の内容はすでに処理済みであり、追加の対応は必要ないことを知らせることができます。例えば、警告の内容をリアルタイムで表示する必要はなくても、その時のコンディションを保存しておき、後からそのコンディションを使いたい場合などに使用します。(ANSI Common Lispに掲載されているサンプルは大まかにこのような動作を想定しているようです。) - Restart:
store-value
- 異常が発生した値を新しい値で置き換えて、異常が発生した場所に回復するという対処方法です。
use-value
とは異なり、新しい値は変数に保存することを想定しているので、再利用可能です。そのため、返り値を得る場合に限らず、副作用ベースの動作でも用いられます。デバッガで対話的にこの対処方法を選択すると新しい値を入力することができます。
abort
しか対処方法が存在しない場合も多いですし、独自の対処方法を提供したい場合もあります。
この節では、
restart-case
マクロを使って対処方法(例外的状況からの回復手段)を確立する仕組みを示します。この節で説明するトピックはrestart-case
マクロに限られますので、以下のサンプルを複数の小節に分けて説明します。
(defun input-for-restart ()
(format t "Enter a new value: ")
(list (eval (read))))
; => INPUT-FOR-RESTART
(defun check-double-float (v)
(restart-case
(if (typep v 'double-float)
v
(error "~a is not the type of double-float." v))
(coerce ()
:report
(lambda (stream)
(format stream "Coerce the value ~a to the type of double-float." v))
(coerce v 'double-float))
(store-value (x)
:report "Input a value to be stored instead."
:interactive input-for-restart
(setq v x)
(format t "New value is ~a.~%" v)
(check-double-float v))))
; => CHECK-DOUBLE-FLOAT
restart-case
マクロ
restart-case
マクロは Restart を確立するためのマクロです。restart-bind
というマクロでも確立することができますが、restart-case
の方がフロー制御が自動化されており、便利なので、restart-bind
はあまり使われません。
restart-case
マクロはhandler-case
マクロと同様の構文を持っており、第1引数の式を評価中に例外的状況が発生してコンディションが通知されるような場合、第2引数以降の Restart を確立してアクティブな状態にして提供します。「最後の砦」であるデバッガによってコンディションがハンドリングされる場合はアクティブな状態にある Restart を回復方法の選択肢として提示することができますし、デバッガの手前でハンドリングされた場合はinvoke-restart
関数やinvoke-restart-interactively
関数で Restart を使うことができます。
全く独自のものを提供することもできますし、ANSI Common Lispの標準で定められた5つの Restart のいずれかを提示することもできます。
restart-case
で標準の Restart を提示する場合も独自のものと同じように定義する必要がありますが、回復方法の意図が標準のものと同じであれば同じ名前を用いて提示すべきです。その方がプログラムの利用者が分かりやすくなります。
ちなみに、関数の設計としては、具体的な処理や計算を担う関数においては
- 例外が発生した場合に備え、コンディションを通知する機能を含めておく
- コンディションが実際に通知された場合に備え、回復方法を含めておく
前節で掲載したサンプルは2つの関数を含みますが、上の
input-for-restart
関数は補助関数です。標準入力からデータを読み取り、評価してリストにして返します。下のcheck-double-float
関数はすでに簡易版を示した'double-float
型かどうかを確かめる関数ですが、述語ではありません。'double-float
型ではない場合はエラーを通知し、2つの回復方法coerce
とstore-value
を提供します。最初から型が一致していた場合や、回復方法の選択により型が一致した場合はその値を返します。つまり、非常に多くの副作用を伴っていますが、返り値を使うことも想定した関数です。
restart-case
マクロの使い方はサンプルの通りなのですが、2つのオプションについて個別に説明します。
:report
による表示
restart-case
マクロで確立できる Restart には「レポーター」を含めることができます。レポーターとはデバッガのような対話的な回復方法の提示において、その回復方法の動作や意図を簡潔に説明するための表示手段です。
「レポーター」は
:report
オプションで指定します。指定の方法はいくつかあります。
-
lambda
式: 出力用ストリームを引数に取る無名関数。 -
シンボル:
defun
やflet
等で定義された関数名。関数の引数はlambda
式の場合と同様。 - 文字列: 単に文字列を表示するだけの場合に使われる文字列。
coerce
という Restart は値を'double-float
型に変換することを想定していますから、現在の値が何なのかを明示的に説明した方が分かりやすいでしょう。そのため、表示はフォーマット文字列を使いたいので、lambda
式を使っています。
他方、
store-value
という Restart はANSI Common Lispでも定められているもので、新しく値を得て、何らかの変数に保存するという動作を想定しています。ここでは既存の値は破棄され、結局使われませんので、表示内容の中に現在の値を含める必要はないと判断しました。そこで、単なる文字列を渡しています。
:report
オプションによるレポーターが存在しないと回復方法の名前だけが表示され、デバッガでもとても分かりにくくなってしまいますから、:report
オプションはほぼ必ず含めるようにしてください。
:interactive
による対話機能
独自に定義している2つの回復方法の意図については前節で示しましたが、store-value
は新しい値を使うため、その値を何らかの方法で取得する必要があります。
invoke-restart
関数でこの回復方法が選択された場合、その新しい値は引数として回復方法名の次のラムダリスト(ここでは(x)
の部分)で変数に束縛されます。この変数束縛はデバッガやinvoke-restart-interactively
関数のような対話的な方法で取得された場合も同様で、この部分で新しい値を得ることになります。coerce
は新しい値が必要ないので、ラムダリストが空リストになっているのが分かると思います。
invoke-restart
の場合は問題がないのですが、対話的な方法で新しい値を得る場合、その方法が問題となります。どのように入力を促し、入力されたデータをどのように「新しい値」として使うかが問題となるでしょう。そこで、新しい値を使って処理を継続するような回復方法の場合に、対話的な値の取得に備えてその場合の関数を用意しておく必要があります。それを指定するのが:interactive
オプションです。
:interactive
オプションはlambda
式かシンボルを取りますが、どちらも(function ...)
スペシャルフォームの形式で評価されるため、最終的には関数の実体として評価されるべきものである必要があります。新しい値を入力してもらって返す、というのは多少汎用性のある動作なので、input-for-restart
関数を別途定義して、そのシンボルを:interactive
オプションで指定しています。
ポイントは以下の2点かと思います。
- 新しい値はリストの形式にしておく必要があります。
-
入力データを
eval
するかどうかは適宜判断してください。
(apply #'store-value x)
のように行われるため、引数はリストの形式にしておかなければなりません。
2つ目については、例えば
3
と(+ 1 2)
の違いをどう考えるか、というような問題です。普段はあまり意識しないかもしれませんが、Common Lispのコード読み取り器にとってはこの2つは別物です。
(read-from-string "3")
; => 3; 1
(read-from-string "(+ 1 2)")
; => (+ 1 2); 7
これらのコードを別物として扱いたいなら
eval
は必要ありません。しかし、同じ値として扱いたいなら、一度「評価 (evaluation)」をする必要があります。
(eval (read-from-string "3"))
; => 3
(eval (read-from-string "(+ 1 2)"))
; => 3
ANSI Common Lispの仕様に掲載されたサンプルでも
eval
していますが、自己評価値のみを受け付けることを前提にすれば、eval
は外しておいてもいいと思います。また、eval
をおいた途端にセキュリティが脆くなるため、そのリスクは十分に考慮してください。もし Restart による回復の最中の入力で悪質なコードを打ち込まれた場合、そのままeval
するとユーザーは何でもできてしまいます。((loop)
と打ち込むだけで無限ループに陥ります。)
Common Lisp以外では Restart という機能がほどんど実用化できていないので、回復手段の理想の設計などについてはあまり研究が進んでいないのではないかと思います。私自身も確固たるポリシーがあるわけではありませんので、各々研究してみてください。
独自コンディションの定義
コンディションに関する最後の主要なトピックは、コンディションの定義です。コンディションは実体としては
condition
クラスを継承するサブクラスのインスタンスです。condition
クラスそのものからコンディションを生成することはほとんどなく、整備された継承関係に基づいてerror
, warn
, signal
という通知関数に対応するようなコンディションをサブクラスから生成することになります。
この節ではコンディションの定義に関するトピックを扱うため、クラスとコンディションの違い、
:report
オプション、標準のコンディションについて説明します。
クラスとコンディションの違い
クラスとコンディションの違いはほとんどなく、コンディションはクラスをベースに、Common Lisp Object Systemを基盤として構築されています。しかし、コンディションがクラスと異なる点が4つあります。(この4点はANSI Common Lispの
condition
型の説明に記述されています。)
-
定義オペレータ:
defclass
マクロではなく、define-condition
マクロを用いる。 -
生成オペレータ:
make-instance
総称関数ではなく、make-condition
関数を用いる。 -
表示オペレータ:
print-object
総称関数ではなく、define-condition
の:report
オプションを用いる。 -
アクセスオペレータ:
slot-value
,slot-boundp
,slot-makunbound
,with-slots
のようなオペレータを使ってスロットにアクセスするのではなく、define-condition
で定める:reader
,:writer
,:accessor
のようなアクセス関数でアクセスする。
:report
オプションだけです。そこで、次節ではこのオプションについて説明します。
:report
オプション
コンディションはインスタンスの一種なので、普通にprint
すると Unreadable Object の形式で表示されます。
しかし、コンディションを表示するケースというのは、エラーに遭遇してデバッガが起動した場合がほとんどで、コンディションの実体そのものを表示するよりは、コンディションがどのような状態を示すかというエラーメッセージを表示した方がユーザーフレンドリーです。
そのため、コンディションはクラスと異なり、
:report
というオプションが設けられています。この:report
オプションにはレポート用の無名関数を設定することができ、print-object
総称関数のように振る舞うことができます。
例えば、このページで示した「
'double-float
型でない場合にエラーを通知する」という場合に、それに適した独自のコンディションを定義すると、以下のようになります。
(define-condition type-error-double-float (simple-type-error)
()
(:report (lambda (condition stream)
(format stream "Error: ~s is not the type of double-float."
(type-error-datum condition)))))
; => TYPE-ERROR-DOUBLE-FLOAT
(make-condition 'type-error-double-float :datum 10)
; => #<TYPE-ERROR-DOUBLE-FLOAT #x0000000200427159>
(error (make-condition 'type-error-double-float :datum 10))
; *** - Error: 10 is not the type of double-float.
; The following restarts are available:
; ABORT :R1 Abort main loop
define-condition
マクロがコンディション定義オペレータです。第1引数はコンディション名、第2引数はスーパークラスのリスト、第3引数はスロット関連の定義、第4引数が全体のオプションです。:report
オプションはdefine-condition
マクロの最後に記述します。
:report
オプションに渡す無名関数はコンディションとストリームの2引数を取る関数でなければなりませんが、これはprint-object
総称関数の構文と同じです。
特徴的なのは、表示形式の違いです。
- 普通にコンディションの実体を表示する場合: Unreadable Object のスタイルで表示される。
-
コンディションを通知してデバッガなどでハンドリングした場合:
:report
オプションのレポーター関数で表示される。
標準のコンディション
独自コンディションの定義をあえてこのページの最後に持ってきたように、ANSI Common Lispでは様々なコンディションがあらかじめ定められており、仕様に準拠した処理系であれば違いを意識せずに利用することができます。そのため、独自のコンディションを定義しなくても十分な例外処理を実現することが可能でしょう。しかし、ある程度の大きさのプログラム(「アプリケーション」と呼ぶレベルのもの)になれば、アプリケーション独自のベースととなるコンディションを定義して、そのサブクラスとして細かいコンディションを定義するようにして、システム標準のコンディションとアプリケーションレベルのコンディションを分けて使った方が良いでしょう。コンディションの型はエラーが発生した場合の状態を見分ける最も有力な情報であり、システムがエラーを発しているのか、アプリケーションがエラーを発しているのかを見分けられることは重要な情報です。
そのため、合理的なコンディションは、2つの種類のコンディションをミックスインしていくことで 定義されます。
- 例外的状況に最も近いANSI Common Lisp標準のコンディションクラス
- 独自に定義して継承関係が構築されているアプリケーションレベルのコンディションクラス
type-error
コンディションを継承しておくべきです。ファイルに関するエラーであれば同様にfile-error
コンディションを継承すべきですし、エラーではなく警告であればsimple-warning
コンディションを継承しておくと何かと便利かもしれません。
このように、独自のコンディションを定義する場合は、標準のコンディションを知っておかなければなりません。そのため、ここで標準のコンディションに関する情報を網羅的に示しておきます。
分類は基本的には継承関係を基に行なっていますが、実際に使う場合は必ずそのコンディションがどのコンディションを継承しているかを確認してください。Function として示しているのはコンディションのスロットアクセス関数で、そのスロットに格納されることが期待されるデータの意味を示しています。
-
Condition:
condition
= 全てのコンディションのスーパークラスです。コンディションとして定義する場合は、必ずこの型を継承しなければなりません。 -
Condition:
simple-condition
=signal
関数のデフォルトコンディションです。-
Function:
simple-condition-format-control
= コンディションの表示に用いるフォーマット文字列です。 -
Function:
simple-condition-format-arguments
= フォーマット文字列の中で用いる引数です。 -
Condition:
simple-error
=simple-condition
とerror
を共に満たすコンディションです。error
関数のデフォルトコンディションです。-
Condition:
simple-type-error
=simple-error
とtype-error
を共に満たすコンディションです。type-error
のスロットであるdatum
とexpected-type
を使うことができるので便利です。
-
Condition:
-
Condition:
simple-warning
=simple-condition
とwarning
を共に満たすコンディションです。warn
関数のデフォルトコンディションです。
-
Function:
-
Condition:
warning
= 警告を示すコンディションです。全ての警告はこのコンディションを継承すべきです。-
Condition:
style-warning
= エラーとするほどではないにせよ、間違いの可能性がある場合に発せられる警告のコンディションです。「未使用の変数」であることを警告する場合に使われますが、このことはANSI Common Lispでも定められています。 -
Condition:
simple-warning
=simple-condition
の項で示した通りです。
-
Condition:
-
Condition:
serious-condition
= 重大な例外的状況を意味するコンディションです。通常はerror
関数で通知されます。また、error
コンディションはこのコンディションのサブクラスです。-
Condition:
strage-condition
= メモリー管理に関するコンディションです。serious-condition
を継承していますが、error
を継承していないという数少ないコンディションの一つです。これは、メモリーの管理はプログラムの責任ではなく処理系の責任であるため、「エラー」という表現が適さないためです。どのような場合にこのコンディションが通知されるべきかは事前には定められておらず、処理系の判断で通知されます。 -
Condition:
error
= いわゆる「エラー」です。他の多くのコンディションのスーパークラスとして利用されています。-
Condition:
type-error
= 型に関するエラーです。-
Function:
type-error-datum
= 型の不正が発生したデータを格納しています。 -
Function:
type-error-expected-type
= 期待される正しい型のシンボルを格納しています。 -
Condition:
simple-type-error
=simple-error
の項で示した通りです。
-
Function:
-
Condition:
file-error
= ファイルに関するエラーです。-
Function:
file-error-pathname
= エラーが発生したファイルやディレクトリのパスを格納しています。
-
Function:
-
Condition:
stream-error
= ストリームに関するエラーです。-
Function:
stream-error-stream
= エラーが発生したストリームを格納しています。 -
Condition:
end-of-file
= ファイルの末尾であることを示すコンディションです。
-
Function:
-
Condition:
control-error
= フロー制御に関するエラーです。throw
やgo
スペシャルオペレータにおいて使われます。 -
Condition:
program-error
= 構文に関するエラーです。go
タグやblock
タグに関して通知されます。 -
Condition:
parse-error
= 構文解析に関するエラーです。-
Function:
reader-error
= Lispリーダーでの区切りの不正や構文解析上のエラーを示します。
-
Function:
-
Condition:
cell-error
= アクセス(配列へのアクセスやsetf
による place の操作)に関するエラーです。-
Function:
cell-error-name
= エラーが発生したアクセスを示す名前(配列名や place )を格納しています。 -
Condition:
unbound-slot
= インスタンスに未束縛のスロットが存在することを示すエラーです。-
Function:
unbound-slot-instance
= インスタンスの名前を格納しています。
-
Function:
-
Condition:
unbound-variable
= 未束縛の変数であることを示すエラーです。 -
Condition:
undefined-function
= 未定義の関数であることを示すエラーです。
-
Function:
-
Condition:
package-error
= パッケージに関するエラーです。-
Function:
package-error-package
= エラーの原因となったパッケージ名を示すシンボルを格納しています。
-
Function:
-
Condition:
print-not-readable
=*print-readably*
スペシャル変数がt
の時に、Lispリーダーでは読み取れないようなオブジェクトを書き込もうとした時に通知されるコンディションです。-
Function:
print-not-readable-object
= 書き込もうとしたオブジェクトを格納しています。
-
Function:
-
Condition:
arithmetic-error
= 計算上のエラーを示すコンディションです。-
Function:
arithmetic-error-operands
= 計算に用いたオペランドのリストを格納しています。オペランドとは計算される数です。 -
Function:
arithmetic-error-operation
= 計算に用いた演算子(関数)を格納しています。 -
Condition:
division-by-zero
= ゼロで除算しようとした際に使われれるコンディションです。 -
Condition:
floating-point-inexact
= 浮動小数点数の計算上で発生したエラーに使われるコンディションです。明確な意味論は定められていません。 -
Condition:
floating-point-invalid-operation
= 浮動小数点数の計算上で発生したエラーに使われるコンディションです。明確な意味論は定められていません。 -
Condition:
floating-point-overflow
= 浮動小数点数の計算において桁あふれ(オーバーフロー)が発生した場合のエラーです。 -
Condition:
floating-point-underflow
= 浮動小数点数の計算において下位桁あふれ(アンダーフロー)が発生した場合のエラーです。
-
Function:
-
Condition:
-
Condition:
補論: 移動ゲーム
第5章「データと制御フロー」のcatch
, throw
の項で示した「4方向移動ゲーム」のコンディションシステム版を示します。
内容としては
catch
, throw
版と同じですが、このページで説明した以下の項目を含んでいます。
- コンディションの定義と継承
- コンディションの通知関数
- 例外的状況のハンドリング
- 回復方法の確立と対話的な起動
(defparameter *commands* '())
(defparameter *first-flag* t)
(define-condition quit-game (condition) ())
(define-condition unknown (error)
((command :initarg :command
:reader unknown-command)))
(defun move-to (v)
(restart-case
(case v
((N S E W) (push v *commands*))
((Q) (signal 'quit-game))
(t (error 'unknown :command v)))
(use-value (v)
:interactive (lambda ()
(format t "Enter a new command instead: ")
(list (read)))
(push v *commands*))))
(defun get-command ()
(when *first-flag*
(format t "Select the direction:~%")
(format t " [N] = North, [S] = South, [E] = East, [W] = West~%")
(format t " [Q] = Quit.~%")
(setq *first-flag* nil))
(format t "(Current commands = [~{~a~^,~}])~%" (reverse *commands*))
(format t "Enter your command: ")
(read))
(defun start ()
(psetq *commands* '()
*first-flag* t)
(handler-bind ((quit-game
#'(lambda (c)
(declare (ignore c))
(format t "The End...~%")
(format t " Commands = [~{~a~^,~}]~%"
(reverse *commands*))
(format t "Bye!!~%")
(return-from start 'game-end)))
(unknown
#'(lambda (c)
(format t "*** Error: You entered [~a]. It's wrong!~%"
(unknown-command c))
(invoke-restart-interactively 'use-value))))
(loop (move-to (get-command)))))
以下が実行例です。
> (start) Select the direction: [N] = North, [S] = South, [E] = East, [W] = West [Q] = Quit. (Current commands = []) Enter your command: e (Current commands = [E]) Enter your command: s (Current commands = [E,S]) Enter your command: w (Current commands = [E,S,W]) Enter your command: n (Current commands = [E,S,W,N]) Enter your command: d *** Error: You entered [D]. It's wrong! Enter a new command instead: s (Current commands = [E,S,W,N,S]) Enter your command: m *** Error: You entered [M]. It's wrong! Enter a new command instead: n (Current commands = [E,S,W,N,S,N]) Enter your command: q The End... Commands = [E,S,W,N,S,N] Bye!! GAME-END >
途中で2回エラーが発生していますが、ハンドラーを使ってデバッガを起動させることなく Restart を使って新しいコマンドを入力しています。また、終了コマンドはシンプルな
signal
関数で通知して、ハンドラーのあるstart
関数まで戻ってくるという仕様です。
コンディションシステムは通知、捕捉、回復という手順を明確にすることができるため、保守性が高まるでしょう。この例で言えば以下の2つの関数がコンディションシステムを担っており、このページで説明してきた下位関数による「通知と回復」の提供と上位関数による「捕捉」という原則に沿って実装しています。
-
move-to
関数: 実際の移動コマンドを扱うので、異常があれば通知する。合わせて回復方法も確立しておく。 -
start
関数: ゲームの開始関数であり、プログラムの最上位関数であるので、例外的状況のハンドリングを行う。今回はゲームの終了と未知のコマンド入力についてハンドリングしている。
0 件のコメント :
コメントを投稿