使用静态类型语言清理和类型安全的状态机实现?

时间:2011-10-08 21:34:50

标签: python c language-agnostic haskell typing

我在Python中实现了一个简单的状态机:

import time

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

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

def c():
    print "c()"
    return a


if __name__ == "__main__":
    state = a
    while True:
        state = state()
        time.sleep(1)

我想把它移植到C,因为它不够快。但是C不允许我创建一个返回相同类型函数的函数。我尝试使用这种类型的函数:typedef *fn(fn)(),但它不起作用,所以我不得不使用结构。现在代码非常难看!

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

typedef struct fn {
    struct fn (*f)(void);
} fn_t;

fn_t a(void);
fn_t b(void);
fn_t c(void);

fn_t a(void)
{
    fn_t f = {b};

    (void)printf("a()\n");

    return f;
}

fn_t b(void)
{
    fn_t f = {c};

    (void)printf("b()\n");

    return f;
}

fn_t c(void)
{
    fn_t f = {a};

    (void)printf("c()\n");

    return f;
}

int main(void)
{
    fn_t state = {a};

    for(;; (void)sleep(1)) state = state.f();

    return EXIT_SUCCESS;
}

所以我认为这是C的破碎型系统的问题。所以我使用了一种真实类型系统(Haskell)的语言,但同样的问题也发生了。我不能只做这样的事情:

type Fn = IO Fn
a :: Fn
a = print "a()" >> return b
b :: Fn
b = print "b()" >> return c
c :: Fn
c = print "c()" >> return a

我收到错误Cycle in type synonym declarations

所以我必须像对待C代码一样制作一些包装器:

import Control.Monad
import System.Posix

data Fn = Fn (IO Fn)

a :: IO Fn
a = print "a()" >> return (Fn b)

b :: IO Fn
b = print "b()" >> return (Fn c)

c :: IO Fn
c = print "c()" >> return (Fn a)

run = foldM (\(Fn f) () -> sleep 1 >> f) (Fn a) (repeat ())

为什么用静态类型语言制作状态机真是太难了?我也必须在静态类型语言中进行不必要的开销。动态类型语言没有这个问题。是否有更简单的方法以静态类型语言执行此操作?

11 个答案:

答案 0 :(得分:22)

在Haskell中,这个习惯就是继续执行下一个状态:

type StateMachine = IO ()
a, b, c :: StateMachine
a = print "a()" >> b
b = print "b()" >> c
c = print "c()" >> a

你不必担心这会溢出堆栈或类似的东西。如果您坚持使用状态,那么您应该更明确地使数据类型:

data PossibleStates = A | B | C
type StateMachine = PossibleStates -> IO PossibleStates
machine A = print "a()" >> return B
machine B = print "b()" >> return C
machine C = print "c()" >> return A

然后,您可以获得有关忘记某些状态的任何StateMachine的编译器警告。

答案 1 :(得分:15)

如果您使用newtype代替data,则不会产生任何开销。此外,您可以在定义点处包装每个状态的函数,因此使用它们的表达式不必:

import Control.Monad

newtype State = State { runState :: IO State }

a :: State
a = State $ print "a()" >> return b

b :: State
b = State $ print "b()" >> return c

c :: State
c = State $ print "c()" >> return a

runMachine :: State -> IO ()
runMachine s = runMachine =<< runState s

main = runMachine a

编辑:让我感到震惊的是runMachine有更一般的形式; iterate的monadic版本:

iterateM :: Monad m => (a -> m a) -> a -> m [a]
iterateM f a = do { b <- f a
                  ; as <- iterateM f b
                  ; return (a:as)
                  }

main = iterateM runState a

编辑:嗯,iterateM导致空间泄漏。也许iterateM_会更好。

iterateM_ :: Monad m => (a -> m a) -> a -> m ()
iterateM_ f a = f a >>= iterateM_ f

main = iterateM_ runState a

