packages feed

eventuo11y-0.5.0.0: src/Control/Monad/Cleanup.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}

-- |
-- Description : Monads that can cleanup within a single monaadic scope
-- Copyright   : Copyright 2022 Shea Levy.
-- License     : Apache-2.0
-- Maintainer  : shea@shealevy.com
module Control.Monad.Cleanup where

import Control.Exception.Safe
import Control.Monad.Catch (ExitCase (..))
import Control.Monad.ST
import Control.Monad.Trans.Except (ExceptT (..), runExceptT)
import Control.Monad.Trans.Identity
import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)
import qualified Control.Monad.Trans.RWS.Lazy as LazyRWS
import qualified Control.Monad.Trans.RWS.Strict as StrictRWS
import Control.Monad.Trans.Reader (ReaderT (..), runReaderT)
import qualified Control.Monad.Trans.State.Lazy as LazyS
import qualified Control.Monad.Trans.State.Strict as StrictS
import qualified Control.Monad.Trans.Writer.Lazy as LazyW
import qualified Control.Monad.Trans.Writer.Strict as StrictW
import Data.Coerce
import Data.Functor.Identity

-- | Monads that can cleanup within a single monadic scope.
--
-- 'MonadCleanup's allow for acquiring some resource and
-- guaranteeing that it will be released,
-- __if computation might continue within that monad__.
--
-- This is very similar to 'MonadMask', with the following differences:
--
-- 1. 'MonadCleanup's may not be able to throw or catch exceptions (no 'MonadCatch'
--    superclass) or mask exceptions.
-- 2. The guarantee of 'generalCleanup' is not as absolute as 'generalBracket' (though the latter
--    always has the @SIGKILL@/power goes out exception). If we can't handle exceptions
--    at all in the monad (at least not without @unsafePerformIO@), then sometimes the cleanup
--    function won't be called, but only in cases where the entire computation the monad is
--    running is going to be aborted.
--
-- This allows 'MonadCleanup' to be used in pure contexts (see 'CleanupNoException') and still
-- provide meaningful semantics.
class (Monad m) => MonadCleanup m where
  -- | Acquire some resource, use it, and clean it up.
  --
  -- cleanup is guaranteed to run
  -- __if computation in the surrounding monadic scope might continue__.
  --
  -- Similar to 'generalBracket', see documentation of 'MonadCleanup' for the differences.
  generalCleanup ::
    -- | Acquire some resource
    m a ->
    -- | Release the resource, observing the outcome of the inner action
    (a -> ExitCase b -> m c) ->
    -- | Inner action to perform with the resource
    (a -> m b) ->
    m (b, c)

-- | An 'Exception' corresponding to the 'ExitCaseAbort' exit case.
data AbortException = AbortException
  deriving stock (Show)
  deriving anyclass (Exception)

-- | Acquire some resource, use it, and clean it up.
--
-- This is to 'bracketWithError' as 'generalCleanup' is to 'generalBracket', see documentation
-- of 'generalCleanup' for more details.
withCleanup ::
  (MonadCleanup m) =>
  m a ->
  (Maybe SomeException -> a -> m b) ->
  (a -> m c) ->
  m c
withCleanup acquire cleanup go = fst <$> generalCleanup acquire release go
  where
    release x (ExitCaseSuccess _) = cleanup Nothing x
    release x ExitCaseAbort = cleanup (Just (toException AbortException)) x
    release x (ExitCaseException e) = cleanup (Just e) x

-- | A [DerivingVia](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/deriving_via.html) helper for deriving 'MonadCleanup' from 'MonadMask'.
newtype CleanupFromMask m a = CleanupFromMask (m a) deriving newtype (Functor, Applicative, Monad)

instance (MonadMask m) => MonadCleanup (CleanupFromMask m) where
  generalCleanup :: forall a b c. CleanupFromMask m a -> (a -> ExitCase b -> CleanupFromMask m c) -> (a -> CleanupFromMask m b) -> CleanupFromMask m (b, c)
  generalCleanup = coerce $ generalBracket @m @a @b @c

deriving via CleanupFromMask IO instance MonadCleanup IO

-- | A [DerivingVia](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/deriving_via.html) for deriving 'MonadCleanup' in a 'Monad' which
-- __can't__ handle exceptions.
--
-- If cleanup runs at all, it will run in 'ExitCaseSuccess'
--
-- __Note that the associated 'MonadCleanup' instance is invalid if it is possible to catch exceptions in the monad!__
newtype CleanupNoException m a = CleanupNoException (m a) deriving newtype (Functor, Applicative, Monad)

