{-# LANGUAGE GADTs #-} {- | Module: Control.Monad.Failable Description: Yet another error monad but for people who are not crazy Copyright: (c) Erick Gonzalez, 2019 License: BSD3 Maintainer: erick@codemonkeylabs.de This library provides a 'Failable' error monad class to unify errors across monads and transformers most commonly used to implement pipelines that can fail. -} module Control.Monad.Failable ( -- | -- I am sure a lot of ink has been spilled in forums and around water coolers all around the -- world, debating the merits and fallacies of one approach or the other. The reason for this -- package is not to participate in this discussion but rather to provide a simple no nonsense -- means of signaling a computation "failure" in those monads that provide the inherent means -- to do so, and to do it in a consistent manner. -- -- When triggering a failure in a monadic context which is an instance of this class, simply -- define your custom exception type and abort the computation with 'failure'. For example: -- -- @ -- data MyException = SomeProblem -- | AnotherProblem -- deriving (Show, Typeable) -- -- instance Exception MyException -- -- foo :: (Failable m) => Int -> m Int -- foo x = do -- y <- bar x -- if y < 0 -- then failure SomeProblem -- else return y -- @ -- -- -- if foo is then called in a 'Maybe' Monad, it would return @Nothing@ in case of error -- or @Just ()@ of course if succesful. In an @Either SomeException@ context, it would -- return @Left SomeProblem@ in case of error or @Right ()@ upon success, etc. -- When it comes to monad transformers incorporating the concept of failure, such as 'MaybeT' or -- 'ExceptT', it preserves the expected semantics upon failure of yielding an @m Nothing@ or -- @m (Either SomeException a)@ when the transformer is "ran", instead of adopting the strategy -- of passing the failure to the underlying monad (transformer) which might for example, throw -- an async exception (as is the case of IO). Since the reason d'etre for something like runMaybeT -- is to provide the underlying monad (transformer) with `Maybe` like behaviour, i.e. have -- @Nothing@ be returned in case of aborting the 'Maybe' pipeline so to speak, then throwing an -- exception defeats IMHO the purpose of using 'MaybeT' in the first place. -- -- >>> foo 2 :: Maybe Int -- >>> Nothing -- -- >>> foo 2 :: Either SomeException Int -- >>> Left SomeProblem -- -- >>> foo 2 :: IO Int -- >>> * * * Exception: SomeProblem -- -- >>> runMaybeT $ foo 2 :: IO (Maybe Int) -- >>> Nothing -- Failable(..), failableIO, hoistEither, hoistMaybe) where import Control.Exception (Exception(..), SomeException(..), throw, catch) import Control.Monad (join) import Control.Monad.Except (ExceptT(..), runExceptT, throwError) import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT) import System.IO.Error (tryIOError) instance Exception () -- | The 'Failable' class. A Monad which is an instance of this class can be used as a context -- in a function running in one with this class constraint, in order to report error conditions class (Monad m) => Failable m where -- | trigger a failure. It takes an exception value as argument and it returns whatever -- might be used to abort a monadic computation in the monad instantiating this class. failure :: (Exception e) => e -> m a -- | recover from a possible failure. Basically a generalized @catch@ for a @Failable@. recover :: (e ~ SomeException) => m a -> (e -> m a) -> m a instance Failable IO where failure = throw recover = catch instance Failable [] where failure _ = [] recover [] f = f $ toException () recover xs _ = xs instance Failable Maybe where failure _ = Nothing recover Nothing f = f $ toException () recover v _ = v instance e ~ SomeException => Failable (Either e) where failure = Left . toException recover (Left err) f = f err recover v _ = v instance (Monad m) => Failable (MaybeT m) where failure _ = MaybeT $ return Nothing recover a f = MaybeT $ runMaybeT a >>= recover' where recover' Nothing = runMaybeT . f $ toException () recover' x = return x instance (Monad m, e ~ SomeException) => Failable (ExceptT e m) where failure = throwError . toException recover a f = ExceptT $ runExceptT a >>= recover' where recover' (Left err) = runExceptT $ f err recover' x = return x -- | "Promote" a 'Maybe' into a 'Failable' context. Useful to reduce boilerplate when using -- functions that might return a 'Maybe' type. If the value is @Nothing@, the error specified is then -- triggered in the 'Failable' context hoistMaybe :: (Failable m, Exception e) => e -> Maybe a -> m a hoistMaybe err = maybe (failure err) return -- | "Promote" an 'Either' value into a 'Failable' context. Useful to reduce verbosity when using -- functions that return an 'Either' type. If the value is @Left err@, the wrapped error is then passed -- as an argument to the provided function which should return an instance of 'Exception'. -- so for example: -- -- @ -- data MyErrors = BadValue deriving (Typeable, Show) -- instance Exception MyErrors -- -- foo :: (Failable m) => String -> m Int -- foo = hoistEither BadValue . readEither -- @ -- -- >>> λ> foo "5" :: Maybe Int -- >>> Just 5 -- >>> λ> foo "5" :: Either SomeException Int -- >>> Right 5 -- >>> λ> foo "X5" :: Either SomeException Int -- >>> Left (BadValue "Prelude.read: no parse") -- >>> λ> foo "5" :: Maybe Int -- >>> Just 5 -- >>> λ> foo "X5" :: Maybe Int -- >>> Nothing -- hoistEither :: (Failable m, Exception e) => (a -> e) -> Either a b -> m b hoistEither f = either (failure . f) return -- | Perform a set of IO actions in a 'Failable' 'MonadIO' instance, triggering a -- 'failure' upon an IO exception, instead of blindly triggering an asynchronos exception. This -- serves ultimately to unify error handling in the 'Failable' context. For example: -- -- @ -- foo :: (Failable m, MonadIO m) => m () -- foo = do -- failableIO $ do -- txt <- readFile "foo.txt" -- putStrLn txt -- @ -- -- >>> λ> runExceptT foo -- >>> Left foo.txt: openFile: does not exist (No such file or directory) -- >>> -- >>> λ> runMaybeT foo -- >>> Nothing -- >>> -- >>> λ> foo -- >>> *** Exception: foo.txt: openFile: does not exist (No such file or directory) -- failableIO :: (Failable m, MonadIO m) => IO a -> m a failableIO actions = do result <- liftIO . tryIOError $ actions either failure return result