Question

I'm writing some code that uses the StateT monad transformer to keep track of some stateful information (logging and more).

The monad I'm passing to StateT is very simple:

data CheckerError a = Bad {errorMessage :: Log} | Good a
    deriving (Eq, Show)


instance Monad CheckerError where
    return x = Good x

    fail msg = Bad msg

    (Bad msg) >>= f = Bad msg
    (Good x) >>= f = f x

type CheckerMonad a = StateT CheckerState CheckerError a

It's just a Left and Right variant.

What troubles me is the definition of fail. In my computation I produce a lot of information inside this monad and I'd like to keep this information even when failing. Currently the only thing I can do is to convert everything to a String and create a Bad instance with the String passed as argument to fail.

What I'd like to do is something like:

fail msg = do
    info <- getInfoOutOfTheComputation
    return $ Bad info

However everything I tried until now gives type errors, probably because this would mix different monads.

Is there anyway in which I can implement fail in order to preserve the information I need without having to convert all of it into a String?

I cannot believe that the best Haskell can achieve is using show+read to pass all the information as the string to fail.

Was it helpful?

Solution

Your CheckerError monad is very similar to the Either monad. I will use the Either monad (and its monad transformer counterpart ErrorT) in my answer.

There is a subtlety with monad trasformers: order matters. Effects in the "inner" monad have primacy over effects caused by the "outer" layers. Consider these two alternative definitions of CheckerMonad:

import Control.Monad.State
import Control.Monad.Error

type CheckerState = Int     -- dummy definitions for convenience
type CheckerError = String

type CheckerMonad a = StateT CheckerState (Either String) a

type CheckerMonad' a = ErrorT String (State CheckerState) a

In CheckerMonad, Either is the inner monad, and this means a failure will wipe the whole state. Notice the type of this run function:

runCM :: CheckerMonad a -> CheckerState -> Either CheckerError (a,CheckerState)
runCM m s = runStateT m s

You either fail, or return a result along with the state up to that point.

In CheckerMonad', State is the inner monad. This means the state will be preserved even in case of failures:

runCM' :: CheckerMonad' a -> CheckerState -> (Either CheckerError a,CheckerState)
runCM' m s = runState (runErrorT m) s

A pair is returned, which contains the state up to that point, and either a failure or a result.

It takes a bit of practice to develop an intuition of how to properly order monad transformers. The chart in the Type juggling section of this Wikibook page is a good starting point.

Also, it is better to avoid using fail directly, because it is considered a bit of a wart in the language. Instead, use the specialized functions for throwing errors provided by the error transformer. When working with ErrorT or some other instance of MonadError, use throwError.

sillycomp :: CheckerMonad' Bool
sillycomp = do
    modify (+1)
    s <- get 
    if s == 3
        then throwError "boo"
        else return True

*Main> runCM' sillycomp 2
Loading package transformers-0.3.0.0 ... linking ... done.
Loading package mtl-2.1.2 ... linking ... done.
(Left "boo",3)

*Main> runCM' sillycomp 3
(Right True,4)

ErrorT is sometimes annoying to use because, unlike Either, it requires an Error constraint on the error type. The Error typeclass forces you to define two error constructors noMsg and strMsg, which may or may not make sense for your type.

You can use EitherT from the either package instead, which lets you use any type whatsoever as the error. When working with EitherT, use the left function to throw errors.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top