general-allocate-0.2.2.0: src/Control/Monad/With.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE UnicodeSyntax #-}
{- |
Description : Lexically-scoped exception-safe resource usage
Copyright : Copyright 2022 Shea Levy.
License : Apache-2.0
Maintainer : shea@shealevy.com
This module defines interfaces for safe resource usage on top of 'GeneralAllocate',
where resource cleanup happens at the end of a lexical scope.
For contexts where nested scope-based allocation and release is insufficient, see
"Control.Monad.Allocate".
-}
module Control.Monad.With where
import Control.Exception.Safe
import Control.Monad
import Control.Monad.Except
import qualified Control.Monad.RWS.Lazy as L
import Control.Monad.RWS.Strict
import Control.Monad.Reader
import Control.Monad.ST
import qualified Control.Monad.State.Lazy as L
import Control.Monad.State.Strict
import Control.Monad.Trans.Identity
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Resource.Internal
import qualified Control.Monad.Writer.Lazy as L
import Control.Monad.Writer.Strict
import Data.Coerce
import Data.Exceptable
import Data.Functor
import Data.Functor.Identity
import Data.GeneralAllocate
import Data.Void
{- | A monad allowing for exception-safe resource usage within a lexical scope.
The guarantees of 'MonadWith' are weaker than t'Control.Monad.Catch.MonadMask': in some
monads, it's possible for resources not to get cleaned up if the /entire/ monadic
computation is going to be aborted (e.g. an async exception sent to a thread executing a monad
with no exception catching). Of course, t'Control.Monad.Catch.MonadMask' itself can't guarantee
cleanup in the presence of @SIGKILL@... In any case, this allows for 'MonadWith' to be implemented
lawfully in more monads (see 'WithNoContinuation'). In particular, the 'MonadWith' instances for
'IO', 'ST', and 'Identity' allow for writing monad-generic exception-safe code which can be properly
instantiated in 'IO' or mocked out in 'ST'/'Identity' without changing the code.
-}
class (Monad m, ∀ x y. Coercible x y ⇒ Coercible (m x) (m y)) ⇒ MonadWith m where
-- | Data characterizing exceptional exit from the scope.
type WithException m
type WithException m = SomeException
-- | Allocate, use, and release a resource in some scope, threading through some state.
--
-- If resource acquisition succeeds, the resource is guaranteed to be released
-- /if the monadic computation itself is going to continue/. This is weaker than
-- the guarantees of v'Control.Monad.Catch.generalBracket', which can't be
-- implemented in monads without exception catching.
--
-- See 'generalWith' for the common use case where state threading isn't needed.
stateThreadingGeneralWith
∷ GeneralAllocate m (WithException m) releaseReturn b a
-- ^ Allocate the resource
→ (a → m b)
-- ^ Use the resource
→ m (b, releaseReturn)
{- | Describe the allocation and release of a resource.
A specialization of 'GeneralAllocate' for the most
common case with 'MonadWith', see 'generalWith'.
-}
type With m = GeneralAllocate m (WithException m) ()
{- | Allocate, use, and release a resource in some scope.
If resource acquisition succeeds, the resource is guaranteed to be released
/if the monadic computation itself is going to continue/. This is weaker than
the guarantees of v'Control.Monad.Catch.generalBracket', which can't be
implemented in monads without exception catching.
-}
generalWith
∷ (MonadWith m)
⇒ With m b a
-- ^ Allocate the resource
→ (a → m b)
-- ^ Use the resource
→ m b
generalWith alloc = (fst <$>) . stateThreadingGeneralWith alloc
{- | Run some action if the first action fails.
Exception propagation will continue after the failure action runs.
If failure occurs, the failure action is guaranteed to run
/if the monadic compuation itself is going to continue/. This is
weaker than the guranatess of v'Control.Monad.Catch.onError', which can't
be implemented in monads without exception catching.
-}
onFailure
∷ (MonadWith m)
⇒ m a
-- ^ Main action
→ (WithException m → m b)
-- ^ Failure action
→ m a
onFailure go err = generalWith alloc (const go)
where
alloc = GeneralAllocate $ \_ → pure $ GeneralAllocated () rel
rel (ReleaseFailure e) = void $ err e
rel _ = pure ()
{- | Run some action after another one completes in an exception-safe manner.
The final action is guaranteed to run
/if the monadic compuation itself is going to continue/. This is
weaker than the guranatess of v'Control.Monad.Catch.finally', which can't
be implemented in monads without exception catching.
-}
generalFinally
∷ (MonadWith m)
⇒ m a
-- ^ Main action
→ m b
-- ^ Final action
→ m (a, b)
generalFinally go fin = stateThreadingGeneralWith alloc $ const go
where
alloc = GeneralAllocate $ \_ → pure $ GeneralAllocated () rel
rel _ = fin
{- | A 'MonadWith' whose exception type can be projected into the Haskell exception hierarchy
Until https://gitlab.haskell.org/ghc/ghc/-/issues/16478 is fixed, you probably want to
add @{\-# OPTIONS_GHC -Wno-simplifiable-class-constraints #-\}@ to modules using this.
-}
class (MonadWith m, Exceptable (WithException m)) ⇒ MonadWithExceptable m
instance (MonadWith m, Exceptable (WithException m)) ⇒ MonadWithExceptable m
instance MonadWith IO where
stateThreadingGeneralWith (GeneralAllocate allocA) go = mask $ \restore → do
GeneralAllocated a releaseA ← allocA restore
b ←
restore (go a) `catch` \e → do
_ ← releaseA $ ReleaseFailure e
throwM e
c ← releaseA $ ReleaseSuccess b
pure (b, c)
{- | A helper for [DerivingVia](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/deriving_via.html) a
'MonadWith' and 'MonadWithExceptable' instance for any 'Monad'.
Note that the derived instance is only valid if the monad satisfies the "no continuation" condition, i.e.
that if execution of a computation exits a given lexical scope we are guaranteed that either all of the
actions within that scope have executed or the entire monadic computation has been terminated.
The most common factors violating "no continuation" are call/cc and exception catching. A monad which allows
exception /throwing/ but not catching is not thereby disqualified, as any thrown exception will of necessity
propagate until it terminates the entire monadic computation.
-}
newtype WithNoContinuation m a = WithNoContinuation (m a) deriving newtype (Functor, Applicative, Monad)
instance (Monad m, ∀ x y. Coercible x y ⇒ Coercible (m x) (m y)) ⇒ MonadWith (WithNoContinuation m) where
type WithException (WithNoContinuation m) = Void
stateThreadingGeneralWith (GeneralAllocate allocA) go = WithNoContinuation $ do
let WithNoContinuation allocA' = allocA id
GeneralAllocated a releaseA ← allocA'
let WithNoContinuation go' = go a
b ← go'
let WithNoContinuation releaseA' = releaseA $ ReleaseSuccess b
c ← releaseA'
pure (b, c)
deriving via WithNoContinuation (ST s) instance MonadWith (ST s)
deriving via WithNoContinuation Identity instance MonadWith Identity
instance (MonadWith m) ⇒ MonadWith (ReaderT r m) where
type WithException (ReaderT r m) = WithException m
stateThreadingGeneralWith
∷ ∀ a b releaseReturn
. GeneralAllocate (ReaderT r m) (WithException m) releaseReturn b a
→ (a → ReaderT r m b)
→ ReaderT r m (b, releaseReturn)
stateThreadingGeneralWith (GeneralAllocate allocA) go = ReaderT $ \r → do
let
allocA' ∷ (∀ x. m x → m x) → m (GeneralAllocated m (WithException m) releaseReturn b a)
allocA' restore = do
let
restore' ∷ ∀ x. ReaderT r m x → ReaderT r m x
restore' mx = ReaderT $ restore . runReaderT mx
GeneralAllocated a releaseA ← runReaderT (allocA restore') r
let
releaseA' relTy = runReaderT (releaseA relTy) r
pure $ GeneralAllocated a releaseA'
stateThreadingGeneralWith (GeneralAllocate allocA') (flip runReaderT r . go)
instance (MonadWith m) ⇒ MonadWith (ResourceT m) where
type WithException (ResourceT m) = WithException m
stateThreadingGeneralWith
∷ ∀ a b releaseReturn
. GeneralAllocate (ResourceT m) (WithException m) releaseReturn b a
→ (a → ResourceT m b)
→ ResourceT m (b, releaseReturn)
stateThreadingGeneralWith (GeneralAllocate allocA) go = ResourceT $ \st → do
let
allocA' ∷ (∀ x. m x → m x) → m (GeneralAllocated m (WithException m) releaseReturn b a)
allocA' restore = do
let
restore' ∷ ∀ x. ResourceT m x → ResourceT m x
restore' mx = ResourceT $ restore . unResourceT mx
GeneralAllocated a releaseA ← unResourceT (allocA restore') st
let
releaseA' relTy = unResourceT (releaseA relTy) st
pure $ GeneralAllocated a releaseA'
stateThreadingGeneralWith (GeneralAllocate allocA') (flip unResourceT st . go)
instance MonadWith (Either e) where
type WithException (Either e) = EitherException e
stateThreadingGeneralWith (GeneralAllocate allocA) go = do
GeneralAllocated a releaseA ← allocA id
b ← case go a of
Left e → do
_ ← releaseA . ReleaseFailure $ EitherException e
Left e
x → x
c ← releaseA $ ReleaseSuccess b
pure (b, c)
instance MonadWith m ⇒ MonadWith (MaybeT m) where
type WithException (MaybeT m) = Maybe (WithException m)
stateThreadingGeneralWith
∷ ∀ a b releaseReturn
. GeneralAllocate (MaybeT m) (Maybe (WithException m)) releaseReturn b a
→ (a → MaybeT m b)
→ MaybeT m (b, releaseReturn)
stateThreadingGeneralWith (GeneralAllocate allocA) go = MaybeT $ do
(mb, mc) ← stateThreadingGeneralWith (GeneralAllocate allocA') $ \case
Just a → runMaybeT $ go a
Nothing → pure Nothing
pure $ (,) <$> mb <*> mc
where
allocA' ∷ (∀ x. m x → m x) → m (GeneralAllocated m (WithException m) (Maybe releaseReturn) (Maybe b) (Maybe a))
allocA' restore =
runMaybeT (allocA restore') <&> \case
Just (GeneralAllocated a releaseA) → GeneralAllocated (Just a) $ \case
ReleaseSuccess (Just b) → runMaybeT . releaseA $ ReleaseSuccess b
ReleaseFailure e → runMaybeT . releaseA . ReleaseFailure $ Just e
_ → runMaybeT . releaseA $ ReleaseFailure Nothing
Nothing → GeneralAllocated Nothing (const $ pure Nothing)
where
restore' ∷ ∀ x. MaybeT m x → MaybeT m x
restore' = MaybeT . restore . runMaybeT
instance MonadWith m ⇒ MonadWith (ExceptT e m) where
type WithException (ExceptT e m) = Either e (WithException m)
stateThreadingGeneralWith
∷ ∀ a b releaseReturn
. GeneralAllocate (ExceptT e m) (Either e (WithException m)) releaseReturn b a
→ (a → ExceptT e m b)
→ ExceptT e m (b, releaseReturn)
stateThreadingGeneralWith (GeneralAllocate allocA) go = ExceptT $ do
(eb, ec) ← stateThreadingGeneralWith (GeneralAllocate allocA') $ \case
Left e → pure $ Left e
Right a → runExceptT $ go a
pure $ do
c ← ec
b ← eb
pure (b, c)
where
allocA' ∷ (∀ x. m x → m x) → m (GeneralAllocated m (WithException m) (Either e releaseReturn) (Either e b) (Either e a))
allocA' restore =
runExceptT (allocA restore') <&> \case
Right (GeneralAllocated a releaseA) → GeneralAllocated (Right a) $ \case
ReleaseSuccess (Right b) → runExceptT . releaseA $ ReleaseSuccess b
ReleaseSuccess (Left e) → runExceptT . releaseA . ReleaseFailure $ Left e
ReleaseFailure e → runExceptT . releaseA . ReleaseFailure $ Right e
Left e → GeneralAllocated (Left e) (const . pure $ Left e)
where
restore' ∷ ∀ x. ExceptT e m x → ExceptT e m x
restore' = ExceptT . restore . runExceptT
instance MonadWith m ⇒ MonadWith (IdentityT m) where
type WithException (IdentityT m) = WithException m
stateThreadingGeneralWith
∷ ∀ a b releaseReturn
. GeneralAllocate (IdentityT m) (WithException m) releaseReturn b a
→ (a → IdentityT m b)
→ IdentityT m (b, releaseReturn)
stateThreadingGeneralWith (GeneralAllocate allocA) go = IdentityT $ do
stateThreadingGeneralWith (GeneralAllocate allocA') $ runIdentityT . go
where
allocA' ∷ (∀ x. m x → m x) → m (GeneralAllocated m (WithException m) releaseReturn b a)
allocA' restore =
runIdentityT (allocA restore') <&> \case
GeneralAllocated a releaseA → GeneralAllocated a $ runIdentityT . releaseA
where
restore' ∷ ∀ x. IdentityT m x → IdentityT m x
restore' = IdentityT . restore . runIdentityT
instance MonadWith m ⇒ MonadWith (L.StateT s m) where
type WithException (L.StateT s m) = (WithException m, s)
stateThreadingGeneralWith
∷ ∀ a b releaseReturn
. GeneralAllocate (L.StateT s m) (WithException m, s) releaseReturn b a
→ (a → L.StateT s m b)
→ L.StateT s m (b, releaseReturn)
stateThreadingGeneralWith (GeneralAllocate allocA) go = L.StateT $ \s0 → do
let allocA' ∷ (∀ x. m x → m x) → m (GeneralAllocated m (WithException m) (releaseReturn, s) (b, s) (a, s))
allocA' restore =
L.runStateT (allocA restore') s0 <&> \case
(GeneralAllocated a releaseA, s1) → GeneralAllocated (a, s1) $ \case
ReleaseSuccess (b, s2) → L.runStateT (releaseA $ ReleaseSuccess b) s2
ReleaseFailure e → L.runStateT (releaseA $ ReleaseFailure (e, s1)) s1
where
restore' ∷ ∀ x. L.StateT s m x → L.StateT s m x
restore' mx = L.StateT $ restore . L.runStateT mx
((b, _s2), (c, s3)) ← stateThreadingGeneralWith (GeneralAllocate allocA') $ \case
(a, s1) → L.runStateT (go a) s1
pure ((b, c), s3)
instance MonadWith m ⇒ MonadWith (StateT s m) where
type WithException (StateT s m) = (WithException m, s)
stateThreadingGeneralWith
∷ ∀ a b releaseReturn
. GeneralAllocate (StateT s m) (WithException m, s) releaseReturn b a
→ (a → StateT s m b)
→ StateT s m (b, releaseReturn)
stateThreadingGeneralWith (GeneralAllocate allocA) go = StateT $ \s0 → do
let allocA' ∷ (∀ x. m x → m x) → m (GeneralAllocated m (WithException m) (releaseReturn, s) (b, s) (a, s))
allocA' restore =
runStateT (allocA restore') s0 <&> \case
(GeneralAllocated a releaseA, s1) → GeneralAllocated (a, s1) $ \case
ReleaseSuccess (b, s2) → runStateT (releaseA $ ReleaseSuccess b) s2
ReleaseFailure e → runStateT (releaseA $ ReleaseFailure (e, s1)) s1
where
restore' ∷ ∀ x. StateT s m x → StateT s m x
restore' mx = StateT $ restore . runStateT mx
((b, _s2), (c, s3)) ← stateThreadingGeneralWith (GeneralAllocate allocA') $ \case
(a, s1) → runStateT (go a) s1
pure ((b, c), s3)
instance (MonadWith m, Monoid w) ⇒ MonadWith (L.WriterT w m) where
type WithException (L.WriterT w m) = (WithException m, w)
stateThreadingGeneralWith
∷ ∀ a b releaseReturn
. GeneralAllocate (L.WriterT w m) (WithException m, w) releaseReturn b a
→ (a → L.WriterT w m b)
→ L.WriterT w m (b, releaseReturn)
stateThreadingGeneralWith (GeneralAllocate allocA) go = L.WriterT $ do
let allocA' ∷ (∀ x. m x → m x) → m (GeneralAllocated m (WithException m) (releaseReturn, w) (b, w) (a, w))
allocA' restore =
L.runWriterT (allocA restore') <&> \case
(GeneralAllocated a releaseA, w0) → GeneralAllocated (a, w0) $ \case
ReleaseSuccess (b, w1) → do
(c, w2) ← L.runWriterT . releaseA $ ReleaseSuccess b
pure (c, w1 <> w2)
ReleaseFailure e → do
(c, w1) ← L.runWriterT . releaseA $ ReleaseFailure (e, w0)
pure (c, w0 <> w1)
where
restore' ∷ ∀ x. L.WriterT w m x → L.WriterT w m x
restore' = L.WriterT . restore . L.runWriterT
((b, _w1), (c, w2)) ← stateThreadingGeneralWith (GeneralAllocate allocA') $ \case
(a, w0) → do
(b, w1) ← L.runWriterT $ go a
pure (b, w0 <> w1)
pure ((b, c), w2)
instance (MonadWith m, Monoid w) ⇒ MonadWith (WriterT w m) where
type WithException (WriterT w m) = (WithException m, w)
stateThreadingGeneralWith
∷ ∀ a b releaseReturn
. GeneralAllocate (WriterT w m) (WithException m, w) releaseReturn b a
→ (a → WriterT w m b)
→ WriterT w m (b, releaseReturn)
stateThreadingGeneralWith (GeneralAllocate allocA) go = WriterT $ do
let allocA' ∷ (∀ x. m x → m x) → m (GeneralAllocated m (WithException m) (releaseReturn, w) (b, w) (a, w))
allocA' restore =
runWriterT (allocA restore') <&> \case
(GeneralAllocated a releaseA, w0) → GeneralAllocated (a, w0) $ \case
ReleaseSuccess (b, w1) → do
(c, w2) ← runWriterT . releaseA $ ReleaseSuccess b
pure (c, w1 <> w2)
ReleaseFailure e → do
(c, w1) ← runWriterT . releaseA $ ReleaseFailure (e, w0)
pure (c, w0 <> w1)
where
restore' ∷ ∀ x. WriterT w m x → WriterT w m x
restore' = WriterT . restore . runWriterT
((b, _w1), (c, w2)) ← stateThreadingGeneralWith (GeneralAllocate allocA') $ \case
(a, w0) → do
(b, w1) ← runWriterT $ go a
pure (b, w0 <> w1)
pure ((b, c), w2)
instance (MonadWith m, Monoid w) ⇒ MonadWith (L.RWST r w s m) where
type WithException (L.RWST r w s m) = (WithException m, s, w)
stateThreadingGeneralWith
∷ ∀ a b releaseReturn
. GeneralAllocate (L.RWST r w s m) (WithException m, s, w) releaseReturn b a
→ (a → L.RWST r w s m b)
→ L.RWST r w s m (b, releaseReturn)
stateThreadingGeneralWith (GeneralAllocate allocA) go = L.RWST $ \r s0 → do
let allocA' ∷ (∀ x. m x → m x) → m (GeneralAllocated m (WithException m) (releaseReturn, s, w) (b, s, w) (a, s, w))
allocA' restore =
L.runRWST (allocA restore') r s0 <&> \case
(GeneralAllocated a releaseA, s1, w0) → GeneralAllocated (a, s1, w0) $ \case
ReleaseSuccess (b, s2, w1) → do
(c, s3, w2) ← L.runRWST (releaseA $ ReleaseSuccess b) r s2
pure (c, s3, w1 <> w2)
ReleaseFailure e → do
(c, s2, w1) ← L.runRWST (releaseA $ ReleaseFailure (e, s1, w0)) r s1
pure (c, s2, w0 <> w1)
where
restore' ∷ ∀ x. L.RWST r w s m x → L.RWST r w s m x
restore' mx = L.RWST $ \r' → restore . L.runRWST mx r'
((b, _s2, _w1), (c, s3, w2)) ← stateThreadingGeneralWith (GeneralAllocate allocA') $ \case
(a, s1, w0) → do
(b, s2, w1) ← L.runRWST (go a) r s1
pure (b, s2, w0 <> w1)
pure ((b, c), s3, w2)
instance (MonadWith m, Monoid w) ⇒ MonadWith (RWST r w s m) where
type WithException (RWST r w s m) = (WithException m, s, w)
stateThreadingGeneralWith
∷ ∀ a b releaseReturn
. GeneralAllocate (RWST r w s m) (WithException m, s, w) releaseReturn b a
→ (a → RWST r w s m b)
→ RWST r w s m (b, releaseReturn)
stateThreadingGeneralWith (GeneralAllocate allocA) go = RWST $ \r s0 → do
let allocA' ∷ (∀ x. m x → m x) → m (GeneralAllocated m (WithException m) (releaseReturn, s, w) (b, s, w) (a, s, w))
allocA' restore =
runRWST (allocA restore') r s0 <&> \case
(GeneralAllocated a releaseA, s1, w0) → GeneralAllocated (a, s1, w0) $ \case
ReleaseSuccess (b, s2, w1) → do
(c, s3, w2) ← runRWST (releaseA $ ReleaseSuccess b) r s2
pure (c, s3, w1 <> w2)
ReleaseFailure e → do
(c, s2, w1) ← runRWST (releaseA $ ReleaseFailure (e, s1, w0)) r s1
pure (c, s2, w0 <> w1)
where
restore' ∷ ∀ x. RWST r w s m x → RWST r w s m x
restore' mx = RWST $ \r' → restore . runRWST mx r'
((b, _s2, _w1), (c, s3, w2)) ← stateThreadingGeneralWith (GeneralAllocate allocA') $ \case
(a, s1, w0) → do
(b, s2, w1) ← runRWST (go a) r s1
pure (b, s2, w0 <> w1)
pure ((b, c), s3, w2)