我在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 ())
为什么用静态类型语言制作状态机真是太难了?我也必须在静态类型语言中进行不必要的开销。动态类型语言没有这个问题。是否有更简单的方法以静态类型语言执行此操作?
答案 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
非常相似。一个重要的限制是,类型的扩展必须是有限的,你不能给你的状态机有限扩展。解决方案是使用newtype
:newtype
是一个仅对类型检查器存在的包装器;绝对零开销(由于无法删除的泛化而导致排除的东西)。这是你的签名;它是一个类型:
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
移动到stateB
到stateC
并返回到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
表示由整数参数化的状态族。在非类型语言中,很容易出错并返回b
或b 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