packages feed

lazy-bracket-0.1.0.0: lib/LazyBracket.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

-- | This module provides variants of the 'bracket' function that delay the
-- acquisition of the resource until it's used for the first time. If
-- the resource is never used, it will never be acquired.
--
-- A trivial example. This bracket code with a faulty acquisition doesn't throw an exception because the
-- resource is never accessed:
--
-- >>> :{
--  lazyBracket 
--    (throwIO (userError "oops")) 
--    (\_ -> pure ()) 
--    \Resource {} -> do 
--      pure () 
-- :}
-- 
-- But this code does:
--
-- >>> :{
--  lazyBracket 
--    (throwIO (userError "oops")) 
--    (\_ -> pure ()) 
--    \Resource {accessResource} -> do 
--      _ <- accessResource
--      pure () 
-- :}
-- *** Exception: user error (oops)
--
-- To be even more lazy, certain kinds of operations on the resource do not
-- trigger acquisition: instead, they are stashed and applied once the resource
-- has been acquired for other reasons.
--
-- Look at the sequence of ouput messages here:
--
-- >>> :{
--  lazyBracket 
--    (putStrLn "acquired!") 
--    (\() -> putStrLn "released!") 
--    \Resource {accessResource, controlResource} -> do 
--      controlResource \() -> putStrLn "control op 1 - delayed"
--      putStrLn "before acquiring"
--      _ <- accessResource
--      putStrLn "after acquiring"
--      controlResource \() -> putStrLn "control op 2 - immediately executed"
--      pure () 
-- :}
-- before acquiring
-- acquired!
-- control op 1 - delayed
-- after acquiring
-- control op 2 - immediately executed
-- released!
--
-- If we never access the resource, the release function and the stashed
-- operations are not executed:
--
-- >>> :{
--  lazyBracket 
--    (putStrLn "acquired!") 
--    (\() -> putStrLn "released!") 
--    \Resource {accessResource, controlResource} -> do 
--      controlResource \() -> putStrLn "control op 1 - never happens"
--      pure () 
-- :}
--
--
module LazyBracket
  ( -- * Lazy brackets that delay resource acquisition.
    lazyBracket,
    lazyGeneralBracket,
    lazyGeneralBracket_,

    -- * Resource wrapper.
    Resource (..),

    -- * Re-exports.
    ExitCase (..),
  )
where

import Control.Concurrent.MVar
import Control.Monad
import Control.Monad.Catch
import Control.Monad.IO.Class

-- | A wrapper type over resources that delays resource acquisition.
--
-- Because one must be careful with the kinds of functions that are passed to 'controlResource',
-- it might be a good idea to define convenience wrappers over 'Resource' with
-- more restricted interfaces.
data Resource a = Resource
  { -- | Action to get hold of the resource. Will trigger resource acquisition
    -- and apply all stashed control operations the first time it's run.
    accessResource :: IO a,
    -- | Immediately apply a \"control\" operation to the underlying resource if
    -- the resource has already been acquired, otherwise stash the operation
    -- with the intention of applying it once the resource is eventually acquired.
    -- If the resource is never acquired, stashed operations are discarded.
    --
    -- By \"control\" operations we mean operations that are not essential in and of
    -- themselves, only serve to modify the behaviour of operations that are actually
    -- essential, and can be omitted if no essential operations take place.
    --
    -- Some examples:
    --
    -- For file handle resources, @hSetBuffering@ is a valid control
    -- operation, whereas actually writing bytes to the handle is not.
    --
    -- For database connection resources, beginning a transaction is a valid control
    -- operation, whereas performing an INSERT is not.
    controlResource :: (a -> IO ()) -> IO ()
  }