instance (Monad m) => MonadCleanup (CleanupNoException m) where
  generalCleanup acquire release go = do
    x <- acquire
    res <- go x
    carry <- release x (ExitCaseSuccess res)
    pure (res, carry)

deriving via CleanupNoException (ST s) instance MonadCleanup (ST s)

deriving via CleanupNoException Identity instance MonadCleanup Identity

instance (MonadCleanup m) => MonadCleanup (IdentityT m) where
  generalCleanup acquire release use =
    IdentityT $
      generalCleanup
        (runIdentityT acquire)
        (\resource exitCase -> runIdentityT (release resource exitCase))
        (\resource -> runIdentityT (use resource))

instance MonadCleanup m => MonadCleanup (LazyS.StateT s m) where
  generalCleanup acquire release use = LazyS.StateT $ \s0 -> do
    ((b, _s2), (c, s3)) <-
      generalCleanup
        (LazyS.runStateT acquire s0)
        ( \(resource, s1) exitCase -> case exitCase of
            ExitCaseSuccess (b, s2) -> LazyS.runStateT (release resource (ExitCaseSuccess b)) s2
            -- In the two other cases, the base monad overrides @use@'s state
            -- changes and the state reverts to @s1@.
            ExitCaseException e -> LazyS.runStateT (release resource (ExitCaseException e)) s1
            ExitCaseAbort -> LazyS.runStateT (release resource ExitCaseAbort) s1
        )
        (\(resource, s1) -> LazyS.runStateT (use resource) s1)
    return ((b, c), s3)

instance MonadCleanup m => MonadCleanup (StrictS.StateT s m) where
  generalCleanup acquire release use = StrictS.StateT $ \s0 -> do
    ((b, _s2), (c, s3)) <-
      generalCleanup
        (StrictS.runStateT acquire s0)
        ( \(resource, s1) exitCase -> case exitCase of
            ExitCaseSuccess (b, s2) -> StrictS.runStateT (release resource (ExitCaseSuccess b)) s2
            -- In the two other cases, the base monad overrides @use@'s state
            -- changes and the state reverts to @s1@.
            ExitCaseException e -> StrictS.runStateT (release resource (ExitCaseException e)) s1
            ExitCaseAbort -> StrictS.runStateT (release resource ExitCaseAbort) s1
        )
        (\(resource, s1) -> StrictS.runStateT (use resource) s1)
    return ((b, c), s3)

instance MonadCleanup m => MonadCleanup (ReaderT r m) where
  generalCleanup acquire release use = ReaderT $ \r ->
    generalCleanup
      (runReaderT acquire r)
      (\resource exitCase -> runReaderT (release resource exitCase) r)
      (\resource -> runReaderT (use resource) r)

instance (MonadCleanup m, Monoid w) => MonadCleanup (StrictW.WriterT w m) where
  generalCleanup acquire release use = StrictW.WriterT $ do
    ((b, _w12), (c, w123)) <-
      generalCleanup
        (StrictW.runWriterT acquire)
        ( \(resource, w1) exitCase -> case exitCase of
            ExitCaseSuccess (b, w12) -> do
              (c, w3) <- StrictW.runWriterT (release resource (ExitCaseSuccess b))
              return (c, mappend w12 w3)
            -- In the two other cases, the base monad overrides @use@'s state
            -- changes and the state reverts to @w1@.
            ExitCaseException e -> do
              (c, w3) <- StrictW.runWriterT (release resource (ExitCaseException e))
              return (c, mappend w1 w3)
            ExitCaseAbort -> do
              (c, w3) <- StrictW.runWriterT (release resource ExitCaseAbort)
              return (c, mappend w1 w3)
        )
        ( \(resource, w1) -> do
            (a, w2) <- StrictW.runWriterT (use resource)
            return (a, mappend w1 w2)
        )
    return ((b, c), w123)

instance (MonadCleanup m, Monoid w) => MonadCleanup (LazyW.WriterT w m) where
  generalCleanup acquire release use = LazyW.WriterT $ do
    ((b, _w12), (c, w123)) <-
      generalCleanup
        (LazyW.runWriterT acquire)
        ( \(resource, w1) exitCase -> case exitCase of
            ExitCaseSuccess (b, w12) -> do
              (c, w3) <- LazyW.runWriterT (release resource (ExitCaseSuccess b))
              return (c, mappend w12 w3)
            -- In the two other cases, the base monad overrides @use@'s state
            -- changes and the state reverts to @w1@.
            ExitCaseException e -> do
              (c, w3) <- LazyW.runWriterT (release resource (ExitCaseException e))
              return (c, mappend w1 w3)
            ExitCaseAbort -> do
              (c, w3) <- LazyW.runWriterT (release resource ExitCaseAbort)
              return (c, mappend w1 w3)
        )
        ( \(resource, w1) -> do
            (a, w2) <- LazyW.runWriterT (use resource)
            return (a, mappend w1 w2)
        )
    return ((b, c), w123)

