crem-0.1.0.0: src/Crem/BaseMachine.hs
{-# LANGUAGE DataKinds #-}
-- | A `BaseMachine` is a Mealy machine constrained by a provided `Topology` of
-- allowed transitions.
module Crem.BaseMachine where
import Crem.Topology
import "base" Data.Bifunctor (Bifunctor (..), first)
import "base" Data.Functor.Identity (Identity (..))
import "base" Data.Kind (Type)
import "profunctors" Data.Profunctor (Choice (..), Profunctor (..), Strong (..))
import "singletons-base" Data.Singletons.Base.TH (STuple0 (..))
-- * Specifying state machines
-- | A @BaseMachineT m topology input output@ describes a state machine with
-- allowed transitions constrained by a given @topology@.
-- A state machine is composed by an `initialState` and an `action`, which
-- defines the @output@ and the new @state@ given the current @state@ and an
-- @input@.
-- The @m@ parameter describes the context where the `action` of the machine is
-- executed
data
BaseMachineT
m
(topology :: Topology vertex)
(input :: Type)
(output :: Type) = forall state.
BaseMachineT
{ initialState :: InitialState state
, action
:: forall initialVertex
. state initialVertex
-> input
-> ActionResult m topology state initialVertex output
}
-- | A `BaseMachine` 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 BaseMachine
(topology :: Topology vertex)
(input :: Type)
(output :: Type) =
forall m. Monad m => BaseMachineT m topology input output
-- * 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@
baseHoist
:: (forall x. m x -> n x)
-> BaseMachineT m topology a b
-> BaseMachineT n topology a b
baseHoist f (BaseMachineT initialState action) =
BaseMachineT
initialState
((hoistResult f .) . action)
instance Functor m => Profunctor (BaseMachineT m topology) where
lmap :: (a -> b) -> BaseMachineT m topology b c -> BaseMachineT m topology a c
lmap f (BaseMachineT initialState action) =
BaseMachineT
{ initialState = initialState
, action = (. f) . action
}
rmap :: (b -> c) -> BaseMachineT m topology a b -> BaseMachineT m topology a c
rmap f (BaseMachineT initialState action) =
BaseMachineT
{ initialState = initialState
, action = ((f <$>) .) . action
}
instance Functor m => Strong (BaseMachineT m topology) where
first' :: BaseMachineT m topology a b -> BaseMachineT m topology (a, c) (b, c)
first' (BaseMachineT initialState action) =
BaseMachineT
{ initialState = initialState
, action = \state (a, c) -> (,c) <$> action state a
}
second' :: BaseMachineT m topology a b -> BaseMachineT m topology (c, a) (c, b)
second' (BaseMachineT initialState action) =
BaseMachineT
{ initialState = initialState
, action = \state (c, a) -> (c,) <$> action state a
}
instance Applicative m => Choice (BaseMachineT m topology) where
left' :: BaseMachineT m topology a b -> BaseMachineT m topology (Either a c) (Either b c)
left' (BaseMachineT initialState action) =
BaseMachineT
{ initialState = initialState
, action = \state -> \case
Left a -> Left <$> action state a
Right c -> ActionResult $ pure (Right c, state)
}
right' :: BaseMachineT m topology a b -> BaseMachineT m topology (Either c a) (Either c b)
right' (BaseMachineT initialState action) =
BaseMachineT
{ initialState = initialState
, action = \state -> \case
Left c -> ActionResult $ pure (Left c, state)
Right a -> Right <$> action state a
}
-- | A value of type @InitialState state@ describes the initial state of a
-- state machine, describing the initial @vertex@ in the @topology@ and the
-- actual initial data of type @state vertex@
data InitialState (state :: vertex -> Type) where
InitialState :: state vertex -> InitialState state
-- | The result of an action of the state machine.
-- An @ActionResult m topology state initialVertex output@ contains an @output@
-- and a @state finalVertex@, where the transition from @initialVertex@ to
-- @finalVertex@ is allowed by the machine @topology@
data
ActionResult
m
(topology :: Topology vertex)
(state :: vertex -> Type)
(initialVertex :: vertex)
(output :: Type)
where
ActionResult
:: AllowedTransition topology initialVertex finalVertex
=> m (output, state finalVertex)
-> ActionResult m topology state initialVertex output
-- | Allows to change the computational context of an `ActionResult` from @m@
-- to @n@, given we have a [natural transformation](https://stackoverflow.com/a/58364172/2718064)
-- from @m@ to @n@.
hoistResult
:: (forall x. m x -> n x)
-> ActionResult m topology state initialVertex output
-> ActionResult n topology state initialVertex output
hoistResult f (ActionResult outputStatePair) = ActionResult $ f outputStatePair
instance Functor m => Functor (ActionResult m topology state initialVertex) where
fmap
:: (a -> b)
-> ActionResult m topology state initialVertex a
-> ActionResult m topology state initialVertex b
fmap f (ActionResult outputStatePair) =
ActionResult $ first f <$> outputStatePair
-- | Create an `ActionResult` without performing any side effect in the @m@
-- context
pureResult
:: (Applicative m, AllowedTransition topology initialVertex finalVertex)
=> output
-> state finalVertex
-> ActionResult m topology state initialVertex output
pureResult output state = ActionResult . pure $ (output, state)
-- | This is fairly similar to `sequenceA` from `Data.Traversable` and in fact
-- it does the same job, with the slight difference that `sequenceA` would
-- return @f (ActionResult Identity topology state initialVertex output)@
sequence
:: Functor f
=> ActionResult Identity topology state initialVertex (f output)
-> ActionResult f topology state initialVertex output
sequence (ActionResult (Identity (outputs, state))) =
ActionResult $ (,state) <$> outputs
-- ** Lift machines
-- | Lift a @BaseMachineT@ to operate with `Maybe` inputs and outputs. If the
-- input is a `Nothing`, then the output will be a `Nothing`. If the input is a
-- `Just`, then the machine will be used to compute the output.
maybeM
:: Applicative m
=> BaseMachineT m topology a b
-> BaseMachineT m topology (Maybe a) (Maybe b)
maybeM (BaseMachineT initialState action) =
BaseMachineT
{ initialState = initialState
, action = \initialState' -> \case
Nothing -> pureResult Nothing initialState'
Just a -> Just <$> action initialState' a
}
-- | Lift a @BaseMachineT@ to operate with `Either` inputs and outputs. If the
-- input is a `Left`, then the output will be that `Left`. If the input is a
-- `Right`, then the machine will be used to compute the output.
eitherM
:: Applicative m
=> BaseMachineT m topology a b
-> BaseMachineT m topology (Either e a) (Either e b)
eitherM (BaseMachineT initialState action) =
BaseMachineT
{ initialState = initialState
, action = \initialState' -> \case
Left e -> pureResult (Left e) initialState'
Right a -> Right <$> action initialState' a
}
-- ** Stateless machines
-- | `statelessBaseT` transforms its input to its output and never changes its
-- state
statelessBaseT :: Applicative m => (a -> m b) -> BaseMachineT m (TrivialTopology @()) a b
statelessBaseT f =
BaseMachineT
{ initialState = InitialState STuple0
, action = \state input ->
ActionResult $ (,state) <$> f input
}
-- | `statelessBase` transforms its input to its output and never changes its
-- state, without performing any side effect
statelessBase :: (a -> b) -> BaseMachine (TrivialTopology @()) a b
statelessBase f = statelessBaseT (pure . f)
-- ** Identity machine
-- | The `identity` machine simply outputs its input and never changes its
-- state.
identity :: BaseMachine (TrivialTopology @()) a a
identity = statelessBase id
-- ** Unrestricted machines
-- | a machine modelled with explicit state, where every transition is allowed
unrestrictedBaseMachineT
:: (forall initialVertex. state initialVertex -> a -> ActionResult m (AllowAllTopology @vertex) state initialVertex b)
-> InitialState (state :: vertex -> Type)
-> BaseMachineT m (AllowAllTopology @vertex) a b
unrestrictedBaseMachineT action initialState =
BaseMachineT
{ initialState = initialState
, action = action
}
-- ** Run a machine
-- | Given an @input@, run the machine to get an output and a new version of
-- the machine
runBaseMachineT
:: Functor m
=> BaseMachineT m topology input output
-> input
-> m (output, BaseMachineT m topology input output)
runBaseMachineT (BaseMachineT (InitialState initialState) action) input =
case action initialState input of
ActionResult outputStatePair ->
second
( \finalState ->
BaseMachineT
{ initialState = InitialState finalState
, action = action
}
)
<$> outputStatePair