packages feed

theatre-dev-0.1: library/TheatreDev/Perpetual.hs

-- |
-- Exploration of perpetual actors.
-- I.e., those that exist for the whole duration of the app.
--
-- This limitation provides for simpler API and most apps
-- are expected not to need more.
module TheatreDev.Perpetual
  ( Actor,
    spawnStateless,
    spawnStateful,
    tell,
  )
where

import Control.Concurrent.Chan.Unagi qualified as Unagi
import TheatreDev.Prelude

-- |
-- Actor, which processes the messages of type @msg@.
--
-- Provides abstraction over the communication channel and threads.
newtype Actor msg
  = Actor (msg -> IO ())

-- |
-- Distributes the message across the merged actors.
instance Semigroup (Actor msg) where
  Actor lTell <> Actor rTell =
    Actor $ \msg -> lTell msg >> rTell msg
  sconcat actors = Actor $ \msg -> forM_ actors $ \(Actor tell) -> tell msg
  stimes n (Actor tell) = Actor $ \msg -> replicateM_ (fromIntegral n) $ tell msg

-- |
-- Provides an identity for merging the actors,
-- which does nothing.
instance Monoid (Actor msg) where
  mempty = Actor (const (return ()))
  mconcat actors = Actor $ \msg -> forM_ actors $ \(Actor tell) -> tell msg

-- |
-- Maps the input message to a different type.
instance Contravariant Actor where
  contramap fn (Actor tell) =
    Actor (tell . fn)

-- |
-- Splits the message between actors.
instance Divisible Actor where
  conquer =
    mempty
  divide divisor (Actor lTell) (Actor rTell) =
    Actor $ \msg -> case divisor msg of
      (lMsg, rMsg) -> lTell lMsg >> rTell rMsg

-- |
-- Provides a choice between alternative actors to process the message.
instance Decidable Actor where
  lose _ =
    Actor $ const $ return ()
  choose decider (Actor lTell) (Actor rTell) =
    Actor $ either lTell rTell . decider

spawnStateless ::
  -- |
  -- Process the next message.
  -- Must not throw any exceptions.
  (msg -> IO ()) ->
  -- |
  -- Action forking a thread to run the actor loop and
  -- producing a handle for sending messages to it.
  IO (Actor msg)
spawnStateless process = do
  (inChan, outChan) <- Unagi.newChan
  forkIO
    $ let loop = do
            msg <- Unagi.readChan outChan
            process msg
            loop
       in loop
  return $ Actor $ Unagi.writeChan inChan

spawnStateful ::
  -- |
  -- Initial state.
  state ->
  -- |
  -- Process the next message updating the state.
  -- The IO action must not throw any exceptions.
  (state -> msg -> IO state) ->
  -- |
  -- Action forking a thread to run the actor loop and
  -- producing a handle for sending messages to it.
  IO (Actor msg)
spawnStateful state process = do
  (inChan, outChan) <- Unagi.newChan
  forkIO
    $ let loop !state = do
            msg <- Unagi.readChan outChan
            state <- process state msg
            loop state
       in loop state
  return $ Actor $ Unagi.writeChan inChan

-- |
-- Schedule a message for the actor to process
-- after the ones already scheduled.
tell :: Actor msg -> msg -> IO ()
tell = coerce