Haskellの例外処理事情

# Haskellの例外処理事情

　Haskellを使うたびに例外について調べ直す癖がついているので、諸々をまとめておく。

## TL;DR

1. 部分関数を使うな。
1. 失敗可能性は`Maybe`？`Either`？`IO`？ -> 迷ったら`MonadThrow`を使え。
1. 予めエラー型を統一しないと例外処理クソだるいんだけど -> `SomeException`を使え。
1. ライブラリどれ使えばいいの？ -> `safe-exceptions`を使え。

## 部分関数を使うな

　死の化身。部分関数自体を避けるしか方法はない。用意された部分関数を使う場合は必ずラップすること。
　パターンマッチの失敗や`undefined`, `error`等のピュアな文脈での例外は、`IO`モナドの中でのみキャッチすることができる。しかし、ピュアな文脈で解決できる問題を神モナドで解決するのはあまり嬉しくない。ピュアなものはピュアなものに、神のものは神に返すべきである。
　もし失敗をどの型で表すのか迷ったら、続くセクションにあるように、`MonadThrow`を使うとよい。

```Haskell
-- 'head'は部分関数なので、ラップして使おう。
safehead :: [a] -> Maybe a
safehead a = head a <$ guard (not $ null a)

-- 'MonadThrow'を使う
safehead' :: MonadThrow m => [a] -> m a
safehead' = maybe (throwString "could not take a head") return . safehead
```

## 失敗可能性は`Either`？ `Maybe`？ `IO`？

　失敗可能性をどのモナドで表すか問題。結論を言えば、`MonadThrow`型クラスを使ってモナドを一般化し、`SomeException`型を使って例外型を一般化するのがよい。
　ここで考えたい問題は以下の3点だ。

1. 失敗可能性を`Either`で表すと、`Left`の型に困る。`do`文は`Left`の型が食い違った`Either`モナドを混ぜて使うことはできない。
1. かといって失敗可能性を`Maybe`で表すと、失敗の原因を伝えられない。
1. `IO`を使えば、上二つの問題を同時に解決できる。しかし、ピュアな文脈が汚染されてしまう。

　これらの問題を解決するために、例外を扱うライブラリが作られている。基本的なアプローチは、失敗の送出と失敗のハンドリングをそれぞれ型クラスを使って抽象化することだ。さらに、存在量化された型を使えば、失敗の型を統一することもできる。

### MonadThrow

　失敗可能性を表す型クラスが`MonadThrow`だ。`IO`や`Maybe`, `Either`はもちろん、`State`や`Cont`等`transformers`に収録される様々なモナドの実装があり、これを使うとこれらモナドの失敗を統一的に表現できる。
　例えば、安全な`div`を下のように実装する。この`safediv`は、返ってくる型を受け取り手が選択することができる。

```haskell
import Control.Monad          (when)
import Control.Exception.Safe (MonadThrow, SomeException, throwString)

safediv :: MonadThrow m => Int -> Int -> m Int
safediv m n = do
    when (n == 0) $
        throwString "divided by 0"
    return $ m `div` n

main :: IO ()
main = do
    print $ (2 `safediv` 0 :: Either SomeException Int) -- 1
    print $ (2 `safediv` 0 :: Maybe Int)                -- 2
    2 `safediv` 0 >>= print                             -- 3
```

　1は例外を`Either`として受け取っている。`SomeException`という型は、`Exception`型クラスを実装した型ならば何でも入れることができる存在量化された型だ。`throwString`が`StringException`という`Exception`型クラスを実装した型を投げる。
　2では例外は`Nothing`となる。例外の送出時に付加した情報は失われる。
　3では例外は`IO`モナドで処理されることになる。すなわち、プログラムはクラッシュするということだ。

#### 便利なスニペット

　たまに欲しくなる、`Maybe`や`Either`を`MonadThrow`に変換するやつ。`maybe`や`|||`を`throw`と`return`で挟む覚えやすい形だ。
　`|||`は`Control.Arrow`に含まれている。余談だが、`Control.Arrow`にはややトリッキーだが`Either`を便利に使う関数がたくさん含まれている。

