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