{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-| Module : Mealstrom.FSM Description : Finite State Machine Definitions Copyright : (c) Max Amanshauser, 2016 License : MIT Maintainer : max@lambdalifting.org These defintions are concerned with the basic functions of finite state machines, keeping a memory and state transitions. -} module Mealstrom.FSM where import Data.Aeson import Data.Foldable (asum) import Data.Hashable (Hashable) import Data.Maybe (fromJust, fromMaybe) import Data.Text (Text) import Data.Time.Clock import Data.Typeable (Typeable) import qualified Data.UUID as UUID import Data.UUID (UUID) import Data.UUID.V4 import GHC.Generics type MachineTransformer s e a = Machine s e a -> IO (Machine s e a) -- |A data type that often comes in handy when describing whether -- updates have succeeded in the backend. data MealyStatus = MealyError | Pending | Done deriving (Eq, Show) -- |FSMs are uniquely identified by a type k, which must be convertible from/to Text. class (Hashable k, Eq k) => FSMKey k where toText :: k -> Text fromText :: Text -> k -- |This typeclass is needed to provide a constraint for the FSMStore abstraction. class (FSMKey k) => MealyInstance k s e a -- |A change in a FSM is either a (Step Timestamp oldState event newState Actions) -- or an increase in a counter. data Change s e a = Step UTCTime s e s [a] | Count Int deriving (Show) -- |Steps are equal to each other when they originated in the same state -- received the same event and ended up in the same state instance (Eq s, Eq e) => Eq (Change s e a) where (==) (Count a) (Count b) = a == b (==) (Step _ os1 e1 ns1 _) (Step _ os2 e2 ns2 _) = (os1 == os2) && (e1 == e2) && (ns1 == ns2) (==) (Count _) Step{} = False (==) Step{} (Count _) = False data Instance k s e a = Instance { key :: k, machine :: Machine s e a } deriving (Eq,Show,Generic,Typeable) data Machine s e a = Machine { inbox :: [Msg e], outbox :: [Msg a], committed :: [UUID], initState :: s, currState :: s, hist :: [Change s e a] } deriving (Eq,Show,Generic,Typeable) mkEmptyMachine :: s -> Machine s e a mkEmptyMachine s = Machine [] [] [] s s [] mkEmptyInstance :: k -> s -> Instance k s e a mkEmptyInstance k s = Instance k (mkEmptyMachine s) mkInstance :: k -> s -> [Msg e] -> Instance k s e a mkInstance k s es = Instance k ((mkEmptyMachine s) {inbox = es}) -- |Type of messages that are sent between FSMs -- Messages are always identified by UUID. -- The purpose of Msg is to attach a unique ID to an event, so that -- certain guarantees can be provided. data Msg e = Msg { msgID :: Maybe UUID, msgContents :: e } deriving (Show,Eq,Generic) mkMsg :: t -> IO (Msg t) mkMsg t = nextRandom >>= \i -> return $ Msg (Just i) t mkMsgs :: [t] -> IO [Msg t] mkMsgs = mapM mkMsg mkBogusMsg :: (Eq t) => t -> Msg t mkBogusMsg = Msg Nothing -- |Append a Change to a history. -- Identical steps are just counted, otherwise they are consed to the history. histAppend :: (Eq s, Eq e) => Change s e a -> [Change s e a] -> [Change s e a] histAppend s1 all@(Count i:s2:rest) | s1 == s2 = Count (i+1):s2:rest | otherwise = s1 : all histAppend s1 all@(s2:_rest) | s1 == s2 = Count 1 : all | otherwise = s1 : all histAppend s ss = s:ss -- ############## -- # JSON Codecs -- ############## instance (ToJSON s, ToJSON e, ToJSON a) => ToJSON (Change s e a) where toJSON (Count i) = object [ "count" .= toJSON i] toJSON (Step ts os ev ns as) = object [ "timestamp" .= toJSON ts, "old_state" .= toJSON os, "event" .= toJSON ev, "new_state" .= toJSON ns, "actions" .= toJSON as ] instance (FromJSON s, FromJSON e, FromJSON a) => FromJSON (Change s e a) where parseJSON = withObject "Change" $ \o -> asum [ Count <$> o .: "count", Step <$> o .: "timestamp" <*> o .: "old_state" <*> o .: "event" <*> o .: "new_state" <*> o .: "actions" ] -- Other Instances instance FSMKey Text where toText = id fromText = id instance FSMKey UUID where toText = UUID.toText fromText a = fromMaybe (error "Conversion from UUID failed") (UUID.fromText a)