-- | A version of 'Contro.Monad.Catch.bracket' for which the resource is not
-- acquired at the beginning, but the first time it's used in the main callback.
-- If the resource is never used, it won't be acquired.
lazyBracket ::
  (MonadIO m, MonadMask m) =>
  -- | Computation to run to acquire the resource.
  IO a ->
  -- | Computation to run to release the resource, in case it was acquired.
  (a -> m c) ->
  -- | Computation to run in-between (might trigger resource acquisition).
  (Resource a -> m b) ->
  -- | Returns the value from the in-between computation
  m b 
lazyBracket acquire release action = do
  lazyGeneralBracket_
    acquire
    (\a _ -> release a)
    action

data ResourceState a
  = NotYetAcquired (a -> IO ())
  | AlreadyAcquired a

-- | A version of 'Contro.Monad.Catch.generalBracket' for which the resource is not
-- acquired at the beginning, but the first time it's used in the main callback.
-- If the resource is never used, it won't be acquired.
--
lazyGeneralBracket ::
  forall m a b c.
  (MonadIO m, MonadMask m) =>
  -- | Computation to run to acquire the resource
  IO a ->
  -- | Computation to run to release the resource, in case it was acquired
  --
  -- The release function has knowledge of how the main callback was exited: by
  -- normal completion, by a runtime exception, or otherwise aborted.
  -- This can be useful when acquiring resources from resource pools,
  -- to decide whether to return the resource to the pool or to destroy it.
  (a -> ExitCase b -> m c) ->
  -- | Computation to run in-between (might trigger resource acquisition)
  (Resource a -> m b) ->
  -- | Returns the value from the in-between computation, and also of the
  -- release computation, if it took place.
  m (b, Maybe c)
lazyGeneralBracket acquire release action = do
  ref <- liftIO $ newMVar @(ResourceState a) (NotYetAcquired mempty)
  let accessResource = do
        (resource, pendingOperations) <- do
          modifyMVarMasked ref \case
            NotYetAcquired pendingOperations -> do
              resource <- acquire
              pure (AlreadyAcquired resource, (resource, pendingOperations))
            resourceState@(AlreadyAcquired a) -> do
              pure (resourceState, (a, mempty))
        pendingOperations resource -- no need to perform these inside the mask
        pure resource
  let controlResource operation = do
        runNow <- do
          modifyMVarMasked ref \case
            NotYetAcquired pendingOperations -> do
              pure (NotYetAcquired (pendingOperations <> operation), mempty)
            resourceState@(AlreadyAcquired a) -> do
              pure (resourceState, operation a)
        runNow
  let lazyResource = Resource {accessResource, controlResource}
  -- We ignore the 'Resource' argument because we extract the unwrapped
  -- version from the 'MVar'.
  let lazyRelease (_ :: Resource a) exitCase = do
        action <- liftIO $ do
          -- we don't mask here, already provided by generalBracket
          modifyMVar ref \case
            NotYetAcquired _ -> do
              pure (NotYetAcquired mempty, \_ -> pure Nothing)
            AlreadyAcquired a -> do
              pure (NotYetAcquired mempty, fmap Just <$> release a)
        -- If we ran this inside the modifyMVar, an exception during release
        -- would prevent resetting the state to NotYetAcquired. Do we want that?
        action exitCase
  generalBracket (pure lazyResource) lazyRelease action


-- | Slightly simpler version of 'lazyGeneralBracket' that doesn't return the result of the 
-- release computation.
lazyGeneralBracket_ ::
  forall m a b c.
  (MonadIO m, MonadMask m) =>
  -- | computation to run to acquire the resource
  IO a ->
  -- | computation to run to release the resource, in case it was acquired
  (a -> ExitCase b -> m c) ->
  -- | computation to run in-between (might trigger resource acquisition)
  (Resource a -> m b) ->
  -- | returns the value from the in-between computation.
  m b
lazyGeneralBracket_ acquire release action = do
  (b, _) <-
    lazyGeneralBracket
      acquire
      release
      action
  pure b

-- $setup
--
-- >>> :set -XBlockArguments
-- >>> :set -XNamedFieldPuns
-- >>> import LazyBracket
-- >>> import Control.Exception