26

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

静的に型付けされた言語でステート マシンを作成するのが難しいのはなぜですか? 静的に型付けされた言語でも不要なオーバーヘッドを作成する必要があります。動的に型付けされた言語には、この問題はありません。静的に型付けされた言語でそれを行う簡単な方法はありますか?

4

11 に答える 11

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その後、いくつかの状態を忘れたものについてコンパイラの警告を受け取ることができます。

于 2011-10-08T22:26:43.083 に答える
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:

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
于 2011-10-08T22:30:48.077 に答える
11

Haskellコードの問題は、Cの場合typeと非常によく似た同義語しか導入されないことです。typedef重要な制限の1つは、型の拡張は有限でなければならないということです。ステートマシンの拡張を有限にすることはできません。解決策は次のnewtypeとおりです。Anewtypeは、型チェッカーにのみ存在するラッパーです。オーバーヘッドは絶対にありません(削除できない一般化のために発生するものを除外します)。これがあなたの署名です。タイプチェック:

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

FNを使用する場合は、最初にを使用して解凍する必要があることに注意してくださいunFN。新しい関数を返すときはいつでも、FNそれをパックするために使用してください。

于 2011-10-08T22:07:05.140 に答える
11

C ライクなタイプのシステムでは、関数は一次市民ではありません。取り扱いには一定の制限があります。これは、実装/実行のシンプルさとスピードのための決定でした。関数をオブジェクトのように動作させるには、通常、クロージャーのサポートが必要です。ただし、これらはほとんどのプロセッサの命令セットでは当然サポートされていません。C は金属に近いように設計されていたため、サポートはありませんでした。

C で再帰構造を宣言する場合、型は完全に展開可能でなければなりません。この結果、構造体宣言ではポインターを自己参照としてのみ持つことができます。

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

また、使用するすべての識別子を宣言する必要があります。関数型の制限の 1 つは、関数型を前方宣言できないことです。

C のステート マシンは、通常、switch ステートメントまたはジャンプ テーブルのいずれかで、整数から関数へのマッピングを作成することによって機能します。

typedef int (*func_t)();

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

    int state = 0;

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

または、Python コードをプロファイリングして、コードが遅い理由を見つけようとすることもできます。重要な部分を C/C++ に移植し、ステート マシンに Python を使用し続けることができます。

于 2011-10-08T21:57:34.043 に答える
6

いつものように、素晴らしい答えがすでに出ているにもかかわらず、私は自分で試してみることに抵抗できませんでした。表示される内容について気になったことの 1 つは、入力が無視されることです。ステート マシン (私がよく知っているマシン) は、入力に基づいてさまざまな遷移を選択します。

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へと移動していることがはっきりとわかります。もう少し複雑なマシンを作ってみましょう:stateBstateCstateA

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 コードがいかに記述的であるかも示していると思います。非常に冗長ですが、ステート マシン図のノードを表す方法も非常に単純です。

于 2011-10-09T04:45:31.967 に答える
4

ステートマシンがモナドではないことに気づいたら、Haskellでステートマシンを作るのは難しくありません!必要なステートマシンは矢印、正確にはオートマトン矢印です。

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

これは、入力値を受け取り、それ自体の新しいバージョンとともに出力値を生成する関数です。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)

楽しむ。

于 2011-10-12T10:01:24.040 に答える
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);
    }
}
于 2011-10-08T23:56:44.237 に答える
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メンバーの構造体はそのメンバーと同一であるため、「オーバーヘッド」はありませんが、「醜い」です。)

言及したいもう 1 つの例は、Go (別の静的型付け言語) です。Go では、typeコンストラクトは新しい型を定義します。typedefこれは ( C やHaskellのような) 単純なエイリアスではありませんが、( typeHaskell のような) 本格的な新しい型を作成します。そのようなnewtype型には、定義できるメソッドの独立した「メソッド セット」があるためです。このため、型定義は再帰的になる可能性があります。

type Fn func () Fn
于 2011-10-09T13:20:38.810 に答える
2

あなたの問題は以前にありました:Cでの関数ポインタの再帰的宣言

Herb SutterがGotW#57:Recursive Declarationsで説明しているように、C ++演算子のオーバーロードを使用して、CおよびHaskellソリューションと本質的に同じもののメカニズムを隠すことができます。

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

于 2011-10-08T22:15:49.477 に答える
1

F# での例:

type Cont = Cont of (unit -> Cont)

let rec a() =
    printfn "a()"
    連続 (楽しい () -> b 42)

そして bn =
    printfn "b(%d)" n
    続き

そして c() =
    printfn "c()"
    続き

let rec run (Cont f) =
    レット f = f()
    走る

実行 (続き)

「静的に型付けされた言語で関数を使用してステート マシンを実装するのがなぜそんなに難しいのですか?」という質問について: それは、 の型とそのa仲間が少し奇妙だからです。関数...

私の例から Cont を削除すると、F# コンパイラは文句を言い、次のように言います。

'a を期待していますが、指定された単位 -> 'a. 'a と unit -> 'a を統合すると、結果の型は無限になります。

別の回答は、型推論が Cont を宣言する必要性を取り除くのに十分強力な OCaml のソリューションを示しています。これは、静的型付けが原因ではなく、多くの静的型付け言語で強力な型推論が欠如していることを示しています。

なぜ F# がそれを行わないのかはわかりませんが、おそらくこれにより、型推論アルゴリズムがより複雑になり、遅くなるか、「強力すぎる」ようになると思います (正しく型付けされていない式の型を推論することができ、後で、理解しにくいエラー メッセージが表示されます)。

あなたが与えたPythonの例は、実際には安全ではないことに注意してください. 私の例でbは、整数によってパラメーター化された状態のファミリーを表します。型付けされていない言語では、コードが実行されるまで、間違いを犯して正しいラムダの代わりに返すbか、その間違いを見逃す可能性があります。b 42

于 2011-10-11T09:07:08.333 に答える
-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
于 2011-10-08T23:10:30.957 に答える