web-dev-qa-db-ja.com

ステートマシンの使用

プログラミングのどの領域でステートマシンを使用しますか?どうして ?どうすれば実装できますか?

編集:質問しすぎない場合は、実用的な例を提供してください。

65
Geo

プログラミングのどの領域でステートマシンを使用しますか?

状態マシンを使用して、限られた数の条件( "states")で存在できる(実または論理)オブジェクトを表し、一連の固定ルールに従って1つの状態から次の状態に進みます。

なぜステートマシンを使用するのですか?

状態機械は、複雑なルールと条件のセットを表し、さまざまな入力を処理するためのveryコンパクトな方法であることがよくあります。メモリが限られている組み込みデバイスのステートマシンが表示されます。適切に実装された状態マシンは、各論理状態が物理的な状態を表すため、自己文書化されます。ステートマシンは、手続き型の同等のものと比較してtiny量のコードで具体化でき、非常に効率的に実行されます。さらに、状態変化を管理するルールは、多くの場合、テーブルにデータとして保存できるため、簡単に維持できるコンパクトな表現が提供されます。

どうすれば実装できますか?

簡単な例:

_enum states {      // Define the states in the state machine.
  NO_PIZZA,        // Exit state machine.
  COUNT_PEOPLE,    // Ask user for # of people.
  COUNT_SLICES,    // Ask user for # slices.
  SERVE_PIZZA,     // Validate and serve.
  EAT_PIZZA        // Task is complete.
} STATE;

STATE state = COUNT_PEOPLE;
int nPeople, nSlices, nSlicesPerPerson;

// Serve slices of pizza to people, so that each person gets
/// the same number of slices.   
while (state != NO_PIZZA)  {
   switch (state)  {
   case COUNT_PEOPLE:  
       if (promptForPeople(&nPeople))  // If input is valid..
           state = COUNT_SLICES;       // .. go to next state..
       break;                          // .. else remain in this state.
   case COUNT_SLICES:  
       if (promptForSlices(&nSlices))
          state = SERVE_PIZZA;
        break;
   case SERVE_PIZZA:
       if (nSlices % nPeople != 0)    // Can't divide the pizza evenly.
       {                             
           getMorePizzaOrFriends();   // Do something about it.
           state = COUNT_PEOPLE;      // Start over.
       }
       else
       {
           nSlicesPerPerson = nSlices/nPeople;
           state = EAT_PIZZA;
       }
       break;
   case EAT_PIZZA:
       // etc...
       state = NO_PIZZA;  // Exit the state machine.
       break;
   } // switch
} // while
_

メモ:

  • この例では、簡単にするために、明示的なcase/break状態のswitch()を使用しています。実際には、caseは次の状態に「フォールスルー」することがよくあります。

  • 大規模なステートマシンの保守を容易にするために、各caseで実行される作業を「ワーカー」関数にカプセル化できます。 while()の先頭にある入力を取得し、それをワーカー関数に渡し、ワーカーの戻り値をチェックして次の状態を計算します。

  • コンパクトにするために、switch()全体を関数ポインタの配列で置き換えることができます。各状態は、戻り値が次の状態へのポインタである関数によって具体化されます。 警告:これは、ステートマシンを単純化するか、完全にメンテナンス不能にする可能性があるため、実装を慎重に検討してください。

  • 組み込みデバイスは、致命的なエラーでのみ終了するステートマシンとして実装できます。その後、ハードリセットを実行して、ステートマシンに再び入ります。

81
Adam Liss

すでにいくつかの素晴らしい答え。少し異なる見方については、より大きな文字列のテキストを検索することを検討してください。誰かがすでに正規表現について言及していますが、これは重要ではありますが、実際には特別なケースです。

次のメソッド呼び出しについて考えてみます。

very_long_text = "Bereshit bara Elohim et hashamayim ve'et ha'arets." …
Word = "Elohim"
position = find_in_string(very_long_text, Word)

find_in_stringをどのように実装しますか?簡単な方法は、次のようなネストされたループを使用することです。

for i in 0 … length(very_long_text) - length(Word):
    found = true
    for j in 0 … length(Word):
        if (very_long_text[i] != Word[j]):
            found = false
            break
    if found: return i
return -1

これが非効率であるという事実は別として、状態マシンを形成する!ここの州はやや隠されています。コードを少し書き換えて、見やすくします。

state = 0
for i in 0 … length(very_long_text) - length(Word):
    if very_long_text[i] == Word[state]:
        state += 1
        if state == length(Word) + 1: return i
    else:
        state = 0
return -1

ここでのさまざまな状態は、検索するWord内のすべてのさまざまな位置を直接表します。グラフの各ノードには2つの遷移があります。文字が一致する場合は、次の状態に進みます。その他すべての入力(つまり、現在の位置にあるその他すべての文字)については、ゼロに戻ります。

