packages feed

automaton 1.7 → 1.8

raw patch · 8 files changed

+895/−5 lines, 8 filesdep +containersdep +freedep +sop-coredep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: containers, free, sop-core, time-domain

Dependency ranges changed: base

API changes (from Hackage documentation)

+ Data.Automaton.Schedule: FinalizeT :: MaybeT m a -> FinalizeT (m :: Type -> Type) a
+ Data.Automaton.Schedule: RunningResult :: Result state b -> RunningResult b state
+ Data.Automaton.Schedule: Step :: ResultStateT state m b -> Step (m :: Type -> Type) b state
+ Data.Automaton.Schedule: Streams :: NP I (state ': states) -> NP (Step m b) (state ': states) -> Streams (m :: Type -> Type) b
+ Data.Automaton.Schedule: [getFinalizeT] :: FinalizeT (m :: Type -> Type) a -> MaybeT m a
+ Data.Automaton.Schedule: [getRunningResult] :: RunningResult b state -> Result state b
+ Data.Automaton.Schedule: [getStep] :: Step (m :: Type -> Type) b state -> ResultStateT state m b
+ Data.Automaton.Schedule: [states] :: Streams (m :: Type -> Type) b -> NP I (state ': states)
+ Data.Automaton.Schedule: [steps] :: Streams (m :: Type -> Type) b -> NP (Step m b) (state ': states)
+ Data.Automaton.Schedule: class MonadSchedule (m :: Type -> Type)
+ Data.Automaton.Schedule: data Streams (m :: Type -> Type) b
+ Data.Automaton.Schedule: hnonemptycollapse :: forall (as :: [Type]) b a. SListI as => NP (K b :: Type -> Type) (a ': as) -> NonEmpty b
+ Data.Automaton.Schedule: instance (GHC.Base.Monad m, Data.Automaton.Schedule.MonadSchedule m) => Data.Automaton.Schedule.MonadSchedule (Control.Monad.Trans.Except.ExceptT e m)
+ Data.Automaton.Schedule: instance (GHC.Base.Monad m, Data.Automaton.Schedule.MonadSchedule m) => Data.Automaton.Schedule.MonadSchedule (Control.Monad.Trans.Maybe.MaybeT m)
+ Data.Automaton.Schedule: instance (GHC.Base.Monad m, Data.Automaton.Schedule.MonadSchedule m) => Data.Automaton.Schedule.MonadSchedule (Control.Monad.Trans.Reader.ReaderT r m)
+ Data.Automaton.Schedule: instance (GHC.Base.Monad m, Data.Automaton.Schedule.MonadSchedule m) => Data.Automaton.Schedule.MonadSchedule (Data.Automaton.Schedule.FinalizeT m)
+ Data.Automaton.Schedule: instance (GHC.Base.Monad m, Data.Automaton.Schedule.MonadSchedule m) => Data.Automaton.Schedule.MonadSchedule (Data.Automaton.Schedule.Trans.SkipT m)
+ Data.Automaton.Schedule: instance (GHC.Base.Monoid w, Data.Monoid.RightAction.RightAction w s, GHC.Base.Monad m, Data.Automaton.Schedule.MonadSchedule m) => Data.Automaton.Schedule.MonadSchedule (Control.Monad.Trans.Changeset.ChangesetT s w m)
+ Data.Automaton.Schedule: instance (GHC.Base.Monoid w, GHC.Base.Monad m, Data.Automaton.Schedule.MonadSchedule m) => Data.Automaton.Schedule.MonadSchedule (Control.Monad.Trans.Accum.AccumT w m)
+ Data.Automaton.Schedule: instance (GHC.Base.Monoid w, GHC.Base.Monad m, Data.Automaton.Schedule.MonadSchedule m) => Data.Automaton.Schedule.MonadSchedule (Control.Monad.Trans.Writer.CPS.WriterT w m)
+ Data.Automaton.Schedule: instance (GHC.Base.Monoid w, GHC.Base.Monad m, Data.Automaton.Schedule.MonadSchedule m) => Data.Automaton.Schedule.MonadSchedule (Control.Monad.Trans.Writer.Lazy.WriterT w m)
+ Data.Automaton.Schedule: instance (GHC.Base.Monoid w, GHC.Base.Monad m, Data.Automaton.Schedule.MonadSchedule m) => Data.Automaton.Schedule.MonadSchedule (Control.Monad.Trans.Writer.Strict.WriterT w m)
+ Data.Automaton.Schedule: instance (GHC.Show.Show diff, GHC.Classes.Ord diff, Data.TimeDomain.TimeDifference diff, GHC.Base.Monad m, Data.Automaton.Schedule.MonadSchedule m) => Data.Automaton.Schedule.MonadSchedule (Data.Automaton.Schedule.Trans.ScheduleT diff m)
+ Data.Automaton.Schedule: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Data.Automaton.Schedule.FinalizeT m)
+ Data.Automaton.Schedule: instance Control.Monad.Morph.MFunctor Data.Automaton.Schedule.FinalizeT
+ Data.Automaton.Schedule: instance Control.Monad.Trans.Class.MonadTrans Data.Automaton.Schedule.FinalizeT
+ Data.Automaton.Schedule: instance Data.Automaton.Schedule.MonadSchedule Data.Functor.Identity.Identity
+ Data.Automaton.Schedule: instance Data.Automaton.Schedule.MonadSchedule GHC.Types.IO
+ Data.Automaton.Schedule: instance GHC.Base.Functor m => GHC.Base.Functor (Data.Automaton.Schedule.FinalizeT m)
+ Data.Automaton.Schedule: instance GHC.Base.Monad m => GHC.Base.Applicative (Data.Automaton.Schedule.FinalizeT m)
+ Data.Automaton.Schedule: instance GHC.Base.Monad m => GHC.Base.Monad (Data.Automaton.Schedule.FinalizeT m)
+ Data.Automaton.Schedule: newtype FinalizeT (m :: Type -> Type) a
+ Data.Automaton.Schedule: newtype RunningResult b state
+ Data.Automaton.Schedule: newtype Step (m :: Type -> Type) b state
+ Data.Automaton.Schedule: roundRobinStreams :: forall (m :: Type -> Type) b. (Functor m, Applicative m) => Streams m b -> StreamT m (NonEmpty b)
+ Data.Automaton.Schedule: schedule :: MonadSchedule m => NonEmpty (Automaton m a b) -> Automaton m a b
+ Data.Automaton.Schedule.Trans: SkipT :: FreeT Identity m a -> SkipT (m :: Type -> Type) a
+ Data.Automaton.Schedule.Trans: Wait :: diff -> a -> Wait diff a
+ Data.Automaton.Schedule.Trans: [awaited] :: Wait diff a -> a
+ Data.Automaton.Schedule.Trans: [getDiff] :: Wait diff a -> diff
+ Data.Automaton.Schedule.Trans: [getSkipT] :: SkipT (m :: Type -> Type) a -> FreeT Identity m a
+ Data.Automaton.Schedule.Trans: compareWait :: Ord diff => Wait diff a -> Wait diff a -> Ordering
+ Data.Automaton.Schedule.Trans: data Wait diff a
+ Data.Automaton.Schedule.Trans: evalSchedule :: Schedule diff a -> a
+ Data.Automaton.Schedule.Trans: evalScheduleT :: Monad m => ScheduleT diff m a -> m a
+ Data.Automaton.Schedule.Trans: execScheduleT :: Monad m => ScheduleT diff m a -> m (a, [diff])
+ Data.Automaton.Schedule.Trans: instance (GHC.Classes.Eq diff, GHC.Classes.Eq a) => GHC.Classes.Eq (Data.Automaton.Schedule.Trans.Wait diff a)
+ Data.Automaton.Schedule.Trans: instance (GHC.Show.Show diff, GHC.Show.Show a) => GHC.Show.Show (Data.Automaton.Schedule.Trans.Wait diff a)
+ Data.Automaton.Schedule.Trans: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Data.Automaton.Schedule.Trans.SkipT m)
+ Data.Automaton.Schedule.Trans: instance Control.Monad.Trans.Class.MonadTrans Data.Automaton.Schedule.Trans.SkipT
+ Data.Automaton.Schedule.Trans: instance GHC.Base.Functor (Data.Automaton.Schedule.Trans.Wait diff)
+ Data.Automaton.Schedule.Trans: instance GHC.Base.Functor m => GHC.Base.Functor (Data.Automaton.Schedule.Trans.SkipT m)
+ Data.Automaton.Schedule.Trans: instance GHC.Base.Monad m => GHC.Base.Applicative (Data.Automaton.Schedule.Trans.SkipT m)
+ Data.Automaton.Schedule.Trans: instance GHC.Base.Monad m => GHC.Base.Monad (Data.Automaton.Schedule.Trans.SkipT m)
+ Data.Automaton.Schedule.Trans: instance GHC.Classes.Eq diff => Data.Functor.Classes.Eq1 (Data.Automaton.Schedule.Trans.Wait diff)
+ Data.Automaton.Schedule.Trans: isZero :: (Eq diff, TimeDifference diff) => diff -> Bool
+ Data.Automaton.Schedule.Trans: newtype SkipT (m :: Type -> Type) a
+ Data.Automaton.Schedule.Trans: runScheduleIO :: (MonadIO m, Integral n) => ScheduleT n m a -> m a
+ Data.Automaton.Schedule.Trans: runScheduleS :: forall (m :: Type -> Type) diff a b. (Functor m, Monad m) => Automaton (ScheduleT diff m) a b -> Automaton m a (Either diff b)
+ Data.Automaton.Schedule.Trans: runScheduleT :: Monad m => (diff -> m ()) -> ScheduleT diff m a -> m a
+ Data.Automaton.Schedule.Trans: runSkipS :: forall (m :: Type -> Type) a b. (Functor m, Monad m) => Automaton (SkipT m) a b -> Automaton m a (Maybe b)
+ Data.Automaton.Schedule.Trans: runSkipT :: Monad m => SkipT m a -> m a
+ Data.Automaton.Schedule.Trans: runSkipTWith :: Monad m => m () -> SkipT m a -> m a
+ Data.Automaton.Schedule.Trans: runYield :: Yield a -> a
+ Data.Automaton.Schedule.Trans: scheduleS :: forall (m :: Type -> Type) a diff b. Monad m => Automaton m a (Either diff b) -> Automaton (ScheduleT diff m) a b
+ Data.Automaton.Schedule.Trans: skip :: forall (m :: Type -> Type). Monad m => SkipT m ()
+ Data.Automaton.Schedule.Trans: type Schedule diff = ScheduleT diff Identity
+ Data.Automaton.Schedule.Trans: type ScheduleT diff = FreeT Wait diff
+ Data.Automaton.Schedule.Trans: type Yield = SkipT Identity
+ Data.Automaton.Schedule.Trans: wait :: forall (m :: Type -> Type) diff. Monad m => diff -> ScheduleT diff m ()
+ Data.Automaton.Schedule.Trans: waitms :: (MonadIO m, Integral n) => n -> m ()

Files

CHANGELOG.md view
@@ -1,5 +1,15 @@ # Revision history for automaton +## 1.8++* Added `Data.Automaton.Schedule` module with a new `MonadSchedule` class that+  works natively on `Automaton` values instead of monadic actions.+  Instances are provided for common monad transformers.+* Added `Data.Automaton.Schedule.Trans` module with `ScheduleT` and related transformers+  providing free waiting and scheduling effects+  (previously in `monad-schedule`).+* Removed the `monad-schedule` package dependency from `automaton`+ ## 1.7  * Add `safely`, `forever` and `foreverE` exception handling functions for streams
automaton.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: automaton-version: 1.7+version: 1.8 synopsis: Effectful streams and automata in coalgebraic encoding description:   Effectful streams have an internal state and a step function.@@ -29,15 +29,19 @@ common opts   build-depends:     MonadRandom >=0.5,-    base >=4.16 && <4.22,+    base >=4.18 && <4.22,     changeset ^>=0.2,+    containers >=0.5,+    free >=5.1,     mmorph ^>=1.2,     mtl >=2.2 && <2.4,     profunctors ^>=5.6,     selective ^>=0.7,     semialign >=1.2 && <=1.4,     simple-affine-space ^>=0.2,+    sop-core ^>=0.5,     these >=1.1 && <=1.3,+    time-domain ^>=1.8,     transformers >=0.5,     witherable ^>=0.5, @@ -80,6 +84,8 @@     Data.Automaton     Data.Automaton.Filter     Data.Automaton.Recursive+    Data.Automaton.Schedule+    Data.Automaton.Schedule.Trans     Data.Automaton.Trans.Accum     Data.Automaton.Trans.Changeset     Data.Automaton.Trans.Except@@ -112,6 +118,7 @@     Automaton     Automaton.Except     Automaton.Filter+    Automaton.Schedule     Automaton.Trans.Accum     Automaton.Trans.Changeset     Stream
+ src/Data/Automaton/Schedule.hs view
@@ -0,0 +1,372 @@+{-# LANGUAGE OverloadedLists #-}++{- | -+This module defines the 'MonadSchedule' class for running several automata concurrently,+and provides instances for common monad transformers.++The central abstraction is:++@'schedule' :: 'NonEmpty' ('Automaton' m a b) -> 'Automaton' m a b@++This takes a non-empty collection of automata and interleaves their outputs,+yielding one output per tick by cycling through the automata in some+monad-specific order (e.g. round-robin for 'Identity', first-available for 'IO').+For a free simulated-time scheduling monad transformer, see 'ScheduleT' in "Data.Automaton.Schedule.Trans".++== Relationship to @monad-schedule@++This class replaces the @MonadSchedule@ class from the @monad-schedule@ package+(which worked on monadic actions @m a@ directly) with one that works natively on 'Automaton' values.+The free waiting effect 'ScheduleT' from the @monad-schedule@ has been moved to @automaton@.+-}+module Data.Automaton.Schedule where++-- base+import Control.Arrow+import Control.Concurrent (forkIO, newEmptyMVar, putMVar, takeMVar)+import Control.Monad (forM_, guard, replicateM_)+import Control.Monad.IO.Class (MonadIO)+import Control.Monad.Identity (Identity (..))+import Data.Bifunctor qualified as Bifunctor+import Data.Foldable1 (Foldable1 (foldrMap1))+import Data.Function ((&))+import Data.Functor ((<&>))+import Data.Functor.Compose (Compose (..))+import Data.Kind (Type)+import Data.List qualified as List+import Data.List.NonEmpty as N+import Data.Maybe (maybeToList)+import Data.Tuple (swap)++-- transformers+import Control.Monad.Trans.Accum (AccumT (..), runAccumT)+import Control.Monad.Trans.Class (MonadTrans (..))+import Control.Monad.Trans.Except (ExceptT (..))+import Control.Monad.Trans.Maybe (MaybeT (..))+import Control.Monad.Trans.Reader (ReaderT (..))+import Control.Monad.Trans.State.Strict (StateT (..), get)+import Control.Monad.Trans.Writer.CPS qualified as CPS+import Control.Monad.Trans.Writer.Lazy qualified as Lazy+import Control.Monad.Trans.Writer.Strict qualified as Strict++-- sop-core+import Data.SOP (HCollapse (hcollapse), HSequence (htraverse'), I (..), K (..), NP (..), SListI, hmap, hzipWith)++-- containers+import Data.IntMap.Strict (IntMap)+import Data.IntMap.Strict qualified as IM+import Data.Sequence (Seq, ViewL (..), viewl)+import Data.Sequence qualified as Seq+import Data.Set qualified as S++-- mmorph+import Control.Monad.Morph (MFunctor)++-- witherable+import Witherable ((<&?>))++-- changeset+import Control.Monad.Trans.Changeset (ChangesetT (..))+import Data.Monoid.RightAction (RightAction)++-- time-domain+import Data.TimeDomain (TimeDifference (..))++-- automaton+import Data.Automaton (Automaton (..), arrM, constM, feedback, hoistS, initialised, liftS, reactimate, withAutomaton_)+import Data.Automaton qualified as Automaton+import Data.Automaton.Schedule.Trans (ScheduleT, SkipT, runScheduleS, runSkipS, scheduleS)+import Data.Automaton.Trans.Except (exceptS)+import Data.Automaton.Trans.Maybe (maybeExit, runMaybeS)+import Data.Automaton.Trans.Reader (readerS, runReaderS)+import Data.Automaton.Trans.State (modify, runStateS__)+import Data.Stream (StreamT (..), concatS)+import Data.Stream.Optimized (OptimizedStreamT (Stateful), toStreamT)+import Data.Stream.Result++{- | Class of monads that support running several 'Automaton's concurrently,+interleaving their outputs into a single 'Automaton'.++The semantics of 'schedule' depend on the monad:++* For 'Identity': round-robin (each automaton advances exactly once per cycle).+* For 'IO': all automata run in separate threads; results are delivered as soon+ as they are produced.+* For transformer stacks: defined compositionally by the individual instances.++The first input may be broadcast to an arbitrary number (1 for 'Identity', all for 'IO') of automata,+but subsequent inputs must be delivered to one automaton each.+-}+class MonadSchedule m where+  -- | Run a nonempty list of automata concurrently.+  schedule :: NonEmpty (Automaton m a b) -> Automaton m a b++{- | Start all streams in the background and send their values to a shared 'MVar'.++The first input is broadcast to all automata,+the following inputs are only broadcast to one each.+-}+instance MonadSchedule IO where+  schedule automata = proc a -> do+    (output, input) <- initialised startStreams -< a+    arrM $ uncurry putMVar -< (input, a)+    arrM takeMVar -< output+    where+      startStreams a0 = do+        output <- newEmptyMVar+        input <- newEmptyMVar+        forkIO $ replicateM_ (N.length automata - 1) $ putMVar input a0+        forM_ automata $ \automaton -> forkIO $ reactimate $ constM (takeMVar input) >>> automaton >>> arrM (putMVar output)+        return (output, input)++instance (Monad m, MonadSchedule m) => MonadSchedule (ReaderT r m) where+  schedule =+    fmap runReaderS+      >>> schedule+      >>> readerS++{- | Schedule automata in 'ExceptT'.++When any automaton throws an exception, all others are stopped immediately.+This is intentional: a thrown exception signals that the computation cannot+continue, so it is consistent to stop all peers.++To let all automata run to completion before propagating an exception, lift+them into a monad that does not short-circuit on exceptions (e.g. wrap the+exception type in 'Either' and post-process the results).+-}+instance (Monad m, MonadSchedule m) => MonadSchedule (ExceptT e m) where+  schedule =+    fmap exceptS+      >>> schedule+      >>> withAutomaton_ (fmap sequenceA >>> ExceptT)++{- | Schedule automata in 'MaybeT'.++When any automaton returns 'Nothing', all others are stopped immediately and the+combined automaton also returns 'Nothing'. This is intentional: 'Nothing'+signals termination, so it is consistent to stop all peers.++To let all automata run to completion before stopping, convert the 'MaybeT'+automata to base-monad automata producing @'Maybe' b@ values and post-process+the results.+-}+instance (Monad m, MonadSchedule m) => MonadSchedule (MaybeT m) where+  schedule =+    fmap runMaybeS+      >>> schedule+      >>> withAutomaton_ (fmap sequenceA >>> MaybeT)++{- | A monad transformer for scheduling automata that all need to run to+completion.++Like 'MaybeT', each automaton can signal termination by returning 'Nothing'.+Unlike 'MaybeT', the combined automaton does __not__ stop when one automaton+finishes; it waits until __all__ scheduled automata have finished.++While any automaton is still running, finished automata contribute no outputs.+Once every automaton has finished, the combined automaton itself terminates.+-}+newtype FinalizeT m a = FinalizeT+  { getFinalizeT :: MaybeT m a+  -- ^ Unwrap 'FinalizeT' to the underlying 'MaybeT'.+  }+  deriving newtype (Functor, Applicative, Monad, MonadIO, MonadTrans, MFunctor)++instance (Monad m, MonadSchedule m) => MonadSchedule (FinalizeT m) where+  schedule automata =+    automata+      & N.zip [1 ..]+      & fmap (\(i, automaton) -> runMaybeS (hoistS getFinalizeT automaton) <&> maybe (Left i) Right)+      & schedule+      & (>>> haveAllFinished)+      & liftS+      & (>>> maybeExit)+      & fmap maybeToList+      & Automaton.concatS+      & hoistS FinalizeT+    where+      allN = S.fromAscList [1 .. N.length automata]+      haveAllFinished = Automaton.unfold S.empty $ \input is -> case input of+        Left i -> let is' = S.insert i is in Result is' $ if is' == allN then Nothing else Just Nothing+        Right b -> Result is $ Just $ Just b++instance (Monoid w, Monad m, MonadSchedule m) => MonadSchedule (CPS.WriterT w m) where+  schedule =+    fmap (withAutomaton_ (CPS.runWriterT >>> fmap (\(Result s a, w) -> Result s (a, w))))+      >>> schedule+      >>> withAutomaton_ (fmap (\(Result s (a, w)) -> (Result s a, w)) >>> CPS.writerT)++instance (Monoid w, Monad m, MonadSchedule m) => MonadSchedule (Strict.WriterT w m) where+  schedule =+    fmap (withAutomaton_ (Strict.runWriterT >>> fmap (\(Result s a, w) -> Result s (a, w))))+      >>> schedule+      >>> withAutomaton_ (fmap (\(Result s (a, w)) -> (Result s a, w)) >>> Strict.WriterT)++instance (Monoid w, Monad m, MonadSchedule m) => MonadSchedule (Lazy.WriterT w m) where+  schedule =+    fmap (withAutomaton_ (Lazy.runWriterT >>> fmap (\(Result s a, w) -> Result s (a, w))))+      >>> schedule+      >>> withAutomaton_ (fmap (\(Result s (a, w)) -> (Result s a, w)) >>> Lazy.WriterT)++-- | This will share the accumulated log from the past with all automata+instance (Monoid w, Monad m, MonadSchedule m) => MonadSchedule (AccumT w m) where+  schedule =+    fmap (withAutomaton_ (runAccumT >>> ReaderT >>> CPS.writerT))+      >>> schedule+      >>> withAutomaton_ (CPS.runWriterT >>> runReaderT >>> AccumT)++-- | This will share the accumulated state from the past with all automata+instance (Monoid w, RightAction w s, Monad m, MonadSchedule m) => MonadSchedule (ChangesetT s w m) where+  schedule =+    fmap (withAutomaton_ (getChangesetT >>> ReaderT >>> fmap swap >>> CPS.writerT))+      >>> schedule+      >>> withAutomaton_ (CPS.runWriterT >>> fmap swap >>> runReaderT >>> ChangesetT)++{- | Cycle through all automata in a round-robin fashion.++On each tick of the combined 'Automaton', exactly one of the component+automata is stepped. The automata are advanced in the order they appear in+the input 'NonEmpty' list, cycling indefinitely.+-}+instance MonadSchedule Identity where+  schedule =+    fmap (getAutomaton >>> toStreamT)+      >>> foldrMap1 buildStreams consStreams+      >>> roundRobinStreams+      >>> fmap N.toList+      >>> concatS+      >>> Stateful+      >>> Automaton+    where+      buildStreams :: StreamT m b -> Streams m b+      buildStreams StreamT {state, step} =+        Streams+          { states = I state :* Nil+          , steps = Step (ResultStateT step) :* Nil+          }++      consStreams :: StreamT m b -> Streams m b -> Streams m b+      consStreams StreamT {state, step} Streams {states, steps} =+        Streams+          { states = I state :* states+          , steps = Step (ResultStateT step) :* steps+          }++-- The order of outputs matches the order of inputs: 'foldrMap1' places the+-- first input element at the head of the 'NP', so 'hnonemptycollapse' extracts+-- outputs in the original order.++{- | Step all streams in a 'Streams' bundle simultaneously and collect the+results into a 'NonEmpty' list, preserving the input order.+-}+roundRobinStreams :: (Functor m, Applicative m) => Streams m b -> StreamT m (NonEmpty b)+roundRobinStreams Streams {states, steps} =+  StreamT+    { state = states+    , step = \s ->+        s+          & hzipWith (\Step {getStep} (I s) -> getResultStateT getStep s <&> RunningResult & Compose) steps+          & htraverse' getCompose+          <&> ( \results ->+                  Result+                    (results & hmap (getRunningResult >>> resultState >>> I))+                    (results & hmap (getRunningResult >>> output >>> K) & hnonemptycollapse)+              )+    }++-- | Collapse a non-empty n-ary product of constant functors into a 'NonEmpty' list.+hnonemptycollapse :: (SListI as) => NP (K b) (a ': as) -> NonEmpty b+hnonemptycollapse (K a :* as) = a :| hcollapse as++-- | A nonempty list of 'StreamT's, unzipped into their states and their steps.+data Streams m b+  = forall state (states :: [Type]).+  (SListI states) =>+  Streams+  { states :: NP I (state ': states)+  , steps :: NP (Step m b) (state ': states)+  }++-- | One step of a stream, with the state type argument going last, so it is usable with sop-core.+newtype Step m b state = Step {getStep :: ResultStateT state m b}++-- | The result of a stream, with the type arguments swapped, so it's usable with sop-core+newtype RunningResult b state = RunningResult {getRunningResult :: Result state b}++instance (Monad m, MonadSchedule m) => MonadSchedule (SkipT m) where+  schedule = fmap runSkipS >>> schedule >>> fmap maybeToList >>> Automaton.concatS >>> liftS++-- | Each scheduled automaton must eventually produce an output or a diff greater than 'zero', otherwise this will loop indefinitely.+instance (Show diff, Ord diff, TimeDifference diff, Monad m, MonadSchedule m) => MonadSchedule (ScheduleT diff m) where+  schedule automata = automata & N.zip [1 ..] & fmap instrument & schedule & backpressure & scheduleS & Automaton.concatS+    where+      nAutomata = List.length automata+      instrument :: (Int, Automaton (ScheduleT diff m) a b) -> Automaton m (a, diff) (Int, Either (Maybe diff) b)+      instrument (i, automaton) = flip runStateS__ mempty $ proc (a, globalTime) -> do+        localTime <- constM get -< ()++        if globalTime < localTime+          -- We are ahead of the consensus time, skip this tick instead of emitting+          -- Each automaton ticks once per round-robin cycle over all N automata,+          -- so each input 'a' is delivered to each automaton exactly once per cycle.+          then do+            returnA -< (i, Left Nothing)+          else do+            diffOrOutput <- liftS $ runScheduleS automaton -< a+            case diffOrOutput of+              Left diffNew -> do+                arrM modify -< (`add` diffNew)+              _ -> returnA -< ()+            returnA -< (i, Bifunctor.first Just diffOrOutput)++      -- Each tick of 'schedule' advances exactly one of the N instrumented automata.+      -- 'backpressure' feeds the current consensus time back as the second input+      -- so that automata which are ahead of the consensus will skip their tick.+      backpressure :: Automaton m (a, diff) (Int, Either (Maybe diff) b) -> Automaton m a (Either diff [b])+      backpressure scheduled = feedback (IM.fromAscList $ (,Seq.Empty) <$> [1 .. nAutomata], mempty) $ proc (a, (queues, lastGlobalTime)) -> do+        (i, outputOrDiffMaybe) <- scheduled -< (a, lastGlobalTime)+        let (output, queues') = popOutput $ enqueueOutput i outputOrDiffMaybe queues+        returnA -< (output, (queues', lastGlobalTime & either add (const id) output))++      enqueueOutput :: Int -> Either (Maybe diff) b -> IntMap (Seq (Either diff b)) -> IntMap (Seq (Either diff b))+      enqueueOutput i = \case+        Left Nothing -> id+        Left (Just diff) -> IM.insertWith (<>) i $ Seq.singleton $ Left diff+        Right b -> IM.insertWith (<>) i $ Seq.singleton $ Right b++      analyseQueue :: IntMap (Seq (Either diff b)) -> Maybe (IntMap (Seq (Either diff b)), (IntMap diff, [b]))+      analyseQueue =+        let peekOutput i queuei = do+              case viewl queuei of+                -- Queue empty: we cannot yet determine a consensus output for this+                -- automaton, so we abort and wait for more input from 'schedule'.+                -- This conservatively waits even if some other automata already+                -- have outputs queued; a more aggressive variant could return+                -- those partial results early.+                EmptyL -> lift Nothing+                -- New diff enqueued.+                Left diff :< queuei' -> do+                  modify $ Bifunctor.first $ IM.insert i diff+                  pure queuei'+                -- New output.+                Right b :< queuei' -> do+                  modify $ Bifunctor.second (b :)+                  pure queuei'+         in flip runStateT (IM.empty, []) . IM.traverseWithKey peekOutput++      pushDiffsBack :: IntMap diff -> IntMap (Seq (Either diff b)) -> IntMap (Seq (Either diff b))+      pushDiffsBack diffs = IM.unionWith (<>) $ diffs <&> pure . Left++      popOutput :: IntMap (Seq (Either diff b)) -> (Either diff [b], IntMap (Seq (Either diff b)))+      popOutput queues = case analyseQueue queues of+        Nothing -> (Right [], queues) -- Queues weren't full yet, need to wait for more input+        Just (queues', (diffs, bs)) -> case N.nonEmpty bs of+          Nothing ->+            if IM.null diffs+              then (Right [], queues') -- No diffs or outputs enqueued+              else+                let minDiff = minimum diffs+                    adjustedDiffs = diffs <&?> \diff -> guard (diff /= minDiff) >> Just (diff `difference` minDiff)+                 in (Left minDiff, pushDiffsBack adjustedDiffs queues')+          Just bs -> (Right $ List.reverse $ toList bs, pushDiffsBack diffs queues')
+ src/Data/Automaton/Schedule/Trans.hs view
@@ -0,0 +1,223 @@+{-# LANGUAGE DeriveFunctor #-}++{- |+This module supplies a general-purpose monad transformer that adds a syntactical+"delay", or "waiting" side effect, used for time-based scheduling of automata.++'ScheduleT' is used as the monad for automata that need to express timing+information: an automaton in @'ScheduleT' diff m@ can @'wait' diff@ to signal+how long until its next output.++The 'Data.Automaton.Schedule.MonadSchedule' instance for 'ScheduleT' interleaves+several such automata in time order.+-}+module Data.Automaton.Schedule.Trans (module Data.Automaton.Schedule.Trans) where++-- base+import Control.Concurrent (threadDelay)+import Data.Functor.Classes (Eq1 (..), liftEq)+import Data.Functor.Identity (Identity (..))+import Data.Ord (comparing)++-- transformers+import Control.Monad.IO.Class (MonadIO, liftIO)++-- free+import Control.Monad.Trans.Free (FreeF (..), FreeT (..), iterT, liftF, runFreeT)++-- time-domain++import Control.Monad.Trans.Class (MonadTrans (..))+import Control.Monad.Trans.Reader (ReaderT (..))+import Data.Automaton (Automaton, handleAutomaton)+import Data.Stream (StreamT (..))+import Data.Stream.Result (Result (..))+import Data.TimeDomain (TimeDifference (..))++-- * Waiting action++-- | A functor implementing a syntactical "waiting" action.+data Wait diff a = Wait+  { getDiff :: diff+  -- ^ The duration to wait.+  , awaited :: a+  -- ^ The encapsulated value.+  }+  deriving (Functor, Eq, Show)++instance (Eq diff) => Eq1 (Wait diff) where+  liftEq eq (Wait diff1 a) (Wait diff2 b) = diff1 == diff2 && eq a b++{- | Compare by the time difference, regardless of the value.++Note that this would not give a lawful 'Ord' instance since we do not compare+the @a@.+-}+compareWait :: (Ord diff) => Wait diff a -> Wait diff a -> Ordering+compareWait = comparing getDiff++-- * 'ScheduleT'++{- |+Values in @ScheduleT diff m@ are delayed computations with side effects in @m@.+Delays can occur between any two side effects, with lengths specified by a @diff@+value.++These delays don't have any semantics on their own; semantics can be given with+'runScheduleT'.++The 'Data.Automaton.Schedule.MonadSchedule' instance for 'ScheduleT' interprets+delays as logical time and interleaves several such computations in order of+their next scheduled time.+-}+type ScheduleT diff = FreeT (Wait diff)++-- | 'ScheduleT' over the 'Identity' monad.+type Schedule diff = ScheduleT diff Identity++-- | The side effect that waits for a specified amount.+wait :: (Monad m) => diff -> ScheduleT diff m ()+wait diff = FreeT $ pure $ Free $ Wait diff $ pure ()++{- | Supply a semantic meaning to 'Wait'.+For every occurrence of @Wait diff@ in the @ScheduleT diff m a@ value,+a waiting action is executed, depending on @diff@.+-}+runScheduleT :: (Monad m) => (diff -> m ()) -> ScheduleT diff m a -> m a+runScheduleT waitAction = iterT $ \(Wait n ma) -> waitAction n >> ma++{- | Run a 'ScheduleT' value, ignoring the waiting actions.++Usually, you would apply this function after having scheduled several automata together with 'schedule',+and you want to get the final result of the schedule without caring about the timing.+-}+evalScheduleT :: (Monad m) => ScheduleT diff m a -> m a+evalScheduleT = runScheduleT $ const $ pure ()++-- | Run a 'Schedule' value, ignoring the waiting actions.+evalSchedule :: Schedule diff a -> a+evalSchedule = runIdentity . evalScheduleT++{- | Run a 'ScheduleT' value in a 'MonadIO',+interpreting the times as milliseconds.+-}+runScheduleIO ::+  (MonadIO m, Integral n) =>+  ScheduleT n m a ->+  m a+runScheduleIO = runScheduleT waitms++{- | Formally execute all waiting actions,+returning the final value and all moments when the schedule would have waited.+-}+execScheduleT :: (Monad m) => ScheduleT diff m a -> m (a, [diff])+execScheduleT action = do+  free <- runFreeT action+  case free of+    Pure a -> pure (a, [])+    Free (Wait diff cont) -> do+      (a, diffs) <- execScheduleT cont+      pure (a, diff : diffs)++{- | Break down the steps of an 'Automaton' in 'ScheduleT' into waiting+effects and returning values.++Each tick either produces a @'Right' b@ (a regular output) or a+@'Left' diff@ (a wait duration), exposing the internal scheduling information+to the caller.  The dual of 'scheduleS'.+-}+runScheduleS :: (Functor m, Monad m) => Automaton (ScheduleT diff m) a b -> Automaton m a (Either diff b)+runScheduleS = handleAutomaton $ \StreamT {state, step} ->+  StreamT+    { state = step state+    , step = \s -> ReaderT $ \a -> do+        oneStep <- runFreeT $ runReaderT s a+        pure $ case oneStep of+          Pure (Result s' b) -> Result (step s') (Right b)+          Free (Wait diff cont) -> Result (lift cont) (Left diff)+    }++{- | Embed an automaton that produces @'Either' diff b@ values into+'ScheduleT', interpreting @'Left' diff@ as a 'wait' instruction.++The dual of 'runScheduleS': whenever the inner automaton emits @'Left' diff@,+'scheduleS' calls @'wait' diff@ and then re-runs the step with the same+input until a @'Right' b@ is produced.+-}+scheduleS :: (Monad m) => Automaton m a (Either diff b) -> Automaton (ScheduleT diff m) a b+scheduleS = handleAutomaton $ \StreamT {state, step} ->+  let step' s = ReaderT $ \a -> do+        Result s' eitherDiffB <- lift $ runReaderT (step s) a+        case eitherDiffB of+          Right b -> pure $ Result s' b+          Left diff -> do+            wait diff+            runReaderT (step' s') a+   in StreamT+        { state+        , step = step'+        }++-- * The symbolic effect of skipping one step of an automaton++{- | A monad transformer that adds the ability to __skip__ one output step.++An automaton in @'SkipT' m@ may call 'skip' to signal that it wants to+defer its output to the next step.  'MonadSchedule' for 'SkipT' uses this to+implement cooperative, non-preemptive round-robin scheduling: an automaton+that has not yet skipped enough times simply skips instead of emitting an+output, giving other automata a chance to catch up.++See also 'runSkipS' and 'skip'.+-}+newtype SkipT m a = SkipT+  { getSkipT :: FreeT Identity m a+  -- ^ Unwrap 'SkipT' to the underlying free-monad transformer.+  }+  deriving newtype (Functor, Applicative, Monad, MonadTrans, MonadIO)++-- | 'SkipT' specialised to 'Identity': a pure automaton with skip steps.+type Yield = SkipT Identity++{- | Run an 'Automaton' in 'SkipT', exposing skipped steps as 'Nothing'.++Each tick of the result automaton corresponds to one tick of the input+automaton.  If the input automaton called 'skip' on that tick the result is+'Nothing'; otherwise it is 'Just' the output.+-}+runSkipS :: (Functor m, Monad m) => Automaton (SkipT m) a b -> Automaton m a (Maybe b)+runSkipS = handleAutomaton $ \StreamT {state, step} ->+  StreamT+    { state = step state+    , step = \s -> ReaderT $ \a -> do+        oneTick <- runFreeT $ getSkipT $ runReaderT s a+        pure $ case oneTick of+          Pure (Result s' b) -> Result (step s') (Just b)+          Free (Identity cont) -> Result (lift $ SkipT cont) Nothing+    }++-- | Signal that the current step should be skipped, deferring output to the next tick.+skip :: (Monad m) => SkipT m ()+skip = SkipT $ liftF $ pure ()++-- | Run a 'SkipT' action, discarding all 'skip' steps.+runSkipT :: (Monad m) => SkipT m a -> m a+runSkipT = iterT runIdentity . getSkipT++-- | Run a 'SkipT' action, executing @action@ for every 'skip' step.+runSkipTWith :: (Monad m) => m () -> SkipT m a -> m a+runSkipTWith action = iterT (\ima -> action >> runIdentity ima) . getSkipT++-- | Run a pure 'Yield' computation, discarding all skipped steps.+runYield :: Yield a -> a+runYield = runIdentity . runSkipT++-- * Helper functions++-- | Wait for the given number of milliseconds.+waitms :: (MonadIO m, Integral n) => n -> m ()+waitms = liftIO . threadDelay . (* 1000) . fromIntegral++-- | Check whether a time difference is zero (i.e. equal to its own difference with itself).+isZero :: (Eq diff, TimeDifference diff) => diff -> Bool+isZero diff = diff `difference` diff == diff
src/Data/Stream.hs view
@@ -263,8 +263,8 @@ reactimate StreamT {state, step} = go state   where     go s = do-      Result s' () <- step s-      go s'+      Result s' unit <- step s+      unit `seq` go s' {-# INLINE reactimate #-}  -- | Run a stream, collecting the outputs in a lazy, infinite list.
test/Automaton.hs view
@@ -29,6 +29,7 @@ -- automaton import Automaton.Except import Automaton.Filter+import Automaton.Schedule import Automaton.Trans.Accum import Automaton.Trans.Changeset import Data.Automaton@@ -84,6 +85,7 @@     , Automaton.Filter.tests     , Automaton.Trans.Accum.tests     , Automaton.Trans.Changeset.tests+    , Automaton.Schedule.tests     ]  inMaybe :: Automaton Maybe (Maybe a) a
+ test/Automaton/Schedule.hs view
@@ -0,0 +1,273 @@+module Automaton.Schedule where++-- base+import Control.Category ((>>>))+import Control.Concurrent (threadDelay, yield)+import Control.Monad (replicateM)+import Control.Monad.IO.Class (MonadIO (..))+import Control.Monad.Identity (Identity (runIdentity))+import Data.Foldable (Foldable (..))+import Data.Functor (($>))+import Data.List qualified as List+import Data.List.NonEmpty (NonEmpty (..))+import Data.Maybe (fromJust, isJust)+import Data.Monoid (Sum (..))++-- quickcheck+import Test.QuickCheck+import Test.QuickCheck.Monadic (monadicIO, run)++-- transformers+import Control.Monad.Trans.Class (MonadTrans (..))+import Control.Monad.Trans.Except (runExceptT, throwE)+import Control.Monad.Trans.Maybe (MaybeT (..))+import Control.Monad.Trans.Writer.Strict (WriterT (..), tell)++-- mmorph+import Control.Monad.Morph (MFunctor (..))++-- changeset+import Control.Monad.Changeset.Class (MonadChangeset (..))+import Control.Monad.Trans.Changeset (Count (Increment), changeSingle, runChangeset)++-- tasty+import Test.Tasty (TestTree, testGroup)++-- tasty-quickcheck+import Test.Tasty.QuickCheck (testProperty)++-- tasty-hunit+import Test.Tasty.HUnit (testCase, (@?=))++-- automaton+import Data.Automaton (Automaton, accumulateWith, arrM, constM, embed, hoistS)+import Data.Automaton qualified as Automaton+import Data.Automaton.Schedule (FinalizeT (..), MonadSchedule, schedule)+import Data.Automaton.Schedule.Trans (ScheduleT, runScheduleT, runSkipS, runYield, skip, wait)+import Data.Automaton.Trans.Maybe (runMaybeS)+import Data.Stream.Result (Result (..))+import Data.TimeDomain (Seconds (..))++tests =+  testGroup+    "Schedule"+    [ testGroup+        "FinalizeT"+        [ testCase "FinalizeT stops when the single automaton completes" $ do+            -- A single automaton counting down from 3 should produce 3 outputs then stop.+            -- We lower to Identity via runMaybeS so outputs are not discarded on termination.+            let outputs = embedFinalize (schedule $ pure (make 3))+            length outputs @?= 3+        , testCase "FinalizeT waits for all automata to complete" $ do+            -- Two automata: A counts down from 2, B from 3.+            -- Identity schedule runs both in lockstep; outputs are interleaved then flattened.+            -- Total outputs: 2 + 3 = 5+            let outputs = embedFinalize (schedule $ make 2 :| [make 3])+            length outputs @?= 5+        , testCase "FinalizeT does not stop when the first automaton completes" $ do+            -- Unlike MaybeT, FinalizeT keeps running while any automaton is still alive.+            -- A (2 steps) finishes first; B (3 steps) finishes last.+            let maybeOutputs = embedMaybe (schedule $ makeMaybe 2 :| [makeMaybe 3])+                finalizeOutputs = embedFinalize (schedule $ make 2 :| [make 3])+            -- MaybeT stops when A finishes: both emit 2 outputs before MaybeT short-circuits+            length maybeOutputs @?= 4+            -- FinalizeT keeps going until B also finishes: 2 + 3 = 5 outputs+            length finalizeOutputs @?= 5+        , testCase "FinalizeT round-robin output order" $ do+            -- Two automata: A emits 10, 20 then stops; B emits 100, 200, 300 then stops.+            -- Identity schedule steps both simultaneously, then concatS flattens:+            --   tick 1: [10, 100], tick 2: [20, 200], tick 3: [300] (A done, B still live)+            -- Outputs: [10, 100, 20, 200, 300]+            let mkCounting startVal stepVal n =+                  Automaton.unfoldM (0 :: Int) $ const $ \k ->+                    if k >= n+                      then FinalizeT $ MaybeT $ pure Nothing+                      else FinalizeT $ MaybeT $ pure $ Just $ Result (k + 1) (startVal + stepVal * k :: Int)+                outputs = embedFinalize (schedule $ mkCounting 10 10 2 :| [mkCounting 100 100 3])+            outputs @?= [10, 100, 20, 200, 300]+        ]+    , testGroup+        "SkipT"+        [ testCase "SkipT skips an output step" $ do+            let output = runIdentity $ embed (runSkipS $ constM (waitSkip 5 $> (5 :: Int)) >>> accumulateWith (+) 0) $ replicate 10 ()+            output @?= [Nothing, Nothing, Nothing, Nothing, Just 5, Nothing, Nothing, Nothing, Nothing, Just 10]+        , testCase "schedule waits chronologically (mirrored)" $ do+            let output = runIdentity $ embed (runSkipS $ constM (waitSkip 3 $> (3 :: Int)) >>> accumulateWith (+) 0) $ replicate 10 ()+            output @?= [Nothing, Nothing, Just 3, Nothing, Nothing, Just 6, Nothing, Nothing, Just 9, Nothing]+        ]+    , testGroup+        "Yield"+        [ testCase "schedule waits chronologically" $ do+            let output = runYield $ embed (schedule $ (\n -> constM (waitSkip n $> n) >>> accumulateWith (+) 0) <$> 3 :| [5]) $ replicate 10 ()+            output @?= [3, 5, 6, 9, 10, 12, 15, 15, 18, 20]+        , testCase "schedule waits chronologically (mirrored)" $ do+            let output = runYield $ embed (schedule $ (\n -> constM (waitSkip n $> n) >>> accumulateWith (+) 0) <$> 5 :| [3]) $ replicate 10 ()+            output @?= [3, 5, 6, 9, 10, 12, 15, 15, 18, 20]+        ]+    , scheduleTests+        "ScheduleT IO busy"+        id+        (const yield)+    , scheduleTests+        "ScheduleT IO with delay"+        id+        (liftIO . threadDelay . (* 10) . fromIntegral)+    , scheduleTests+        "ScheduleT Identity"+        (pure . runIdentity)+        (const $ pure ())+    , testGroup+        "ChangesetT"+        [ testCase "Single automaton is unchanged" $ do+            let output = flip runChangeset (0 :: Int) $ flip embed (replicate 5 ()) $ schedule $ pure $ constM $ changeSingle Increment >> current+            output @?= ([1, 2, 3, 4, 5], 5)+        , testCase "Two automata see global state" $ do+            let output = flip runChangeset (0 :: Int) $ flip embed (replicate 10 ()) $ schedule $ constM (changeSingle Increment >> pure (-1)) :| [constM current]+            output+              @?= (+                    [ -1+                    , 0 -- First tick of both automata: Second one doesn't yet see the log of the other+                    , -1+                    , 1 -- Second joint tick: Log from the first reaches the second automaton+                    , -1+                    , 2+                    , -1+                    , 3+                    , -1+                    , 4+                    ]+                  , 5+                  )+        , testCase "Two automata see global state (mirrored)" $ do+            let output = flip runChangeset (0 :: Int) $ flip embed (replicate 10 ()) $ schedule $ constM current :| [constM (changeSingle Increment >> pure (-1))]+            output+              @?= (+                    [ 0 -- First tick of both automata: Second one doesn't yet see the log of the other+                    , -1+                    , 1 -- Second joint tick: Log from the first reaches the second automaton+                    , -1+                    , 2+                    , -1+                    , 3+                    , -1+                    , 4+                    , -1+                    ]+                  , 5+                  )+        ]+    , testGroup+        "MaybeT"+        [ testCase "MaybeT stops all automata when one returns Nothing" $ do+            -- Two automata: one finite (stops after 3 steps), one infinite.+            -- When the finite one stops, the whole scheduled automaton stops too.+            let finite =+                  Automaton.unfoldM (3 :: Int) $ const $ \n ->+                    if n <= 0+                      then MaybeT $ pure Nothing+                      else pure $ Result (n - 1) n+                infinite = constM $ pure (0 :: Int)+                output = runIdentity $ embed (runMaybeS (schedule $ finite :| [infinite])) $ replicate 10 ()+            -- Stops as soon as finite returns Nothing+            output @?= [Just 3, Just 0, Just 2, Just 0, Just 1, Just 0, Nothing, Nothing, Nothing, Nothing]+        , testCase "MaybeT all-Nothing immediately gives Nothing" $ do+            let alwaysNothing = constM (MaybeT $ pure Nothing) :: Automaton (MaybeT Identity) () Int+                output = runIdentity $ embed (runMaybeS (schedule $ alwaysNothing :| [alwaysNothing])) (replicate 5 ())+            output @?= [Nothing, Nothing, Nothing, Nothing, Nothing]+        ]+    , testGroup+        "ExceptT"+        [ testCase "ExceptT stops all automata when one throws" $ do+            -- One automaton throws after 3 steps, the other never throws.+            let throwing =+                  Automaton.unfoldM (3 :: Int) $ const $ \n ->+                    if n <= 0+                      then throwE ("done" :: String)+                      else pure $ Result (n - 1) n+                nonthrowing = constM $ pure (0 :: Int)+                output = runIdentity $ runExceptT $ embed (schedule $ throwing :| [nonthrowing]) $ replicate 10 ()+            -- Should terminate with Left, not run all 10 steps+            case output of+              Left _ -> pure ()+              Right xs -> fail $ "Expected Left but got Right with " <> (show (length xs) <> " elements")+        ]+    ]+  where+    waitSkip n = replicateM (n - 1) skip+    -- \| Automaton in FinalizeT that counts down from n, outputting each value, then terminates.+    make :: Int -> Automaton (FinalizeT Identity) () Int+    make n = Automaton.unfoldM n $ const $ \k ->+      if k <= 0+        then FinalizeT $ MaybeT $ pure Nothing+        else FinalizeT $ MaybeT $ pure $ Just $ Result (k - 1) k+    -- \| Automaton in MaybeT that counts down from n, outputting each value, then terminates.+    makeMaybe :: Int -> Automaton (MaybeT Identity) () Int+    makeMaybe n = Automaton.unfoldM n $ const $ \k ->+      if k <= 0+        then MaybeT $ pure Nothing+        else MaybeT $ pure $ Just $ Result (k - 1) k+    -- \| Run a FinalizeT automaton and collect all outputs produced before it terminates.+    -- Uses runMaybeS to lower to Identity, so the outputs are not discarded on termination.+    embedFinalize :: Automaton (FinalizeT Identity) () b -> [b]+    embedFinalize automaton =+      fmap fromJust+        . takeWhile isJust+        . runIdentity+        $ embed (runMaybeS $ hoistS getFinalizeT automaton) (replicate 100 ())+    -- \| Run a MaybeT automaton and collect all outputs produced before it terminates.+    embedMaybe :: Automaton (MaybeT Identity) () b -> [b]+    embedMaybe automaton =+      fmap fromJust+        . takeWhile isJust+        . runIdentity+        $ embed (runMaybeS automaton) (replicate 100 ())++scheduleTests ::+  (Monad m, MonadSchedule m) =>+  String ->+  (forall a. m a -> IO a) ->+  (Seconds Integer -> m ()) ->+  TestTree+scheduleTests name runProperty interpretSchedule =+  testGroup+    name+    [ testCase "schedule waits chronologically" $ do+        output <- runProperty $ runScheduleT interpretSchedule $ embed (schedule $ mkClock <$> 3 :| [5]) $ replicate 10 ()+        output @?= [3, 5, 6, 9, 10, 12, 15, 15, 18, 20]+    , testCase "schedule chronologically (mirrored)" $ do+        output <- runProperty $ runScheduleT interpretSchedule $ embed (schedule $ mkClock <$> 5 :| [3]) $ replicate 10 ()+        output @?= [3, 5, 6, 9, 10, 12, 15, 15, 18, 20]+    , testProperty "multiple automata schedule correctly" $ within 1_000_000 $ \(diffss :: NonEmpty Diffs) -> monadicIO $ do+        output <- fmap (List.nub . snd) $ run $ runProperty $ runScheduleT interpretSchedule $ runWriterT $ runMaybeT $ embed (hoistS (hoist lift) (schedule $ runningClock <$> diffss) >>> arrM (lift . tell . pure)) $ repeat ()+        pure $ output === take (length output) (List.nub $ List.sort $ concatMap accDiffs diffss)+    ]+  where+    mkClock n = constM (wait n $> n) >>> accumulateWith (+) (0 :: Seconds Integer)++-- These instances are needed because 'time-domain' does not yet provide+-- 'Semigroup'/'Monoid' for 'Integer'. Remove once time-domain is updated.+deriving via (Sum Integer) instance Semigroup Integer++deriving via (Sum Integer) instance Monoid Integer++type Diffs = [Positive (Seconds Integer)]++type Times = [Seconds Integer]++accDiffs :: Diffs -> Times+accDiffs = drop 1 . scanl (\t (Positive dt) -> t + dt) 0 . toList++interpretDiffs :: (Monad m) => Diffs -> Automaton (MaybeT (ScheduleT (Seconds Integer) m)) () (Seconds Integer)+interpretDiffs diffs0 = Automaton.unfoldM (toList diffs0) $ const $ \case+  [] -> MaybeT $ pure Nothing+  (Positive diff : diffs) -> lift (wait diff) >> pure (Result diffs diff)++runningClock :: (Monad m) => Diffs -> Automaton (MaybeT (ScheduleT (Seconds Integer) m)) () (Seconds Integer)+runningClock diffs = interpretDiffs diffs >>> accumulateWith (+) 0++-- 'NonEmpty' has no 'Arbitrary' instance in QuickCheck as of 2.14.+instance (Arbitrary a) => Arbitrary (NonEmpty a) where+  arbitrary = (:|) <$> arbitrary <*> arbitrary++instance (Arbitrary a) => Arbitrary (Seconds a) where+  arbitrary = Seconds <$> arbitrary
test/Stream.hs view
@@ -20,7 +20,7 @@ import Test.Tasty.HUnit (testCase, (@?=))  -- automaton-import Data.Stream (StreamT (..), constM, foreverExceptE, handleExceptT, handleWriterT, mmap, snapshot, streamToList, unfold, unfold_)+import Data.Stream (StreamT (..), concatS, constM, foreverExceptE, handleExceptT, handleWriterT, mmap, snapshot, streamToList, unfold, unfold_) import Data.Stream.Except qualified as StreamExcept import Data.Stream.Optimized qualified as StreamOptimized import Data.Stream.Result@@ -87,6 +87,9 @@              in take 10 (runIdentity $ streamToList $ StreamOptimized.toStreamT $ StreamExcept.foreverE 0 recursive)                   @?= [0, 0, 1, 0, 1, 2, 0, 1, 2, 3]         ]+    , testCase "concatS" $+        let stream = concatS $ unfold 0 (\n -> Result (n + 1) [n, n + 10])+         in take 10 (runIdentity $ streamToList stream) @?= [0, 10, 1, 11, 2, 12, 3, 13, 4, 14]     ]  nats :: (Applicative m) => StreamT m Int