Haskellの例外処理は,IOやSTMといった特定のモナドの内部でのみ扱えるようにデザインされています。しかし,こうした制限が行き届くのは,あくまで例外を捕捉し処理する場面に限られます。例外やエラーの発生する個所自体はこうした制限にはとらわれません。ありとあらゆる場所で発生する可能性のある例外やエラーに対する処理を,より統一的に扱うにはどうすればよいでしょうか?
この問題に対する解決策として,第15回で説明したモナド変換子を利用できます。利用できる範囲はあくまでモナドの内部に限られますが,特定のモナドでしか例外を扱えないことと比べれば一歩前進でしょう。今回はモナド変換子を利用する2種類の方法を紹介します。
成功時と失敗時の情報を保持するEither型
第5回では,失敗する可能性のある計算結果を表すのにMaybe型を利用できることを説明しました。Maybeに対してモナドを定義することで,失敗する可能性のある計算を合成して,失敗する可能性のある,より大きな計算を定義できる,ということも説明しました。これらのことから「Maybeモナドを使うことで,様々な場所で発生するエラーを統一的に扱えるのでは?」と考えるかもしれません。
しかし,こうした用途にはMaybeは能力が不足しています。Maybeはあくまで処理の成功と失敗を知らせるだけです。失敗した理由を情報として含むことはできません。このためMaybeは,失敗した理由によって処理を変えたい場合には役に立ちません。
失敗した理由を情報として知らせるにはどうすればよいでしょうか?
Maybeの限界は,成功時の値しか保持できないところにあります。成功時と失敗時の2種類の状態に付随する値を保持できる型があればよいのです。そのような型として「Either型」がPreludeに用意されています(参考リンク1,参考リンク2)。
Prelude> :i Either data Either a b = Left a | Right b -- Defined in Data.Either instance (Eq a, Eq b) => Eq (Either a b) -- Defined in Data.Either instance (Ord a, Ord b) => Ord (Either a b) -- Defined in Data.Either instance (Read a, Read b) => Read (Either a b) -- Defined in Data.Either instance (Show a, Show b) => Show (Either a b) -- Defined in Data.Either
Eitherは「Right」と「Left」という2種類の状態を持ちます。Eitherを処理の成功と失敗を示すのに使う場合には,Rightで成功,Leftで失敗を表します。
例を見てみましょう。Control.ExceptionおよびSystem.IO.Errorでは,成功と失敗の値をEitherに包んで返すtryという関数を用意しています。
Prelude Control.Exception> :i try try :: (Exception e) => IO a -> IO (Either e a) -- Defined in Control.Exception.Base Prelude Control.Exception> :m System.IO.Error Prelude System.IO.Error> :i try try :: IO a -> IO (Either IOError a) -- Defined in System.IO.Error
それぞれの処理の対象に違いはあるものも,どちらも意味するところは同じです。処理が成功裏に終わればRightに値を包んで返し,処理の途中で例外やエラーが発生した場合にはその例外やエラーをLeftに包んで返します。
Prelude Control.Exception> try (return 2009)::IO (Either ErrorCall Int) Right 2009 Prelude Control.Exception> try (evaluate undefined)::IO (Either SomeException Int) Left Prelude.undefined Prelude Control.Exception> try (evaluate $ error "error ocurr.")::IO (Either ErrorCall Int) Left error ocurr. Prelude Control.Exception> try (evaluate $ 100 `div` 0)::IO (Either ArithException Int) Left divide by zero Prelude Control.Exception> :m System.IO.Error Prelude System.IO.Error> try (return 28) Right 28 Prelude System.IO.Error> try (ioError $ userError "error ocurr.") Left user error (error ocurr.)
Control.Exceptionでのtryの定義は以下の通りです。System.IO.Errorのtryもほぼ同じような定義になっています(参考リンク)。
try :: Exception e => IO a -> IO (Either e a) try a = catch (a >>= \ v -> return (Right v)) (\e -> return (Left e))
tryの定義にcatchが使われていることから想像できると思いますが,catchと同様にtryも特定の例外型に対する処理になっています。要求する例外型が実際に発生した例外型と一致しない場合,tryはLeftを返さずに例外を再送出します。
Prelude Control.Exception> try (evaluate undefined):: IO (Either ArithException Int) *** Exception: Prelude.undefined Prelude Control.Exception> try (evaluate $ 100 `div` 0):: IO (Either ErrorCall Int) *** Exception: divide by zero
ここではEitherを成功と失敗を示すのに使いましたが,それ以外の用途にも利用できます。例えば,二つの異なるデータ型を一つにまとめて扱いたい場合,新しくデータ型を定義する代わりにEither型を利用できます。
Prelude> Right 4 Right 4 Prelude> Left 0.01 Left 1.0e-2 Prelude> [Right 4, Left 0.01] [Right 4,Left 1.0e-2]
次に,Either型に対して処理を行う関数を見ていきましょう。Preludeのeitherは,RightとLeftの両方に作用する関数として定義されています。Preludeでのeither関数の定義は以下の通りです(参考リンク)。
either :: (a -> c) -> (b -> c) -> Either a b -> c either f g (Left x) = f x either f g (Right y) = g y
Rightに格納された値とLeftに格納された値はそれぞれの型が違う可能性があるため,eitherはそれぞれに適用させるための二つの関数を取るよう定義されています。
ただ,すべて処理でeitherのように「RightとLeftに対して適用すべき二つの関数をいちいち与える」のは面倒です。そこでRightとLeftのどちらか一方だけに作用する関数を利用することもあります。
RightとLeftのどちらか一方に作用する関数としては,Control.Monad.Instancesモジュールで定義されているFunctorクラスのインスタンスがあります。
class Functor f where fmap :: (a -> b) -> f a -> f b -- Defined in GHC.Base instance Functor Maybe -- Defined in Data.Maybe instance Functor ((->) r) -- Defined in Control.Monad.Instances instance Functor ((,) a) -- Defined in Control.Monad.Instances instance Functor (Either a) -- Defined in Control.Monad.Instances instance Functor IO -- Defined in GHC.IOBase instance Functor [] -- Defined in GHC.Base
Either型のFunctorクラスのインスタンスは,以下のように定義されています。
Prelude Control.Monad.Instances> :i Functor instance Functor (Either a) where fmap _ (Left x) = Left x fmap f (Right y) = Right (f y)
fmapがRightにだけ作用する関数になっているのがわかりますね。逆に,Leftにだけ作用するようなmap関数を定義することも可能です。
このように,Either型に対する処理には,RightとLeftの両方に対して作用するものと,どちらか一方に対して作用するものの2種類があります。途中までは「RightとLeftのどちらか一方に対して作用する処理」を行い,最終的に「RightとLeftの両方に対して作用する処理」を使って結果をまとめる,といったようにこれらを使い分けるとよいでしょう。