How does EitherT work?

I spent half my day trying to figure out how to use EitherT as a way to handle bugs in my code.

I defined such a transformer stack.

-- Stuff Monad data StuffConfig = StuffConfig { appId :: T.Text, appSecret :: T.Text } data StuffState = StuffState { stateToken :: Maybe Token, stateTime :: POSIXTime } newtype Stuff a = Stuff { runStuff :: (ReaderT StuffConfig (StateT StuffState (EitherT T.Text IO))) a } deriving (Monad, Functor, Applicative, MonadIO, MonadReader StuffConfig, MonadState StuffState ) askStuff :: StuffConfig -> Stuff a -> IO (Either T.Text a) askStuff config a = do t <- getPOSIXTime runEitherT (evalStateT (runReaderT (runStuff a) config) (StuffState Nothing t)) 

This works pretty well as long as I use only the ReaderT and StateT . I get the impression that I can now write something like this:

 faultyFunction :: String -> Stuff String faultyFunction s = do when s == "left" $ left "breaking out" "right" 

More important is the capture of Either return values, which should be possible with hoistEither from the errors package:

 faultyLookup :: Map -> String -> Stuff String faultyLookup mk = do hoistEither $ lookup km 

I read a chapter on real-world peace about monastic transformers and step-by-step lift . But I can not do anything with typecheck.

+8
haskell error-handling monad-transformers either
source share
2 answers

The reason you can't just use the left and hoistEither functions is because, unlike StateT and ReaderT from mtl , either package does not provide a class similar to MonadReader or MonadState .

The above class classes will take care of removing the monode stack transparently, but for EitherT you need to do the upgrade itself (or write a MonadEither class, similar to MonadReader , etc.).

 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 might want to write utility functions for yourself, for example

 stuffLeft :: T.Text -> Stuff a stuffLeft = Stuff . lift . lift . left 

Then you can simply use it as follows:

 faultyFunction :: String -> Stuff String faultyFunction s = do when (s == "left") $ stuffLeft "breaking out" return "right" 

Alternatively, you can 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 as follows:

 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 checks without manual lifting.

You can also write down common implementations for left and hoistEither that work for any MonadError instance (using either from Data.Either ):

 left :: MonadError em => e -> ma left = throwError hoistEither :: MonadError em => Either ea -> ma hoistEither = either throwError return 
+9
source share

Just add the shang answer: MonadError is basically a matching class of type EitherT . You can add your instance for EitherT (for some reason it commented out in either library):

 import Control.Monad.Trans.Either hiding (left, right, hoistEither) instance Monad m => MonadError e (EitherT em) where throwError = EitherT . return . Left EitherT m `catchError` h = EitherT $ m >>= \a -> case a of Left l -> runEitherT (hl) Right r -> return (Right r) 

Then define your own methods that are generalized to MonadError :

 left :: MonadError em => e -> ma left = throwError {-# INLINE left #-} right :: MonadError em => a -> ma right = return {-# INLINE right #-} hoistEither :: MonadError em => Either ea -> ma hoistEither (Left a) = throwError a hoistEither (Right e) = return e {-# INLINE hoistEither #-} 

Now you can do things like:

 import qualified Data.Map as Map newtype Stuff a = Stuff { runStuff :: (ReaderT Int (StateT Char (EitherT T.Text IO))) a } deriving (Monad, Functor, MonadReader Int, MonadError T.Text, -- <--- MonadError instance MonadState Char ) faultyLookup :: (Ord k) => Map.Map ka -> k -> Stuff a faultyLookup mk = maybe (left $ T.pack "Lookup error") right $ Map.lookup km 

or generalize it to

 faultyLookup :: (MonadError T.Text m, Ord k) => Map.Map ka -> k -> ma faultyLookup mk = maybe (left $ T.pack "Lookup error") right $ Map.lookup km 
+2
source share

All Articles