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

后端 未结 11 1665
南笙
南笙 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

    The problem with your Haskell code is, that type only introduces a synonym, which is quite similar to what typedef in C does. One important restriction is, that the expansion of the type must be finite, you can't give a finite expansion of your state machine. A solution is using a newtype: A newtype is a wrapper that does only exist for the type checker; there is absolutely zero overhead (excluded stuff that occurs because of generalization that can't be removed). Here is your signature; it typechecks:

    newtype FN = FN { unFM :: (IO FN) }
    

    Please note, that whenever you want to use an FN, you have to unpack it first using unFN. Whenever you return a new function, use FN to pack it.

    0 讨论(0)
  • 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.

    0 讨论(0)
  • 2021-02-01 05:24

    Iit's not hard to make state machines in Haskell, once you realize that they are not monads! A state machine like the one you want is an arrow, an automaton arrow to be exact:

    newtype State a b = State (a -> (b, State a b))
    

    This is a function, which takes an input value and produces an output value along with a new version of itself. This is not a monad, because you cannot write join or (>>=) for it. Equivalently once you have turned this into an arrow you will realize that it's impossible to write an ArrowApply instance for it.

    Here are the instances:

    import Control.Arrow
    import Control.Category
    import Prelude hiding ((.), id)
    
    instance Category State where
        id = State $ \x -> (x, id)
    
        State f . State g =
            State $ \x ->
                let (y, s2) = g x
                    (z, s1) = f y
                in (z, s1 . s2)
    
    instance Arrow State where
        arr f = let s = State $ \x -> (f x, s) in s
        first (State f) =
            State $ \(x1, x2) ->
                let (y1, s) = f x1
                in ((y1, x2), first s)
    

    Have fun.

    0 讨论(0)
  • 2021-02-01 05:29

    You can get the same effect in C as in Python code,- just declare that functions return (void*):

    #include "stdio.h"
    
    typedef void* (*myFunc)(void);
    
    void* a(void);
    void* b(void);
    void* c(void);
    
    void* a(void) {
        printf("a()\n");
        return b;
    }
    
    void* b(void) {
        printf("b()\n");
        return c;
    }
    
    void* c(void) {
        printf("c()\n");
        return a;
    }
    
    void main() {
        void* state = a;
        while (1) {
            state = ((myFunc)state)();
            sleep(1);
        }
    }
    
    0 讨论(0)
  • 2021-02-01 05:29

    The Python code you posted will be transformed into a recursive function, but it will not be tail call optimized because Python has no tail call optimization, so it will stack overflow at some point. So the Python code is actually broken, and would take more work to get it as good as the Haskell or C versions.

    Here is an example of what I mean:

    so.py:

    import threading
    stack_size_bytes = 10**5
    threading.stack_size(10**5)
    machine_word_size = 4
    
    def t1():
        print "start t1"
        n = stack_size_bytes/machine_word_size
        while n:
            n -= 1
        print "done t1"
    
    def t2():
        print "start t2"
        n = stack_size_bytes/machine_word_size+1
        while n:
            n -= 1
        print "done t2"
    
    if __name__ == "__main__":
        t = threading.Thread(target=t1)
        t.start()
        t.join()
        t = threading.Thread(target=t2)
        t.start()
        t.join()
    

    shell:

    $ python so.py
    start t1
    done t1
    start t2
    Exception in thread Thread-2:
    Traceback (most recent call last):
      File "/usr/lib/python2.7/threading.py", line 530, in __bootstrap_inner
        self.run()
      File "/usr/lib/python2.7/threading.py", line 483, in run
        self.__target(*self.__args, **self.__kwargs)
      File "so.py", line 18, in t2
        print "done t2"
    RuntimeError: maximum recursion depth exceeded
    
    0 讨论(0)
提交回复
热议问题