packages feed

crem-0.1.1.0: src/Crem/StateMachine.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeFamilies #-}

-- | This is the main module of the whole library. It defines the central
-- `StateMachineT` type, which allows us to create composable state machines.
module Crem.StateMachine where

import Crem.BaseMachine as BaseMachine
import Crem.Render.RenderableVertices (RenderableVertices)
import Crem.Topology (AllowAllTopology, Topology)
import "base" Control.Arrow (Arrow (arr, first), ArrowChoice (left))
import "base" Control.Category (Category (..))
import "base" Data.Bifunctor (Bifunctor (second), bimap)
import "base" Data.Foldable (foldlM)
import "base" Data.Kind (Type)
import "machines" Data.Machine.MealyT qualified as Machines
import "machines" Data.Machine.Process qualified as Machines
import "nothunks" NoThunks.Class (NoThunks (..), allNoThunks)
import "profunctors" Data.Profunctor (Choice (..), Profunctor (..), Strong (..))
import "singletons-base" Data.Singletons (Demote, SingI, SingKind)
import Prelude hiding ((.))

-- | A `StateMachineT` is an effectful [Mealy machine](https://en.wikipedia.org/wiki/Mealy_machine)
-- with inputs of type @input@ and outputs of type @output@
--
-- Effects are described by the context @m@ in which the action of the machine
-- is executed
--
-- `StateMachineT` is a tree, where leaves are `BaseMachineT` and other nodes
-- describe how to combine the subtrees to obtain more complex machines.
--
-- Please refer to https://github.com/tweag/crem/blob/main/docs/how-to-create-a-machine.md
-- for a more complete discussion on the various constructors.
data StateMachineT m input output where
  -- | `Basic` allows to interpret a `BaseMachineT` as a `StateMachineT`,
  -- making the @topology@ type variable existential
  Basic
    :: forall m vertex (topology :: Topology vertex) input output
     . ( Demote vertex ~ vertex
       , SingKind vertex
       , SingI topology
       , Eq vertex
       , Show vertex
       , RenderableVertices vertex
       )
    => BaseMachineT m topology input output
    -> StateMachineT m input output
  -- | `Sequential` adds categorical composition for `StateMachineT`
  Sequential
    :: StateMachineT m a b
    -> StateMachineT m b c
    -> StateMachineT m a c
  -- | `Parallel` allows to process two machine simultaneously
  Parallel
    :: StateMachineT m a b
    -> StateMachineT m c d
    -> StateMachineT m (a, c) (b, d)
  -- | `Alternative` allows to process one out of two machines depending on the
  -- input
  Alternative
    :: StateMachineT m a b
    -> StateMachineT m c d
    -> StateMachineT m (Either a c) (Either b d)
  -- | `Feedback` allows to compose two machine going in oppositive directions
  -- and run them in a loop
  Feedback
    :: (Foldable n, Monoid (n a), Monoid (n b))
    => StateMachineT m a (n b)
    -> StateMachineT m b (n a)
    -> StateMachineT m a (n b)
  -- | `Kleisli` allows to compose sequentially machines which emit multiple
  -- outputs
  Kleisli
    :: (Foldable n, Monoid (n c))
    => StateMachineT m a (n b)
    -> StateMachineT m b (n c)
    -> StateMachineT m a (n c)

type role StateMachineT representational nominal nominal

instance NoThunks (StateMachineT m input output) where
  showTypeOf _ = "StateMachineT"
  wNoThunks ctxt sm =
    case sm of
      Basic _ -> return Nothing
      Sequential x y -> allNoThunks [noThunks ctxt x, noThunks ctxt y]
      Parallel x y -> allNoThunks [noThunks ctxt x, noThunks ctxt y]
      Alternative x y -> allNoThunks [noThunks ctxt x, noThunks ctxt y]
      Feedback x y -> allNoThunks [noThunks ctxt x, noThunks ctxt y]
      Kleisli x y -> allNoThunks [noThunks ctxt x, noThunks ctxt y]

-- | A `StateMachine` is an effectful machine for every possible monad @m@.
-- Needing to work for every monad, in fact it can not perform any kind of
-- effect and needs to be pure in nature.
type StateMachine a b = forall m. (Monad m) => StateMachineT m a b

-- * Hoist

-- | Allows to change the context @m@ where the machine operates to another
-- context @n@, provided we have a [natural transformation](https://stackoverflow.com/a/58364172/2718064)
-- from @m@ to @n@
hoist :: (forall x. m x -> n x) -> StateMachineT m a b -> StateMachineT n a b
hoist f machine = case machine of
  Basic baseMachine -> Basic $ baseHoist f baseMachine
  Sequential machine1 machine2 -> Sequential (hoist f machine1) (hoist f machine2)
  Parallel machine1 machine2 -> Parallel (hoist f machine1) (hoist f machine2)
  Alternative machine1 machine2 -> Alternative (hoist f machine1) (hoist f machine2)
  Feedback machine1 machine2 -> Feedback (hoist f machine1) (hoist f machine2)
  Kleisli machine1 machine2 -> Kleisli (hoist f machine1) (hoist f machine2)

-- | a state machine which does not rely on state
statelessT :: (Applicative m) => (a -> m b) -> StateMachineT m a b
statelessT f = Basic $ statelessBaseT f

-- | a state machine which does not rely on state and does not perform side
-- effects
stateless :: (Applicative m) => (a -> b) -> StateMachineT m a b
stateless f = statelessT (pure . f)

-- | a machine modelled with explicit state, where every transition is allowed
unrestrictedMachine
  :: ( Demote vertex ~ vertex
     , SingKind vertex
     , SingI (AllowAllTopology @vertex)
     , Eq vertex
     , Show vertex
     , RenderableVertices vertex
     )
  => ( forall initialVertex
        . state initialVertex
       -> a
       -> ActionResult m (AllowAllTopology @vertex) state initialVertex b
     )
  -> InitialState (state :: vertex -> Type)
  -> StateMachineT m a b
unrestrictedMachine action state = Basic $ unrestrictedBaseMachineT action state

-- * Category

instance (Monad m) => Category (StateMachineT m) where
  id :: StateMachineT m a a
  id = Basic identity

  (.) :: StateMachineT m b c -> StateMachineT m a b -> StateMachineT m a c
  (.) = flip Sequential

-- * Profunctor

instance (Applicative m) => Profunctor (StateMachineT m) where
  lmap :: (a -> b) -> StateMachineT m b c -> StateMachineT m a c
  lmap f (Basic baseMachine) = Basic $ lmap f baseMachine
  lmap f (Sequential machine1 machine2) = Sequential (lmap f machine1) machine2
  lmap f machine = Sequential (stateless f) machine

  rmap :: (b -> c) -> StateMachineT m a b -> StateMachineT m a c
  rmap f (Basic baseMachine) = Basic $ rmap f baseMachine
  rmap f (Sequential machine1 machine2) = Sequential machine1 (rmap f machine2)
  rmap f machine = Sequential machine (stateless f)

-- * Strong

instance (Monad m) => Strong (StateMachineT m) where
  first' :: StateMachineT m a b -> StateMachineT m (a, c) (b, c)
  first' = flip Parallel Control.Category.id

  second' :: StateMachineT m a b -> StateMachineT m (c, a) (c, b)
  second' = Parallel Control.Category.id

-- * Choice

-- | An instance of `Choice` allows us to have parallel composition of state
-- machines, meaning that we can pass two inputs to two state machines and get
-- out the outputs of both
instance (Monad m) => Choice (StateMachineT m) where
  left' :: StateMachineT m a b -> StateMachineT m (Either a c) (Either b c)
  left' = flip Alternative Control.Category.id

  right' :: StateMachineT m a b -> StateMachineT m (Either c a) (Either c b)
  right' = Alternative Control.Category.id

-- * Arrow

instance (Monad m) => Arrow (StateMachineT m) where
  arr :: (a -> b) -> StateMachineT m a b
  arr = stateless

  first :: StateMachineT m a b -> StateMachineT m (a, c) (b, c)
  first = first'

-- * ArrowChoice

instance (Monad m) => ArrowChoice (StateMachineT m) where
  left :: StateMachineT m a b -> StateMachineT m (Either a c) (Either b c)
  left = left'

-- * AutomatonM

instance Machines.AutomatonM StateMachineT where
  autoT :: (Monad m) => StateMachineT m a b -> Machines.ProcessT m a b
  autoT = Machines.autoT . go
    where
      go :: (Monad m) => StateMachineT m a b -> Machines.MealyT m a b
      go sm = Machines.MealyT $ fmap (fmap go) . run sm

-- * Run a state machine

-- | Given an @input@, run the machine to get an output and a new version of
-- the machine
run :: (Monad m) => StateMachineT m a b -> a -> m (b, StateMachineT m a b)
run (Basic baseMachine) a = second Basic <$> runBaseMachineT baseMachine a
run (Sequential machine1 machine2) a = do
  (output1, machine1') <- run machine1 a
  (output2, machine2') <- run machine2 output1
  pure (output2, Sequential machine1' machine2')
run (Parallel machine1 machine2) a = do
  (output1, machine1') <- run machine1 (fst a)
  (output2, machine2') <- run machine2 (snd a)
  pure ((output1, output2), Parallel machine1' machine2')
run (Alternative machine1 machine2) a =
  case a of
    Left a1 -> bimap Left (`Alternative` machine2) <$> run machine1 a1
    Right a2 -> bimap Right (machine1 `Alternative`) <$> run machine2 a2
run (Feedback machine1 machine2) a = do
  (bs, machine1') <- run machine1 a
  (as, machine2') <- runMultiple machine2 bs
  first (bs <>) <$> runMultiple (Feedback machine1' machine2') as
run (Kleisli machine1 machine2) a = do
  (bs, machine1') <- run machine1 a
  (cs, machine2') <- runMultiple machine2 bs
  pure (cs, Kleisli machine1' machine2')

-- | process multiple inputs in one go, accumulating the results in a monoid
runMultiple
  :: (Monad m, Foldable f, Monoid b)
  => StateMachineT m a b
  -> f a
  -> m (b, StateMachineT m a b)
runMultiple machine =
  foldlM
    (\(b, machine') a -> first (b <>) <$> run machine' a)
    (mempty, machine)