例外について
ちゃんとしたプログラムを作ろうとすると、
正常な動作をするようにプログラムを行うだけではなく
エラーが起こった際の処理もきちんとこなすように
プログラミングする必要があります。
そのため、プログラマはエラー処理を書く訳ですが
統一的にエラー処理を行うために、F#には例外機構が用意されています。
例外の説明に入る前に
まず、覚えておきたいのは次のとんでもない事実です。
ポイント
例外が発生してもキャッチしなければ、プログラムは「即、終了する」
そのうえ、例外は簡単に発生してしまいます。
0での割り算による例外発生
> 1/0;;
System.DivideByZeroException: 0 で除算しようとしました。
場所 <StartupCode$FSI_0004>.$FSI_0004._main()
stopped due to error
この例では、0での割り算によって
System.DivideByZeroExceptionが発生し
特に何もしていないのでプログラムが異常終了しています。
このプログラムならすぐ終了しても問題ないと思いますが
頑張って作ったGUIアプリが一瞬で終了したら悲しいですよね。
このようなことが起きないようにするためには
・例外を確実に処理すること
・どういう場合に例外が発生するかを知ること
が必要になります。
例えば、.Netフレームワークを使うのであれば
MSのドキュメントに、各クラスのメソッドがどんな例外を投げるのかが載っています。
クラスのコンストラクタなんかも例外をなげるので良く確認する必要があります。
実際にプログラミングを行う上では
具体的にどういった例外を投げるかまではわからなくとも、最低でも
「該当の関数が例外を投げるかどうか」
ぐらいはちゃんと調べておく必要があります。
(自分は趣味なので結構適当にやってますが・・)
ところで、F#の元になったOCamlという言語にも
.Net Frameworkにもどちらにも例外機構が存在します。
F#の例外機構はどちらよりなのでしょう?
これについては、次の段で説明します。
例外の基本的な操作
F#での例外はexn型で表します。
exn型は、.NetでいうSystem.Exceptionクラスの省略記法になります。
.Net的に例外を使おうと思った場合は
通常Exceptionクラスを使わずに
Exceptionクラスを継承したクラスを使います。
(継承については、オブジェクト指向の節を参照ください)
F#では、.NET由来の例外とF#由来の例外の2種類を両方扱えます。
ただ、基本的にはF#由来の例外を使用し、>NET由来の例外は
.NET Frameworkとのやり取りの上で使用するという形になるでしょう。
F#の例外を定義するにはexceptionキーワードを用います
例外の定義
//例外の定義。例外名の先頭文字が小文字だとエラーなので注意
> exception Siyou_desu of string;;
exception Siyou_desu of string
//特に引数は無くてもOK
//> exception Siyou_desu;;
//以後、普通のDiscriminated Unionのように使える。
> Siyou_desu "easter egg";;
val it : exn = Siyou_desuException ()
これはSiyou_desuExceptionの定義です。
(名前は若干不真面目なので参考にしないでね)
下の行で生成した値は、
実際に例外を投げる際に使用します。
#ちなみにEasterEggとは、
#本来の目的とは無関係なおまけ的機能のことです。
#おまけ機能で例外が発生してプログラムが落ちたら
#おまけにならないですが:-p
実際に例外を発生させてみたコードが以下になります。
例外を投げるにはraise 例外とします。
インタプリタでは型エラーで弾かれるので
コンパイラで実行した結果次のようになります。
raiseにより例外を発生させる
exception Siyou_desu of string;;
raise (Siyou_desu "EasterEgg");;
実行結果
//問題が発生したため・・といったダイアログが出る
ハンドルされていない例外: Program+Siyou_desuException: EasterEgg
場所 Microsoft.FSharp.Core.Operators.raise[A](Exception exn)
場所 <StartupCode$excp>.$Program._main() 場所 C:¥Documents and Settings¥username¥My D
ocuments¥Visual Studio 2008¥Projects¥excp¥Program.fs:行 4
続行するには何かキーを押してください . . .
また、raise以外にも、
例外を投げるのに便利な関数が定義されています。
failwith 文字列
FailureExceptionを発生させる(F#省略形はFailure)
例:failwith "寝坊した";;
invalidArg パラメータ文字列 エラーメッセージ文字列
System.ArgumentExceptionを発生させる
例:invalidArg "month" "13"
これらの型は次のようになっています。
ちょっと特徴的なのは、通常の方法で関数が終わる必要がない(例外発生)ため、
->の右側の型が具体的には決まらずに'aという多相型になっている点です。
例外関係の関数の型
> raise;;
val it: (System.Exception -> 'a)
> failwith;;
val it: (string -> 'a)
> invalidArg;;
val it: (string -> string -> 'a)
次に発生した例外をキャッチします。
例外のキャッチには次の構文が使用できます。
- try-with expression
-
try 式 with ルールの繰り返し
- try-finally expression
-
try 式1 finally 式2
最初の構文(try-with)は、式が例外を投げた場合、
ルールの繰り返し(パターンマッチ)に当てはまれば
その箇所の式を評価するという構文です。
2番目の構文(try-finally)は、式1が例外を投げようが投げまいが
式2も必ず評価されることが保証されます。
例外自体はキャッチされません。
また、他の言語ではtry-catch-finallyと3段階バージョンもあったりしますが
F#では(おそらくOCamlがサポートしていないため)サポートしていないようです。
気をつけるべき点として、try-finallyを使う場合は
別の箇所でキャッチする必要がある、という点になります。
ここで、ルールはパターンマッチの際の構文と同じものになります。
もう少し詳しく書けばこうなります
- try-finally
- ルールの繰り返し := ['|'] ルール '|' ... '|' ルール
ルール := パターン [when 式] -> 式
早速、実際に例外をキャッチして処理してみます。
例外のキャッチ(try-catch expression)
try
//invalidArg "お金" "足りる?"
failwith "10円足りない"
with
| Failure "10円足りない" -> printfn "おまけするよ"
| Failure "100円足りない" -> printfn "仕方ないな"
| Failure "1000円足りない" -> printfn "つけとくね"
| Failure x -> printfn "携帯電話預かります"
| x -> reraise();;
このコードを実行すると10円足りない場合のケースが実行され
"おまけするよ"と表示されます。
また、コメントを外した場合はSystem.ArgumentException例外が発生するため
最下段のケースに当てはまり、reraise()が実行されます。
このreraiseというのは、キャッチした例外を再度投げるための構文です。
次はfinallyを使った例です
例外のキャッチ(try-finally expression)
try
1/0
finally
printfn "まぁ気にしない";;
このコードは、1/0を実行した時点でゼロ除算の例外が発生してしまいます。
finallyは別にコードをキャッチしているわけではないため、プログラムは
異常終了してしまいます。
ただし、finallyの下のコードはちゃんと実行されるため
以下の実行結果ではちゃんと文字列が出力されています。
上記プログラムの実行結果
ハンドルされていない例外: System.DivideByZeroException: 0 で除算しようとしまし
。
場所 <StartupCode$zerodiv>.$Program._main() 場所 C:¥Documents and Settings¥username¥My
ocuments¥Visual Studio 2008¥Projects¥zerodiv¥Program.fs:行 4
まぁ気にしない
このケースでは、例外のキャッチは別で行う必要があることになります。
finallyを使うのは、基本的には次のようなケースになると思います。
1.メモリやリソースなどの資源を確保
2.例外が発生するコードを実行
3.メモリやリソースなどの資源を解放
(2.の部分をtryで囲み、3.の部分をfinallyで囲む)
もし、2.の部分で例外が発生してしまうと
3.の部分は実行されないため
リソースやメモリのリークが発生してしまいます。
try-finallyの使用例2
open System.IO;;
let fs = new FileStream("a.txt",FileMode.Create) in
try
fs.WriteByte(48uy);
finally
fs.Dispose();;
このプログラムは0(文字コード48)とだけかいた
a.txtファイルを作成するプログラムです。
finallyによってfs.Dispose()が必ず呼び出されます。
Disposeというのは、いわば「後処理するよ」、というような意味の関数で
.NetではSystem.IDisposableというインタフェースを継承しているクラスでは
必ずこのメソッドを呼び出せます。
ただ、このケースの場合次のuse構文を用いるとすっきりかけます。
上記と等価なコード(冗語構文)
open System.IO;;
let f () =
use fs = new FileStream("a.txt",FileMode.Create) in
fs.WriteByte(48uy);;
f();;
上記と等価なコード(軽量構文)
open System.IO
let f () =
use fs = new FileStream("a.txt",FileMode.Create)
fs.WriteByte(48uy)
f()
use構文は、inで囲まれたスコープのブロックを
tryで囲み、finallyでfsがNULLでなければDisposeを呼び出す、
というコードに内部的に置き換えてくれます。
トップレベルのモジュールで直接useを使うと警告が出てuseはletに置き換わるようなので
一旦関数fを定義してその中でuseを使っています。
もしトップレベルにuesを書いた場合、letに置き換わってしまうとa.txtは出来ますがテキストの中身は
空になってしまいます。
なお、単にDisposeを呼び出す以外の処理がしたい場合は
自分でtry〜を書く必要があります。
また、注意点としては、
上のコードは単純にDisposeを呼び出す保証がされただけで
例外はキャッチされていない
ので気をつけてください。
例えばa.txtを読み取り専用属性にして再度プログラムを実行すると
例外が発生し、プログラムがクラッシュするので
このことを確かめることができます。
その他、基本的な例外と例外を扱う関数について
付録1にまとめてありますので
参考にしてみてください。
.Net用の参考資料
(2009/02/22修正。嘘書いてました^^; いげ太様 ご指摘ありがとうございます)
.Net使う上で、例外については以下のMicrosoftのサイトが参考になります。
例外の推奨事項(コードはC#ですが)
ApplicationException クラス
以下一部抜粋
ほとんどのアプリケーションでは、Exception クラスからカスタム例外を派生します。
本来、カスタム例外は ApplicationException クラスから派生しなければ
ならないと考えられていましたが、実際には、
それによって大きな価値が付加されたことはないようです。
「ユーザが自分で作るアプリケーションは
ApplicationException クラスを継承したクラスを
そのアプリケーション用の基本例外クラスとして作る」
というガイドラインがあるようです。
Expert F#(p88)によると
exception構文で作った例外は
実はコンパイラ的にはSystem.Exceptionクラスの派生クラスに翻訳されるため
.Netフレームワークの例外機構と組み合わせてもうまくいくように
なっているようです。
次のように.NETの例外を直接投げることもできます。
.NETの例外を直接発生させる
try
raise (System.ArgumentException("hoge"))
with
| e-> reraise()