circuits-0.2.0.0: src/Circuit/Ends.hs
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
-- | Free channel ends over a base arrow, plus concrete box and queue helpers.
--
-- A channel has exactly two ends:
--
-- * @Out@ — the companion (read / emit end), covariant in the payload.
-- * @In@ — the conjoint (write / commit end), contravariant in the payload.
--
-- @Ends@ is the record that pairs one @In@ with one @Out@. The ends are
-- defined purely in terms of the base arrow @arr@.
--
-- 'open' produces a matched pair; 'close' plugs the pair back together by
-- feeding the @Out@ into the @In@.
--
-- This module also provides the concrete helpers built on top of channel
-- ends:
--
-- * 'box' and 'boxAsymmetric' — embed an @Ends@ into a plain 'Loop'.
-- * 'Queue' strategies and STM / IO @Ends@ constructors ('openSTM',
-- 'openIO').
module Circuit.Ends
( -- * Channel ends (bi-polar contract)
Out (..),
In (..),
-- * Matched pair
Ends (..),
-- * Counit
close,
-- * Prefixing an action to an @In@
prefixIn,
-- * Suffixing an action to an @Out@
suffixOut,
-- * Build an @Ends@ from primitive actions
ends,
endsK,
-- * Extract primitive actions from an @Ends@
splay,
-- * Unit ends (requires constant morphisms)
HasUnit (..),
-- * Boxes
box,
boxAsymmetric,
-- * Queue strategies
Queue (..),
-- * STM @Ends@
openSTM,
-- * IO @Ends@
openIO,
)
where
import Circuit.Category (Category (..), Discrete (..), (.>))
import Circuit.Loop (Loop (..))
import Circuit.Tensor (Tensor (..), Unit)
import Control.Applicative
import Control.Arrow (Kleisli (..))
import Control.Concurrent.STM
import Control.Monad (void)
import Prelude hiding (id, (.))
-- $setup
-- >> :set -XTypeApplications
-- >> import Circuit.Category ((.>))
-- >> import Circuit.Ends
-- >> import Circuit.Layer (run)
-- >> import Control.Arrow (Kleisli(..), runKleisli)
-- >> import Control.Concurrent.STM (STM, atomically)
-- ---------------------------------------------------------------------------
-- Channel ends — the companion and conjoint of the identity functor.
-- ---------------------------------------------------------------------------
-- | @Out@ is the companion of the identity functor. Covariant in @a@
-- (sits in the output position).
newtype Out arr a = Out
{ -- | Emit through the companion, supplying the other end.
emit :: forall x. In arr x -> arr x a
}
-- | @In@ is the conjoint of the identity functor. Contravariant in
-- @a@ (sits in the input position).
newtype In arr a = In
{ -- | Commit through the conjoint, supplying the other end.
commit :: forall x. Out arr x -> arr a x
}
-- | A matched pair of channel ends: one @In@ and one @Out@.
--
-- This is the bi-polar communication contract. The conjoint (@In@)
-- consumes payloads of type @a@; the companion (@Out@) produces payloads
-- of type @b@. For symmetric channels such as queues @a = b@.
--
-- Together with 'prefixIn' and 'suffixOut', @Ends@ carries an /enriched/
-- profunctor structure over the base category @arr@: 'prefixIn' is the
-- left action of @arr@ on @In@ ends, and 'suffixOut' is the right action
-- of @arr@ on @Out@ ends.
data Ends arr a b = Ends
{ -- | Write end (producer), the conjoint.
conjoint :: In arr a,
-- | Read end (consumer), the companion.
companion :: Out arr b
}
-- | Plug an @In@ and an @Out@ of the same payload type together.
--
-- 'close' feeds the @Out@ into the @In@ end, producing a morphism
-- @arr a a@ from the paired payload type.
--
-- Yanking: for the unit ends from 'open',
-- @close (conjoint ends) (companion ends) = id@.
close :: In arr a -> Out arr a -> arr a a
close contra = commit contra
-- | Precompose an @arr@-morphism with an @In@ end.
--
-- Given @f :: arr a b@ and an @In@ end at type @b@, produce an @In@ end
-- at type @a@. Running the resulting end first executes @f@ and then
-- commits through the original end.
--
-- This is the left (contravariant) action of the base category on @In@
-- ends. Specialised to unit ends it is the canonical way to build
-- effectful write ends.
--
-- .> let endsU = open :: Ends (->) () ()
-- .> let inA = prefixIn (const ()) (conjoint endsU) :: In (->) Int
-- .> commit inA (companion endsU) 42
-- ()
prefixIn :: forall arr a b. (Discrete arr) => arr a b -> In arr b -> In arr a
prefixIn f i = In $ \(o :: Out arr x) -> withOb @arr @a $ withOb @arr @b $ withOb @arr @x $ f .> commit i o
-- | Postcompose an @arr@-morphism with an @Out@ end.
--
-- Given an @Out@ end at type @a@ and @g :: arr a b@, produce an @Out@
-- end at type @b@. Running the resulting end first emits through the
-- original end and then executes @g@ on the emitted value.
--
-- This is the right (covariant) action of the base category on @Out@
-- ends. Specialised to unit ends it is the canonical way to build
-- effectful read ends.
--
-- .> let endsU = open :: Ends (->) () ()
-- .> let outA = suffixOut (companion endsU) (const 42) :: Out (->) Int
-- .> emit outA (conjoint endsU) ()
-- 42
suffixOut :: forall arr a b. (Discrete arr) => Out arr a -> arr a b -> Out arr b
suffixOut o g = Out $ \(i :: In arr x) -> withOb @arr @x $ withOb @arr @a $ withOb @arr @b $ emit o i .> g
-- ---------------------------------------------------------------------------
-- Unit ends
-- ---------------------------------------------------------------------------
-- | Arrows that have unit channel ends for a given unit object @u@.
--
-- The unit ends are the identity-on-@u@ morphism split into its two
-- polar halves. The companion is constant; the conjoint delegates to
-- the opposing companion.
--
-- These ends require the base arrow to support constant morphisms, so
-- they are captured by this class rather than being definable for all
-- arrows.
class (Category arr) => HasUnit u arr where
-- | The monoidal unit as channel ends.
--
-- === Yank
--
-- >>> let ends = open :: Ends (->) () ()
-- >>> close (conjoint ends) (companion ends) ()
-- ()
--
-- === Unit plug
--
-- >>> let endsA = open :: Ends (->) () ()
-- >>> let endsU = open :: Ends (->) () ()
-- >>> commit (conjoint endsA) (companion endsU) ()
-- ()
-- >>> emit (companion endsA) (conjoint endsU) ()
-- ()
open :: Ends arr u u
-- | Build an @Ends@ from a write morphism and a read morphism.
--
-- @write :: arr a ()@ consumes the input payload and produces the unit;
-- @read :: arr () b@ consumes the unit and produces the output payload.
-- The unit ends wire the two halves together.
--
-- This is the canonical way to turn a pair of primitive channel actions
-- into a matched pair of @In@ and @Out@ ends.
--
-- Compositional spelling:
--
-- @
-- ends write receive =
-- Ends (prefixIn write (conjoint open)) (suffixOut (companion open) receive)
-- @
ends ::
forall arr a b.
(Discrete arr, HasUnit () arr) =>
arr a () ->
arr () b ->
Ends arr a b
ends write receive =
Ends
(prefixIn write (conjoint open))
(suffixOut (companion open) receive)
-- | Specialization of 'ends' for @Kleisli@ actions.
--
-- @write :: a -> m ()@ consumes the input payload; @receive :: m b@
-- produces the output payload. The unit handling is hidden inside the
-- @Kleisli@ wrappers.
endsK ::
forall m a b.
(Monad m) =>
(a -> m ()) ->
m b ->
Ends (Kleisli m) a b
endsK write receive = ends (Kleisli write) (Kleisli $ const receive)
-- | Extract the primitive write and read actions from an @Ends@ by
-- plugging each end with the unit ends.
--
-- For an @Ends@ built with 'ends', this recovers the original
-- @write :: arr a ()@ and @receive :: arr () b@.
--
-- .> let e = ends (\() -> ()) (const (42 :: Int)) :: Ends (->) () Int
-- .> let (write, receive) = splay e
-- .> (write (), receive ())
-- ((),42)
splay ::
forall arr a b.
(HasUnit () arr) =>
Ends arr a b ->
(arr a (), arr () b)
splay e =
( commit (conjoint e) (companion (open :: Ends arr () ())),
emit (companion e) (conjoint (open :: Ends arr () ()))
)
-- | Unit ends for @(->)@ with unit @()@.
--
-- The companion is the constant function returning @()@; the conjoint
-- recursively emits through the supplied companion.
instance HasUnit () (->) where
open = Ends inU outU
where
outU = Out $ \_ -> const ()
inU = In $ \o -> emit o inU
-- | Unit ends for @Kleisli@ @m@ with unit @()@.
--
-- Same shape as the @(->)@ instance, but the constant companion returns
-- @()@ in the monad.
instance (Monad m) => HasUnit () (Kleisli m) where
open = Ends inU outU
where
outU = Out $ \_ -> Kleisli $ \_ -> pure ()
inU = In $ \o -> emit o inU
-- ---------------------------------------------------------------------------
-- Boxes
-- ---------------------------------------------------------------------------
-- | String-diagram boxes from channel ends.
--
-- A matched pair of free ends (@Ends@) is a box with one input wire and
-- one output wire. The helpers below embed that box into a traced
-- monoidal category by unit-plugging the remaining two slots.
-- | Embed an @Ends@ into a plain @Loop t arr a b@.
--
-- Connects the two channel ends through the unit object, giving a plain
-- @Loop t arr a b@. This is the version most users expect: input on the
-- left, output on the right, with the unit plumbing hidden.
--
-- .> let e = ends (const ()) (const 42) :: Ends (->) () Int
-- .> run (box @(,) e) ()
-- 42
box ::
forall t arr a b.
(HasUnit (Unit t) arr, Ob arr a, Ob arr b, Ob arr (Unit t)) =>
Ends arr a b ->
Loop t arr a b
box ends' =
Lift $
commit (conjoint ends') (companion (open :: Ends arr (Unit t) (Unit t)))
.> emit (companion ends') (conjoint (open :: Ends arr (Unit t) (Unit t)))
-- | Asymmetric box with units exposed on opposite sides.
--
-- Uses 'par' at the base arrow level and lifts the result with 'Lift'.
-- The input carries the unit on the right and the output carries the unit
-- on the left; most users will prefer the unit-normalised 'box'.
--
-- .> let e = ends (const ()) (const 42) :: Ends (->) () Int
-- .> run (boxAsymmetric @(,) e) ((), ())
-- ((),42)
boxAsymmetric ::
forall t arr a b.
(HasUnit (Unit t) arr, Tensor t arr) =>
Ends arr a b ->
Loop t arr (t a (Unit t)) (t (Unit t) b)
boxAsymmetric ends' =
Lift $
par
(commit (conjoint ends') (companion open))
(emit (companion ends') (conjoint open))
-- ---------------------------------------------------------------------------
-- Queue strategies and STM @Ends@
-- ---------------------------------------------------------------------------
-- | How messages are queued between producer and consumer.
data Queue a
= -- | Unbounded FIFO queue.
Unbounded
| -- | Bounded FIFO with backpressure (write blocks when full).
Bounded Int
| -- | Single-slot buffer (write blocks when full).
Single
| -- | Single-slot buffer, overwrite-on-full.
-- Write always succeeds; read empties.
SwapQ
| -- | Always holds the latest value (overwrites, never blocks).
Latest a
| -- | Like @Bounded@ but drops oldest when full.
Newest Int
deriving (Show, Eq)
-- ---------------------------------------------------------------------------
-- STM ends
-- ---------------------------------------------------------------------------
-- | Internal STM primitive for a queue strategy.
--
-- Returns the raw write/read actions used by 'openSTM'. Not exported;
-- the canonical API is 'openSTM'.
endsSTM :: Queue a -> STM (a -> STM (), STM a)
endsSTM = \case
Bounded n -> do
q <- newTBQueue (fromIntegral n)
pure (writeTBQueue q, readTBQueue q)
Unbounded -> do
q <- newTQueue
pure (writeTQueue q, readTQueue q)
Single -> do
m <- newEmptyTMVar
pure (putTMVar m, takeTMVar m)
SwapQ -> do
v <- newEmptyTMVar
let write x = tryPutTMVar v x >>= \case True -> pure (); False -> void (swapTMVar v x)
pure (write, takeTMVar v)
Latest a -> do
t <- newTVar a
pure (writeTVar t, readTVar t)
Newest n -> do
q <- newTBQueue (fromIntegral n)
let write x = writeTBQueue q x <|> (tryReadTBQueue q *> write x)
pure (write, readTBQueue q)
-- ---------------------------------------------------------------------------
-- IO @Ends@
-- ---------------------------------------------------------------------------
-- | Open a queue strategy as STM @Ends@.
--
-- Allocates STM primitives and returns a matched pair of ends sharing
-- the same mutable channel. Both ends live in 'STM', so you can compose
-- operations across channels in a single 'atomically' block.
--
-- === Unbounded
--
-- .> let endsU = open :: Ends (Kleisli STM) () ()
-- .> ends <- atomically (openSTM Unbounded :: STM (Ends (Kleisli STM) Int Int))
-- .> atomically $ runKleisli (commit (conjoint ends) (companion endsU)) 42
-- .> atomically $ runKleisli (emit (companion ends) (conjoint endsU)) ()
-- 42
--
-- Multi-op compose in one 'atomically' (both writes + read):
--
-- .> ends <- atomically (openSTM Unbounded :: STM (Ends (Kleisli STM) Int Int))
-- .> atomically $ runKleisli (commit (conjoint ends) (companion endsU)) 1 >> runKleisli (commit (conjoint ends) (companion endsU)) 2 >> runKleisli (emit (companion ends) (conjoint endsU)) ()
-- 1
--
-- 'close' recovers the value through the queue:
--
-- .> ends <- atomically (openSTM Unbounded :: STM (Ends (Kleisli STM) Int Int))
-- .> atomically $ runKleisli (close (conjoint ends) (companion ends)) 7
-- 7
--
-- === SwapQ (overwrite on write)
--
-- .> ends <- atomically (openSTM SwapQ :: STM (Ends (Kleisli STM) Int Int))
-- .> atomically $ runKleisli (commit (conjoint ends) (companion endsU)) 1 >> runKleisli (commit (conjoint ends) (companion endsU)) 2 >> runKleisli (emit (companion ends) (conjoint endsU)) ()
-- 2
openSTM :: Queue a -> STM (Ends (Kleisli STM) a a)
openSTM q = do
(write, read') <- endsSTM q
pure (endsK write read')
-- | Open a queue strategy as IO @Ends@.
--
-- Like 'openSTM', but each primitive operation is wrapped in its own
-- 'atomically'. You cannot batch multiple writes or a write-plus-read
-- into a single STM transaction; for that use 'openSTM' and wrap in
-- 'atomically' yourself.
--
-- .> let endsU = open :: Ends (Kleisli IO) () ()
-- .> ends <- openIO Unbounded :: IO (Ends (Kleisli IO) Int Int)
-- .> runKleisli (commit (conjoint ends) (companion endsU)) 42
-- .> runKleisli (emit (companion ends) (conjoint endsU)) ()
-- 42
openIO :: Queue a -> IO (Ends (Kleisli IO) a a)
openIO q = do
e <- atomically (openSTM q)
let (Kleisli write, Kleisli receive) = splay e
pure (endsK (atomically . write) (atomically (receive ())))