有没有更简洁的方法将 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 . returnliftEither 做同样的事情。 (它有一个稍微复杂的实现,以便与 MonadError 的任何实例一起工作)

如果您可以控制 f1f2,那么一种方法是将它们概括化:

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"

无论如何,一旦你有了更通用的类型,你就可以直接将它用作 EitherExceptT

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

如果您 没有 控制 f1f2,那么您可能会喜欢 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 的原生对象。

运算符的对齐是我在使用包时一直做的事情,所以你可以忽略它们,只读快乐路径代码。在大型应用程序中,这种风格非常有效并且使代码审查更好,因为您可以忽略与错误相关的代码并专注于逻辑。