The reason you can't just use the left
and hoistEither
functions directly is that unlike StateT
and ReaderT
from the mtl
package, the either
package doesn't provide a typeclass similar to MonadReader
or MonadState
.
The aforementioned typeclasses take care of lifting in the monad stack transparently, but for EitherT
, you have to do the lifting yourself (or write a MonadEither
typeclass similar to MonadReader
et al).
faultyFunction :: String -> Stuff String
faultyFunction s = do
when (s == "left") $ Stuff $ lift $ lift $ left "breaking out"
return "right"
First you need to apply the Stuff
wrapper, then lift
over the ReaderT
transformer and then lift
again over the StateT
transformer.
You probably want to write utility functions for yourself such as
stuffLeft :: T.Text -> Stuff a
stuffLeft = Stuff . lift . lift . left
Then you can simply use it like this:
faultyFunction :: String -> Stuff String
faultyFunction s = do
when (s == "left") $ stuffLeft "breaking out"
return "right"
Alternatively, you could use Control.Monad.Error
from mtl
, if you define an Error
instance for Text
.
instance Error T.Text where
strMsg = T.pack
Now you can change the definition of Stuff
implement left
and hoistEither
like this:
newtype Stuff a = Stuff {
runStuff :: (ReaderT StuffConfig (StateT StuffState (ErrorT T.Text IO))) a
} deriving (Monad, Functor, Applicative,
MonadIO,
MonadReader StuffConfig,
MonadState StuffState,
MonadError T.Text
)
left :: T.Text -> Stuff a
left = throwError
hoistEither :: Either T.Text a -> Stuff a
hoistEither = Stuff . lift . lift . ErrorT . return
With this your original faultyFunction
type-checks without any manual lifting.
You can also write generic implementations for left
and hoistEither
which work for any instance of MonadError
(using either
from Data.Either
):
left :: MonadError e m => e -> m a
left = throwError
hoistEither :: MonadError e m => Either e a -> m a
hoistEither = either throwError return