编辑:如果要通过状态机线程某些状态,可以对State使用相同的定义,但将状态函数更改为:

a :: Int -> State
a i = State $ do{ print $ "a(" ++ show i ++ ")"
                ; return $ b (i+1)
                }

b :: Int -> State
b i = State $ do{ print $ "b(" ++ show i ++ ")"
                ; return $ c (i+1)
                }

c :: Int -> State
c i = State $ do{ print $ "c(" ++ show i ++ ")"
                ; return $ a (i+1)
                }

main = iterateM_ runState $ a 1

答案 2 :(得分:11)

在类C系统中,功能不是一阶公民。处理它们有一定的限制。这是对简单性和执行/执行速度的决定。为了使函数像对象一样,通常需要支持闭包。然而,大多数处理器的指令集自然不支持这些。由于C设计为接近金属,因此无法支持它们。

在C中声明递归结构时,类型必须是完全可扩展的。这样做的结果是,您只能在struct声明中将指针作为自引用:

struct rec;
struct rec {
    struct rec *next;
};

我们使用的每个标识符也必须声明。函数类型的一个限制是,无法转发声明它们。

C中的状态机通常通过在switch语句或跳转表中进行从整数到函数的映射来工作:

typedef int (*func_t)();

void run() {
    func_t table[] = {a, b, c};

    int state = 0;

    while(True) {
        state = table[state]();
    }
}

或者您可以profile your Python code并尝试找出代码缓慢的原因。您可以将关键部分移植到C / C ++并继续使用Python作为状态机。

答案 3 :(得分:11)

你的Haskell代码的问题是,type只引入了同义词,这与C中的typedef非常相似。一个重要的限制是,类型的扩展必须是有限的,你不能给你的状态机有限扩展。解决方案是使用newtypenewtype是一个仅对类型检查器存在的包装器;绝对零开销(由于无法删除的泛化而导致排除的东西)。这是你的签名;它是一个类型:

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

请注意,只要您想使用FN,就必须先使用unFN将其解包。每当您返回新功能时,请使用FN将其打包。

答案 4 :(得分:6)

像往常一样,尽管已经有了很好的答案,但我忍不住为自己尝试。令我困惑的一件事是它忽略了输入。状态机 - 我熟悉的 - 根据输入在各种可能的转换之间进行选择。

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)

这里我定义了一个类型State,它由词汇表类型vocab参数化。现在让我们定义一种方法,通过提供输入来跟踪状态机的执行。

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

现在让我们在一台忽略其输入的简单机器上试一试。请注意:我选择的格式相当冗长。但是,后面的每个函数都可以被视为状态机图中的一个节点,我认为你会发现详细程度与描述这样一个节点完全相关。我使用stateId以字符串格式编码一些有关该状态行为的可视信息。

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
               }

由于输入对此状态机无关紧要,因此您可以将其输入任何内容。

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

我不会包含输出,这也非常详细,但您可以看到它明确地从stateA移动到stateBstateC并返回到stateA再次。现在让我们制作一个稍微复杂的机器:

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. :)"

每个节点的可能输入和转换不需要进一步解释,因为它们在代码中详细描述。我会让你自己玩traceMachine lessSipmleMachine inputs。查看当inputs无效时(不符合“可能的输入”限制),或者在输入结束前点击目标节点时会发生什么。

我认为我的解决方案的冗长程度有点像你基本上要求的那样,这是为了减少这个问题。但我认为它也说明了Haskell代码的描述。即使它非常冗长,它在表示状态机图的节点方面也非常简单。

答案 5 :(得分:4)

一旦你意识到他们 monad,我就不难在Haskell中创建状态机!像你想要的状态机是一个箭头,确切地说是一个自动机箭头:

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

这是一个函数,它接受一个输入值并产生一个输出值以及它自身的新版本。这不是monad,因为您无法为其编写join(>>=)。等效地,一旦你把它变成箭头,你就会意识到为它写一个ArrowApply实例是不可能的。

