polysemy-1.9.2.0: src/Polysemy/Error.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_HADDOCK prune #-}
-- | Description: The effect 'Error' and its interpreters
module Polysemy.Error
( -- * Effect
Error (..)
-- * Actions
, throw
, catch
, fromEither
, fromEitherM
, fromException
, fromExceptionVia
, fromExceptionSem
, fromExceptionSemVia
, note
, try
, tryJust
, catchJust
-- * Interpretations
, runError
, mapError
, errorToIOFinal
) where
import qualified Control.Exception as X
import Control.Monad
import qualified Control.Monad.Trans.Except as E
import Data.Unique (Unique, hashUnique, newUnique)
import GHC.Exts (Any)
import Polysemy
import Polysemy.Final
import Polysemy.Internal
import Polysemy.Internal.Union
import Unsafe.Coerce (unsafeCoerce)
------------------------------------------------------------------------------
-- | This effect abstracts the throwing and catching of errors, leaving
-- it up to the interpreter whether to use exceptions or monad transformers
-- like 'E.ExceptT' to perform the short-circuiting mechanism.
data Error e m a where
-- | Short-circuit the current program using the given error value.
Throw :: e -> Error e m a
-- | Recover from an error that might have been thrown in the higher-order
-- action given by the first argument by passing the error to the handler
-- given by the second argument.
Catch :: ∀ e m a. m a -> (e -> m a) -> Error e m a
makeSem ''Error
hush :: Either e a -> Maybe a
hush (Right a) = Just a
hush (Left _) = Nothing
------------------------------------------------------------------------------
-- | Upgrade an 'Either' into an 'Error' effect.
--
-- @since 0.5.1.0
fromEither
:: Member (Error e) r
=> Either e a
-> Sem r a
fromEither (Left e) = throw e
fromEither (Right a) = pure a
{-# INLINABLE fromEither #-}
------------------------------------------------------------------------------
-- | A combinator doing 'embed' and 'fromEither' at the same time. Useful for
-- interoperating with 'IO'.
--
-- @since 0.5.1.0
fromEitherM
:: forall e m r a
. ( Member (Error e) r
, Member (Embed m) r
)
=> m (Either e a)
-> Sem r a
fromEitherM = fromEither <=< embed
{-# INLINABLE fromEitherM #-}
------------------------------------------------------------------------------
-- | Lift an exception generated from an 'IO' action into an 'Error'.
fromException
:: forall e r a
. ( X.Exception e
, Member (Error e) r
, Member (Embed IO) r
)
=> IO a
-> Sem r a
fromException = fromExceptionVia @e id
{-# INLINABLE fromException #-}
------------------------------------------------------------------------------
-- | Like 'fromException', but with the ability to transform the exception
-- before turning it into an 'Error'.
fromExceptionVia
:: ( X.Exception exc
, Member (Error err) r
, Member (Embed IO) r
)
=> (exc -> err)
-> IO a
-> Sem r a
fromExceptionVia f m = do
r <- embed $ X.try m
case r of
Left e -> throw $ f e
Right a -> pure a
{-# INLINABLE fromExceptionVia #-}
------------------------------------------------------------------------------
-- | Run a @Sem r@ action, converting any 'IO' exception generated by it into an 'Error'.
fromExceptionSem
:: forall e r a
. ( X.Exception e
, Member (Error e) r
, Member (Final IO) r
)
=> Sem r a
-> Sem r a
fromExceptionSem = fromExceptionSemVia @e id
{-# INLINABLE fromExceptionSem #-}
------------------------------------------------------------------------------
-- | Like 'fromExceptionSem', but with the ability to transform the exception
-- before turning it into an 'Error'.
fromExceptionSemVia
:: ( X.Exception exc
, Member (Error err) r
, Member (Final IO) r
)
=> (exc -> err)
-> Sem r a
-> Sem r a
fromExceptionSemVia f m = do
r <- withStrategicToFinal $ do
m' <- runS m
s <- getInitialStateS
pure $ (fmap . fmap) Right m' `X.catch` \e -> (pure (Left e <$ s))
case r of
Left e -> throw $ f e
Right a -> pure a
{-# INLINABLE fromExceptionSemVia #-}
------------------------------------------------------------------------------
-- | Attempt to extract a @'Just' a@ from a @'Maybe' a@, throwing the
-- provided exception upon 'Nothing'.
note :: Member (Error e) r => e -> Maybe a -> Sem r a
note e Nothing = throw e
note _ (Just a) = pure a
{-# INLINABLE note #-}
------------------------------------------------------------------------------
-- | Similar to @'catch'@, but returns an @'Either'@ result which is (@'Right' a@)
-- if no exception of type @e@ was @'throw'@n, or (@'Left' ex@) if an exception of type
-- @e@ was @'throw'@n and its value is @ex@.
try :: Member (Error e) r => Sem r a -> Sem r (Either e a)
try m = catch (Right <$> m) (return . Left)
{-# INLINABLE try #-}
------------------------------------------------------------------------------
-- | A variant of @'try'@ that takes an exception predicate to select which exceptions
-- are caught (c.f. @'catchJust'@). If the exception does not match the predicate,
-- it is re-@'throw'@n.
tryJust :: Member (Error e) r => (e -> Maybe b) -> Sem r a -> Sem r (Either b a)
tryJust f m = do
r <- try m
case r of
Right v -> return (Right v)
Left e -> case f e of
Nothing -> throw e
Just b -> return $ Left b
{-# INLINABLE tryJust #-}
------------------------------------------------------------------------------
-- | The function @'catchJust'@ is like @'catch'@, but it takes an extra argument
-- which is an exception predicate, a function which selects which type of exceptions
-- we're interested in.
catchJust :: Member (Error e) r
=> (e -> Maybe b) -- ^ Predicate to select exceptions
-> Sem r a -- ^ Computation to run
-> (b -> Sem r a) -- ^ Handler
-> Sem r a
catchJust ef m bf = catch m handler
where
handler e = case ef e of
Nothing -> throw e
Just b -> bf b
{-# INLINABLE catchJust #-}
------------------------------------------------------------------------------
-- | Run an 'Error' effect in the style of
-- 'Control.Monad.Trans.Except.ExceptT'.
runError
:: Sem (Error e ': r) a
-> Sem r (Either e a)
runError (Sem m) = Sem $ \k -> E.runExceptT $ m $ \u ->
case decomp u of
Left x -> E.ExceptT $ k $
weave (Right ())
(either (pure . Left) runError)
hush
x
Right (Weaving (Throw e) _ _ _ _) -> E.throwE e
Right (Weaving (Catch main handle) s d y _) ->
E.ExceptT $ usingSem k $ do
ma <- runError $ d $ main <$ s
case ma of
Right a -> pure . Right $ y a
Left e -> do
ma' <- runError $ d $ (<$ s) $ handle e
case ma' of
Left e' -> pure $ Left e'
Right a -> pure . Right $ y a
{-# INLINE runError #-}
------------------------------------------------------------------------------
-- | Transform one 'Error' into another. This function can be used to aggregate
-- multiple errors into a single type.
--
-- @since 1.0.0.0
mapError
:: forall e1 e2 r a
. Member (Error e2) r
=> (e1 -> e2)
-> Sem (Error e1 ': r) a
-> Sem r a
mapError f = interpretH $ \case
Throw e -> throw $ f e
Catch action handler -> do
a <- runT action
h <- bindT handler
mx <- raise $ runError a
case mx of
Right x -> pure x
Left e -> do
istate <- getInitialStateT
mx' <- raise $ runError $ h $ e <$ istate
case mx' of
Right x -> pure x
Left e' -> throw $ f e'
{-# INLINE mapError #-}
data WrappedExc = WrappedExc !Unique Any
instance Show WrappedExc where
show (WrappedExc uid _) =
"errorToIOFinal: Escaped opaque exception. Unique hash is: " <>
show (hashUnique uid) <> "This should only happen if the computation that " <>
"threw the exception was somehow invoked outside of the argument of 'errorToIOFinal'; " <>
"for example, if you 'async' an exceptional computation inside of the argument " <>
"provided to 'errorToIOFinal', and then 'await' on it *outside* of the argument " <>
"provided to 'errorToIOFinal'. If that or any similar shenanigans seems unlikely, " <>
"please open an issue on the GitHub repository."
instance X.Exception WrappedExc
catchWithUid :: forall e a. Unique -> IO a -> (e -> IO a) -> IO a
catchWithUid uid m h = X.catch m $ \exc@(WrappedExc uid' e) ->
if uid == uid' then h (unsafeCoerce e) else X.throwIO exc
{-# INLINE catchWithUid #-}
------------------------------------------------------------------------------
-- | Run an 'Error' effect as an 'IO' 'X.Exception' through final 'IO'. This
-- interpretation is significantly faster than 'runError'.
--
-- /Beware/: Effects that aren't interpreted in terms of 'IO'
-- will have local state semantics in regards to 'Error' effects
-- interpreted this way. See 'Final'.
--
-- @since 1.2.0.0
errorToIOFinal
:: forall e r a
. ( Member (Final IO) r
)
=> Sem (Error e ': r) a
-> Sem r (Either e a)
errorToIOFinal sem = withStrategicToFinal @IO $ do
m' <- bindS (`runErrorAsExcFinal` sem)
s <- getInitialStateS
pure $ do
uid <- newUnique
catchWithUid @e uid (fmap Right <$> m' (uid <$ s)) (pure . (<$ s) . Left)
{-# INLINE errorToIOFinal #-}
runErrorAsExcFinal
:: forall e r a
. ( Member (Final IO) r
)
=> Unique
-> Sem (Error e ': r) a
-> Sem r a
runErrorAsExcFinal uid = interpretFinal $ \case
Throw e -> pure $ X.throwIO $ WrappedExc uid (unsafeCoerce e)
Catch m h -> do
m' <- runS m
h' <- bindS h
s <- getInitialStateS
pure $ catchWithUid uid m' $ \e -> h' (e <$ s)
{-# INLINE runErrorAsExcFinal #-}