packages feed

heftia-effects-0.6.0.0: src/Control/Monad/Hefty/Concurrent/Stream.hs

{-# LANGUAGE UndecidableInstances #-}

-- SPDX-License-Identifier: MPL-2.0

{- |
Copyright   :  (c) 2024 Sayo contributors
License     :  MPL-2.0 (see the LICENSE file)
Maintainer  :  ymdfield@outlook.jp

Coroutine-based, composable, and resumable concurrent streams.
-}
module Control.Monad.Hefty.Concurrent.Stream (
    module Control.Monad.Hefty.Concurrent.Stream,
    module Control.Monad.Hefty.Input,
    module Control.Monad.Hefty.Output,
)
where

import Control.Arrow (Arrow, ArrowChoice, arr, first, left, (>>>))
import Control.Category (Category)
import Control.Category qualified as C
import Control.Monad (forM_, forever)
import Control.Monad.Hefty (
    Eff,
    Emb,
    FOEs,
    RemoveHOEs,
    WeakenHOEs,
    interpret,
    interpretsBy,
    nil,
    onlyFOEs,
    raise,
    reinterprets,
    untag,
    (!:),
    (&),
    (:>),
    type (~>),
 )
import Control.Monad.Hefty.Concurrent.Parallel (Parallel, liftP2)
import Control.Monad.Hefty.Input
import Control.Monad.Hefty.Output
import Control.Monad.Hefty.State (State, evalState, evalStateIORef, get'', put'')
import Data.Effect.Unlift (UnliftIO, withRunInIO)
import Data.Function (fix)
import Data.Sequence (Seq ((:|>)))
import Data.Sequence qualified as Seq
import UnliftIO (
    atomically,
    liftIO,
    mask,
    newEmptyTMVarIO,
    putTMVar,
    readTMVar,
    takeTMVar,
    uninterruptibleMask_,
 )
import UnliftIO.Concurrent (forkIO, killThread)

data Machinery es ans i o where
    Unit
        :: forall i o ans es
         . Eff (Input i ': Output o ': es) ans
        -> Machinery es ans i o
    Connect
        :: forall a b c ans es
         . Machinery es ans a b
        -> Machinery es ans b c
        -> Machinery es ans a c

instance Category (Machinery es ans) where
    id :: forall a. Machinery es ans a a
    id =
        Unit . forever $
            input @a >>= output

    (.) = flip Connect

    {-# INLINE id #-}
    {-# INLINE (.) #-}

instance (FOEs es) => Arrow (Machinery es ans) where
    arr (f :: b -> c) =
        Unit . forever $
            input @b >>= output . f

    first
        :: forall b c d
         . Machinery es ans b c
        -> Machinery es ans (b, d) (c, d)
    first = \case
        Unit m -> Unit $ evalState (Left Seq.Empty) $ buffering m
        Connect a b -> Connect (first a) (first b)

    {-# INLINE arr #-}
    {-# INLINE first #-}

buffering
    :: forall b c d ans es
     . Eff (Input b ': Output c ': es) ans
    -> Eff (State (Either (Seq c) d) ': Input (b, d) ': Output (c, d) ': es) ans
buffering =
    reinterprets
        ( ( \Input -> do
                (b, d) <- input

                get'' @"buffer" >>= \case
                    Right _ -> pure ()
                    Left outputQueue -> forM_ outputQueue \c -> output (c, d)

                put'' @"buffer" $ Right d

                pure b
          )
            !: ( \(Output c) ->
                    get'' @"buffer" >>= \case
                        Right d -> output (c, d)
                        Left outputQueue -> put'' @"buffer" $ Left $ outputQueue :|> c
               )
            !: nil
        )
        >>> untag @"buffer"

instance (FOEs es) => ArrowChoice (Machinery es ans) where
    left = leftMachinery
    {-# INLINE left #-}

leftMachinery
    :: forall b c d ans es
     . Machinery es ans b c
    -> Machinery es ans (Either b d) (Either c d)
leftMachinery = \case
    Unit m ->
        m
            & reinterprets
                ( ( \Input -> fix \next ->
                        input @(Either b d) >>= \case
                            Left x -> pure x
                            Right o -> do
                                output @(Either c d) $ Right o
                                next
                  )
                    !: (\(Output o) -> output @(Either c d) $ Left o)
                    !: nil
                )
            & Unit
    Connect a b -> Connect (leftMachinery a) (leftMachinery b)

newtype Machine f ans i o = Machine
    {runMachine :: f (MachineStatus f ans i o)}

data MachineStatus f ans i o
    = Terminated ans
    | Waiting (i -> Machine f ans i o)
    | Produced o (Machine f ans i o)

machine :: (WeakenHOEs es) => Eff (Input i ': Output o ': RemoveHOEs es) ans -> Machine (Eff es) ans i o
machine =
    interpretsBy
        (pure . Terminated)
        ( (\Input k -> pure $ Waiting $ Machine . onlyFOEs . k)
            !: (\(Output o) k -> pure $ Produced o $ Machine $ onlyFOEs $ k ())
            !: nil
        )
        >>> onlyFOEs
        >>> Machine

runMachinery
    :: forall i o ans es
     . (Parallel :> es, Semigroup ans, WeakenHOEs es)
    => Machinery (RemoveHOEs es) ans i o
    -> Eff es (MachineStatus (Eff es) ans i o)
runMachinery = runMachineryL . mviewl

runMachineryL
    :: forall i o ans es
     . (Parallel :> es, Semigroup ans, WeakenHOEs es)
    => MachineryViewL (RemoveHOEs es) ans i o
    -> Eff es (MachineStatus (Eff es) ans i o)
runMachineryL = \case
    MOne m -> runMachine $ machine m
    MCons m ms -> do
        liftP2 (,) (runMachine $ machine m) (runMachinery ms) >>= loop
      where
        loop = \case
            (Terminated ans, Terminated ans') -> pure $ Terminated $ ans <> ans'
            (Produced o k1, Waiting k2) ->
                liftP2 (,) (runMachine k1) (runMachine $ k2 o) >>= loop
            (Waiting k, s) ->
                pure $ Waiting \i -> Machine do
                    s' <- runMachine $ k i
                    loop (s', s)
            (s, Produced o k) ->
                pure $ Produced o $ Machine do
                    s' <- runMachine k
                    loop (s, s')
            (Terminated ans, Waiting _) -> pure $ Terminated ans
            (Produced _ _, Terminated ans) -> pure $ Terminated ans

newtype MachineryIO es ans i o = MachineryIO {unMachineryIO :: Machinery es ans i o}
    deriving newtype (Category)

instance (Emb IO :> es) => Arrow (MachineryIO es ans) where
    arr (f :: b -> c) =
        MachineryIO . Unit . forever $
            input @b >>= output . f

    first :: forall b c d. MachineryIO es ans b c -> MachineryIO es ans (b, d) (c, d)
    first =
        unMachineryIO
            >>> MachineryIO . \case
                Unit m ->
                    Unit $ evalStateIORef (Left Seq.Empty) $ buffering m
                Connect a b ->
                    Connect
                        (unMachineryIO $ first $ MachineryIO a)
                        (unMachineryIO $ first $ MachineryIO b)

    {-# INLINE arr #-}
    {-# INLINE first #-}

instance (Emb IO :> es) => ArrowChoice (MachineryIO es ans) where
    left = MachineryIO . leftMachinery . unMachineryIO
    {-# INLINE left #-}

runMachineryIO
    :: forall i o ans es
     . (UnliftIO :> es, Emb IO :> es)
    => Eff es i
    -> (o -> Eff es ())
    -> Machinery es ans i o
    -> Eff es ans
runMachineryIO i o = runMachineryIOL i o . mviewl

runMachineryIOL
    :: forall i o ans es
     . (UnliftIO :> es, Emb IO :> es)
    => Eff es i
    -> (o -> Eff es ())
    -> MachineryViewL es ans i o
    -> Eff es ans
runMachineryIOL i o = \case
    MOne m -> runUnit o m
    MCons a b ->
        withRunInIO \run -> do
            chan <- newEmptyTMVarIO
            ans <- newEmptyTMVarIO
            mask \restore -> do
                let runThread m = forkIO do
                        x <- restore $ run m
                        atomically $ putTMVar ans x

                t1 <- runThread $ runUnit (liftIO . atomically . putTMVar chan) a
                t2 <- runThread $ runMachineryIO (liftIO . atomically $ takeTMVar chan) o b

                atomically (readTMVar ans)
                    <* uninterruptibleMask_ (killThread t1 *> killThread t2)
  where
    runUnit :: (o' -> Eff es ()) -> Eff (Input i ': Output o' ': es) ~> Eff es
    runUnit o' m =
        m
            & interpret (\Input -> raise i)
            & interpret (\(Output x) -> o' x)

runMachineryIO_
    :: forall ans es
     . (UnliftIO :> es, Emb IO :> es)
    => Machinery es ans () ()
    -> Eff es ans
runMachineryIO_ = runMachineryIO (pure ()) (const $ pure ())
{-# INLINE runMachineryIO_ #-}

-- Inspired by https://hackage.haskell.org/package/freer-simple-1.2.1.2/docs/Data-FTCQueue.html

{- |
Left view deconstruction data structure for Machinery Pipeline.

This allows the number of generated threads to be reduced to the number of machine units.
-}
data MachineryViewL es ans i o where
    MOne
        :: forall i o ans es
         . Eff (Input i ': Output o ': es) ans
        -> MachineryViewL es ans i o
    MCons
        :: forall a b c ans es
         . Eff (Input a ': Output b ': es) ans
        -> Machinery es ans b c
        -> MachineryViewL es ans a c

-- | Left view deconstruction for Machinery Pipeline. [average O(1)]
mviewl :: Machinery es ans i o -> MachineryViewL es ans i o
mviewl = \case
    Unit m -> MOne m
    Connect a b -> connect a b
  where
    connect
        :: Machinery es ans a b
        -> Machinery es ans b c
        -> MachineryViewL es ans a c
    connect (Unit m) r = m `MCons` r
    connect (Connect a b) r = connect a (Connect b r)