failable 0.1.1.0 → 1.0.0.0
raw patch · 3 files changed
+95/−14 lines, 3 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Control.Monad.Failable: hoistEither :: (Failable m, Exception e) => (a -> e) -> Either a b -> m b
+ Control.Monad.Failable: hoistMaybe :: (Failable m, Exception e) => e -> Maybe a -> m a
+ Control.Monad.Failable: instance GHC.Exception.Type.Exception ()
+ Control.Monad.Failable: recover :: (Failable m, e ~ SomeException) => m a -> (e -> m a) -> m a
Files
- README.md +25/−0
- failable.cabal +12/−7
- src/Control/Monad/Failable.hs +58/−7
README.md view
@@ -56,3 +56,28 @@ And the same thing applies to `runExceptT` etc. +## The IO problem++One of the most common complaints about error monads is that they erroneously give the impression that if the user deals with the returned failed condition (i.e. `Nothing` or `Left <SomeError>` for `Maybe(MaybeT)` or `Either(ExceptT)` respectively) the job is done and the code is now "safe", when in reality all one has done is opened up an additional error "path" on top of IO exceptions. Regarldess of one's position on IO exceptions, truth is they are not going to go away.. probably _ever_. So one has to find a way to live with them+in the best possible manner. To this effect, this library offers a utility function `failableIO`. This function can be used if the Failable monad is also an instance of MonadIO and it lifts an IO operation into the monad but in the event of an IO error, it returns this as a failure in the right context. So for example:++```haskell+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++but if ran directly on IO:++ > foo+ > *** Exception: foo.txt: openFile: does not exist (No such file or directory)++IMHO this is an improvement from having `foo` fail with an IO exception _or_ a failure value depending on the context.
failable.cabal view
@@ -1,16 +1,17 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.0.+-- This file has been generated from package.yaml by hpack version 0.31.1. -- -- see: https://github.com/sol/hpack ----- hash: b01d6b6dff5f662173986cc7e39625f19f05b65bebdc8744818c97741cfd6c90+-- hash: 802186e92dd2e28073f828c705992da99962737bcc8ef0e229e7a0f10fe02697 name: failable-version: 0.1.1.0+version: 1.0.0.0 synopsis: A 'Failable' error monad class to unify failure across monads that can fail-description: Please see the README on Gitlab at <https://gitlab.com/codemonkeylabs/failable#readme>+description: This library contains a 'Failable' error monad class to unify failure across monads and transformers most commonly used to implement pipelines that can fail and does so in a simple nonsense way by providing the means of signaling a computation "failure" while striving to keep the failure behaviour consistent with the actual definition of the monad/transformer. Please refer to the README file for a more elaborate description and some examples. category: control, exceptions, monad+bug-reports: https://gitlab.com/codemonkeylabs/failable/issues author: Erick Gonzalez maintainer: erick@codemonkeylabs.de copyright: 2019 Erick Gonzalez@@ -21,6 +22,10 @@ README.md ChangeLog.md +source-repository head+ type: git+ location: https://gitlab.com/codemonkeylabs/failable+ library exposed-modules: Control.Monad.Failable@@ -29,7 +34,7 @@ hs-source-dirs: src build-depends:- base >=4.7 && <5- , mtl- , transformers+ base >=4.8 && <5+ , mtl >=2.2 && <2.3+ , transformers >=0.4.2 && <0.6 default-language: Haskell2010
src/Control/Monad/Failable.hs view
@@ -62,14 +62,16 @@ -- >>> runMaybeT $ foo 2 :: IO (Maybe Int) -- >>> Nothing --- Failable(..), failableIO) where+ Failable(..), failableIO, hoistEither, hoistMaybe) where -import Control.Exception (Exception(..), SomeException, throw)-import Control.Monad.Except (ExceptT, throwError)+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(..))+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@@ -79,23 +81,72 @@ -- 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 _ = []+ failure _ = []+ recover [] f = f $ toException ()+ recover xs _ = xs instance Failable Maybe where- failure _ = Nothing+ 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 $ pure Nothing+ 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