PR

エラー・モナドとしてのEitherモナド

 mtlパッケージのControl.Monad.Errorモジュールでは,処理の成功と失敗を示すエラー・モナドとしてEither型のMonadクラスとMonadPlusクラスのインスタンスを定義しています。このモナドは例外モナドとも呼ばれています(参考リンク)。

 第6回で説明したように,mtlパッケージは処理系に付属していないことがあります。使用している処理系にmtlパッケージが付属していない場合には,別途インストールしてください。

 Either型のMonadクラスのインスタンスやMonadPlusクラスのインスタンスの定義は,第5回で説明したMaybeモナドのインスタンスの定義とほぼ同じです。

instance (Error e) => Monad (Either e) where
    return        = Right
    Left  l >>= _ = Left l
    Right r >>= k = k r
    fail msg      = Left (strMsg msg)

instance (Error e) => MonadPlus (Either e) where
    mzero            = Left noMsg
    Left _ `mplus` n = n
    m      `mplus` _ = m

 ただ定義内には,「strMsg」や「noMsg」といった見慣れないものが使われています。これらはエラーを表現するためのものです。Either型の型変数eに対する制約として付けられているErrorクラスで定義されています。

class Error a where
    -- | Creates an exception without a message.
    -- Default implementation is @'strMsg' \"\"@.
    noMsg  :: a
    -- | Creates an exception with a message.
    -- Default implementation is 'noMsg'.
    strMsg :: String -> a

    noMsg    = strMsg ""
    strMsg _ = noMsg

 strMsgはメッセージ付きのエラー,noMsgはメッセージなしのエラーを意味します。なお,Errorクラスのインスタンスとして定義済みなのは,StringとIOErrorの二つです。

-- | A string can be thrown as an error.
instance Error String where
    noMsg  = ""
    strMsg = id

instance Error IOError where
    strMsg = userError

 なぜエラー・モナドは,例外処理で使われているExceptionクラスではなく,新たに定義したErrorクラスを利用しているのでしょうか? これだと例外型をエラー・モナドの内部で使いたい場合,ExceptionクラスだけではなくErrorクラスのインスタンスも定義する必要があります。二度手間ではないでしょうか?

 エラー・モナドがこのようなデザインになっているのは,歴史的事情によるものです。第25回で述べたように,GHCやHugsにExceptionクラスや新しい例外処理の仕組みが導入されたのがごく最近であるのに対し,エラー・モナドはそれ以前から存在していました。このため,エラー・モナドはExceptionクラスにまだ対応していないのです。

 では実際にエラー・モナドを使ってみましょう。型注釈なしにそのまま式を評価させると,第15回で説明したように,GHCiが勝手にIOモナドでの実行だと判断して処理してしまいます。これを防ぐため,式の最後に型注釈を付けることで,どのモナドから発生したどのようなエラーかをはっきりさせています。

Prelude Control.Monad.Error> return 1::Either String Int
Right 1
Prelude Control.Monad.Error> return 1 >> return "eto"::Either String String
Right "eto"
Prelude Control.Monad.Error> fail "error occur."::Either String Int
Left "error occur."
Prelude Control.Monad.Error> fail "error occur." >> return (-1)::Either String Int
Left "error occur."
Prelude Control.Monad.Error> return 0 >> fail "error occur."::Either IOError Int
Left user error (error occur.)
Prelude Control.Monad.Error> fail "error occur." `mplus` return (-1)::Either String Int
Right (-1)
Prelude Control.Monad.Error> return 0 `mplus` fail "error occur."::Either IOError Int
Right 0
Prelude Control.Monad.Error> fail "error occur by left." `mplus` fail "error occur by right."::Either IOError Int
Left user error (error occur by right.)

 StringとIOErrorの両方で,Either型での処理の失敗を表現できていますね。

 エラー・モナドでは,failやmplusのほかにもエラーの発生やエラーからの回復を扱うための手段を用意しています。それがControl.Monad.Error.Classモジュールで定義されているMonadErrorクラスです。

Prelude Control.Monad.Error.Class> :i MonadError
class (Monad m) => MonadError e m | m -> e where
  throwError :: e -> m a
  catchError :: m a -> (e -> m a) -> m a
        -- Defined in Control.Monad.Error.Class
instance MonadError IOError IO -- Defined in Control.Monad.Error
instance (Error e) => MonadError e (Either e)
  -- Defined in Control.Monad.Error

 throwErrorは例外処理のthrow,catchErrorは例外処理のcatchに相当します。

 Either型でのインスタンスの定義は以下の通りです。

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

 throwErrorやcatchErrorを使うことで,例外やエラーを処理するコードをより直感的に記述できます。

module ErrorMonad where
import Control.Monad.Error

causeError :: Either String Int
causeError = throwError noMsg

causeError' :: Either String Int
causeError' = throwError $ strMsg "error occur."

causeError'' :: Either IOError Int
causeError'' = do
    return (-1)
    throwError noMsg
    throwError $ strMsg "error occur."
    return 0