以下是实例:

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)

玩得开心。

答案 6 :(得分:3)

您可以在C中获得与Python代码相同的效果, - 只需声明函数返回(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);
    }
}

答案 7 :(得分:3)

你想要的是递归类型。不同的语言有不同的方法。

例如,在OCaml(一种静态类型语言)中,有一个可选的编译器/解释器标志-rectypes,它支持递归类型,允许你定义这样的东西:

let rec a () = print_endline "a()"; b
and b () = print_endline "b()"; c
and c () = print_endline "c()"; a
;;

虽然你在C示例中抱怨这不是“丑陋”,但下面的仍然是相同的。编译器只是为你而烦恼,而不是强迫你把它写出来。

正如其他人所指出的那样,在Haskell中你可以使用newtype并且不会有任何“开销”。但你抱怨必须明确地包装和解包递归类型,这是“丑陋的”。 (与您的C示例类似;没有“开销”,因为在机器级别,1成员结构与其成员相同,但它“丑陋”。)

我想提到的另一个例子是Go(另一种静态类型的语言)。在Go中,type构造定义了一种新类型。它不是一个简单的别名(如C中的typedef或Haskell中的type),但会创建一个完整的新类型(如Haskell中的newtype),因为这种类型具有独立性您可以在其上定义的方法的“方法集”。因此,类型定义可以是递归的:

type Fn func () Fn

答案 8 :(得分:2)

您之前遇到的问题是:Recursive declaration of function pointer in C

C ++运算符重载可用于隐藏与C和Haskell解决方案基本相同的机制,正如Herb Sutter在GotW #57: Recursive Declarations中所述。

struct FuncPtr_;
typedef FuncPtr_ (*FuncPtr)();

struct FuncPtr_
{
  FuncPtr_( FuncPtr pp ) : p( pp ) { }
  operator FuncPtr() { return p; }
  FuncPtr p;
};

FuncPtr_ f() { return f; } // natural return syntax

int main()
{
  FuncPtr p = f();  // natural usage syntax
  p();
}

但是这项具有功能的业务很可能会比使用数字状态的业务表现更差。您应该使用switch语句或状态表,因为在这种情况下您真正想要的是与goto等效的结构化语义。

答案 9 :(得分:1)

F#中的一个例子:

type Cont = Cont of (unit -> Cont)

let rec a() =
    printfn "a()"
    Cont (fun () -> b 42)

and b n =
    printfn "b(%d)" n
    Cont c

and c() =
    printfn "c()"
    Cont a

let rec run (Cont f) =
    let f = f()
    run f

run (Cont a)

关于“为什么使用静态类型语言中的函数实现状态机这么难?”:这是因为a和朋友的类型有点奇怪:返回时的函数返回函数的函数返回函数...

如果我从我的示例中删除Cont,F#编译器会抱怨并说:

Expecting 'a but given unit -> 'a. The resulting type would be infinite when unifying 'a and unit -> 'a.

另一个答案显示了OCaml中的解决方案,其类型推断足够强大,无需声明Cont,这表明静态类型不应该受到指责,而是在许多静态类型语言中缺乏强大的类型推断。

我不知道为什么F#没有这样做,我猜想这可能会使类型推断算法更复杂,更慢或“太强大”(它可以设法推断错误输入表达式的类型,稍后失败,给出难以理解的错误消息。

请注意,您提供的Python示例并不安全。在我的示例中,b表示由整数参数化的状态族。在非类型语言中,很容易出错并返回bb 42而不是正确的lambda,并在执行代码之前错过该错误。

答案 10 :(得分:-6)

你发布的Python代码将被转换为递归函数,但它不会被尾调用优化,因为Python没有尾调用优化,因此它会在某些时候堆栈溢出。因此,Python代码实际上已被破坏,并且需要做更多工作才能使其与Haskell或C版本一样好。

这是我的意思的一个例子:

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()

壳:

$ 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