このわずかな再編成には大きな利点があります。いくつかの基本的なテクニックを使用して、パフォーマンスを向上させるために微調整できるようになりました。実際、すべての高度な文字列検索アルゴリズム(現時点ではインデックスデータ構造を割引)は、この状態マシンの上に構築されており、そのいくつかの側面を改善しています。

22
Konrad Rudolph

どのようなタスクですか?

私が見てきたこと以外のあらゆるタスクは、あらゆる種類の構文解析がステートマシンとして頻繁に実装されます。

なぜ?

文法の解析は、通常、簡単な作業ではありません。設計段階では、構文解析アルゴリズムをテストするために状態図が作成されることがよくあります。これをステートマシン実装に変換することは、かなり単純な作業です。

どうやって?

まあ、あなたはあなたの想像力によってのみ制限されます。

caseステートメントとループ でそれを実行したことを確認しました。

labels and goto ステートメントを使用してそれを実行したことを確認しました

私はそれが現在の状態を表す関数ポインタの構造で行われることさえ見ました。状態が変化すると、1つ以上の 関数ポインタ が更新されます。

私はそれがコードのみで行われるのを見てきましたが、状態の変化は単にコードの別のセクションで実行していることを意味します。 (状態変数はなく、必要に応じて冗長なコード。これは非常に単純な並べ替えとして示すことができます。これは非常に小さなデータセットにのみ役立ちます。

int a[10] = {some unsorted integers};

not_sorted_state:;
    z = -1;
    while (z < (sizeof(a) / sizeof(a[0]) - 1)
    {
        z = z + 1
        if (a[z] > a[z + 1])
        {
            // ASSERT The array is not in order
            swap(a[z], a[z + 1];        // make the array more sorted
            goto not_sorted_state;      // change state to sort the array
        }
    }
    // ASSERT the array is in order

状態変数はありませんが、コード自体が状態を表します

13
EvilTeach

State 設計パターンは、有限状態機械を使用してオブジェクトの状態を表すオブジェクト指向の方法です。それは通常、そのオブジェクトの実装の論理的な複雑さを軽減するのに役立ちます(ネストされている場合、多くのフラグなど)。

8

ほとんどのワークフローは、ステートマシンとして実装できます。たとえば、休暇申請や注文の処理などです。

.NETを使用している場合は、Windows Workflow Foundationを試してください。ステートマシンワークフローを非常に迅速に実装できます。

6
Maxam

C#を使用している場合、イテレーターブロックを作成するときはいつでも、コンパイラーにステートマシンを構築するように要求します(イテレーターのどこにいるかなどを追跡します)。

4
Jon Skeet

以下は、テスト済みで動作する状態マシンの例です。シリアルストリームを使用しているとします(シリアルポート、tcp/ipデータ、またはファイルが典型的な例です)。この場合、同期、長さ、ペイロードの3つの部分に分割できる特定のパケット構造を探しています。私は3つの状態があります。1つはアイドル、同期待ち、2つ目は適切な同期、次のバイトは長さ、3つ目はペイロードの蓄積です。

この例は、バッファが1つしかない純粋なシリアルです。ここに記述されているように、不良バイトまたはパケットから回復し、パケットを破棄する可能性がありますが、最終的に回復します。スライディングウィンドウなどの他のことを実行して、すぐに回復できます。これは、部分的なパケットが短くなり、新しい完全なパケットが始まると言うところです。以下のコードはこれを検出せず、部分的なパケットとパケット全体を破棄し、次のパケットから回復します。パケット全体を処理する必要がある場合は、スライディングウィンドウを使用すると、ウィンドウを節約できます。

シリアルデータストリーム、tcp/ip、ファイルi/oなど、この種類のステートマシンを常に使用しています。または、tcp/ipプロトコル自体、たとえば、電子メールを送信し、ポートを開き、サーバーが応答を送信するのを待つ、HELOを送信する、サーバーがパケットを送信するのを待つ、パケットを送信する、応答を待つ、など。基本的にその場合と以下の場合では、次のバイト/パケットの着信を待機している可能性があります。待機していたものを思い出すために、使用できるものを待機するコードを再利用します。状態変数。ステートマシンがロジックで使用されるのと同じ方法(次のクロックを待つ、何を待っていたか)。

ロジックの場合と同様に、状態ごとに異なる処理を実行することができます。この場合、同期パターンが適切であれば、オフセットをストレージにリセットし、チェックサムアキュムレータをリセットします。パケット長の状態は、通常の制御パスを打ち切りたい場合を示しています。すべてではありませんが、実際には、多くのステートマシンがジャンプしたり、通常のパス内でループしたりする可能性があります。

これがお役に立てば幸いです。また、ステートマシンがソフトウェアでより多く使用されることを願っています。

テストデータには、ステートマシンが回復する意図的な問題があります。最初の正常なパケット、不正なチェックサムのパケット、および無効な長さのパケットの後にいくつかのガベージデータがあります。私の出力は:

正常なパケット:FA0712345678EB無効な同期パターン0x12無効な同期パターン0x34無効な同期パターン0x56チェックサムエラー0xBF無効なパケット長0無効な同期パターン0x12無効な同期パターン0x34無効な同期パターン0x78無効な同期パターン0xEB正常なパケット:FA081234567800EAデータ

不正なデータにもかかわらず、ストリーム内の2つの適切なパケットが抽出されました。そして、不正なデータが検出されて処理されました。

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

unsigned char testdata[] =
{
    0xFA,0x07,0x12,0x34,0x56,0x78,0xEB,  
    0x12,0x34,0x56,  
    0xFA,0x07,0x12,0x34,0x56,0x78,0xAA,  
    0xFA,0x00,0x12,0x34,0x56,0x78,0xEB,  
    0xFA,0x08,0x12,0x34,0x56,0x78,0x00,0xEA  
};

unsigned int testoff=0;

//packet structure  
// [0] packet header 0xFA  
// [1] bytes in packet (n)  
// [2] payload  
// ... payload  
// [n-1] checksum  
//  

unsigned int state;

unsigned int packlen;  
unsigned int packoff;  
unsigned char packet[256];  
unsigned int checksum;  

int process_packet( unsigned char *data, unsigned int len )  
{  
    unsigned int ra;  

    printf("good packet:");
    for(ra=0;ra<len;ra++) printf("%02X",data[ra]);
    printf("\n");
}  
int getbyte ( unsigned char *d )  
{  
    //check peripheral for a new byte  
    //or serialize a packet or file  

    if(testoff<sizeof(testdata))
    {
        *d=testdata[testoff++];
        return(1);
    }
    else
    {
        printf("no more test data\n");
        exit(0);
    }
    return(0);
}

int main ( void )  
{  
    unsigned char b;

    state=0; //idle

    while(1)
    {
        if(getbyte(&b))
        {
            switch(state)
            {
                case 0: //idle
                    if(b!=0xFA)
                    {
                        printf("Invalid sync pattern 0x%02X\n",b);
                        break;
                    }
                    packoff=0;
                    checksum=b;
                    packet[packoff++]=b;

                    state++;
                    break;
                case 1: //packet length
                    checksum+=b;
                    packet[packoff++]=b;

                    packlen=b;
                    if(packlen<3)
                    {
                        printf("Invalid packet length %u\n",packlen);
                        state=0;
                        break;
                    }

                    state++;
                    break;
                case 2: //payload
                    checksum+=b;
                    packet[packoff++]=b;

                    if(packoff>=packlen)
                    {
                        state=0;
                        checksum=checksum&0xFF;
                        if(checksum)
                        {
                            printf("Checksum error 0x%02X\n",checksum);
                        }
                        else
                        {
                            process_packet(packet,packlen);
                        }
                    }
                    break;
            }
        }

        //do other stuff, handle other devices/interfaces

    }
}
4
old_timer

ステートマシンはいたるところにあります。ステートマシンは、メッセージが受信されるときにメッセージを解析する必要がある通信インターフェイスの重要な要素です。また、組み込みシステムの開発では、厳しいタイミング制約のために、タスクを複数のタスクに分割する必要が何度もありました。

2
Nate

QAインフラストラクチャ。スクリーンスクレイピングやテスト対象のプロセスの実行を目的としています。 (これは私の特別な経験の領域です。Pythonでステートマシンフレームワークをビルドしました。現在の状態をスタックにプッシュし、さまざまな方法で状態ハンドラーを選択して使用するためのサポートを提供しています。すべてのTTYベースの画面スクレイパーで使用できます。概念モデルは、TTYアプリケーションを実行する場合と同様に、限られた数の既知の状態を通過し、古い状態に戻すことができます(ネストされたメニューの使用を検討してください)。これは(上記の雇用者の許可を得て)リリースされています Bazaar を使用してチェックアウトしてくださいhttp://web.dyfis.net/bzr/isg_state_machine_framework/コードを確認したい場合。

チケット、プロセス管理、ワークフローシステム-チケットに、新規、トリアージ、進行中、ニードQA、フェイルドQA、検証済みなどの間の移動を決定する一連のルールがある場合、単純な状態機械。

小さくて容易に証明可能な組み込みシステムの構築-信号機の信号は、すべての可能な状態のリストhasが完全に列挙されて知られている重要な例です。

パーサーとレクサーは、状態マシンに大きく依存しています。これは、ストリーミングの対象が決定される方法が、その時点での場所に基づいているためです。

2
Charles Duffy

ここでは、実際に使用されている理由を説明するものは何もありませんでした。

実用的な目的のために、プログラマーは通常、操作の途中でスレッド/出口を強制的に戻すときに1を追加する必要があります。

たとえば、マルチステートHTTPリクエストがある場合、次のようなサーバーコードが存在する可能性があります。

Show form 1
process form 1
show form 2
process form 2

問題は、コードがすべて論理的に流れ、同じ変数を使用する場合でも、フォームを表示するたびに、サーバー上のスレッド全体(ほとんどの言語)を終了する必要があることです。

コードにブレークを入れてスレッドを返すという行為は、通常、switchステートメントで行われ、いわゆるステートマシン(Very Basicバージョン)を作成します。

より複雑になると、どの状態が有効であるかを理解することが非常に難しくなる可能性があります。人々は通常、「 状態遷移表 」を定義して、すべての状態遷移を記述します。

私は state machine library を書きました。主な概念は、実際に状態遷移表を直接実装できるということです。それは本当にきちんとした演習でしたが、どれだけうまくいくかはわかりませんが...

2
Bill K

正規表現 は、有限状態機械(または「有限状態オートマトン」)が機能する別の例です。

コンパイルされた正規表現は有限状態機械であり、正規表現が一致できる文字列のセットは、有限状態オートマトンが受け入れることができる言語(「通常の言語」と呼ばれます)とまったく同じです。

2

多くのデジタルハードウェア設計では、回路の動作を指定するステートマシンを作成する必要があります。あなたがVHDLを書いているなら、それはかなり出てきます。

2
Ryan Fox

FSMは、複数の状態があり、刺激で別の状態に遷移する必要があるすべての場所で使用されます。

(これには、少なくとも理論的には、ほとんどの問題が含まれることがわかります)

2
Paul Nathan

現在取り組んでいるシステムの例があります。株式取引システムを構築中です。注文の状態を追跡するプロセスは複雑になる可能性がありますが、注文のライフサイクルの状態図を作成すると、既存の注文への新しい着信トランザクションの適用がはるかに簡単になります。現在の状態から、新しいトランザクションが20のいずれかではなく3のいずれかだけであることがわかっている場合、そのトランザクションの適用に必要な比較ははるかに少なくなります。これにより、コードの効率が大幅に向上します。

1
dviljoen

有限状態機械は、任意の自然言語での形態学的解析に使用できます。

理論的には、これは形態と構文が計算レベル間で分割されることを意味します。1つは最大で有限状態で、もう1つは最大で穏やかな状況依存です(したがって、他の理論モデルがWord-to-Wordではなく、形態素と形態素の関係)。

これは、機械翻訳や単語のグロスの分野で役立ちます。一見すると、これらは低コストの機能であり、構文や依存関係の解析など、NLPでの簡単な機械学習アプリケーション用に抽出できます。

詳細については、BeesleyとKarttunenによるFinite State Morphologyと、PARCで設計したXerox Finite State Toolkitをご覧ください。

1
Robert Elwell

典型的な使用例は信号機です。

実装に関する注記:Java 5の列挙型には抽象メソッドを含めることができます。これは、状態依存の動作をカプセル化する優れた方法です。

0
thSoft

良い答え。これが私の2セントです。有限状態機械は、テーブルやwhileスイッチなど、複数の異なる方法で実装できる理論的なアイデアです(しかし、gotohorrors)。 FSMは正規表現に対応し、その逆も同様であるというのは定理です。正規表現は構造化プログラムに対応しているため、FSMを実装するための構造化プログラムをときどき書くことができます。たとえば、数値の単純なパーサーは、次のように書くことができます。

/* implement dd*[.d*] */
if (isdigit(*p)){
    while(isdigit(*p)) p++;
    if (*p=='.'){
        p++;
        while(isdigit(*p)) p++;
    }
    /* got it! */
}

あなたはアイデアを得ます。そして、より高速に実行できる方法がある場合、それが何であるかはわかりません。

0
Mike Dunlavey

状態駆動型コードは、特定のタイプのロジックを実装するための優れた方法です(パーサーがその例です)。たとえば、次のようないくつかの方法で実行できます。

  • 特定のポイントで実際に実行されているコードのビットを駆動する状態(つまり、状態は、作成しているコードの一部に暗黙的に含まれます)。 再帰下降パーサー は、このタイプのコードの良い例です。

  • Switchステートメントなどの条件で何を行うかを示す状態。

  • LexYacc などのパーサー生成ツールによって生成されるような明示的な状態マシン。

すべての状態駆動型コードが解析に使用されるわけではありません。一般的なステートマシンジェネレーターは smc です。ステートマシンの定義をその言語で取り込み、さまざまな言語でステートマシンのコードを吐き出します。