instance (MonadCleanup m, Monoid w) => MonadCleanup (LazyRWS.RWST r w s m) where
  generalCleanup acquire release use = LazyRWS.RWST $ \r s0 -> do
    ((b, _s2, _w12), (c, s3, w123)) <-
      generalCleanup
        (LazyRWS.runRWST acquire r s0)
        ( \(resource, s1, w1) exitCase -> case exitCase of
            ExitCaseSuccess (b, s2, w12) -> do
              (c, s3, w3) <- LazyRWS.runRWST (release resource (ExitCaseSuccess b)) r s2
              return (c, s3, mappend w12 w3)
            -- In the two other cases, the base monad overrides @use@'s state
            -- changes and the state reverts to @s1@ and @w1@.
            ExitCaseException e -> do
              (c, s3, w3) <- LazyRWS.runRWST (release resource (ExitCaseException e)) r s1
              return (c, s3, mappend w1 w3)
            ExitCaseAbort -> do
              (c, s3, w3) <- LazyRWS.runRWST (release resource ExitCaseAbort) r s1
              return (c, s3, mappend w1 w3)
        )
        ( \(resource, s1, w1) -> do
            (a, s2, w2) <- LazyRWS.runRWST (use resource) r s1
            return (a, s2, mappend w1 w2)
        )
    return ((b, c), s3, w123)

instance (MonadCleanup m, Monoid w) => MonadCleanup (StrictRWS.RWST r w s m) where
  generalCleanup acquire release use = StrictRWS.RWST $ \r s0 -> do
    ((b, _s2, _w12), (c, s3, w123)) <-
      generalCleanup
        (StrictRWS.runRWST acquire r s0)
        ( \(resource, s1, w1) exitCase -> case exitCase of
            ExitCaseSuccess (b, s2, w12) -> do
              (c, s3, w3) <- StrictRWS.runRWST (release resource (ExitCaseSuccess b)) r s2
              return (c, s3, mappend w12 w3)
            -- In the two other cases, the base monad overrides @use@'s state
            -- changes and the state reverts to @s1@ and @w1@.
            ExitCaseException e -> do
              (c, s3, w3) <- StrictRWS.runRWST (release resource (ExitCaseException e)) r s1
              return (c, s3, mappend w1 w3)
            ExitCaseAbort -> do
              (c, s3, w3) <- StrictRWS.runRWST (release resource ExitCaseAbort) r s1
              return (c, s3, mappend w1 w3)
        )
        ( \(resource, s1, w1) -> do
            (a, s2, w2) <- StrictRWS.runRWST (use resource) r s1
            return (a, s2, mappend w1 w2)
        )
    return ((b, c), s3, w123)

instance MonadCleanup m => MonadCleanup (MaybeT m) where
  generalCleanup acquire release use = MaybeT $ do
    (eb, ec) <-
      generalCleanup
        (runMaybeT acquire)
        ( \resourceMay exitCase -> case resourceMay of
            Nothing -> return Nothing -- nothing to release, acquire didn't succeed
            Just resource -> case exitCase of
              ExitCaseSuccess (Just b) -> runMaybeT (release resource (ExitCaseSuccess b))
              ExitCaseException e -> runMaybeT (release resource (ExitCaseException e))
              _ -> runMaybeT (release resource ExitCaseAbort)
        )
        ( \resourceMay -> case resourceMay of
            Nothing -> return Nothing
            Just resource -> runMaybeT (use resource)
        )
    -- The order in which we perform those two 'Maybe' effects doesn't matter,
    -- since the error message is the same regardless.
    return ((,) <$> eb <*> ec)

instance MonadCleanup m => MonadCleanup (ExceptT e m) where
  generalCleanup acquire release use = ExceptT $ do
    (eb, ec) <-
      generalCleanup
        (runExceptT acquire)
        ( \eresource exitCase -> case eresource of
            Left e -> return (Left e) -- nothing to release, acquire didn't succeed
            Right resource -> case exitCase of
              ExitCaseSuccess (Right b) -> runExceptT (release resource (ExitCaseSuccess b))
              ExitCaseException e -> runExceptT (release resource (ExitCaseException e))
              _ -> runExceptT (release resource ExitCaseAbort)
        )
        (either (return . Left) (runExceptT . use))
    return $ do
      -- The order in which we perform those two 'Either' effects determines
      -- which error will win if they are both 'Left's. We want the error from
      -- 'release' to win.
      c <- ec
      b <- eb
      return (b, c)