packages feed

Rasenschach-0.1.3.2: Data/FSM.hs

{-# LANGUAGE Arrows #-}

module Data.FSM
  (FSM, State, state, content, addTransition, runTrans,
   fromList, checkStates, Problem, reactMachine,
   reactMachineMult, reactMachineHist, fsmToDot)

where

import FRP.Yampa
import qualified Data.Map as M
import Data.List (nub, (\\), foldl')
import Data.Monoid
import Data.Function (on)


-- *************************************************************************
--
-- Datatypes for States and FSMs
--
-- *************************************************************************

type SId = Int

data Problem t = NonUniqueState SId
                 | MissingTarget SId t
 deriving (Show)

-- State consists of an unique Id, a content describing the state
-- (possibly an enum), functions for generating messages on
-- leaving the current state or entering a new state, resp., and
-- a transitiion table from a given state to its neighboring states
data State a t perception messages = State {
    stateId :: SId,
    content :: a,
    while, onEnter, onExit :: perception -> messages,
    transition :: M.Map t Int
}
instance (Show t, Show a) => Show (State a t p m)
-- where show s = "State " ++ show (content s) ++ ", ("
--                 ++ concatMap (\(t,y) -> ' ' : (show t ++ "->" ++ show y)) (M.toList (transition s))
--                 ++ " )"
 where show s = "{" ++ show (stateId s) ++ "}"

instance Eq (State a t p m)
 where (==) = (==) `on` stateId

-- a FSM is defined as a Map of State Ids to its corresponding State data
type FSM a t p m = M.Map SId (State a t p m)


-- *************************************************************************
--
-- Basic functions for creating States and FSMs and for running transitions
-- on a given FSM
--
-- *************************************************************************

-- Construction of new State; new States instance start with an empty
-- transition table
state :: Int -> a -> (p -> m)-> (p -> m)-> (p -> m)-> State a t p m
state id' a' while' onEnter' onExit' = State id' a' while' onEnter' onExit' M.empty

-- adds a new transition to a given State instance
addTransition :: Ord t => t -> Int -> State a t p m -> State a t p m
addTransition t transStateId s =
    let newTrans = M.insert t transStateId (transition s)
    in s {transition = newTrans }

-- Creates a new FSM from a list of previously generated states, returns
-- either a Right FSM or - in case of duplicate states or transitions to
-- non-existent states - a list of detected problems in the Leftt
fromList :: [State a t p m] -> Either [Problem t] (FSM a t p m)
fromList ss =
   case checkStates ss of
      [] -> Right $ foldl (\m s -> M.insert (stateId s) s m) M.empty ss
      ps -> Left ps

-- Given a FSM, a current state and a transition, the function computes the
-- next state (if transition is applicable) or Nothing (if transition is not
-- applicable); if transition was applicable, the messages from leaving the
-- old state and entering the new state will be collected
runTrans :: (Ord t, Monoid m) => FSM a t p m -> State a t p m -> t -> p -> Maybe (State a t p m, m)
runTrans fsm currentState trans perception = do
    newId <- M.lookup trans (transition currentState)
    let Just newState = M.lookup newId fsm
    return (newState, onExit currentState perception `mappend` onEnter newState perception)


-- Same as runTrans, but takes a list of transistions instead of a single transistion, returns
-- an additional Bool that indicates whether any transition occured
runTransMult :: (Ord t, Monoid m) => FSM a t p m -> State a t p m -> [t] -> p -> (State a t p m, m, Bool)
runTransMult _ currentState [] _ =
   (currentState, mempty, False)
runTransMult fsm currentState (trans:transs) perception =
        case runTrans fsm currentState trans perception of
            Just (s1, m1) -> let (s2, m2, _) = runTransMult fsm s1 transs perception in (s2, m1 `mappend` m2, True)
            Nothing -> let (s2, m2, occ) = runTransMult fsm currentState transs perception in (s2, mempty `mappend` m2, occ)


-- *************************************************************************
--
-- Create dot-output for rendering with graphviz. To do so: Capture the
-- output of fsmToDot in a file, then use dot to convert
--
-- Example of dot-format:
--
-- digraph simple_hierarchy {
--     B [label="The boss"]      // node B
--     E [label="The employee"]  // node E
--     B->E [label="commands", fontcolor=darkgreen] // edge B->E
-- }
-- *************************************************************************

fsmToDot :: (Show a, Show t) => FSM a t p m -> String
fsmToDot fsm =
   "digraph fsm\n{\n" ++
   (foldl (++) "" $ map statesToDot (M.elems fsm)) ++
   "}\n"
   where
       formatTrans source k a result =
           result ++ show source ++ "->" ++ show a ++ " [label=\"" ++ show k ++ "\"]\n"
       statesToDot s =
           let ts = M.map (\dest -> content ((M.!) fsm dest)) $ transition s
               curr = content s
           in
               M.foldrWithKey (formatTrans curr) "" ts -- (M.fromList [(5,"a"), (3,"b")])


-- *************************************************************************
--
-- Sanity checks on a set of FSM states, checks for duplicate states and
-- transitions with missing target states
--
-- *************************************************************************

checkStates :: [State a e p m] -> [Problem e]
checkStates states =
    map NonUniqueState (checkDoubles states) ++
    map (\(i,e,_) -> MissingTarget i e) (checkTransitions states)

checkTransitions :: [State a e p m] -> [(Int, e, Int)]
checkTransitions = foldl' checkProblem [] . allElems

checkProblem :: [(Int, e, Int)] -> (State a e p m, [State a e p m]) -> [(Int, e, Int)]
checkProblem ps (s, ts) = ps ++ missingStates (stateId s) (transition s) ts

missingStates :: Int -> M.Map e Int -> [State a e p m] -> [(Int, e, Int)]
missingStates s trans ss =
    let sIds = map stateId ss
        ts = M.toList trans
    in concatMap (\(e, sId) -> if elem sId sIds then [] else [(s, e, sId)]) ts

allElems :: [a] -> [(a, [a])]
allElems xs =
    let ixs = zip [0..length xs - 1] xs
    in map (\(i,_) -> ((xs!!i), xs)) ixs

checkDoubles :: [State a e p m] -> [Int]
checkDoubles states =
    let ids = map stateId states
    in ids \\ nub ids


-- *************************************************************************
--
-- Reactivity
--
-- *************************************************************************

-- Wrapper function for easy access to runTrans by accumHold; if no transition
-- applies, the wrapper returns the old state and no messages
-- additionaly, the function pushes through a state parameter that will be attached
-- to the new state
runMachine :: (Ord t, Monoid m) =>
    FSM a t (s, p) m -> (State a t (s, p) m, (m, s)) -> ((t, s), p) -> (State a t (s, p) m, (m, s))
runMachine fsm curr ((trans, stateParam), perc) =
  let (curr', (_, currParam)) = curr
  in
    case runTrans fsm curr' trans (stateParam, perc) of
         Nothing  -> (curr', (mempty, currParam))
         Just (newState, messages) -> (newState, (messages, stateParam))

-- Reactive FSM transition: yields the (time-varying) current state and
-- for every transition (defined by an Event containing the transition
-- and the perception for the onExit / onEnter functions) a Monoid of
-- the collected messages from the onExit / onEnter functions
-- additionaly, the function pushes through a state parameter that will be attached
-- to the new state
reactTransition :: (Ord t, Monoid m) =>
    FSM a t (s, p) m -> State a t (s, p) m -> s -> SF (Event ((t, s), p)) (State a t (s, p) m, Event (m, s))
reactTransition fsm init' initParam =
    accumBy (runMachine fsm) (init', (mempty, initParam)) >>>
    (fst ^<< hold (init', (mempty, initParam))) &&& arr (snd . splitE)

-- Yields the (time-varying) current state and the messages (that originate from
-- either the onExit / onEnter-function on a transition or the while-function
-- if no transition occured)
-- The event that yields a state transition (Event (t, s)) consist of the actual
-- transition t that advances the FSM, and a state parameter s that will be attached
-- to the new state. E.g. a event could be "Event (RunTo, Position 10 20)", telling
-- a player in state "Waiting" to change to state "RunTo", and attaching the additional
-- information "Position 10 20" to the new state
reactMachine :: (Ord t, Monoid m) =>
    FSM a t (s, p) m -> State a t (s, p) m -> s -> SF (p, Event (t, s)) ((State a t (s, p) m, s), m)
reactMachine fsm initState initParam = proc (perception, ets) -> do
    (state', result) <- reactTransition fsm initState initParam -< attach ets perception
    param <- hold initParam -< snd (splitE result)
    returnA -< ((state', param), if isEvent result then fst (fromEvent result) else while state' (param, perception))


-- *************************************************************************
--
-- Similar to reactMachine, but puts a whole list of transition through
-- the FSM at a given point in time
--
-- *************************************************************************

rMM :: (Ord t, Monoid m) =>
    FSM a t (s, p) m -> (State a t (s, p) m, s, Event [(t, s)], p) -> (State a t (s, p) m, (m, s))
rMM _ (s0, sp0, Event [], _) = (s0, (mempty, sp0))
rMM fsm (s0, sp0, Event ((t, s):tss), perc) =
    let (s1, (m1, sp1)) = case runTrans fsm s0 t (s, perc) of
                 Nothing -> (s0, (mempty, sp0))
                 Just (s', m') -> (s', (m', s))
        (s2, (m2, sp2)) =  rMM fsm (s1, sp1, Event tss, perc)
    in (s2, (m1 `mappend` m2, sp2))
rMM _ (s0, sp0, _, _) = (s0, (mempty, sp0))

reactMachineMult :: (Ord t, Monoid m, Eq m) =>
    FSM a t (s, p) m -> State a t (s, p) m -> s -> SF (p, Event [(t, s)]) ((State a t (s, p) m, s), m)
reactMachineMult fsm initState initParam = proc (perception, tss) -> do
    rec
       (s2, sp2) <- iPre (initState, initParam) -< (s1, sp1)
       (s1, (ms, sp1)) <- arr (rMM fsm) -< (s2, sp2, tss, perception)
-- Achtung, hier auch die while-Events aufsammeln!!!
    returnA -< ((s1, sp1), ms `mappend` (while s1 (sp1, perception)))

-- instance (Show e) => Show (Event e) where
--  show NoEvent = "NoEvent"
--  show (Event e) = "Event " ++ show e


-- *************************************************************************
--
-- Similar to reactMachineMult, but also provides the state parameter from
-- the previous state to the message generators. (Special case, used by
-- the key parser to determine the duration between keydown and keyup states
--
-- *************************************************************************

runMachineHist :: (Ord t, Monoid m) =>
    FSM a t (s, (p,s)) m -> (State a t (s, (p,s)) m, (m, s)) -> (([t], s), (s,p)) -> (State a t (s, (p,s)) m, (m, s))
runMachineHist fsm (curr, (_,currParam)) ((transs, stateParam), (oldParam, perc)) =
    let (state', messages, transOccured) = runTransMult fsm curr transs (stateParam, (perc, oldParam))
    in (state', (messages, if transOccured then stateParam else currParam))

reactHistTransitions :: (Ord t, Monoid m) =>
    FSM a t (s, (p,s)) m -> State a t (s, (p,s)) m -> s -> SF (Event (([t], s), (s, p))) (State a t (s, (p,s)) m, Event (m, s))
reactHistTransitions fsm init' initParam =
    accumBy (runMachineHist fsm) (init', (mempty, initParam)) >>>
    (fst ^<< hold (init', (mempty, initParam))) &&& arr (snd . splitE)

-- This is a bit more complicated than it's single input counterpart: the onEnter etc. functions
-- are not only provided with the stateParam s and the perception p, but also with the state
-- param of the previous state, so the input to the onEnter etc. functions is (s, (p,s)) and
-- not (s, p) as in reactMachine.
reactMachineHist :: (Ord t, Monoid m)
    => FSM a t (s, (p,s)) m -> State a t (s, (p,s)) m -> s -> SF (p, Event ([t], s)) ((State a t (s, (p,s)) m, s), m)
reactMachineHist fsm initState initParam = proc (perception, ets) -> do
    rec
      (state, result) <- reactHistTransitions fsm initState initParam -< attach ets (oldParam, perception)
      param <- hold initParam -< snd (splitE result)
      oldParam <- iPre initParam -< param
    returnA -< ((state, param), if isEvent result then fst (fromEvent result) else while state (param, (perception, oldParam)))