packages feed

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