packages feed

rescue-0.4.0: library/Control/Monad/Rescue.hs

{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications    #-}
{-# LANGUAGE TypeFamilies        #-}
{-# LANGUAGE TypeOperators       #-}

-- | Rescue semantics & helpers
--
-- Essentially a type-directed version of 'Control.Monad.Catch'.
--
-- This is the opposite of 'Control.Monad.Raise', which embeds en error.
-- 'Rescue' takes a potential error out of the surrounding context
-- and either handles or exposes it.
module Control.Monad.Rescue
  ( attemptM

  -- * Recover from exceptions

  , rescue
  , rescueT
  , rescueM
  , rescueBase

  , rescueEach
  , rescueEachM
  , rescueEachT

  , rescueAll

  -- * Guaranteed runs

  , reattempt
  , report
  , lastly

  -- * Error access

  , mapError
  , replaceError
  , asNotFound

  -- * Reexports

  , module Control.Monad.Raise
  , module Control.Monad.Rescue.Class
  , module Control.Monad.Rescue.Constraint
  ) where

import           Numeric.Natural

import           Data.Bifunctor                  as Bifunctor
import           Data.Exception.Types
import           Data.WorldPeace

import           Control.Monad.Base
import           Control.Monad.Raise
import           Control.Monad.Rescue.Class
import           Control.Monad.Rescue.Constraint
import           Control.Monad.Trans.Error

-- $setup
--
-- >>> :set -XDataKinds
-- >>> :set -XFlexibleContexts
-- >>> :set -XTypeApplications
-- >>> :set -XLambdaCase
--
-- >>> import Control.Monad.Trans.Rescue
-- >>> import Data.Proxy
-- >>> import Data.WorldPeace as OpenUnion
--
-- >>> data FooErr  = FooErr  deriving Show
-- >>> data BarErr  = BarErr  deriving Show
-- >>> data QuuxErr = QuuxErr deriving Show

-- | Simpler helper to eliminate the bind operator from an attempt flow
--
-- >>> type MyErrs = '[FooErr, BarErr]
--
-- >>> :{
-- boom :: Rescue MyErrs String
-- boom = raise FooErr
-- :}
--
-- >>> :{
-- attempt boom >>= \case
--   Left  err -> return ("err: " ++ show err)
--   Right val -> return val
-- :}
-- RescueT (Identity (Right "err: Identity FooErr"))
--
-- >>> :{
-- attemptM boom $ \case
--   Left  err -> return ("err: " ++ show err)
--   Right val -> return val
-- :}
-- RescueT (Identity (Right "err: Identity FooErr"))
attemptM :: MonadRescue m => m a -> (Either (ErrorCase m) a -> m b) -> m b
attemptM action handler = attempt action >>= handler

rescue
  :: ( Bifunctor m
     , ElemRemove err errs
     )
  => (err -> OpenUnion (Remove err errs))
  -> m (OpenUnion             errs)  a
  -> m (OpenUnion (Remove err errs)) a
rescue handler = Bifunctor.first (openUnionHandle id handler)

-- | Handle and eliminate a single error
rescueT ::
  ( MonadTransError t errs m
  , MonadRaise  (t (Remove err errs) m)
  , CheckErrors (t (Remove err errs) m)
  , ElemRemove err (Errors (t errs m))
  , Remove     err (Errors (t errs m)) ~ Errors (t (Remove err errs) m)
  )
  => (err -> (t (Remove err errs)) m a)
  -> t             errs  m a
  -> t (Remove err errs) m a
rescueT handler = onRaise (openUnionHandle raise handler)

-- | The more generic (MonadBase-ified) version of handle
rescueBase
  :: ( MonadRescue wide
     , MonadBase   wide narrow
     , MonadRaise       narrow
     , CheckErrors      narrow
     , Remove     err (Errors wide) ~ Errors narrow
     , ElemRemove err (Errors wide)
     )
  => (err -> narrow a)
  -> wide   a
  -> narrow a
rescueBase handler action =
  liftBase (attempt action) >>= \case
    Left err    -> openUnionHandle raise handler err
    Right value -> return value

rescueM
  :: ( MonadBase   (m (OpenUnion wide)) (m (OpenUnion (Remove err wide)))
     , MonadRescue (m (OpenUnion wide))
     , MonadRaise  (m (OpenUnion narrow))
     --
     , wide   ~ Errors (m (OpenUnion wide))
     , narrow ~ Errors (m (OpenUnion narrow))
     , narrow ~ Remove err wide
     , CheckErrors (m (OpenUnion narrow))
     , ElemRemove err wide
     )
  => (err -> m (OpenUnion narrow) a)
  -> m (OpenUnion wide)   a
  -> m (OpenUnion narrow) a
rescueM handler action =
  liftBase (attempt action) >>= \case
    Right val ->
      return val

    Left errs ->
      case openUnionRemove errs of
        Left  remainingErrs -> raise remainingErrs
        Right matchedErr    -> handler matchedErr

rescueEach
  :: ( Bifunctor m
     , ToOpenProduct handlerTuple (ReturnX (OpenUnion targetErrs) errs)
     )
  => handlerTuple
  -> m (OpenUnion errs)       a
  -> m (OpenUnion targetErrs) a
rescueEach handleCases = Bifunctor.first (catchesOpenUnion handleCases)

rescueEachM
  :: ( sourceErrs ~ Errors (m (OpenUnion sourceErrs))
     , MonadRescue         (m (OpenUnion sourceErrs))
     , MonadBase           (m (OpenUnion sourceErrs)) (m (OpenUnion targetErrs))
     , ToOpenProduct handlerTuple            (ReturnX (m (OpenUnion targetErrs) a) sourceErrs)
     )
  => handlerTuple
  -> m (OpenUnion sourceErrs) a
  -> m (OpenUnion targetErrs) a
rescueEachM handleCases action =
  liftBase (attempt action) >>= \case
    Left errs -> catchesOpenUnion handleCases errs
    Right val -> return val

rescueEachT
  :: ( sourceErrs ~ Errors (t sourceErrs m)
     , MonadTransError      t sourceErrs m
     , ToOpenProduct handlerTuple (ReturnX (t targetErrs m a) sourceErrs)
     )
  => handlerTuple
  -> t sourceErrs m a
  -> t targetErrs m a
rescueEachT handleCases = onRaise (catchesOpenUnion handleCases)

rescueAll
  :: ( MonadRescue   (m (OpenUnion errs))
     , MonadBase     (m (OpenUnion errs)) (m ())
     , errs ~ Errors (m (OpenUnion errs))
     )
  => (OpenUnion errs -> m () a)
  -> m (OpenUnion errs) a
  -> m () a
rescueAll handler action =
  liftBase (attempt action) >>= \case
    Left errs -> handler errs
    Right val -> return val

report
  :: ( MonadRescue m
     , RaisesOnly  m errs
     , CheckErrors m
     )
  => (ErrorCase m -> m ())
  -> m a
  -> m a
report withErr action =
  attempt action >>= \case
    Left err -> do
      withErr err
      raise err

    Right val ->
      return val

-- | 'retry' without asynchoronous exception cleanup.
--   Useful when not dealing with external resources that may
--   be dangerous to close suddenly.
reattempt :: MonadRescue m => Natural -> m a -> m a
reattempt 0     action = action
reattempt times action =
  attempt action >>= \case
    Left  _   -> reattempt (times - 1) action
    Right val -> return val

-- | Run an additional step, and throw away the result.
--   Return the result of the action passed.
lastly :: (CheckErrors m, MonadRescue m) => m a -> m b -> m a
lastly action finalizer = do
  errOrOk <- attempt action
  _       <- finalizer
  ensure errOrOk

-- AKA reinterpret
mapError
  :: ( MonadRescue m
     , MonadBase   m n
     , MonadRaise    n
     , CheckErrors   n
     )
  => (ErrorCase m -> ErrorCase n)
  -> m a
  -> n a
mapError mapper action =
  liftBase (attempt action) >>= \case
    Left  errCaseN -> raise $ mapper errCaseN
    Right value    -> return value

replaceError
  :: ( MonadRescue m
     , MonadBase   m n
     , MonadRaise    n
     , n `Raises` err
     )
  => err
  -> m a
  -> n a
replaceError err action =
  liftBase (attempt action) >>= \case
    Left  _     -> raise err
    Right value -> return value

asNotFound
  :: forall n m a .
    ( MonadRescue m
    , MonadBase   m n
    , MonadRaise    n
    , n `Raises` NotFound a
    )
  => m a
  -> n a
asNotFound = replaceError (NotFound @a)