I can define a toy state machine (with trivial input) as follows:
--------------------------------------------
-- module State where
data State = A | B Int
--------------------------------------------
-- module A where
-- import State
transitionA :: State
transitionA = B 10
--------------------------------------------
-- module B where
-- import State
transitionB :: Int -> State
transitionB i
| i < 0 = A
| otherwise = B (i-1)
--------------------------------------------
-- module StateMachine where
-- import State
-- import A
-- import B
transition :: State -> State
transition A = transitionA
transition (B i) = transitionB i
If I now decide to add a new state, I have to:
- modify the State module to add the new state, say
data State = A | B Int | C Double Double
add a new transition function transitionC in a module C
import C in the last module, and add the C case to the pattern match
I'd like to set things up so that I only have to perform step 2 (write a new transition function) and everything else gets automatically taken care of.
For instance, one could try to use existential types to do something like the following:
--------------------------------------------
{-# LANGUAGE ExistentialQuantification #-}
-- module State where
class State s where
transition :: s -> AState
data AState = forall s. State s => AState s
instance State AState where
transition (AState s) = transition s
-------------------------------------
-- module A where
-- import State
-- import B
data A = A
instance State A where
transition _ = AState (B 10)
-------------------------------------
-- module B where
-- import State
-- import A
data B = B Int
instance State B where
transition (B i)
| i < 0 = AState ( A )
| otherwise = AState ( B (i-1) )
This is very convenient: to add a new state, we only need to do one thing, which is to write a data type and its associated transition function in a new module, and nothing else needs to be changed. Unfortunately, this approach doesn't work because it creates cyclic dependencies, e.g. in this case A needs to refer to B and B needs to refer to A.
I also tried to look into using extensible sum types (polymorphic variants), but the same problem arises unless we declare all the possible states ahead of time in a separate module so that subsequent modules can refer to them. In other words, it can eliminate step 3, but not step 1.
Is this the kind of problem that can be tackled using (Conor McBride's version of) indexed monads? It seems we could use some kind of indexed state monad where we don't know the return state in advance, which I gather, from his answer to What is indexed monad?, is something that MonadIx achieves.