causeError''' :: Either String String
causeError''' = throwError $ strMsg "error occur."

 この例で定義した「causeError」から「causeError'''」までの関数(以下,causeError*と表記します)は型宣言が必要であることに注意してください。Either型に対して直接throwErrorが定義されているのではなく,MonadErrorクラスのメソッドとしてthrowErrorが定義されているため,型宣言が必要になるのです。関数を型クラスのメソッドとして定義することは,関数の柔軟性を増すための重要な手段です。しかし,この柔軟性がHaskellやGHCiの型システム上では不都合なこともあります。

 もし型宣言を書かなければ,まずHaskell 98の「単相性制限(monomorphism restriction)」による型エラーに遭遇することになります。

module ErrorMonad where
import Control.Monad.Error

causeError = throwError noMsg

causeError' = throwError $ strMsg "error occur."

causeError'' = do
    return (-1)
    throwError noMsg
    throwError $ strMsg "error occur."
    return 0

causeError''' = throwError $ strMsg "error occur."

$ ghci ErrorMonad
~ 略 ~
[1 of 1] Compiling ErrorMonad       ( ErrorMonad.hs, interpreted )

ErrorMonad.hs:4:13:
    Ambiguous type variables `a3', `m3' in the constraint:
      `MonadError a3 m3'
        arising from a use of `throwError' at ErrorMonad.hs:4:13-28
    Possible cause: the monomorphism restriction applied to the following:
      causeError :: forall a4. m3 a4 (bound at ErrorMonad.hs:4:0)
    Probable fix: give these definition(s) an explicit type signature
                  or use -XNoMonomorphismRestriction

ErrorMonad.hs:4:24:
    Ambiguous type variable `a3' in the constraint:
      `Error a3' arising from a use of `noMsg' at ErrorMonad.hs:4:24-28
    Probable fix: add a type signature that fixes these type variable(s)
~ 略 ~
Prelude> 

 単相性制限とは「パターン束縛の対象である変数(または関数)が陽に型シグネチャを持たない場合,型は単相的(monomorphic)でなければならない」という制約です。この制限によりHaskell処理系は,型宣言のないcauseError*を「EitherやIOなどの特定のモナド」で「StringまたはIOErrorという特定のエラー」に対して処理を行う単相的な関数として推論しようとします。しかし,causeError*の定義に使われているthrowErrorは,MonadErrorクラスのインスタンスとして定義された「型変数mのモナド」で「型変数eのエラー」に対する処理を行う多相的な関数であるため,単相的な型定義を実際に導き出すことはできず型エラーになるのです(参考リンク)。

 単相性制限について詳細に知りたい場合には「Haskell 98 言語とライブラリ 改訂レポート」の4.5.5 単相性制限の項目を見てください。単相性制限は,GHCなら-X*オプションを使うかLANGUAGE指示文でNoMonomorphismRestrictionを指定することで解除できます(参考リンク)。

{-# LANGUAGE NoMonomorphismRestriction #-}
~ 略 ~

Prelude> :r
[1 of 1] Compiling ErrorMonad       ( ErrorMonad.hs, interpreted )
Ok, modules loaded: ErrorMonad.

 なお,次期標準であるHaskell'では単相性制限は取り除かれることになっています(参考リンク)。

 単相性制限は上に示した方法で回避できます。しかし,もしcauseError*で型宣言を行わなければ,問題がもう一つあります。

 throwErrorやcatchError,そして単相性制限の解除によって自動的に推論されるcauseError*は,いずれも「MonadErrorクラスのインスタンスとして定義されたモナドとエラー」に対して作用する多相的なものになっています。このまま型シグネチャのない式を評価させるとどうなるでしょうか?

 不特定のモナドを使った式をGHCiで評価した場合,第15回で説明したように,GHCiはIOモナドでの実行だと判断して処理してしまいます。これでは,Eitherモナドをエラー・モナドとして利用した場合の評価結果は期待できません。Eitherモナドでの評価結果を見るには,使用しているモナドの型がEitherであることをどこかで確定させる必要があります。

 いずれ,どのモナドから発生したどの種類のエラーかをはっきりさせなければならないのなら,最初から面倒がらずに型宣言を書くべきです。

 型宣言を行ったcauseError*の動作を確認してみましょう。

*ErrorMonad> causeError
Left ""
*ErrorMonad> causeError'
Left "error occur."
*ErrorMonad> causeError''
Left user error
*ErrorMonad> causeError'''
Left "error occur."
*ErrorMonad> return 1 `catchError` (\_ -> return 12)
1
*ErrorMonad> causeError `catchError` (\_ -> return 12)
Right 12
*ErrorMonad> causeError' `catchError` (\_ -> return 12)
Right 12
*ErrorMonad> causeError'' `catchError` (\_ -> return 12)
Right 12
*ErrorMonad> causeError''' `catchError` (\str -> return $ "error caught: " ++ str)
Right "error caught: error occur."

 いずれもEitherモナドを使ったエラー・モナドの評価結果になっていますね。