{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE UndecidableInstances #-} {- | 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(..), Hoistable(..), failableIO) where import Data.Bifunctor (second) import Control.Exception (Exception(..), SomeException(..), throw, catch) import Control.Monad.Except (ExceptT(..), runExceptT, throwError) import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.State.Class (MonadState, get, put) import Control.Monad.State.Strict (StateT, runStateT) import Control.Monad.Writer.Strict (WriterT, runWriterT, tell) import Control.Monad.Reader (ReaderT, runReaderT, ask) import Control.Monad.Trans (MonadTrans, lift) 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 -- | A 'Hoistable' is a type that can be "promoted" or "hoisted" to a Failable monad. class (Failable m) => Hoistable m t e' | t -> e' where -- | Given a transformation function to obtain an error in the target 'Failable' context from -- an eventual errored value from the value being hoisted, It promotes such value to a 'Failable' -- operation. For example: -- -- @ -- foo :: (Failable m) => String -> m Int -- foo = hoist BadValue . readEither -- @ -- -- >>> λ> runExceptT $ foo "5" -- >>> Right 5 -- >>> λ> foo "X5" -- >>> *** Exception: BadValue "Prelude.read: no parse" -- >>> λ> runMaybeT $ foo "X5" -- >>> Nothing -- hoist :: (Exception e) => (e' -> e) -> t 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 class (MonadTrans t, Monad m) => RunnableStateT t s m where runS :: t m a -> s -> m (a, s) instance (Monad m) => RunnableStateT (StateT s) s m where runS = runStateT instance (Monad m) => RunnableStateT (ReaderT r) r m where runS a s = (, s) <$> runReaderT a s instance (Monad m, Monoid w) => RunnableStateT (WriterT w) w m where runS a s = fmap (second $ mappend s) $ runWriterT a instance {-# OVERLAPPABLE #-} (Monad (t m), MonadTrans t, Failable m, RunnableStateT t s m, MonadState s (t m)) => Failable (t m) where failure = lift . failure recover a f = get >>= foo where foo s = do (r, s') <- lift $ runS a s `recover` \e -> runS (f e) s put s' return r instance (Monoid w, Failable m) => Failable (WriterT w m) where failure = lift . failure recover a f = do (x, w) <- lift $ runWriterT a `recover` \e -> runWriterT (f e) tell w return x instance (Failable m) => Failable (ReaderT r m) where failure = lift . failure recover a f = do r <- ask lift $ runReaderT a r `recover` \e -> runReaderT (f e) r instance (Failable m) => Hoistable m Maybe () where hoist f = maybe (failure $ f ()) return instance (Failable m) => Hoistable m (Either e') e' where hoist 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