packages feed

resource-effectful-0.1.0.0: src/Effectful/Resource.hs

{-# LANGUAGE DataKinds    #-}
{-# LANGUAGE TypeFamilies #-}
-----------------------------------------------------------------------------
-- |
-- Module      : Effectful.Resource
-- Copyright   : (c) Michael Szvetits, 2023
-- License     : BSD-3-Clause (see the file LICENSE)
-- Maintainer  : typedbyte@qualified.name
-- Stability   : stable
-- Portability : portable
--
-- A region-based resource effect for the effectful ecosystem.
-----------------------------------------------------------------------------
module Effectful.Resource
  ( -- * Resource Effect
    Resource
  , runResource
  -- * Manage Regions
  , Region
  , withRegion
  , currentRegion
  -- * Manage Resources
  , Key
  , InvalidKey(..)
  , manage
  , allocate
  , free
  , freeAll
  , move
  , move_
  , defer
  ) where

-- base
import Control.Exception (Exception, bracket, finally, mask_, uninterruptibleMask_)
import Control.Monad     (join)
import Data.Functor      (void)

-- effectful-core
import Effectful                 (Dispatch(..), DispatchOf, Eff, Effect, IOE, (:>))
import Effectful.Dispatch.Static (SideEffects(..), StaticRep, evalStaticRep,
                                  getStaticRep, localStaticRep, unsafeEff_,
                                  unsafeSeqUnliftIO)

-- stm
import Control.Concurrent.STM (TVar, atomically, modifyTVar', newTVarIO, readTVar,
                               stateTVar, throwSTM, writeTVar)

-- | A region owns resources and frees them on close.
data Region = Region
  { resources :: TVar [(Key, IO ())]
  , nextID    :: TVar Int
  }
  deriving Eq

-- | Each resource is identified by a unique key.
data Key = Key
  { _keyID    :: Int
  , keyRegion :: Region
  }
  deriving Eq

open :: IO Region
open =
  Region
    <$> newTVarIO []
    <*> newTVarIO 0

close :: Region -> IO ()
close region = uninterruptibleMask_ $ do
  rs <-
    atomically $
      stateTVar
        ( resources region )
        ( \r -> (r, []) )
  freeList rs
    where
      freeList []         = pure ()
      freeList ((_,m):ms) = m `finally` freeList ms

manageIO
  :: Region
  -> IO a
  -> (a -> IO ())
  -> IO (a, Key)
manageIO region create destroy = mask_ $ do
  a <- create
  atomically $ do
    next <- readTVar idTVar
    let key = Key next region
    modifyTVar' idTVar succ
    modifyTVar' rsTVar ((key, destroy a) :)
    pure (a, key)
  where
    idTVar = nextID region
    rsTVar = resources region

-- | An error which occurs if a key is freed\/moved that has already been freed\/moved.
data InvalidKey = InvalidKey
  deriving (Eq, Ord, Show)

instance Exception InvalidKey

extract :: Eq k => k -> [(k,v)] -> Maybe (v, [(k,v)])
extract k = extract' id
  where
    extract' _ [] = Nothing
    extract' f (x@(k',v):xs)
      | k == k'   = Just (v, f xs)
      | otherwise = extract' (f . (x:)) xs

moveIO :: Key -> Region -> IO Key
moveIO key region = atomically $ do
  rs <- readTVar keyRsTVar
  case extract key rs of
    Nothing -> throwSTM InvalidKey
    Just (m, rs') -> do
      writeTVar keyRsTVar rs'
      next <- readTVar idTVar
      modifyTVar' idTVar succ
      let newKey = Key next region
      modifyTVar' rsTVar ((newKey, m) :)
      pure newKey
  where
    keyRsTVar = resources $ keyRegion key
    idTVar    = nextID region
    rsTVar    = resources region

-- | The region-based resource effect.
data Resource :: Effect

type instance DispatchOf Resource = Static WithSideEffects

newtype instance StaticRep Resource = Resource Region

-- | Runs the resource effect.
runResource :: IOE :> es => Eff (Resource : es) a -> Eff es a
runResource m =
  unsafeSeqUnliftIO $ \run ->
    bracket open close $ \emptyRegion ->
      run $
        evalStaticRep (Resource emptyRegion) m

-- | Runs a computation in a new region.
withRegion :: Resource :> es => Eff es a -> Eff es a
withRegion m =
  unsafeSeqUnliftIO $ \run ->
    bracket open close $ \emptyRegion ->
      run $
        localStaticRep (\_ -> Resource emptyRegion) m

-- | Gets the current region.
currentRegion :: Resource :> es => Eff es Region
currentRegion = do
  Resource region <- getStaticRep
  pure region

-- | Allocates a resource in the current region which can be moved and freed
-- manually using its key.
allocate
  :: Resource :> es
  => IO a            -- ^ The computation which acquires the resource.
  -> (a -> IO b)     -- ^ The computation which releases the resource.
  -> Eff es (a, Key) -- ^ The acquired resource and its corresponding key.
allocate create destroy = do
  Resource region <- getStaticRep
  unsafeEff_ $ manageIO region create (void . destroy)

-- | Allocates a resource in the current region which is automatically freed at
-- the end of the region.
manage
  :: Resource :> es
  => IO a        -- ^ The computation which acquires the resource.
  -> (a -> IO b) -- ^ The computation which releases the resource.
  -> Eff es a    -- ^ The acquired resource.
manage create destroy =
  fmap fst $ allocate create destroy

-- | Moves a resource to the specified region, yielding a new key for the resource.
-- The old key is invalid after the movement.
move :: Resource :> es => Key -> Region -> Eff es Key
move key region = 
  unsafeEff_ $ moveIO key region

-- | Moves a resource to the specified region. It is freed at the end of this region.
-- The key of the moved resource is invalid after the movement.
move_ :: Resource :> es => Key -> Region -> Eff es ()
move_ key region =
  void $ move key region

freeIO :: Key -> IO ()
freeIO key =
  join . atomically $ do
    rs <- readTVar keyRsTVar
    case extract key rs of
      Nothing -> throwSTM InvalidKey
      Just (m, rs') -> do
        writeTVar keyRsTVar rs'
        pure m
  where
    keyRsTVar = resources $ keyRegion key
{-# INLINE freeIO #-}

-- | Frees a resource manually.
free :: Key -> Eff es ()
free = unsafeEff_ . uninterruptibleMask_ . freeIO

-- | Frees a collection of resources manually.
freeAll :: Foldable t => t Key -> Eff es ()
freeAll = unsafeEff_ . uninterruptibleMask_ . mapM_ freeIO

-- | Associats a cleanup action with the current region which is executed when the
-- region is closed.
defer :: Resource :> es => IO a -> Eff es ()
defer action =
  manage (pure ()) (const action)