from-sum 0.2.2.0 → 0.2.3.0
raw patch · 3 files changed
+335/−5 lines, 3 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Control.FromSum: collapseErrExceptT :: Monad m => ExceptT (m a) m a -> m a
+ Control.FromSum: fromEitherExceptT :: Monad m => (e -> x) -> Either e a -> ExceptT x m a
+ Control.FromSum: fromEitherMExceptT :: Monad m => (e -> x) -> m (Either e a) -> ExceptT x m a
+ Control.FromSum: fromEitherOrExceptT :: Monad m => Either e a -> (e -> x) -> ExceptT x m a
+ Control.FromSum: fromEitherOrMExceptT :: Monad m => m (Either e a) -> (e -> x) -> ExceptT x m a
+ Control.FromSum: fromMaybeExceptT :: Monad m => x -> Maybe a -> ExceptT x m a
+ Control.FromSum: fromMaybeMExceptT :: Monad m => x -> m (Maybe a) -> ExceptT x m a
+ Control.FromSum: fromMaybeOrExceptT :: Monad m => Maybe a -> x -> ExceptT x m a
+ Control.FromSum: fromMaybeOrMExceptT :: Monad m => m (Maybe a) -> x -> ExceptT x m a
+ Control.FromSum: guardExceptT :: Monad m => Bool -> x -> ExceptT x m ()
+ Control.FromSum: guardMExceptT :: Monad m => m Bool -> x -> ExceptT x m ()
+ Control.FromSum: liftEitherExceptT :: Applicative m => Either e a -> ExceptT e m a
Files
- CHANGELOG.md +8/−0
- from-sum.cabal +3/−3
- src/Control/FromSum.hs +324/−2
CHANGELOG.md view
@@ -1,3 +1,11 @@+## 0.2.3.0++* Add functions `collapseErrExceptT`, `liftEitherExceptT`,+ `fromEitherExceptT`, `fromEitherOrExceptT`, `fromEitherMExceptT`,+ `fromEitherOrMExceptT`, `fromMaybeExceptT`, `fromMaybeOrExceptT`,+ `fromMaybeMExceptT`, `fromMaybeOrMExceptT`, `guardExceptT`, and+ `guardMExceptT`. These functions help convert `Maybe`, `Either`, and+ `Bool` to `ExceptT`. [#2](https://github.com/cdepillabout/from-sum/pull/2) ## 0.2.2.0
from-sum.cabal view
@@ -1,7 +1,7 @@ name: from-sum-version: 0.2.2.0-synopsis: Canonical fromMaybeM and fromEitherM functions.-description: Provides many functions for working with 'Maybe' and 'Either'. Please see @README.md@.+version: 0.2.3.0+synopsis: Combinators for working with Maybe and Either+description: Provides many functions for working with 'Maybe' and 'Either', including canonical 'fromMaybeM' and 'fromEitherM' functions. Please see @README.md@. homepage: https://github.com/cdepillabout/from-sum license: BSD3 license-file: LICENSE
src/Control/FromSum.hs view
@@ -38,6 +38,23 @@ -- * Collapsing funtions , collapseEither , collapseExceptT+ , collapseErrExceptT+ , -- * Converting to 'ExceptT'+ liftEitherExceptT+ , fromEitherExceptT+ , fromEitherOrExceptT+ , fromEitherMExceptT+ , fromEitherOrMExceptT+ , fromMaybeExceptT+ , fromMaybeOrExceptT+ , fromMaybeMExceptT+ , fromMaybeOrMExceptT+ , guardExceptT+ , guardMExceptT+ -- * Example converting to 'ExceptT'+ -- $exampleExceptT+ -- * Doctests+ -- $setup ) where #if __GLASGOW_HASKELL__ < 710@@ -46,9 +63,17 @@ import Control.Applicative #endif import Control.Monad ((<=<))-import Control.Monad.Trans.Except (ExceptT, runExceptT)+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Except (ExceptT(ExceptT), runExceptT, throwE) import Data.Maybe (fromMaybe) +-- $setup+--+-- The following is setup code for doctests in this module.+--+-- >>> import Data.Functor.Identity (Identity(Identity))++ -- | A monadic version of 'fromEither'. -- -- @@@ -248,7 +273,6 @@ -- | Similar to 'collapseEither', but for 'ExceptT'. ----- >>> import Control.Monad.Trans.Except (ExceptT(ExceptT)) -- >>> collapseExceptT (ExceptT $ pure (Right 3)) -- 3 -- >>> collapseExceptT (ExceptT $ pure (Left "hello"))@@ -256,6 +280,44 @@ collapseExceptT :: Monad m => ExceptT a m a -> m a collapseExceptT = pure . collapseEither <=< runExceptT +-- | Collapse an 'ExceptT' where the error returns the same type as the whole+-- computation.+--+-- >>> let exceptTOne = pure 3 :: ExceptT (IO Int) IO Int+-- >>> collapseErrExceptT exceptTOne :: IO Int+-- 3+--+-- This is helpful when writing short-circuiting computations where you+-- throw errors that match the type of the underlying computation.+--+-- >>> :{+-- let go :: Int -> ExceptT (IO ()) IO ()+-- go x = do+-- bar <-+-- if x < 10+-- then+-- pure "hello"+-- else+-- throwE (putStrLn "Error occurred, x too big!")+-- lift $ putStrLn $ bar ++ " world"+-- :}+--+-- >>> collapseErrExceptT (go 100) :: IO ()+-- Error occurred, x too big!+-- >>> collapseErrExceptT (go 3) :: IO ()+-- hello world+--+-- In this example, the error type in the 'ExceptT' is @'IO' ()@.+-- This allows us to easily short-circuit the remaining computations.+-- In this example, the remaining computation is just printing+-- @bar '++' \" world\"@.+collapseErrExceptT :: Monad m => ExceptT (m a) m a -> m a+collapseErrExceptT exceptT = do+ res <- runExceptT exceptT+ case res of+ Left errHandler -> errHandler+ Right success -> pure success+ -- | Convert a 'Maybe' to an 'Either'. -- -- If the 'Maybe' is 'Just', then return the value in 'Right'.@@ -295,3 +357,263 @@ eitherToMaybe :: Either e a -> Maybe a eitherToMaybe (Left _) = Nothing eitherToMaybe (Right a) = Just a++-- | Lift an 'Either' into an 'ExceptT'.+--+-- This is the same as 'Control.Monad.Except.liftEither', but the return type+-- is specialized for 'ExceptT'.+--+-- >>> liftEitherExceptT (Right 3) :: ExceptT String Identity Int+-- ExceptT (Identity (Right 3))+--+-- Note that if you want to lift @m ('Either' e a)@ to @'ExceptT' e m a@,+-- just use 'ExceptT':+--+-- >>> action = Identity (Left "error") :: Identity (Either String Int)+-- >>> ExceptT action :: ExceptT String Identity Int+-- ExceptT (Identity (Left "error"))+liftEitherExceptT :: Applicative m => Either e a -> ExceptT e m a+liftEitherExceptT = ExceptT . pure++-- | Lift an 'Either' to an 'ExceptT' with a handler for transforming the error+-- value.+--+-- If the input 'Either' is 'Right', then just return it like normal:+--+-- >>> let rightEither = Right () :: Either String ()+-- >>> fromEitherExceptT (\str -> length str) rightEither :: ExceptT Int Identity ()+-- ExceptT (Identity (Right ()))+--+-- If the input 'Either' is 'Left', then pass the value to the handler:+--+-- >>> let leftEither = Left "hello" :: Either String ()+-- >>> fromEitherExceptT (\str -> length str) leftEither :: ExceptT Int Identity ()+-- ExceptT (Identity (Left 5))+fromEitherExceptT :: Monad m => (e -> x) -> Either e a -> ExceptT x m a+fromEitherExceptT handler eith = fromEitherMExceptT handler (pure eith)++-- | Just like 'fromEitherExceptT', but the arguments are flipped.+fromEitherOrExceptT :: Monad m => Either e a -> (e -> x) -> ExceptT x m a+fromEitherOrExceptT eith handler = fromEitherExceptT handler eith++-- | Similar to 'fromEitherExceptT' but the 'Either' value is lifted in a+-- 'Monad'.+--+-- >>> let identityLeft = Identity (Left "hello") :: Identity (Either String ())+-- >>> fromEitherMExceptT (\str -> length str) identityLeft :: ExceptT Int Identity ()+-- ExceptT (Identity (Left 5))+--+-- This is similar to 'Control.Monad.Trans.Except.withExceptT', but the second+-- argument is the unwrapped 'ExceptT' computation.+fromEitherMExceptT :: Monad m => (e -> x) -> m (Either e a) -> ExceptT x m a+fromEitherMExceptT handler action = do+ res <- lift action+ case res of+ Left e -> throwE (handler e)+ Right a -> pure a++-- | Just like 'fromEitherOrMExceptT', but the arguments are flipped.+fromEitherOrMExceptT :: Monad m => m (Either e a) -> (e -> x) -> ExceptT x m a+fromEitherOrMExceptT action handler = fromEitherMExceptT handler action++-- | Lift a 'Maybe' to an 'ExceptT' with a default value for the case when+-- the 'Maybe' is 'Nothing'.+--+-- If the 'Maybe' is 'Just', then just return the value like normal:+--+-- >>> let justVal = Just True :: Maybe Bool+-- >>> fromMaybeExceptT 5 justVal :: ExceptT Int Identity Bool+-- ExceptT (Identity (Right True))+--+-- If the 'Maybe' is 'Nothing', then use the default value as the error value:+--+-- >>> let nothingVal = Nothing :: Maybe Bool+-- >>> fromMaybeExceptT 5 nothingVal :: ExceptT Int Identity Bool+-- ExceptT (Identity (Left 5))+fromMaybeExceptT :: Monad m => x -> Maybe a -> ExceptT x m a+fromMaybeExceptT handler mayb = fromMaybeMExceptT handler (pure mayb)++-- | Just like 'fromMaybeExceptT' but with the arguments flipped.+fromMaybeOrExceptT :: Monad m => Maybe a -> x -> ExceptT x m a+fromMaybeOrExceptT = flip fromMaybeExceptT++-- | Similar to 'fromMaybeExceptT' except the 'Maybe' value is lifted in a+-- 'Monad'.+--+-- >>> let identityNothing = Identity Nothing :: Identity (Maybe Bool)+-- >>> fromMaybeMExceptT 5 identityNothing :: ExceptT Int Identity Bool+-- ExceptT (Identity (Left 5))+fromMaybeMExceptT :: Monad m => x -> m (Maybe a) -> ExceptT x m a+fromMaybeMExceptT handler action = do+ res <- lift action+ case res of+ Nothing -> throwE handler+ Just a -> pure a++-- | Just like 'fromMaybeMExceptT' but with the arguments flipped.+fromMaybeOrMExceptT :: Monad m => m (Maybe a) -> x -> ExceptT x m a+fromMaybeOrMExceptT = flip fromMaybeMExceptT++-- | Similar to 'guard', but for 'ExceptT'.+--+-- If the 'Bool' is 'True', then do nothing.+--+-- >>> guardExceptT True "error occurred" :: ExceptT String Identity ()+-- ExceptT (Identity (Right ()))+--+-- If the 'Bool' is 'False', then return the error case:+--+-- >>> guardExceptT False "error occurred" :: ExceptT String Identity ()+-- ExceptT (Identity (Left "error occurred"))+guardExceptT :: Monad m => Bool -> x -> ExceptT x m ()+guardExceptT True _ = pure ()+guardExceptT False handler = throwE handler++-- | Just like 'guardExceptT' (and similar to 'guardM'), except the boolean is+-- lifted in a 'Monad'.+--+-- >>> guardMExceptT (Identity False) "error occurred" :: ExceptT String Identity ()+-- ExceptT (Identity (Left "error occurred"))+guardMExceptT :: Monad m => m Bool -> x -> ExceptT x m ()+guardMExceptT action handler = do+ res <- lift action+ guardExceptT res handler++-- $exampleExceptT+--+-- Functions like 'fromMaybeExceptT' and 'fromEitherExceptT' are convenient+-- when paired with 'collapseErrExceptT'. This section explains how+-- you can use these functions together.+--+-- Imagine you're writing a function that pulls user names from a database,+-- reads the first character of the name, and prints it to the console.+-- The functions for reading names from a database, and for+-- parsing the first character of the name could fail, so we will+-- handle these errors by logging to the console.+--+-- Here's the function we will be using for pulling user names from+-- the database. If we pass @0@, it returns @\"SPJ\"@. If we pass @1@,+-- it returns an empty string. Otherwise it returns 'Nothing':+--+-- >>> :{+-- let getUserNameFromDb :: Int -> Maybe String+-- getUserNameFromDb 0 = Just "SPJ"+-- getUserNameFromDb 1 = Just ""+-- getUserNameFromDb _ = Nothing+-- :}+--+-- Here's the function we will be using for parsing the first character+-- of a user name. If the user name is an empty string, we return+-- 'Left' with an error message. Otherwise we return the first+-- character of the user name:+--+-- >>> :{+-- let parseFirstCharFromName :: String -> Either String Char+-- parseFirstCharFromName [] = Left "user name is empty"+-- parseFirstCharFromName (h:_) = Right h+-- :}+--+-- Now let's write our function. If you didn't have the combinators from above+-- like 'fromEitherExceptT' and 'collapseErrExceptT', you might be tempted to+-- write nested @case@ patterns:+--+-- >>> :{+-- let nestedPrintFirstCharOfUserName :: Int -> IO ()+-- nestedPrintFirstCharOfUserName i =+-- -- Try to get the username for id i.+-- case getUserNameFromDb i of+-- -- If we couldn't get the user name from the database+-- -- print an error to the console.+-- Nothing -> putStrLn $ "ERROR: couldn't get user name for user " ++ show i+-- Just name ->+-- -- Try to parse the first character of the user name.+-- case parseFirstCharFromName name of+-- -- If we couldn't parse the first character of the user name,+-- -- print an error to the console.+-- Left err -> putStrLn $ "ERROR: " ++ err+-- Right firstChar ->+-- -- Print the first character of the user name to the console.+-- putStrLn $+-- "Got first character of name for id " ++ show i ++ ": " ++ [firstChar]+-- :}+--+-- Here's an example of using this function, including the error cases:+--+-- >>> nestedPrintFirstCharOfUserName 100+-- ERROR: couldn't get user name for user 100+-- >>> nestedPrintFirstCharOfUserName 1+-- ERROR: user name is empty+-- >>> nestedPrintFirstCharOfUserName 0+-- Got first character of name for id 0: S+--+-- This works, and is understandable, but it gets unwieldy when there+-- are even more parsing steps. You can get very deeply nested cases.+--+-- In order to write this without deeply nested error handling, you need a+-- short-circuiting 'Monad'. Two popular examples are 'MaybeT' and 'ExceptT'.+--+-- Using 'collapseErrExceptT' (and 'ExcepT'), it is possible to write this+-- function by short-circuiting on errors:+--+-- >>> :{+-- let printFirstCharOfUserName :: Int -> IO ()+-- printFirstCharOfUserName i =+-- -- The argument to collapseErrExceptT is @ExceptT (IO ()) IO ()@.+-- -- This can be thought of as an action that can short-circuit+-- -- with @IO ()@ error-handling actions.+-- --+-- -- The error-handling actions below just log to the console.+-- collapseErrExceptT $ do+-- -- Get the user name from the db.+-- -- If getUserNameFromDb returns Nothing, then this whole+-- -- block will short circuit and collapseErrExceptT will+-- -- run our error handler.+-- --+-- -- Note that for the type of the error handler to work+-- -- correctly with @collapseErrExceptT@, the error+-- -- handler has to return @IO ()@.+-- name <-+-- fromMaybeOrExceptT (getUserNameFromDb i) $+-- putStrLn $ "ERROR: couldn't get user name for user " ++ show i+-- -- Parse out the first character from the user name.+-- -- If parseFirstCharFromName returns Left, then this whole+-- -- block will short circuit and collapseErrExceptT will+-- -- run our error handler.+-- firstChar <-+-- fromEitherOrExceptT (parseFirstCharFromName name) $ \err ->+-- putStrLn $ "ERROR: " ++ err+-- -- Print the first character of the name.+-- -- This needs to be 'lift'ed because this whole block is+-- -- actually @ExceptT (IO ()) IO ()@.+-- lift $+-- putStrLn $+-- "Got first character of name for id " ++ show i ++ ": " ++ [firstChar]+-- :}+--+-- The main good point here is that using the short-circuiting functionality of+-- 'ExceptT', we can write everything without nesting.+--+-- Here's a few examples of calling @printFirstCharOfUserName@.+--+-- Here we pass a user id that doesn't exist, so @getUserNameFromDb@ will+-- return 'Nothing'. This causes the function to short-circuit and the+-- first error handler to be called.+--+-- >>> printFirstCharOfUserName 100+-- ERROR: couldn't get user name for user 100+--+-- Here we pass a user id that does exist, but the user name for this user id+-- is empty. This causes the function to short-circuit and the+-- second error handler to be called.+--+-- >>> printFirstCharOfUserName 1+-- ERROR: user name is empty+--+-- This time the function succeeds:+--+-- >>> printFirstCharOfUserName 0+-- Got first character of name for id 0: S+--+-- In real code, the functions @getUserNameFromDb@ and @parseFirstCharFromName@+-- will have monadic return values. In that case, you can use+-- 'fromMaybeOrMExceptT' and 'fromEitherOrMExceptT'.