エラー処理

Control.Exception

エラーを扱う低レベルの(とても危険な)方法は、throw[例外の発生]関数と catch[例外の捕捉]関数を使うことです。これらの関数を使えば、純粋なコードで拡張可能な例外を投げることができますが、得られる例外は IO 内で捕捉します。特に指摘しておくべきなのは、throw の返す値は任意の型を持ちうるということです。低レベルのシステム操作を使わないカスタムコードでこれを使う理由はありません。

throw :: Exception e => e -> a
catch :: Exception e => IO a -> (e -> IO a) -> IO a
try :: Exception e => IO a -> IO (Either e a)
evaluate :: a -> IO a
{-# LANGUAGE DeriveDataTypeable #-}

import Data.Typeable
import Control.Exception

data MyException = MyException
    deriving (Show, Typeable)

instance Exception MyException

evil :: [Int]
evil = [throw MyException]

example1 :: Int
example1 = head evil

example2 :: Int
example2 = length evil

main :: IO ()
main = do
  a <- try (evaluate example1) :: IO (Either MyException Int)
  print a

  b <- try (return example2) :: IO (Either MyException Int)
  print b

値は必要でなければ評価されないので、例外は例外が捕捉されるかどうか確証が欲しければ、catch を呼び出す前に標準形へと深く評価してもよいでしょう。strictCatch は標準ライブラリでは提供されていませんが、deepseq を使えば単純に実装できます。

strictCatch :: (NFData a, Exception e) => IO a -> (e -> IO a) -> IO a
strictCatch = catch . (toNF =<<)

例外

先ほどのアプローチの問題は、基本操作を捕捉するのに IO の内部の GHC の非同期例外処理に頼らねばならないことです。exceptions ライブラリは Control.Exception と同じ API を提供していますが、IO への依存度が低くなっています。

{-# LANGUAGE DeriveDataTypeable #-}

import Data.Typeable
import Control.Monad.Catch
import Control.Monad.Identity

data MyException = MyException
    deriving (Show, Typeable)

instance Exception MyException

example :: MonadCatch m => Int -> Int -> m Int
example x y | y == 0 = throwM MyException
            | otherwise = return $ x `div` y

pure :: MonadCatch m => m (Either MyException Int)
pure = do
  a <- try (example 1 2)
  b <- try (example 1 0)
  return (a >> b)

参照:

Either

Either のモナドインスタンスは単純です。束縛 (bind) で Left を偏重していることに注目してください。

instance Monad (Either e) where
  return x = Right x

  (Left x)  >>= f = Left x
  (Right x) >>= f = f x

いつも見かけるバカバカしい例は、ゼロによる割り算が生じた時に Left の値で失敗し、それ以外の時に Right の値で結果を持つ、安全な割り算の関数を書くものです。

sdiv :: Double -> Double -> Either String Double
sdiv _ 0 = throwError "divide by zero"
sdiv i j = return $ i / j

example :: Double -> Double -> Either String Double
example n m = do
  a <- sdiv n m
  b <- sdiv 2 a
  c <- sdiv 2 b
  return c

throwError :: String -> Either String b
throwError a = Left a

main :: IO ()
main = do
  print $ example 1 5
  print $ example 1 0

これがかなり馬鹿だということは認めざるを得ませんが、Either や EitherT が例外捕捉に適したモナドである理由の本質を確かに掴んでいます。

ErrorT

モナド変換子のスタイルでは、ErrorT 変換子を Identity モナドと合成して、Either Exception a へと展開するようにして、使うことができます。この方法は単純ですが、カスタムのException の型が欲しい場合、Exception(あるいは Typeable)型クラスを手動でインスタンス化する必要があります。

import Control.Monad.Error
import Control.Monad.Identity

data Exception
  = Failure String
  | GenericFailure
  deriving Show

instance Error Exception where
  noMsg = GenericFailure

type ErrMonad a = ErrorT Exception Identity a

example :: Int -> Int -> ErrMonad Int
example x y = do
  case y of
    0 -> throwError $ Failure "division by zero"
    x -> return $ x `div` y

runFail :: ErrMonad a -> Either Exception a
runFail = runIdentity . runErrorT

example1 :: Either Exception Int
example1 = runFail $ example 2 3

example2 :: Either Exception Int
example2 = runFail $ example 2 0

ExceptT

mtl 2.2 以降では、ErrorT クラスに代わって ExceptT クラスを使うようになりました。このクラスは、古いクラスの問題の多くを修正しています。

変換子のレベルでは:

newtype ExceptT e m a = ExceptT (m (Either e a))

runExceptT :: ExceptT e m a -> m (Either e a)
runExceptT (ExceptT m) = m

instance (Monad m) => Monad (ExceptT e m) where
    return a = ExceptT $ return (Right a)
    m >>= k = ExceptT $ do
        a <- runExceptT m
        case a of
            Left e -> return (Left e)
            Right x -> runExceptT (k x)
    fail = ExceptT . fail

throwE :: (Monad m) => e -> ExceptT e m a
throwE = ExceptT . return . Left

catchE :: (Monad m) =>
    ExceptT e m a               -- ^ 内部の計算
    -> (e -> ExceptT e' m a)    -- ^ 内部の計算の例外のハンドラ
    -> ExceptT e' m a
m `catchE` h = ExceptT $ do
    a <- runExceptT m
    case a of
        Left  l -> runExceptT (h l)
        Right r -> return (Right r)

MTL のレベルでは:

instance MonadTrans (ExceptT e) where
    lift = ExceptT . liftM Right

class (Monad m) => MonadError e m | m -> e where
    throwError :: e -> m a
    catchError :: m a -> (e -> m a) -> m a

instance MonadError IOException IO where
    throwError = ioError
    catchError = catch

instance MonadError e (Either e) where
    throwError             = Left
    Left  l `catchError` h = h l
    Right r `catchError` _ = Right r

参照:

EitherT

newtype EitherT e m a = EitherT {runEitherT :: m (Either e a)}
        -- Defined in `Control.Monad.Trans.Either'
runEitherT :: EitherT e m a -> m (Either e a)
tryIO :: MonadIO m => IO a -> EitherT IOException m a

throwT  :: Monad m => e -> EitherT e m r
catchT  :: Monad m => EitherT a m r -> (a -> EitherT b m r) -> EitherT b m r
handleT :: Monad m => (a -> EitherT b m r) -> EitherT a m r -> EitherT b m

使うべき理想のモナドは単純に EitherT のモナドであり、これを ErrorT と似た API で使えることを望んでいます。例えば、read を使って標準入力の正の整数を読もうとしているとしましょう。2 つの失敗のモードと 2 つの失敗のケースがあります。一方は、Prelude.readIO からのエラーで失敗する、パースエラーに対するものであり、一方は、検査によるカスタムの例外で失敗する、非正整数に対するものです。同じ変換子の中で 2 つのケースを統一して扱いたいものです。

safe ライブラリと errors ライブラリを組み合わせて使えば、EitherT を使う暮らしはより楽なものになります。safe ライブラリは、失敗を、Maybe の値、明示的に渡されたデフォルト値、あるいはより情報の多い”注釈”という例外として扱う、標準の Prelude の関数のより安全な変種をいろいろと提供しています。一方で、errors ライブラリは安全な Maybe の関数を再エクスポートし、それらの関数を EitherT モナドへと持ち上げるために、try という接頭辞を持つ関数のグループを提供しています。これらの関数は、アクションを実行し、例外で失敗する可能性があります。

-- `read` と等価な例外処理
tryRead :: (Monad m, Read a) => e -> String -> EitherT e m a

-- `head` と等価な例外処理
tryHead :: Monad m => e -> [a] -> EitherT e m a

-- `(!!)` と等価な例外処理
tryAt :: Monad m => e -> [a] -> Int -> EitherT e m a
import Control.Error
import Control.Monad.Trans

data Failure
  = NonPositive Int
  | ReadError String
  deriving Show

main :: IO ()
main = do
  putStrLn "正の数を入力してください"
  s <- getLine

  e <- runEitherT $ do
      n <- tryRead (ReadError s) s
      if n > 0
        then return $ n + 1
        else throwT $ NonPositive n

  case e of
      Left  e -> putStrLn $ "この例外で失敗:" ++ show e
      Right n -> putStrLn $ "この値で成功:" ++ show n

参照:

results matching ""

    No results matching ""