リツイート数が30を超えたので、Haskell での例外処理について説明します。僕が思うに、Haskell での例外処理が分かりにくいのには、2つ理由があります。
- ライブラリの混乱
- パラダイムの違い
歴史的経緯により、Prelude にも Control.OldException にも Control.Exception にも catch があります。歴史的経緯を説明するのは面倒なので、これだけ覚えて下さい。「Control.Exception だけを使って、それ以外は忘れる」
そもそも純粋関数型で catch とか言われても分からないかもしれません。Haskell では、純粋な関数と IO とでは、例外処理の方法が異なります。命令的な catch などを使うのは IO です。純粋な関数には Maybe か、Either を使います。
純粋な関数
純粋な関数では、原則として例外を投げてはいけません。では、どうするのでしょうか? ほら、C 言語には例外なんてなかったことを思い出して下さい。純粋な関数では、返り値で例外を表現するのです。
とにかく失敗だと分かればいいのであれば、Maybe を使います。失敗は Nothing で表します。以下、Data.List の lookup の例です:
lookup :: (Eq a) => a -> [(a,b)] -> Maybe b lookup _key [] = Nothing lookup key ((x,y):xys) | key == x = Just y | otherwise = lookup key xys
正常の場合と例外の場合は、case 式で処理しましょう。(case 式が面倒になってきたら、Data.Maybe の maybe を使いましょう。)
Haskell では、純粋な関数であれば、失敗する可能性があるのか、型から判断できます。Haskell 初心者にはよく分からないかもしれませんが、これは素晴らしい機能です。Haskell では、Null ポインターを踏むことがなくなるのですから。
失敗した理由も伝えたい場合は、Either を使います。以下は、Parsec の parser の型です:
parse :: Stream s Identity t => Parsec s () a -> SourceName -> s -> Either ParseError a
正常の場合と例外の場合は、case 式で処理しましょう。
とは言っても、お行儀の悪い純粋な関数はあります。たとえば、純粋な関数である head や tail は例外を投げますね。これを Haskell の恥の一つと考える人もいます。head や tail は、例外を投げようがない場合(リストが空でないのを確かめた場合)に限り、使ってもいいのです。
IO
IO は、命令型と同じような感じで、例外を使います。IO という型自体が、失敗するかもしれないことを意味しています。
これ以降、Control.Exception を import しているとして話を進めます。
import Control.Exception as E
例外を止める系
例外を捕まえて、そこで止める、つまり上位に伝えないためには、catch を使います。
catch :: Exception e => IO a -> (e -> IO a) -> IO a
こういう風に使います。
catch (したいこと) (例外処理)
catch を二項演算子にして使うとおしゃれです。
(したいこと) `catch` (例外処理)
(したいこと)は、普通の IO です。問題は、(例外処理)が引数に Exeption e を取る関数だと言うことです。初心者には e の型を決めるのが、とても困難でしょう。とりあえず、どんな例外も合致する SomeException という型があるので、最初はそれを使ってみましょう。
以下は、ファイル関係のエラーを無視する例です:
ignore :: SomeException -> IO () ignore _ = return () copy :: FilePath -> IO () copy file = do xs <- readFile file writeFile file xs safeCopy :: FilePath -> IO () safeCopy file = copy file `E.catch` ignore
mapM に対して forM が定義してあるように、Haskeller としては本質的なところを目立たせて書きたくなります。そこで、引数の順番を入れ替えた、handle という関数もあります。これを使えば、copy という補助関数が不要になります。
safeCopy :: FilePath -> IO () safeCopy file = handle ignore $ do xs <- readFile file writeFile file xs
例外をスルーする系
例外が起こったときに何かをするけど、その例外は止めない、つまり上位に伝達する関数に onException があります。
onException :: IO a -> IO b -> IO a
どんな例外が起こったかは、上位の関数が catch して調べることを想定しているので、例外処理をする第二引数は関数でないことに注意して下さい。こういう風に使います。
(したいこと) `onException` do -- 例外は上位関数に伝搬するよ 例外処理を書く 例外処理を書く
似たような関数に finally があります。これは、正常終了したときと、異常終了したときの両方で、後処理を実行します。
(したいこと) `finally` do -- 例外は上位関数に伝搬するよ 後処理を書く -- 正常終了してもここにくるし 後処理を書く -- 異常終了してもここにくるよ
ローンパターン
ある資源を確保して、正常終了しても異常終了しても、資源を解放したいことはよくあります。それには、bracket という関数を使います。
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
こういう風に使います。
bracket (資源割当) (資源解放) (したいこと)
ポート番号から、リッスンソケットを開き、したいことをした後、必ずソケットを解放する withSocket という関数は以下のように定義できます。
import Network withSocket :: PortID -> (Socket -> IO ()) -> IO () withSocket port body = bracket (listenOn port) sClose body
bracket も例外を上位に伝搬します。
おまけ
Haskell は、非同期例外も処理できます。非同期例外というのは、そのスレッドの動作によって引き起こされた訳ではない例外です。簡単に言えば、自分が他のスレッドから kill されたときの例外も捕まえられるのです。
> tid <- forkIO ((threadDelay 5000000 >> putStrLn "Completed") `onException` putStrLn "Killed") > killThread tid Killed ← 非同期例外を捕まえた > tid <- forkIO ((threadDelay 5000000 >> putStrLn "Completed") `onException` putStrLn "Killed") > Completed ← 5秒間生き延びた
明日に続く
時間切れなので、例外の投げ方と自分用の例外を作る方法は、明日書きます。