Haskellでのエラー処理

なんでもセミナーで質問があった手前、GHCガベージコレクションの事とか調べようと思っていたのだけれど、このところ大学の実験で余裕がないので、しばらく実験中に気づいた事とか書いてこうと思います。

Haskellでのエラー処理の難しさ

Haskellで手軽に使えるエラー処理としてはerror関数を使う方法がありますが、例えば以下のコードの場合

parse '1' = 1
parse '2' = 2
parse '3' = 3
parse x   = error $ "invalid input : "  ++ show x

main = do
    input <- getContents

    putStrLn "parsing ... "
    nums <- return $ map parse input

    putStrLn "add numbers ... "
    result <- return $ sum nums

    putStr $ "result : " ++ show result

出力はこうなってしまいます。

% echo "123x123" | ./gen_error                                                                                                       
parsing ... 
add numbers ... 
gen_error: invalid input : 'x'

通常の感覚だとパースの時点でエラーになってほしいわけですが、Haskellは遅延評価なのでerrorが初めて評価されるsumのタイミングでエラーになっています。error関数の実体はErrorCallという例外の送出なわけですが、安易に多用するとおかしなことになります。

エラーをうまく扱うには

Haskellでエラーを扱う場合に問題になることには主に
1. エラー情報の扱い
1. エラー発生のタイミング
1. I/O処理
の3つが挙げられると思いますが、大きなプログラムでこれらを上手に扱う為には多くの場合

エラーを収集して、例外を飛ばす

という方法をとることなると思います。

エラーを収集というのは、エラー情報に適切に型を与え、特定の一箇所で確実に評価をしてやるということです。
例外を送出する関数は、戻り値が任意の型へ変換されるのが普通なので、他の値に紛れてしまい情報の正しい伝達と評価タイミングの操作が困難になります。
処理が一段落するポイントで確実にエラーを評価した上で、main関数に向かって例外としてthrowしてやれば、上の3つの問題にうまく対処することができます。

もちろん、プログラムの規模によっては例外を使わない方が安全だと思います。

エラーの収集

まず、エラーを起こしうる関数は中で例外を飛ばすのではなく、EitherやMaybeなどを使って戻り値としてエラーを返してやるようにします。
例えば

if (エラー発生)
  then Left "ERROR"
  else Right x

という感じで、Leftにエラーメッセージを入れて返すなどといった事をします。
後でパターンマッチによって中身を取り出す分けですが、その時にはLeftかRightかが分かるまでしか評価されないので、中身の値の評価は上手く遅延されてくれます。
データの持ち運びにはモナドを利用する事が多いと思いますが、自分で定義する以外にControl.Monad.Errorを使うという選択肢もあります。

import Control.Monad.Error

f x | x >= 0    = Right x
    | otherwise = Left "ERROR: negative number"

g x | x `mod` 2 == 0 = Right x
    | otherwise      = Left "ERROR: odd number"

-- Eitherをモナドとして扱える。
check x = do
    a <- f x
    b <- g a
    return b

main = 
    case check 3 of
         Right x    -> putStrLn "OK"
         Left msg   -> putStrLn msg

例外の送出

例外を送出する場合にはthrow系の関数を使います。とりあえず今回はthrowDynの紹介。
throwDynはユーザ定義型をDynamic型にして例外として送出する為の関数です。Typeableなものを何でも投げられます。

{-# OPTIONS_GHC -fglasgow-exts #-}
import Data.Typeable

data MyException 
    = ExceptionA String
    | ExceptionB ...
    | ...
    deriving (Show, Typeable)

投げるときは

throwDyn $ ExceptionA "Error"

の様にします。

捕捉する時にはcatchDynを使います。

main = do
    ......
    `catchDyn`
    (\e ->   ...
              case e::MyException of  -- Dynamicなので型の指定が必要
                   ExceptionA msg   ->  ....
                   ExceptionB ....
   )

例外はいつ飛んでくるのか分からないので、捕捉する側でしっかり配慮するべきです。他の言語に当てはまる注意がHaskellでも当てはまります。
特に、ファイルI/Oの最中などに例外に割り込まれると大変なので、block/unblock/bracket/finallyなど例外をブロックする関数が用意されています。使い方については、ソースコード中に丁寧に例が書いてあるので、そちらを参照してください。