atelier-core-0.1.0.0: src/Atelier/Effects/Chan.hs
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
-- | A typed, unbounded channel effect backed by @unagi-chan@.
--
-- Exposes the split in\/out ends of a fast concurrent queue as an effect, plus a
-- batched read ('readChanBatched') for draining several items at once. The
-- 'InChan' and 'OutChan' types are re-exported so callers need not depend on
-- @unagi-chan@ directly.
--
-- @
-- -- write a few items (writeChan never blocks), then drain them in one batch:
-- pipeline :: (Chan :> es, Timeout :> es) => Eff es (NonEmpty Int)
-- pipeline = do
-- (inn, out) <- newChan
-- traverse_ (writeChan inn) [1 .. 10]
-- readChanBatched (50 :: Millisecond) 100 out -- one blocking read, up to 100 items
--
-- -- dupChan gives a second, independent read end that sees later writes:
-- broadcast :: (Chan :> es) => Eff es (Int, Int)
-- broadcast = do
-- (inn, out1) <- newChan
-- out2 <- dupChan inn
-- writeChan inn 7
-- (,) <$> readChan out1 <*> readChan out2 -- (7, 7)
-- @
module Atelier.Effects.Chan
( -- * Effect
Chan
, newChan
, readChan
, writeChan
, dupChan
, runChan
-- * Channel Types
-- | Re-exported from the underlying channel implementation.
-- Import these from this module rather than directly from Unagi
-- to maintain abstraction boundaries.
, InChan
, OutChan
, readChanBatched
) where
import Control.Concurrent.Chan.Unagi (InChan, OutChan)
import Data.Time.Units (TimeUnit, toMicroseconds)
import Effectful (Dispatch (..), DispatchOf, Effect, IOE)
import Effectful.Dispatch.Static (SideEffects (..), StaticRep, evalStaticRep, unsafeEff_)
import Effectful.State.Static.Shared (evalState, get, modify)
import Effectful.Timeout (Timeout, timeout)
import Control.Concurrent.Chan.Unagi qualified as Unagi
-- | Effect for creating and operating on bidirectional channels.
data Chan :: Effect
type instance DispatchOf Chan = Static WithSideEffects
data instance StaticRep Chan = Chan
-- | Run the 'Chan' effect, allowing channel operations to perform their IO.
runChan :: forall a es. (IOE :> es) => Eff (Chan : es) a -> Eff es a
runChan = evalStaticRep Chan
-- | Create a new channel, returning its write ('InChan') and read ('OutChan')
-- ends.
newChan :: forall a es. (Chan :> es) => Eff es (InChan a, OutChan a)
newChan =
unsafeEff_ Unagi.newChan
-- | Read the next item from a channel, blocking until one is available.
readChan :: forall a es. (Chan :> es) => OutChan a -> Eff es a
readChan outChan =
unsafeEff_ $ Unagi.readChan outChan
-- | Write an item to a channel. Never blocks (the channel is unbounded).
writeChan :: forall a es. (Chan :> es) => InChan a -> a -> Eff es ()
writeChan inChan val =
unsafeEff_ $ Unagi.writeChan inChan val
-- | Duplicate a channel, producing a new read end that observes every item
-- written after the duplication.
dupChan :: forall a es. (Chan :> es) => InChan a -> Eff es (OutChan a)
dupChan inChan =
unsafeEff_ $ Unagi.dupChan inChan
-- | Read a batch of items from a channel.
--
-- Blocks until at least one item is available, then attempts to read
-- up to @batchSize@ items total within the given timeout. Always returns
-- at least one item.
readChanBatched
:: forall t a es
. ( Chan :> es
, TimeUnit t
, Timeout :> es
)
=> t
-- ^ Timeout for reading additional items after the first
-> Int
-- ^ Maximum number of items to read (batch size)
-> OutChan a
-- ^ Channel to read from
-> Eff es (NonEmpty a)
-- ^ Non-empty batch of items (at least one, up to batch size)
readChanBatched timeoutDuration batchSize outChan = do
evalState @[a] [] $ do
h <- readChan outChan -- blocking, get first item
_ <- timeout (fromIntegral (toMicroseconds timeoutDuration))
$ replicateM_ (batchSize - 1)
$ do
x <- readChan outChan
modify (x :)
rest <- get
pure $ h :| reverse rest