'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 |