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

# Haskellの例外、今はコレ！ Control.Monad.Catchモジュール
　ドーモ、最近Haskellを拠り所の一つとして生きているあいやです。
Twitterで「ソースをいかにHaskellらしく書くか？」ということを質問して、例外型についての知見が得られたので記事書きます。

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

# **追記**
　`exceptions`パッケージよりも**スレッドセーフ**である（らしい）、`safe-exceptions`パッケージがリリースされています。  
(Please see [続・Haskellの最近の例外ハンドリング - syocy’s diary](http://syocy.hatenablog.com/entry/2016/08/28/175500))

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

`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](https://hackage.haskell.org/package/exceptions-0.8.3/docs/Control-Monad-Catch.html)モジュールとは、
既存のHaskellの例外モジュール([Control.Exception](https://hackage.haskell.org/package/base-4.9.0.0/docs/Control-Exception.html))がIO型に依存している問題を除去し、例外型を抽象化したものです。

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

1. [純粋関数での例外の利用が可能になる](#example-for-pure)
    - `Either e a`として例外を受け取れる
    - `StateT`とかでもできるよ！ (ここの記事では解説しません)
2. [例外が起こることを型で明示できる](#example-for-io)
    - 従来の関数型との簡単な比較
        - 従来の関数型: `readFile :: FilePath -> IO String`
        - MonadCatchでの関数型: `readFile :: (MonadCatch m, MonadIO m) => FilePath -> m a`


## 提供される3つの型クラス
　Control.Monad.Catchは以下の3つの型クラスを提供し、必要レベルに応じて使用することができます。

```haskell
-- 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で成された例外型の抽象化の例
```haskell
-- Control.Exceptionモジュールのthrow
throwIO :: Exception e => e -> IO a

-- Control.Monad.Catchモジュールのthrow
throwM :: (MonadThrow m, Exception e) => e -> m a
```

```haskell
-- 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`型に置き換わっているのがわかるかと想います。


### 純粋関数の例外の利用が可能になる <a name="example-for-pure"></a>
　純粋関数での例外では、異常値を受け取れる`Either`型を使います :)

　ここで`Control.Exception`を用いているのは、ArithException型を扱いたいという理由のみであることに注意です。

```haskell
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を関数型に明示しています。

```haskell
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


### 例外が起こることを型で明示できる <a name="example-for-io"></a>
　従来どおりIOで例外を扱うこともできます。

　Preludeで定義されている関数`readFile :: FilePath -> IO String`はIO型を使い、
環境にあるファイルをStringとして読み込みますが、
もし指定されたファイルパスにファイルがなかった場合、無言でIO例外を投げてきます X(

```haskell
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で例外を安全に受け取ってみます。

```haskell
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


## 参考ページ
- [Control.Monad.Catch](https://hackage.haskell.org/package/exceptions-0.8.3/docs/Control-Monad-Catch.html)
- [Control.Exception](https://hackage.haskell.org/package/base-4.9.0.0/docs/Control-Exception.html)
- [Haskellのthrowできる例外型と値コンストラクタ一覧](http://qiita.com/aiya000/items/20f6f842056d7e475005)