LoginSignup
33
26

More than 5 years have passed since last update.

Haskellの例外、今はコレ! Control.Monad.Catch

Last updated at Posted at 2016-07-30

Haskellの例外、今はコレ! Control.Monad.Catchモジュール

 ドーモ、最近Haskellを拠り所の一つとして生きているあいやです。
Twitterで「ソースをいかにHaskellらしく書くか?」ということを質問して、例外型についての知見が得られたので記事書きます。

更なる知見もまだまだもっと募集中。

追記

 exceptionsパッケージよりもスレッドセーフである(らしい)、safe-exceptionsパッケージがリリースされています。

(Please see 続・Haskellの最近の例外ハンドリング - syocy’s diary)

非同期例外を用いる場合はこちらを使ったほうが良いようです。

exceptionsControl.Monad.Catchモジュールが
safe-exceptionsControl.Exception.Safeモジュールに対応しており、
exceptionsControl.Monad.Catch.Pureを使っていない限りは、本記事の以下の内容と同様の使い方ができます。

(safe-exceptionsには、Control.Monad.Catch.Pure相当のモジュールはないようです)

インストール方法

  • stack
    • stack install exceptions

Control.Monad.Catchモジュールとは

 Control.Monad.Catchモジュールとは、
既存のHaskellの例外モジュール(Control.Exception)がIO型に依存している問題を除去し、例外型を抽象化したものです。

これにより、以下の恩恵を受けられます。

  1. 純粋関数での例外の利用が可能になる
    • Either e aとして例外を受け取れる
    • StateTとかでもできるよ! (ここの記事では解説しません)
  2. 例外が起こることを型で明示できる
    • 従来の関数型との簡単な比較
      • 従来の関数型: 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

参考ページ

33
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
26