'Is there a cleaner way of converting an Either to an ExceptT?

I have some functions which return Either values and I'd like to use them in an IO do block with what I think is called "short circuit" behaviour so that any Left values cause the rest of the items on the IO block to be bypassed. The best I've come up with so far looks something like this (an artificial example which illustrates what I'm trying to do):

import Control.Monad.Except

main :: IO ()
main = handleErrors <=< runExceptT $ do
  liftIO $ putStrLn "Starting..."
  let n = 6
  n' <- ExceptT . return . f1 $ n
  liftIO $ putStrLn "First check complete."
  n'' <- ExceptT . return . f2 $ n'
  liftIO $ putStrLn "Second check complete."
  liftIO $ putStrLn $ "Done: " ++ show n''

handleErrors :: Either String () -> IO ()
handleErrors (Left err) = putStrLn $ "*** ERROR: " ++ err
handleErrors (Right _) = return ()

f1 :: Integer -> Either String Integer
f1 n
  | n < 5 = Left "Too small"
  | otherwise = Right n


f2 :: Integer -> Either String Integer
f2 n
  | n == 10 = Left "Don't want 10"
  | otherwise = Right n

The ExceptT . return looks clumsy - is there a better way? I don't want to change the functions themselves to return ExceptT values.



Solution 1:[1]

If I'm reading the types right, ExceptT . return does the same thing as liftEither. (It has a mildly more complicated implementation in order to work with any instance of MonadError)

Solution 2:[2]

If you have control of f1 and f2, then one way would be to generalize them:

f1 :: MonadError String m => Integer -> m Integer
f1 n | n < 5 = throwError "Too small" | otherwise = pure n

f2 :: MonadError String m => Integer -> m Integer
f2 n | n == 10 = throwError "Don't want 10" | otherwise = pure n

Actually, I'd be tempted to even abstract this pattern:

avoiding :: MonadError e m => (a -> Bool) -> e -> a -> m a
avoiding p e a = if p a then throwError e else pure a

f1 = avoiding (<5) "Too small"
f2 = avoiding (==10) "Don't want 10"

Anyway, once you have the more general type, then you can use it directly either as an Either or an ExceptT.

main = ... $ do
    ...
    n' <- f1 n
    n'' <- f2 n'
    ...

If you do not have control of f1 and f2, then you might enjoy the errors package, which offers, among many other handy things, hoistEither. (This function may be available other places as well, but the errors package offers many useful things for converting between error types when you must call other people's functions.)

Solution 3:[3]

The answers above are good, but it's worth knowing about a package which solves this problem in three very common cases - lifting Maybe, Either and ExceptT into monads that look like ExceptT - hoist-error

It defines this somewhat cryptic looking class

class Monad m => HoistError m t e e' | t -> e where

  -- | Given a conversion from the error in @t a@ to @e'@, we can hoist the
  -- computation into @m@.
  --
  -- @
  -- 'hoistError' :: 'MonadError' e m -> (() -> e) -> 'Maybe'       a -> m a
  -- 'hoistError' :: 'MonadError' e m -> (a  -> e) -> 'Either'  a   b -> m b
  -- 'hoistError' :: 'MonadError' e m -> (a  -> e) -> 'ExceptT' a m b -> m b
  -- @
  hoistError
    :: (e -> e')
    -> t a
    -> m a

and several operators, such as:

-- Take the error returns and wrap it in your own error type
(<%?>) :: HoistError m t e e' => t a -> (e -> e') -> m a
-- If an error occurs, return this specific error
(<?>) :: HoistError m t e e' => t a -> e' -> m a

Through the instances of the class, these functions can have these types:

(<%?>) :: MonadError e m => Maybe       a -> (() -> e) ->           m a
(<%?>) :: MonadError e m => Either  a   b -> (a  -> e) ->           m b
(<%?>) :: MonadError e m => ExceptT a m b -> (a  -> e) -> ExceptT e m b
(<?>) :: MonadError e m => Maybe       a -> e ->           m a
(<?>) :: MonadError e m => Either  a   b -> e ->           m b
(<?>) :: MonadError e m => ExceptT a m b -> e -> ExceptT e m b

which make working with errors simpler:

data Errors
  = ValidationError String
  | ValidationNonNegative
  | DatabaseError DBError
  | GeneralError String


storeResult :: Integer -> ExceptT DBError IO ()
storeResult n = <write to the database>

log :: MonadIO m => String -> m ()
log str = liftIO $ putStrLn str

validateNonNeg :: Integer -> Maybe Integer
validateNonNeg n | n < 0 = Nothing | otherwise = Just n

main :: IO ()
main = handleErrors <=< runExceptT $ do
  liftIO $ putStrLn "Starting..."
  let n = 6
  n' <- f1 n                      <%?> ValidationError
  log "First check complete."
  n'' <- f2 n'                    <%?> ValidationError
  log "Second check complete."
  validateNonNeg                  <?>  ValidationNonNegative
  storeResult n''                 <%?> DatabaseError
  log "Stored to database"
  log $ "Done: " ++ show n''

In this example, you can see that we can now treat Either, Maybe and ExceptT as if they are native to our own applications monad.

The alignment of the operators is something I've always done when I use the package, so you can ignore them and just read the happy path code. In large applications, this style works really well and makes code review high nicer because you can ignore the error related code and focus on the logic.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Carl
Solution 2 Daniel Wagner
Solution 3 Axman6