Safe Haskell | None |
---|---|
Language | Haskell2010 |
Motor is an experimental Haskell library for building finite-state machines with type-safe transitions and effects. It draws inspiration from the Idris ST library.
- class IxMonad m => MonadFSM m where
- data Name n where
- Name :: KnownSymbol n => Name n
- data n :-> a
- data To a b
- data Add s
- data Delete s
- type family FromActions (as :: [*]) (rs :: Row *) :: Row * where ...
- type NoActions m r a = m r r a
- type Actions m as r a = m r (FromActions as r) a
- type OnlyActions m as a = Actions m as Empty a
- type (!-->) i o = To i o
- type (!+) n s = n :-> Add s
- type (!-) n s = n :-> Delete s
- data FSM m i o a
- runFSM :: Monad m => FSM m Empty Empty a -> m a
Usage
The central finite-state machine abstraction in Motor is the MonadFSM
type class.
MonadFSM
is an indexed monad type class, meaning that it has not one,
but three type parameters:
The MonadFSM
parameter kinds might look a bit scary, but they state
the same:
class IxMonad m => MonadFSM (m :: (Row *) -> (Row *) -> * -> *) where ...
The rows describe how the FSM computation will affect the state of its resources when evaluated. A row is essentially a type-level map, from resource names to state types, and the FSM computation's rows describe the resource states before and after the computation.
An FSM computation newConn
that adds a resource named "connection"
with state Idle
could have the following type:
>>>
:t newConn
newConn :: MonadFSM m => m r ("connection" ::= Idle :| r) ()
A computation spawnTwoPlayers
that adds two resources could have
this type:
>>>
:t spawnTwoPlayers
spawnTwoPlayers :: :: MonadFSM m => m r ("hero2" ::= Standing :| "hero1" ::= Standing :| r) ()
Motor uses the extensible records in Data.OpenRecords, provided by the CTRex library, for row kinds. Have a look at it's documentation to learn more about the type-level operators available for rows.
Indexed Monads
As mentioned above, MonadFSM
is an indexed monad. It uses the
definition from Control.Monad.Indexed, in the
[indexed](https:/hackage.haskell.orgpackage/indexed-0.1.3)
package. This means that you can use ibind
and friends to compose
FSM computations.
c1 >>>= \_ -> c2
You can combine this with the RebindableSyntax
language extension to
get do-syntax for FSM programs:
test :: MonadFSM m => m Empty Empty () test = do c1 c2 r <- c3 c4 r where (>>) a = (>>>=) a . const (>>=) = (>>>=)
See 24 Days of GHC Extensions: Rebindable
Syntax
for some more information on how to use RebindableSyntax
.
State Actions
To make it easier to read and write FSM computation types, there is some syntax sugar available.
State actions allow you two describe state changes of named resources with a single list, as opposed two writing two rows. They also take care of matching the CTRex row combinators with the expectations of Motor, which can be tricky to do by hand.
There are three state actions:
A mapping between a resource name is written using the :->
type operator,
with a Symbol
on the left, and a state action type on the right. Here are
some examples:
"container" :-> Add Empty "list" :-> To Empty NonEmpty "game" :-> Delete GameEnded
So, the list of mappings from resource names to state actions describe
what happens to each resource. Together with an initial row of
resources r
, and a return value a
, we can declare the type of an
FSM computation using the Actions
type:
MonadFSM m => Actions m '[ n1 :-> a1, n2 :-> a2, ... ] r a
A computation that adds two resources could have the following type:
addingTwoThings :: MonadFSM m => Actions m '[ "container" :-> Add Empty, "game" :-> Add Started ] r ()
Infix Operators
As an alternative to the Add
, To
, and Delete
types,
Motor offers infix operator aliases. These start with !
to indicate
that they can be effectful.
The !-->
operator is an infix alias for To
:
useStateMachines :: MonadFSM m => Actions m '[ "program" :-> NotCool !--> Cool ] r ()
The !+
and !-
are infix aliases for mappings from resource names to Add
and Delete
state actions, respectively:
startNewGame :: MonadFSM m => Actions m '[ "game" !+ Started ] r () endGameWhenWon :: MonadFSM m => Actions m '[ "game" !- Won ] r ()
Row Polymorphism
Because of how CTRex works, FSM computations that have a free variable as their input row of resources, i.e. that are polymorphic in the sense of other resource states, must list all their actions in reverse order.
doFourThings :: Game m => Actions m '[ "hero2" !- Standing , "hero1" !- Standing , "hero2" !+ Standing , "hero1" !+ Standing ] r () doFourThings = spawn hero1 >>>= _ -> spawn hero2 >>>= _ -> perish hero1 >>>= _ -> perish hero2
Had the r
been replaced by Empty
in the type signature above, it could
have had type NoActions m Empty ()
instead.
If the computation removes all resources that it creates, i.e. that it
could be run as NoActions m Empty ()
, you can use call
to run it
in a row-polymorphic computation without having to list all actions:
doFourThings :: Game m => NoActions m r () doFourThings = call $ spawn hero1 >>>= _ -> spawn hero2 >>>= _ -> perish hero1 >>>= _ -> perish hero2
In a future version, call
might support the rows of the called
computation being subsets of the resulting computation's rows.
Examples
The GitHub repository includes some examples, check that out.
API
MonadFSM Class
class IxMonad m => MonadFSM m where Source #
An indexed monad for finite-state machines, managing the state of named resources.
new :: Name n -> a -> m r (Extend n a r) () Source #
Creates a new resource and returns its Name
.
delete :: Name n -> m r (r :- n) () Source #
Deletes an existing resource named by its Name
.
enter :: Name n -> b -> m r ((n ::= b) :| (r :- n)) () Source #
Replaces the state of an existing resource named by its Name
.
call :: m Empty Empty a -> m r r a Source #
Run another MonadFSM
computation, with empty resource rows,
in this computation.
Resource Names
A name of a resource, represented using a Symbol
.
Name :: KnownSymbol n => Name n |
State Actions
type family FromActions (as :: [*]) (rs :: Row *) :: Row * where ... Source #
Translates a list of Action
s to a Row
.
FromActions '[] rs = rs | |
FromActions ((n :-> Add a) ': ts) r = Extend n a (FromActions ts r) | |
FromActions ((n :-> Delete a) ': ts) r = FromActions ts r :- n | |
FromActions ((n :-> To a b) ': ts) r = Extend n b (FromActions ts r :- n) |
type Actions m as r a = m r (FromActions as r) a Source #
Alias for MonadFSM
that uses FromActions
to construct rows.
type OnlyActions m as a = Actions m as Empty a Source #
Alias for MonadFSM
that uses FromActions
to construct rows,
starting from an Empty
row, i.e. allowing no other resources.
Aliases
FSM
IxStateT-based implementation of MonadFSM
.
Monad m => MonadFSM (FSM m) Source # | |
IxMonadTrans (Row *) FSM Source # | |
Monad m => IxMonad (Row *) (FSM m) Source # | |
Monad m => IxApplicative (Row *) (FSM m) Source # | |
Monad m => IxPointed (Row *) (FSM m) Source # | |
Monad m => IxFunctor (Row *) (Row *) (FSM m) Source # | |
Monad m => Monad (FSM m i i) Source # | |
Monad m => Functor (FSM m i i) Source # | |
Monad m => Applicative (FSM m i i) Source # | |
MonadIO m => MonadIO (FSM m i i) Source # | |