Есть ли более чистый способ преобразования Либо в ExceptT?

У меня есть некоторые функции, которые возвращают значения Either, и я хотел бы использовать их в блоке ввода-вывода do с тем, что я думаю называется поведением «короткого замыкания», так что любые значения Left вызывают обход остальных элементов в блоке ввода-вывода. . Лучшее, что я придумал до сих пор, выглядит примерно так (искусственный пример, иллюстрирующий то, что я пытаюсь сделать):

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 выглядит неуклюже — есть ли лучший способ? Я не хочу изменять сами функции, чтобы они возвращали значения ExceptT.

🤔 А знаете ли вы, что...
Haskell способствует использованию неизменяемых структур данных, что способствует безопасности кода.


1
80
3

Ответы:

Решено

Если я правильно понимаю типы, 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, Someone и ExceptT в монады, похожие на ExceptT — ошибка подъема

Он определяет этот несколько загадочно выглядящий класс

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

Через экземпляры класса эти функции могут иметь следующие типы:

(<%?>) :: 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''

В этом примере вы можете видеть, что теперь мы можем обращаться с Someone, Maybe и ExceptT так, как если бы они были нативными для монады наших собственных приложений.

Выравнивание операторов — это то, что я всегда делал, когда использую пакет, поэтому вы можете игнорировать их и просто читать код счастливого пути. В больших приложениях этот стиль работает очень хорошо и делает просмотр кода намного приятнее, потому что вы можете игнорировать код, связанный с ошибками, и сосредоточиться на логике.