Skip to content

Commit

Permalink
Add the Effectful.Exception module with appropriate re-exports (#252)
Browse files Browse the repository at this point in the history
  • Loading branch information
arybczak authored Oct 5, 2024
1 parent 89b5791 commit fa24cca
Show file tree
Hide file tree
Showing 14 changed files with 163 additions and 29 deletions.
1 change: 1 addition & 0 deletions effectful-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* Add a `SeqForkUnlift` strategy to support running unlifting functions outside
of the scope of effects they capture.
* Ensure that a `LocalEnv` is only used in a thread it belongs to.
* Add the `Effectful.Exception` module with appropriate re-exports.
* **Breaking changes**:
- `localSeqLend`, `localLend`, `localSeqBorrow` and `localBorrow` now take a
list of effects instead of a single one.
Expand Down
3 changes: 3 additions & 0 deletions effectful-core/effectful-core.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ library

build-depends: base >= 4.14 && < 5
, containers >= 0.6
, deepseq >= 1.2
, exceptions >= 0.10.4
, monad-control >= 1.0.3
, primitive >= 0.7.3.0
, safe-exceptions >= 0.1.7.2
, strict-mutable-base >= 1.1.0.0
, transformers-base >= 0.4.6
, unliftio-core >= 0.2.0.1
Expand All @@ -83,6 +85,7 @@ library
Effectful.Dispatch.Static.Unsafe
Effectful.Error.Dynamic
Effectful.Error.Static
Effectful.Exception
Effectful.Fail
Effectful.Internal.Effect
Effectful.Internal.Env
Expand Down
2 changes: 1 addition & 1 deletion effectful-core/src/Effectful.hs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ import Effectful.Internal.Monad
--
-- These libraries can trivially be used with the 'Eff' monad since it provides
-- typical instances that these libraries require the underlying monad to have,
-- such as t'Control.Monad.Catch.MonadMask' or 'MonadUnliftIO'.
-- such as t'Effectful.Exception.MonadMask' or 'MonadUnliftIO'.
--
-- In case the 'Eff' monad doesn't provide a specific instance out of the box,
-- it can be supplied via an effect. As an example see how the instance of
Expand Down
7 changes: 2 additions & 5 deletions effectful-core/src/Effectful/Dispatch/Dynamic.hs
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,10 @@ import Effectful.Internal.Utils
-- The following defines an 'EffectHandler' that reads and writes files from the
-- drive:
--
-- >>> import Control.Exception (IOException)
-- >>> import Control.Monad.Catch (catch)
-- >>> import Control.Monad.IO.Class
-- >>> import qualified System.IO as IO
--
-- >>> import Effectful.Error.Static
-- >>> import Effectful.Exception
--
-- >>> newtype FsError = FsError String deriving Show
--
Expand Down Expand Up @@ -249,7 +247,6 @@ import Effectful.Internal.Utils
--
-- If we naively try to interpret it, we will run into trouble:
--
-- >>> import Control.Monad.IO.Class
-- >>> import GHC.Clock (getMonotonicTime)
--
-- >>> :{
Expand Down Expand Up @@ -491,7 +488,6 @@ reinterpretWith runHandlerEs m handler = reinterpret runHandlerEs handler m
-- type instance DispatchOf E = Dynamic
-- :}
--
-- >>> import Control.Monad.IO.Class
-- >>> :{
-- runE :: IOE :> es => Eff (E : es) a -> Eff es a
-- runE = interpret_ $ \case
Expand Down Expand Up @@ -1199,4 +1195,5 @@ instance

-- $setup
-- >>> import Control.Concurrent (ThreadId, forkIOWithUnmask)
-- >>> import Control.Monad.IO.Class
-- >>> import Effectful.Reader.Static
14 changes: 7 additions & 7 deletions effectful-core/src/Effectful/Error/Static.hs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
-- | Support for handling errors of a particular type, i.e. checked exceptions.
--
-- The 'Error' effect is __not__ a general mechanism for handling regular
-- exceptions, that's what functions from the @exceptions@ library are for (see
-- "Control.Monad.Catch" for more information).
-- exceptions, that's what functions from the "Effectful.Exception" module are
-- for.
--
-- In particular, regular exceptions of type @e@ are distinct from errors of
-- type @e@ and will __not__ be caught by functions from this module:
--
-- >>> import qualified Control.Monad.Catch as E
-- >>> import qualified Effectful.Exception as E
--
-- >>> boom = error "BOOM!"
--
Expand All @@ -16,14 +16,14 @@
-- ...
--
-- If you want to catch regular exceptions, you should use
-- 'Control.Monad.Catch.catch' (or a similar function):
-- 'Effectful.Exception.catch' (or a similar function):
--
-- >>> runEff $ boom `E.catch` \(_::ErrorCall) -> pure "caught"
-- "caught"
--
-- On the other hand, functions for safe finalization and management of
-- resources such as 'Control.Monad.Catch.finally' and
-- 'Control.Monad.Catch.bracket' work as expected:
-- resources such as 'Effectful.Exception.finally' and
-- 'Effectful.Exception.bracket' work as expected:
--
-- >>> msg = liftIO . putStrLn
--
Expand Down Expand Up @@ -74,7 +74,7 @@
--
-- /Hint:/ if you'd like to reproduce the transactional behavior with the
-- t'Effectful.State.Static.Local.State' effect, appropriate usage of
-- 'Control.Monad.Catch.bracketOnError' will do the trick.
-- 'Effectful.Exception.bracketOnError' will do the trick.
module Effectful.Error.Static
( -- * Effect
Error
Expand Down
130 changes: 130 additions & 0 deletions effectful-core/src/Effectful/Exception.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
-- | The 'Eff' monad comes with instances of 'MonadThrow', 'MonadCatch' and
-- 'MonadMask' from the
-- [@exceptions@](https://hackage.haskell.org/package/exceptions) library, so
-- this module simply re-exports the interface of the
-- [@safe-exceptions@](https://hackage.haskell.org/package/safe-exceptions)
-- library.
--
-- Why @safe-exceptions@ and not @exceptions@? Because the former makes it much
-- easier to correctly deal with asynchronous exceptions (for more information
-- see its [README](https://github.com/fpco/safe-exceptions#readme)) and
-- provides more convenience functions.
module Effectful.Exception
( -- * Throwing
C.MonadThrow(..)
, Safe.throwString
, Safe.StringException(..)

-- * Catching (with recovery)
, C.MonadCatch(..)
, Safe.catchIO
, Safe.catchIOError
, Safe.catchAny
, Safe.catchDeep
, Safe.catchAnyDeep
, Safe.catchAsync
, Safe.catchJust

, Safe.handle
, Safe.handleIO
, Safe.handleIOError
, Safe.handleAny
, Safe.handleDeep
, Safe.handleAnyDeep
, Safe.handleAsync
, Safe.handleJust

, Safe.try
, Safe.tryIO
, Safe.tryAny
, Safe.tryDeep
, Safe.tryAnyDeep
, Safe.tryAsync
, Safe.tryJust

, Safe.Handler(..)
, Safe.catches
, Safe.catchesDeep
, Safe.catchesAsync

-- * Cleanup (no recovery)
, C.MonadMask(..)
, C.ExitCase(..)
, Safe.onException
, Safe.bracket
, Safe.bracket_
, Safe.finally
, Safe.withException
, Safe.bracketOnError
, Safe.bracketOnError_
, Safe.bracketWithError

-- * Utilities

-- ** Coercion to sync and async
, Safe.SyncExceptionWrapper(..)
, Safe.toSyncException
, Safe.AsyncExceptionWrapper(..)
, Safe.toAsyncException

-- ** Check exception type
, Safe.isSyncException
, Safe.isAsyncException

-- ** Evaluation
, evaluate
, evaluateDeep

-- * Re-exports from "Control.Exception"

-- ** The 'SomeException' type
, E.SomeException(..)

-- ** The 'Exception' class
, E.Exception(..)

-- ** Concrete exception types
, E.IOException
, E.ArithException(..)
, E.ArrayException(..)
, E.AssertionFailed(..)
, E.NoMethodError(..)
, E.PatternMatchFail(..)
, E.RecConError(..)
, E.RecSelError(..)
, E.RecUpdError(..)
, E.ErrorCall(..)
, E.TypeError(..)

-- ** Asynchronous exceptions
, E.SomeAsyncException(..)
, E.AsyncException(..)
, E.asyncExceptionToException
, E.asyncExceptionFromException
, E.NonTermination(..)
, E.NestedAtomically(..)
, E.BlockedIndefinitelyOnMVar(..)
, E.BlockedIndefinitelyOnSTM(..)
, E.AllocationLimitExceeded(..)
, E.CompactionFailed(..)
, E.Deadlock(..)

-- ** Assertions
, E.assert
) where

import Control.DeepSeq
import Control.Exception qualified as E
import Control.Exception.Safe qualified as Safe
import Control.Monad.Catch qualified as C

import Effectful
import Effectful.Dispatch.Static

-- | Lifted version of 'E.evaluate'.
evaluate :: a -> Eff es a
evaluate = unsafeEff_ . E.evaluate

-- | Deeply evaluate a value using 'evaluate' and 'NFData'.
evaluateDeep :: NFData a => a -> Eff es a
evaluateDeep = unsafeEff_ . E.evaluate . force
3 changes: 1 addition & 2 deletions effectful-core/src/Effectful/State/Static/Local.hs
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,4 @@ modifyM
modifyM f = stateM (\s -> ((), ) <$> f s)

-- $setup
-- >>> import Control.Exception (ErrorCall)
-- >>> import Control.Monad.Catch
-- >>> import Effectful.Exception
3 changes: 1 addition & 2 deletions effectful-core/src/Effectful/State/Static/Shared.hs
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,4 @@ modifyM :: (HasCallStack, State s :> es) => (s -> Eff es s) -> Eff es ()
modifyM f = stateM (\s -> ((), ) <$> f s)

-- $setup
-- >>> import Control.Exception (ErrorCall)
-- >>> import Control.Monad.Catch
-- >>> import Effectful.Exception
2 changes: 1 addition & 1 deletion effectful-core/src/Effectful/Writer/Static/Local.hs
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,4 @@ listens f m = do

-- $setup
-- >>> import Control.Exception (ErrorCall)
-- >>> import Control.Monad.Catch
-- >>> import Effectful.Exception
2 changes: 1 addition & 1 deletion effectful-core/src/Effectful/Writer/Static/Shared.hs
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,4 @@ listens f m = do

-- $setup
-- >>> import Control.Exception (ErrorCall)
-- >>> import Control.Monad.Catch
-- >>> import Effectful.Exception
1 change: 1 addition & 0 deletions effectful/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* Add a `SeqForkUnlift` strategy to support running unlifting functions outside
of the scope of effects they capture.
* Ensure that a `LocalEnv` is only used in a thread it belongs to.
* Add the `Effectful.Exception` module with appropriate re-exports.
* **Breaking changes**:
- `localSeqLend`, `localLend`, `localSeqBorrow` and `localBorrow` now take a
list of effects instead of a single one.
Expand Down
2 changes: 2 additions & 0 deletions effectful/effectful.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ library
, Effectful.Dispatch.Static
, Effectful.Error.Static
, Effectful.Error.Dynamic
, Effectful.Exception
, Effectful.Fail
, Effectful.Labeled
, Effectful.Labeled.Error
Expand Down Expand Up @@ -144,6 +145,7 @@ test-suite test
, exceptions
, lifted-base
, primitive
, safe-exceptions
, strict-mutable-base
, tasty
, tasty-hunit
Expand Down
19 changes: 11 additions & 8 deletions effectful/tests/StateTests.hs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
module StateTests (stateTests) where

import Control.Exception.Lifted qualified as LE
import Control.Exception.Safe qualified as Safe
import Control.Monad
import Control.Monad.Catch qualified as E
import Control.Monad.Catch qualified as C
import Data.IORef.Strict
import Test.Tasty
import Test.Tasty.HUnit
Expand Down Expand Up @@ -66,12 +67,14 @@ test_deepStack = runEff $ do

test_exceptions :: Assertion
test_exceptions = runEff $ do
testTry "exceptions" E.try
testCatch "exceptions" E.catch
testTry "lifted-base" LE.try
testCatch "lifted-base" LE.catch
testTry "unliftio" UE.try
testCatch "unliftio" UE.catch
testTry "exceptions" C.try
testCatch "exceptions" C.catch
testTry "safe-exceptions" Safe.try
testCatch "safe-exceptions" Safe.catch
testTry "lifted-base" LE.try
testCatch "lifted-base" LE.catch
testTry "unliftio" UE.try
testCatch "unliftio" UE.catch
where
testTry
:: String
Expand All @@ -96,7 +99,7 @@ test_exceptions = runEff $ do
action :: State Int :> es => Eff es ()
action = do
modify @Int (+1)
_ <- E.throwM U.Ex
_ <- C.throwM U.Ex
modify @Int (+2)

test_localEffects :: Assertion
Expand Down
3 changes: 1 addition & 2 deletions effectful/tests/Utils.hs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ module Utils
, Ex(..)
) where

import Control.Exception (ErrorCall(..))
import Control.Monad.Catch
import GHC.Stack
import Test.Tasty.HUnit qualified as T

import Effectful
import Effectful.Exception

assertBool :: (HasCallStack, IOE :> es) => String -> Bool -> Eff es ()
assertBool msg p = liftIO $ T.assertBool msg p
Expand Down

0 comments on commit fa24cca

Please sign in to comment.