例外と例外処理、と言うのはモダンな言語だと大体組み込まれている。
一方、意外とこれが初心者には分かりづらい機能でもある。
以前、ババ抜きの記事で軽く説明したつもりだったが。
いや、自画自賛だけど、ぶっちゃけ、あれ以上に上手く説明してる記事なり本なり、ってのは正直無いと思っている。
でも一応、もう一回整理してまとめておこうか、と思った。
それくらい概念的には例外処理は分かりづらいのは事実だから、だ。
まずは「エラー処理にまつわる歴史」を書いておこう。
例外と例外処理、と言う機能を多くの人に初めて紹介したプログラミング言語はやっぱりJavaだと思う。例外処理はそれ以前には尖ったプログラミング言語しか持ってなかった、って言ってイイだろう。
Javaは設計上、それほど優れた言語だとは全く思ってないが、それまで「尖った言語処理系にしか存在しなかった」機能を広く知らしめた、と言う意味では功績が大きい。消えたSun Microsystems(※1)の慧眼と言っていいだろう。
要するにSunは研究室で眠ってた「秘宝」を、扉をこじ開けて回収し、Javaと言う媒体を使って宣伝してくれたわけだ。
恐らく、Javaの設計者のジム・ゴスリングは、Lisp系語族で研究対象になってた「例外処理」と言うシステムを比較的分かりやすいカタチで移植したんじゃないか。あまり知られてないが、実はこの人も元々Lisperだ(※2)。
そもそも「エラーを投げる」と言う機能そのものがLisp出自だ。これはある種必要があって実装された機能となる。
あんまピンと来ないかもしれないけど、「エラーを投げ」なければプログラムやコンピュータは一体どうなるんだろうか。単純に「ソフトウェアが落ちる」か、場合によっては「ハードウェアが止まる」って事になる。
あまり出来の良くないビデオゲームとかで経験があるんじゃないか。快適に遊んでた筈なのに、何らかの障害があってそのゲームが止まっちまって「うんともすんとも言わない」とか(笑)。そうなるとユーザーはテの施しようがない。
Lispはインタプリタとして生まれた。そして「プログラミング言語」自体がプログラムである以上、書いたプログラムかあるいは入力に間違いがあった場合、Lispが「落ちる」事はありえるわけだ。
しかしながら、実際にはそういう事はほぼない。Lispインタプリタは「どんなエラーが起きたか」表示はするが、まるで「エラーなんて無かったかのように」正常に動き続ける。この機能がインタプリタとして生まれたLispには必要だったんだ。
Lispでは1960年前後に登場したLisp1.5で既に豊富な「エラー通知機能」を持っていた。
恐らくその役目は、上で書いた通り、まずは「ユーザーがLispをquitしようと思わない限り」動き続ける為、ってのが一つ。あとはデバッグ目的だな。この頃から書いたプログラムの「動作」を逐一追いかけるtraceと言う機能を持ってたLispは、ユーザーのプログラム開発支援の為に豊富な「エラーの種類」をこの時点で抱えている。その数、軽く40種を超える。
言わば、Lispは人前に姿を現した時点で、デバッガ自体を抱えたような構成になってたわけだ(※3)。
そしてLispはエラー通知機能を足がかりとして、プログラムを書いてる側にも「エラーで止まる必要がないプログラムが書ける」機能を提供しよう、って進化していく。そうだな、例外処理、と言う機能がそのうち研究されて追加されていくようになるわけだ。
さて、それが歴史だ、って事なんだが。一方、それでも概念的には「例外処理」ってのは初心者には難しい。っつーか「フツーの条件分岐」との違いがわからんのだよな。概念的な問題だ。
ちとここで、オーソドックスな「例外処理」を持ってると思われるPythonで見てみよう。
良くある例として、割り算の例を考える。
42を7で割ると6になる、っつーのはまぁいいだろう。
問題は次のような事をした場合だ。
42は0じゃ割れない。当然エラーが生じるわけだ。ここではZeroDivisionErrorだと言われている。
何らかの関数の中で、割り算を使っていたとして、この除数(割る方の数)が0だ、ってのは常に起こりうる事だ・・・例えばユーザーに入力を促した際に、ユーザーが0を間違って入力しちゃったり、とかな。
例外や例外処理が無い、例えばC言語のような言語だと、条件分岐を書いて「0が入力された時の挙動」を書かないとならない。
一方、例外処理の場合は次のように記述する。
確かにこの2つは構文的には良く似てる。返ってくる結果も同じだ。
そうすると、プログラミング初心者はこう思うわけだ。
「今まで条件分岐の練習を散々やらされてきたのに何だこれ?」
と(笑)。今までの苦労を水の泡と化すようなシステムの登場、だ。
もう一つは、上の例だとexceptに付随するZeroDivisionErrorと言う記述だ。聞けばこのテのエラーのパターンはこの他にもっとあると言う(※4)。そして適切なエラーを選ばないとならない。
そうなると、プログラミング初学者はこう思うわけだ。
「ただでさえプログラミングは覚える事が多いのにまだ覚えろってぇの?」
と。
強烈に暗記を要求するなら使ってなんかいられない、って思うのは当然だろう。
プログラミングに慣れた層は「例外処理は便利だ」と言う。しかし、引き換えに暗記力を要するならとてもじゃないけど「便利」とは言えないよな。「数学公式のパラドックス」に似てる。公式は計算を簡単にしてくれる武器だけど、暗記を強要するなら意味がねーだろ、っつーアレだ。
その辺でプログラミング初心者は「例外処理」に抵抗感を覚えるんだ。「こんなん使わなくても条件分岐で充分じゃね?」と。
構文も構造的にはそっくりだし、出てくる結果も似たようなモノだ、と。
だったら「例外処理」なんて使わずにフツーに条件分岐で充分なんじゃないか、と。
と言うわけで、「例外処理」に対しては本当は「一体どういうコンセプトなんだ?」と言う解説が必要な筈なんだけど。
残念ながら、知ってる限り、「例外処理」に対してそのコンセプトまでツッコんだ初心者向け解説ってのは見たことがない。
大方のケースでは単にtry〜except構文の「使い方」だけ解説して、あとは「自分でも例外を作って投げる事が出来ます」と言ったような事しか書いてないんだ。
だからこそ、プログラミング初心者は「例外処理って何なんだ」かサッパリ分からず、抵抗を感じるか、あるいは単に「言われたまま使う」しかないような状況になっている。後者がいわゆる「習うより慣れろ」って話になるわけだが。
そこで、ここでは、特に男性向けに分かりやすい、「例外処理」とは何をやってるのか、を説明してみる事とする。
野郎ども、耳の穴かっぽじって良く聞けよ(笑)。
例えば貴方が女子高生をナンパしたとする。
当然エッチするよな。
ここでエラーは「女子高生が妊娠しちまう」事だ。遊びな以上、妊娠は避けたい。
つまり、「避妊」をせなアカンわけだ。貴方がゴムを持ってなかったとしたら、「エッチしない」と言うのが妊娠を避けるには必要なわけだ。
これが条件分岐、だ。そしてコンドームが無かったとしたらナンパ出来てもエッチは「しない」んだ。涙を飲んで女子高生と別れなアカン。
一方、例外処理は違う。とにかくエッチをするんだ。ゴムを持ってようと持っていまいと関係ない。妊娠した時は妊娠した時だ、と開き直る。つまり中出し前提だ。
妊娠させる、ってのは貴方の人生に於けるエラーだ。しかし、構わない。何故なら妊娠させても時間を巻き戻して妊娠を無かった事に出来るからだ。
現実世界ではこんなこたぁありえねぇが、プログラミングの世界じゃあ何でもありだ。つまり時間を巻き戻して中出しによる妊娠を無かった事にするのを「例外処理」と言う。
鬼畜の所業だろ(笑)?でもそれこそが例外処理で、条件分岐との意味の違いだ。
例えば上のようなケースだとこんな風に書く事が出来る。
どっちにせよ一回生でヤッとく。妊娠しようがしまいが構わない。
そして妊娠した場合のみ、例外処理、と言う能力を兼ね備えた時間跳躍者であるアナタは、時間を巻き戻して妊娠を無かった事にする。そして改めてゴムを付けて、アナタの観点ではもう一発ヤる事となるわけだ(※5)。
分かる?条件分岐だとゴムが無かった場合は「エッチをする」事自体を諦めなければならない。何故なら条件分岐によるエラー回避は転ばぬ先の杖、事前にあるかどうか分からない妊娠を避ける為に「エッチが出来ない」事を許容せなアカン。
一方、例外処理は違う。とりあえず一回はハメるんだ。その結果がどうなるかは分からんが、ハメた結果、妊娠した場合のみ時間を巻き戻してそれを「無かった事」にして、改めて未来を選択しなおす(それがexceptの内容だ)。言わば例外処理は事後処理だ。事後が望ましくない結果だった場合に、未来を選択しなおすと言うのが例外処理なんだ。
野郎ども、分かったか(笑)。
我ながらヒデェ説明だとは思うよ(笑)?しかしながらヒデェ説明の方が記憶に残りやすいだろ(笑)?脳科学はそういう事を示唆してる。
だから俺が鬼畜なんじゃなくって(言い訳)、そう説明する方がみんなの為なんだ(笑)。俺様は人類の平和の為に敢えてヒデェ説明をする殉教者である(爆
ちなみに、なにかにつけて「みんなの為」って言うヤツにはロクなヤツがいない、って事も申し添えておく(謎
さて、妊娠はさすがにトラブルだ。しかし、時間を巻き戻してヤッてみたら思わぬトラブルが新たに生じる事があり得る。例えばその女子高生が実は性病持ちだったりな(笑)。
これは最初から予測は出来ない。つまり、それこそヤッてみなきゃ分からんのだ。
そして性病に羅漢したとしたら、その時点でコードを修正して時間を巻き戻さないとならない。次のように、だ。
あるいは、性病に羅漢しなくても実はその女子高生には美人局がいるかもしんない。エッチせずに逃げるかあるいは警察に通報するか。依然として、何が起こるか分からん以上、対処療法的にコードを修正するしかない。必然的にプログラマ側も事後対応に追われる事になる。
一体今度は何のメタファなんだろうか。
いや、どんなトラブルが起きるか端から全部想定するこたぁ不可能だ、って言ってるんだ。時を戻せる機能があっても未来は予測出来ない。従って対処療法的に「起きた事象に対して」対策を書いていかないとならない。
まだ分からないかしら。つまり、例外処理が便利なのは、エラーに対してガンガン処理を追加可能な構文になってる辺りなんだ。言い換えるとある言語処理系に付随してる例外を全部覚える、なんつー事はせんで良い。現実的には、実際エラーが起きた際にそのエラーメッセージを読んで例外処理を追加すればイイ、ってこった。
前もって条件分岐で「どんなエラーが起こり得るか」考えて穴がないようにプログラムしよう、ってしなくてもいい。プログラマ側も「実際エラーが起きてから考えればいいよな」と怠惰な態度になれるのが例外処理なんだよ。そういうスタンスなんだ、ってのを通常のプログラミング入門書は教えないんだよな。
分かる?大体、プログラミング言語に於いて、抽象化の何が一番いいのか、と言うと決定を後回しに出来る自由を手に出来る、ってこった。エラーが起きる前にエラーの事を考えてもしゃーないだろ、ってのが例外処理の背景にあるアイディアなんだ。ここでも「決定は今行う」事ではなく、「起きてから考えればいい」と言う怠惰さをプログラマに与えている。
もう一つ例外処理の利点がある。例外処理があるプログラミング言語では例外自体がデータ型なんだ。つまり、プログラミングに於いて操れる対象になる。
つまり、例外処理が無いプログラミング言語だとエラーは忌避されるべきモノなんだけど、一方、例外処理があるプログラミング言語に於いてはエラーは忌避されるべき対象ではない。エラーは友達なんだ。
むしろエラーをわざと起こして、それを利用したプログラミングさえ可能になってる、って事だ。
これはある意味スゴイだろ?言っちゃえばエラーに対してパラダイム変換が生じてる。エラーを怖がったり恐れたりする必要がない、って事だ。
例えばPythonではStopIteration例外ってのが頻出する。これは、例えばリスト相手に繰り返しをする際、「リストの長さがこれ以上はありません」と言うカンジで送出されるエラーだ。言い換えると、原理的には、Pythonには構文的に繰り返しを止める手段が存在しない。しかし、リストの長さを超えた時にエラーが送出される事によって強制的に繰り返しを止めてるんだ。
この辺がパラダイム変換だ、と言う事だ。それまでのフツーのプログラミング言語だとリストの長さを調べて、その長さと一致した時に反復を止めるように条件分岐を使ってプログラムを書かなければならなかった。反復が止まらない場合、それは忌避すべきエラーだ、と言う認識だった。
しかしPythonのようなモダンな言語だと違う。繰り返しは止まらない機構として、つまり無限ループをするように設計されている。そしてエラーは忌避されるべき対象ではなく、エラーを利用して繰り返しを止めるようになってるんだ。
そう、現代に生きる僕らにとってはもはやエラーは忌避すべき対象ではない。例外処理がある以上、エラーは利用する対象にまでなったんだ。
むしろ、現代ではエラーをわざと起こしてそれを利用してプログラムするところまで来ている(※6)。
パラダイム変換、と言った意味が分かるだろうか。21世紀を生きる僕らにとってはエラーは既に、単なるプログラミング上の間違い、と言う意味を逸脱しはじめているんだ。
とまぁ、これが例外と例外処理、と言うモノのアイディアだ。繰り返すが、酷い例示もあったが脳に焼き付けただろうか?
ここからちとSchemeに於ける例外と例外処理に付いて書いていく。Javaのtry〜catchやPythonのtry〜exceptとスタイルは違うが、基本コンセプトは変わらない。
とは言っても、元々、SchemeはLisp本流であるANSI Common Lispと違って(※7)、長い間例外処理が無かった。
と言うのも、Schemeには「強力な材料」が色々とある。「時間が巻き戻る」と言う表現を聞いて「どっかで聞いたことがある」と思うかもしんない。そう、Schemeではcall/ccを用いて自分で例外処理機構を書く事が出来るわけだ。=> 参考: 例外処理機構
とにかくSchemeは、強力な基礎機能を提供する代わりに極端なDIY指向言語だったわけだ。
基本的に、Schemeの例外処理機構をguardと呼ぶ。
ただし、厄介なのが、Schemeの仕様では「どんな例外が投げられるのか」は実装依存として良い、と言う事になっている。
従って、どんな種類の例外が返るのか、と言うのは処理系依存なんで、仕様上はどことなく「ふんわりとした」一般論にならん一般論で話さざるを得ない。
例えば上の「女子高生ナンパ」の例だとSchemeでは以下のように書く。
Pythonで言うトコのtryとexceptが逆になってるような配置になっている。
また、Schemeの仕様上、「どんな例外が投げられるか分からない」ので、「エッチする」で生じた例外は、何であれ単純に変数トラブルに束縛される事となる。
そしてそのトラブルがどんなトラブルか見て、それに従ったブツ(この例だと「コンドームを付ける」「エッチする」)が実行される。
この例だと、例外で'妊娠と言うシンボルが投げられる前提になってるんで、単純に「妊娠」エラーかどうかはeq?でチェックする。ただし、処理系依存で「色んな例外」が独自に定義されてる場合はこの限りじゃない。
Pythonの例のように、exceptがたくさんある場合は、次のような記述になる。
次に、もうちょっとマシな例を考えてみよう。
例外処理を追えるだろうか?
関数fは例外'DataNashiと'(Urikire "tomato")を投げる。引数nが0の時'DataNashiを投げ、1の時を'(Urikire "tomato")を投げる。それ以外はnに1足した数を返す。
関数gは(f n)に10を足す関数だが、(f n)は引数によっては例外を投げてくる。'DataNashiが投げられた時、例外を補足し、問答無用で0を返す。
関数hは(g n)に100を足す関数だ。しかし、(g n)も例外を投げてくる場合がある。と言うのも(g n)は例外'(Urikire "tomato")を補足しない。従ってここで'(Urikire "tomato")を補足し、エラーの中身(つまり変数conditionに含まれる)の文字列の長さを返す。
実行結果は以下のようになる。
今度は、名前、点数、成績スロットを持つstudent-t構造体のリストを作って、それから名前に従って成績を引っこ抜いてくる関数tsuuchiを作ってみよう。
関数tsuuchiは関数get-seisekiを使って作られる。get-seisekiは受け取ったリストに名前があれば成績を返すが、無かった場合には'DataNashiと言う例外を投げる。
関数tsuuchiは基本的には印字の為の関数だが、'DataNashi例外を補足すると「データがありません」と言う文字列を生成する。
これも簡単ではあるが、例外処理の例となる。
さて、Schemeのguard。見た目は違うが、JavaやPythonの例外処理と同じ事をやってる事が分かっただろうか。いや、元々は例外処理、ってアイディア自体がLispベースに発展してきたんだから、JavaやPythonの例外処理の見た目の方が違う、っつった方が正確ではあるんだけどね。
しかしながら、try〜なんちゃら、の方が今じゃスタンダードで、Lisp型の見た目には慣れないかもしんない。
でも大丈夫だ。なんせLispにはマクロがあるんで、気に喰わなかったら見た目を変えてしまえばいい。
例えばtry〜なんちゃら、は次のようなマクロを書けば実現出来る。
実際はこのマクロはユニバーサルかどうか、ってのは保証の限りじゃない。なんつったって、「送出される例外」が仕様上実装依存なんで、predの部分が汎用性がある記述法だ、とは言えないんだ。
しかしながら、いずれにせよ、define-syntaxで似たようなモノは作れるだろう。実装がどんな例外を投げてくるのか、そして対処法の基本として、どうやるのが推奨されてるのか。
アナタ自身が「実装依存の」機能を作る事となるが、それがその実装への理解を深める事になるだろう。
※1: 死語になった「ワークステーション」企業、Sunは元々ハッカーが集って作ったような会社だ。黎明期のBSD UNIXの開発者(つまり、カリフォルニア大学バークレー校の出身者)やスタンフォード大学(ヒューレッドパッカード・Yahoo!・Googleの創業者は皆ここの卒業生)の出身者が集って結成した。
その機能をコピーして、UNIX用テキストエディタとして初めて制作されたのが、Java開発者、ジム・ゴスリングによる「Gosling Emacs」だ。これはUNIX用の商用テキストエディタとなり、またMock Lispと言う拡張用スクリプト言語を備えていた(Emacs Lispとは全く違う)。
「あれは酷いLispだった。見た目はLispでも中身は違う。エディタを自由に拡張出来るような性質のモノじゃない。」
と大変お冠だった(笑)。
※3: それに比すると、仕様上ではSchemeはかなり貧弱だ、とは言えるんじゃないか。
※4: Pythonの標準エラーは少なくとも30種類以上はある。
※5: 例えば次のようなプログラミング構造を持つ2つのプログラムを書いてみる。
前者の「条件分岐」版は変数your_viewpointはどっちにせよ1になる。片方の節に入ればもう一方は「実行されない」からだ。
一方、後者の「例外処理」版では、「妊娠」が発覚した時点で変数your_viewpointは2になる・・・時間を巻き戻した「アナタの経験」だ。
言い換えると、try節は必ず実行される。その結果によって(つまり妊娠が発覚したら)「時が巻き戻る」わけだ。しかしながらexcept内でのアナタの視点は「二回目のエッチ」になる。
※6: 僕もまだプログラミングに於いてチンコに毛が生えてなかった頃、例にもれず「例外処理」ってのは苦手で嫌いだった。
そんな僕の無知蒙昧を晴らしてくれたのが、またもや、現在Googleの人工知能研究の第一人者になっているピーター・ノーヴィック先生、だ。何度も出してる実用Common Lisp(PAIP)の著者だ。
ノーヴィックセンセはWeb上で((Pythonで) 書く (Lisp) インタプリタ)と言う文書を書いて公開している。題名の通り、PythonでLispを書こう、と言った(比較的)短い文書だ。
これを読んでた時、驚くべきコーディングが目に入った。
def atom(token):
"数は数にし、それ以外のトークンはシンボルにする。"
try: return int(token)
except ValueError:
try: return float(token)
except ValueError:
return Symbol(token)
アトム、と言うのはLispの最小単位、ある意味トークンなんだけど、この定義では例外処理を連鎖させてる事に驚いたんだ。
通常、あるデータが一体どんなデータなのか調べて変換、と言うのはデータ型を調べて適切な処理を施す「条件分岐」が必須となる。
ところがこのコードだと(対象がどんなデータ型なのか分からないのに)「無理矢理データ型を変換」しようとしてる。エラーが起きたら起きたでまた別のやり方を・・・と「エラーが起きるのをまるで怖がらない(気にしない)」コーディングだ。
これにはマジでビックリした。そして「こんなやり方があったんか!」とドギモを抜かれたわけだ。
この6行足らずのコードには現代の、モダンなプログラミング言語を扱う際の「叡智」が詰まってると思う。
さすがはノーヴィック先生、だ。
日本のへなちょこなプログラマが書いた本なんか読むのは止めて、とりあえずノーヴィックセンセの著作さえ読んどきゃいい、とか思ってるのはこういうトコなんだよな。
ノーヴィックセンセの書いたモノにはプログラミングの叡智、とかセンスが詰まってると思ってる。
※7: ANSI Common Lispの例外処理機構はhandler-caseと言う。