```Haskell
maybeToMonadThrow :: (MonadThrow m, Exception e) => e -> Maybe a -> m a
maybeToMonadThrow e = throw e `maybe` return

eitherToMonadThrow :: (MonadThrow m, Exception e) => Either e a -> m a
eitherToMonadThrow = throw ||| return
```

### SomeException

　失敗の原因を外部に伝えようとするとその失敗を表す型の扱いに困る問題を解決するのが、`SomeException`型だ。これは存在量化された型で、`Exception`型クラスを実装している型なら何でも突っ込むことができる型である。
　`SomeException`はおおよそ以下のような定義となっている。

```Haskell
{-# LANGUAGE ExistentialQuantification #-}

data SomeException = forall e. Exception e => SomeException e
instance Exception SomeException
```

　自作の例外を作るときは以下のようにする。これで`MyException`を`SomeException`に変換できるようになる。
　`throw`を使えば、自作の例外を`SomeException`に変換したうえで文脈から脱出できる。

```Haskell
data MyException = ZeroDivisionException deriving (Show)
instance Exception MyException

safediv :: MonadThrow m => Int -> Int -> m Int
safediv m n = m `div` n <$ (when (n == 0) $ throw ZeroDivisionException)
```

　外部ライブラリの関数が独自の例外型を定義していても、統一的に扱うことができる。

```Haskell
-- 外部ライブラリの提供する例外型と、それを投げる関数
data ExternalException = ReadException deriving (Show)
instance Exception ExternalException

saferead :: MonadThrow m => String -> m Int
saferead = maybe (throw ReadException) return . readMaybe

-- 外部ライブラリの例外と自作ライブラリの例外を混ぜて扱う
readAndDivide :: (MonadCatch m) => String -> String -> m Int
readAndDivide m n = do
    m' <- saferead m
    n' <- saferead n
    m' `safediv` n'
```

### MonadCatch

　`MonadThrow`は例外を投げる部分を統一的に書けるようにした。しかし、投げられた例外をハンドルする部分はどうだろうか。
　`readAndDivide`が投げるのは`ZeroDivisionException`と`ReadException`で、そのうち`ZeroDivisionException`を処理すると、以下のような処理になる。

```Haskell
sample :: String -> String -> Either SomeException String
sample m n = case (readAndDivide m n :: Either SomeException Int) of
    Right r -> Right $ show r
    Left e -> case fromException e of
        Just ZeroDivisionException -> Right $ show ZeroDivisionException
        Nothing -> Left e
```

　これを見れば、`MonadThrow`同様、例外をキャッチする箇所も抽象化しようと思うのは自然な発想だろう。
　`MonadCatch`とそのメソッド`catch`を使えば、これを下にように書ける。

```Haskell
sample' :: MonadCatch m => String -> String -> m String
sample' m n = do
    show <$> readAndDivide m n
    `catch` \(ZeroDivisionException) ->
        return $ show ZeroDivisionException
```

　複数の例外をハンドルしたい場合は、`catch`をネストすればよい。

```Haskell
sample'' :: MonadCatch m => String -> String -> m String
sample'' m n = (do
    show <$> readAndDivide m n
    `catch` \ZeroDivisionException
        -> return $ show ZeroDivisionException)
    `catch` \ReadException
        -> return $ show ReadException
```

　あるいは、`catches`という関数を使う。

```Haskell
sample'' :: MonadCatch m => String -> String -> m String
sample'' m n = do
    show <$> readAndDivide m n
    `catches`
        [ Handler $ \ZeroDivisionException -> return $ show ZeroDivisionException
        , Handler $ \ReadException -> return $ show ReadException
        ]
```

## ライブラリはどれ使えばいいの？

　`safe-exceptions`を使うのが良い（らしい）。
　上のような機能が使える例外ライブラリはいくつかあるが、`safe-exceptions`は必要に応じて`base`, `mtl`, `transformers`, `exceptoins`あたりの主要なライブラリの内容を再エクスポートしつつ、非同期例外も安全に扱えるパッケージになっている。
　必要なものは再エクスポートされているため、例外を扱う目的では`safe-exceptions`だけインポートしていればよい。

## まとめ

　Haskellの例外処理事情についてざっくりとまとめた。大体この辺が思い出せればHaskellの例外周りでは困らないはず。
　より新しい情報があれば、そちらへのリンクをコメントに頂けると幸いです。