Haskellの例外、今はコレ! Control.Monad.Catchモジュール
ドーモ、最近Haskellを拠り所の一つとして生きているあいやです。
Twitterで「ソースをいかにHaskellらしく書くか?」ということを質問して、例外型についての知見が得られたので記事書きます。
更なる知見もまだまだもっと募集中。
追記
exceptions
パッケージよりもスレッドセーフである(らしい)、safe-exceptions
パッケージがリリースされています。
(Please see 続・Haskellの最近の例外ハンドリング - syocy’s diary)
非同期例外を用いる場合はこちらを使ったほうが良いようです。
exceptions
のControl.Monad.Catch
モジュールが
safe-exceptions
のControl.Exception.Safe
モジュールに対応しており、
exceptions
のControl.Monad.Catch.Pure
を使っていない限りは、本記事の以下の内容と同様の使い方ができます。
(safe-exceptions
には、Control.Monad.Catch.Pure
相当のモジュールはないようです)
インストール方法
- stack
stack install exceptions
Control.Monad.Catchモジュールとは
Control.Monad.Catchモジュールとは、
既存のHaskellの例外モジュール(Control.Exception)がIO型に依存している問題を除去し、例外型を抽象化したものです。
これにより、以下の恩恵を受けられます。
-
純粋関数での例外の利用が可能になる
-
Either e a
として例外を受け取れる -
StateT
とかでもできるよ! (ここの記事では解説しません)
-
-
例外が起こることを型で明示できる
- 従来の関数型との簡単な比較
- 従来の関数型:
readFile :: FilePath -> IO String
- MonadCatchでの関数型:
readFile :: (MonadCatch m, MonadIO m) => FilePath -> m a
- 従来の関数型:
- 従来の関数型との簡単な比較
提供される3つの型クラス
Control.Monad.Catchは以下の3つの型クラスを提供し、必要レベルに応じて使用することができます。
-- throwを扱うためのMonadThrow型クラス
class Monad m => MonadThrow m
-- catchを扱うためのMonadCatch型クラス
class MonadThrow m => MonadCatch m
-- finally, catches, その他多くのユーティリティ関数を扱うための
-- Control.Monad.Catchの例外型クラスの親玉
class MonadCatch m => MonadMask m
例えば…関数内でthrowは使用したいけど、throwした例外を__明示的には__catchされなくてもいい…といった場合には
Control.Monad.Catchで成された例外型の抽象化の例
-- Control.Exceptionモジュールのthrow
throwIO :: Exception e => e -> IO a
-- Control.Monad.Catchモジュールのthrow
throwM :: (MonadThrow m, Exception e) => e -> m a
-- Control.Exceptionモジュールのcatch
catch :: Exception e => IO a -> (e -> IO a) -> IO a
-- Control.Monad.Catchモジュールのcatch
catch :: (MonadCatch m, Exception e) => m a -> (e -> m a) -> m a
IO型がそのままMonadCatch m => m
型に置き換わっているのがわかるかと想います。
純粋関数の例外の利用が可能になる
純粋関数での例外では、異常値を受け取れるEither
型を使います :)
ここでControl.Exception
を用いているのは、ArithException型を扱いたいという理由のみであることに注意です。
import Control.Exception (ArithException (..))
import Control.Monad.Catch
main :: IO ()
main = do
let x = 10 `safeDiv` 0
case x of
Left a -> print (a :: SomeException)
Right b -> print b
safeDiv :: MonadThrow m => Int -> Int -> m Int
safeDiv x y =
if y == 0
then throwM RatioZeroDenominator
else return $ x `div` y
上記の例ではEither値をパターンマッチしてますが、catchを使って必ずRightを受け取るように仕組むこともできますよ!
catchを使いたいので、MonadCatchを関数型に明示しています。
import Control.Exception (ArithException (..))
import Control.Monad.Catch
main :: IO ()
main = do
-- 10/0 が計算できなかったら 代わりに 10 を返す
-- この場合、Left値は絶対に返らないので安心
let Right x = 10 `safeDiv` 0 `catch` ten
print x
where
ten :: SomeException -> Either SomeException Int
ten _ = return 10
safeDiv :: MonadCatch m => Int -> Int -> m Int
safeDiv x y =
if y == 0
then throwM RatioZeroDenominator
else return $ x `div` y
「例外が起こったら決まった値を返す」ユースケースにとても便利です。
またこれらの例で、型レベルではEither型が隠蔽されていることについても重要です。
それはEither型を直接指定するよりも、MonadCatch型クラスを用いた方が__例外としての__型の表現が適切だからです :D
例外が起こることを型で明示できる
従来どおりIOで例外を扱うこともできます。
Preludeで定義されている関数readFile :: FilePath -> IO String
はIO型を使い、
環境にあるファイルをStringとして読み込みますが、
もし指定されたファイルパスにファイルがなかった場合、無言でIO例外を投げてきます X(
main :: IO ()
main = do
x <- readFile "aho.txt"
putStrLn x
putStrLn "end"
出力結果
aho.txt: openFile: does not exist (No such file or directory)
とても怖いですね。
ですのでこれらもMonadCatchで例外が起こることを主張してしまいましょう :P
安全ついでに、runEitherTで例外を安全に受け取ってみます。
import Control.Monad.Catch
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Either (runEitherT)
import Prelude hiding (readFile) -- readFileは自前で定義するよ!
import System.Directory (doesFileExist)
import qualified Prelude as P
data FileException = FileException String deriving (Show)
instance Exception FileException
main :: IO ()
main = do
x <- runEitherT $ readFile "aho.txt"
case x of
Left a -> print (a :: FileException)
Right b -> putStrLn b
putStrLn "end"
readFile :: (MonadThrow m, MonadIO m) => FilePath -> m String
readFile fp = do
b <- liftIO $ doesFileExist fp
if not b
then throwM $ FileException (fp ++ " was not found")
else liftIO $ P.readFile fp
aho.txtがない場合の出力結果
Test.hs: FileException "aho.txt was not found"
aho.txtがちゃんとある場合の出力結果 (aho.txtの内容が表示される)
{detail of the aho.txt}
end
Good !!!
ちなみに、このIOコードもEitherT FileException IO String
という型が隠蔽されて使われています!!
だからmain関数ではrunEitherTが使えています :D
MonadThrow/MonadCatch/MonadMaskを積極的に使っていきましょう
MonadThrow/MonadCatch/MonadMaskは貴方のコードを__Haskellらしく__ドレスアップしていくことでしょう。
それは、Haskellが様々なコードの振る舞いを型で表現する言語だからです :)
enjoy :D
Thanks
- この記事の検閲もしてくれたTwitterの某氏
- and you :P