packages feed

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 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'.