Clean and type-safe state machine implementation in a statically typed language?

后端 未结 11 1710
南笙
南笙 2021-02-01 04:26

I implemented a simple state machine in Python:

import time

def a():
    print \"a()\"
    return b

def b():
    print \"b()\"
    return c

def c():
    print         


        
11条回答
  •  无人共我
    2021-02-01 05:24

    As usual, despite the great answers already present, I couldn't resist trying it out for myself. One thing that bothered me about what is presented is that it ignores input. State machines--the ones that I am familiar with--choose between various possible transitions based on input.

    data State vocab = State { stateId :: String
                             , possibleInputs :: [vocab]
                             , _runTrans :: (vocab -> State vocab)
                             }
                          | GoalState { stateId :: String }
    
    instance Show (State a) where
      show = stateId
    
    runTransition :: Eq vocab => State vocab -> vocab -> Maybe (State vocab)
    runTransition (GoalState id) _                   = Nothing
    runTransition s x | x `notElem` possibleInputs s = Nothing
                      | otherwise                    = Just (_runTrans s x)
    

    Here I define a type State, which is parameterized by a vocabulary type vocab. Now let's define a way that we can trace the execution of a state machine by feeding it inputs.

    traceMachine :: (Show vocab, Eq vocab) => State vocab -> [vocab] -> IO ()
    traceMachine _ [] = putStrLn "End of input"
    traceMachine s (x:xs) = do
      putStrLn "Current state: "
      print s
      putStrLn "Current input: "
      print x
      putStrLn "-----------------------"
      case runTransition s x of
        Nothing -> putStrLn "Invalid transition"
        Just s' -> case s' of
          goal@(GoalState _) -> do
            putStrLn "Goal state reached:"
            print s'
            putStrLn "Input remaining:"
            print xs
          _ -> traceMachine s' xs
    

    Now let's try it out on a simple machine that ignores its inputs. Be warned: the format I have chosen is rather verbose. However, each function that follows can be viewed as a node in a state machine diagram, and I think you'll find the verbosity to be completely relevant to describing such a node. I've used stateId to encode in string format some visual information about how that state behaves.

    data SimpleVocab = A | B | C deriving (Eq, Ord, Show, Enum)
    
    simpleMachine :: State SimpleVocab
    simpleMachine = stateA
    
    stateA :: State SimpleVocab
    stateA = State { stateId = "A state. * -> B"
                   , possibleInputs = [A,B,C]
                   , _runTrans = \_ -> stateB
                   }
    
    stateB :: State SimpleVocab
    stateB = State { stateId = "B state. * -> C"
                   , possibleInputs = [A,B,C]
                   , _runTrans = \_ -> stateC
                   }
    
    stateC :: State SimpleVocab
    stateC = State { stateId = "C state. * -> A"
                   , possibleInputs = [A,B,C]
                   , _runTrans = \_ -> stateA
                   }
    

    Since the inputs don't matter for this state machine, you can feed it anything.

    ghci> traceMachine simpleMachine [A,A,A,A]
    

    I won't include the output, which is also very verbose, but you can see it clearly moves from stateA to stateB to stateC and back to stateA again. Now let's make a slightly more complicated machine:

    lessSimpleMachine :: State SimpleVocab
    lessSimpleMachine = startNode
    
    startNode :: State SimpleVocab
    startNode = State { stateId = "Start node. A -> 1, C -> 2"
                      , possibleInputs = [A,C]
                      , _runTrans = startNodeTrans
                      }
      where startNodeTrans C = node2
            startNodeTrans A = node1
    
    node1 :: State SimpleVocab
    node1 = State { stateId = "node1. B -> start, A -> goal"
                  , possibleInputs = [B, A]
                  , _runTrans = node1trans
                  }
      where node1trans B = startNode
            node1trans A = goalNode
    
    node2 :: State SimpleVocab
    node2 = State { stateId = "node2. C -> goal, A -> 1, B -> 2"
                  , possibleInputs = [A,B,C]
                  , _runTrans = node2trans
                  }
      where node2trans A = node1
            node2trans B = node2
            node2trans C = goalNode
    
    goalNode :: State SimpleVocab
    goalNode = GoalState "Goal. :)"
    

    The possible inputs and transitions for each node should require no further explanation, as they are verbosely described in the code. I'll let you play with traceMachine lessSipmleMachine inputs for yourself. See what happens when inputs is invalid (does not adhere to the "possible inputs" restrictions), or when you hit a goal node before the end of input.

    I suppose the verbosity of my solution sort of fails what you were basically asking, which was to cut down on the cruft. But I think it also illustrates how descriptive Haskell code can be. Even though it is very verbose, it is also very straightforward in how it represents nodes of a state machine diagram.

提交回复
热议问题