有没有更简洁的方法将 Either 转换为 ExceptT?
Is there a cleaner way of converting an Either to an ExceptT?
我有一些 return Either
值的函数,我想在 IO do
块中使用它们,我认为这是所谓的“短路”行为所以任何 Left
值都会导致绕过 IO 块上的其余项目。到目前为止我想出的最好的看起来像这样(一个说明我正在尝试做的人工例子):
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
ExceptT . return
看起来很笨拙 - 有更好的方法吗?我不想将函数本身更改为 return ExceptT 值。
如果我没看错类型,ExceptT . return
和 liftEither
做同样的事情。 (它有一个稍微复杂的实现,以便与 MonadError
的任何实例一起工作)
如果您可以控制 f1
和 f2
,那么一种方法是将它们概括化:
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
实际上,我很想抽象这个模式:
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"
无论如何,一旦你有了更通用的类型,你就可以直接将它用作 Either
或 ExceptT
。
main = ... $ do
...
n' <- f1 n
n'' <- f2 n'
...
如果您 没有 控制 f1
和 f2
,那么您可能会喜欢 errors
套餐,其中包括许多其他方便的东西,hoistEither
。 (这个函数可能在其他地方也可用,但是 errors
包提供了很多有用的东西,可以在您必须调用其他人的函数时在错误类型之间进行转换。)
上面的答案很好,但值得了解一个在三种非常常见的情况下解决此问题的包 - 将 Maybe、Either 和 ExceptT 提升为看起来像 ExceptT 的 monad - hoist-error
它定义了这个看起来有些神秘的东西 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
和几个运算符,例如:
-- 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
通过class的实例,这些函数可以有这些类型:
(<%?>) :: 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
这使得处理错误更简单:
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''
在此示例中,您可以看到我们现在可以将 Either、Maybe 和 ExceptT 视为我们自己的应用程序 monad 的原生对象。
运算符的对齐是我在使用包时一直做的事情,所以你可以忽略它们,只读快乐路径代码。在大型应用程序中,这种风格非常有效并且使代码审查更好,因为您可以忽略与错误相关的代码并专注于逻辑。
我有一些 return Either
值的函数,我想在 IO do
块中使用它们,我认为这是所谓的“短路”行为所以任何 Left
值都会导致绕过 IO 块上的其余项目。到目前为止我想出的最好的看起来像这样(一个说明我正在尝试做的人工例子):
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
ExceptT . return
看起来很笨拙 - 有更好的方法吗?我不想将函数本身更改为 return ExceptT 值。
如果我没看错类型,ExceptT . return
和 liftEither
做同样的事情。 (它有一个稍微复杂的实现,以便与 MonadError
的任何实例一起工作)
如果您可以控制 f1
和 f2
,那么一种方法是将它们概括化:
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
实际上,我很想抽象这个模式:
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"
无论如何,一旦你有了更通用的类型,你就可以直接将它用作 Either
或 ExceptT
。
main = ... $ do
...
n' <- f1 n
n'' <- f2 n'
...
如果您 没有 控制 f1
和 f2
,那么您可能会喜欢 errors
套餐,其中包括许多其他方便的东西,hoistEither
。 (这个函数可能在其他地方也可用,但是 errors
包提供了很多有用的东西,可以在您必须调用其他人的函数时在错误类型之间进行转换。)
上面的答案很好,但值得了解一个在三种非常常见的情况下解决此问题的包 - 将 Maybe、Either 和 ExceptT 提升为看起来像 ExceptT 的 monad - hoist-error
它定义了这个看起来有些神秘的东西 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
和几个运算符,例如:
-- 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
通过class的实例,这些函数可以有这些类型:
(<%?>) :: 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
这使得处理错误更简单:
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''
在此示例中,您可以看到我们现在可以将 Either、Maybe 和 ExceptT 视为我们自己的应用程序 monad 的原生对象。
运算符的对齐是我在使用包时一直做的事情,所以你可以忽略它们,只读快乐路径代码。在大型应用程序中,这种风格非常有效并且使代码审查更好,因为您可以忽略与错误相关的代码并专注于逻辑。