自動機編程

自動機編程英語:Automata-based programming)是編程範式中的一種,是指程式或其中的部份是以有限狀態機(FSM)為模型的程式,有些程式則會用其他型式(也更複雜)的自動機為其模型。

有限狀態機編程英語:FSM-based programming)大致上等同於自動機編程,但有限狀態機編程專指以有限狀態機為模型的程式。

自動機編程有以下的二項特徵:

  1. 程式執行的時間中可以清楚劃分成數個自動機的步驟(step),每一個步驟即為一個程式區段,有單一的進入點,可以是一個函數或其他程式。若有需要時,程式區段可以再依其狀態的不同,劃分為子區段。
  2. 不同步驟的程式區段只能透過一組清楚標示的變數交換資訊,這些變數稱為狀態(state),使用自動機編程的程式不能用其他不顯然可見的方式標示狀態,例如區域變數的數值、回傳地址、目前程式指標的位置等。因此一程式在任二個不同時間下的差異,只有狀態數值的不同,其餘都相同。

自動機編程的執行過程是一個由自動機步驟形成的循環。

自動機編程中處理問題的思考方式很類似在利用圖靈機馬爾可夫演算法處理問題時的思考方式。

歷史

自動機編程的技術常用在以自動機原理為基礎的演算法中,例如形式語言分析[1]

約翰遜等在1968年發表的《Automatic generation of efficient lexical processors using finite state techniques》論文是早期提到自動機編程的論文[2]。 Peter Naur在1963年的論文將自動機編程當成一種通用的軟件技術[3]。作者將此技術稱為「圖靈機的方法」,不過此論文是以自動機的狀態及步驟為基礎,沒有提到圖靈機

範例

考慮一個C語言的程式,由標準輸入流一行一行的讀取資料,列印各一行的第一個英文單字。因此一開始需確認第一個英文單字之前是否有空白,若有,需讀取所有空白後略過不列印,讀取第一個英文單字然後列印,之後讀取其他內容略過不列印,直到讀到換行符號為止。任何情形下只要讀到換行符號,就重新開始此演算法,任何情形下只要讀到檔案結束(end-of-file)的符號,就結束程式。

傳統C語言的程式

以下是傳統指令式編程的C語言程式:

#include <stdio.h>
int main(void)
{
    int c;
    do {
        c = getchar();
        while(c == ' ')
            c = getchar();
        while(c != EOF && c != ' ' && c != '\n') {
            putchar(c);
            c = getchar();
        }
        putchar('\n');
        while(c != EOF && c != '\n')
            c = getchar();
    } while(c != EOF);
    return 0;
}

自動機編程的程式

上述問題也可以用有有限狀態機的方式處理,此程式有三個不同的階段:讀取並跳過第一個單詞前的空白、讀取第一個單詞並且列印、跳過後續的所有字元。以下將這三個階段定義為三個狀態beforeinsideafter。自動機編程的程式如下:

#include <stdio.h>
int main(void)
{
    enum states {
        before, inside, after
    } state;
    int c;
    state = before;
    while((c = getchar()) != EOF) {
        switch(state) {
            case before:
                if(c == '\n') {
                    putchar('\n');
                } else
                if(c != ' ') {
                    putchar(c);
                    state = inside;
                }
                break;
            case inside:
                switch(c) {
                    case ' ':  state = after; break;
                    case '\n':
                        putchar('\n');
                        state = before;
                        break;
                    default:   putchar(c);
                }
                break;
            case after:
                if(c == '\n') {
                    putchar('\n');
                    state = before;
                }
        }
    }
    return 0;
}

雖然此程式較長,至少有一個明顯的好處,程式中只呼叫一個讀取字元的getchar()函數,而且程式中只有一個迴圈,不像之前程式使用四個迴圈。

此程式中while迴圈內的程式即為自動機的步驟,而迴圈本身即可重覆的執行自動機的程式。

 
對應的有限狀態機示意圖

此程式實現如右圖所示的有限狀態機,其中N表示換行字元、S表示空白、A表示其他的字元。自動機依目前狀態及讀取的字元不同,會執行圖中一個箭頭所示的動作,可能是由一個狀態跳到下一個狀態,也者停在原來的狀態。其中有些箭頭有標示星號,表示需列印讀到的字元。

自動機編程中,不一定要為每一個狀態撰寫獨立的處理程式,而且有時狀態是由許多變數組成,無法針對每一個狀態規劃個別的處理程式。此想法有時有助於程式的精簡,例如在上述程式中,不論是在哪一個狀態,針對換行字元的處理都一様,因此程式可以先處理換行字元,其他輸入字元時才依不同狀態進行處理,簡化後變成以下的程式:

#include <stdio.h>
int main(void)
{
    enum states {
        before, inside, after
    } state;
    int c;
    state = before;
    while((c = getchar()) != EOF) {
        if(c == '\n') {
            putchar('\n');
            state = before;
        } else
        switch(state) {
            case before:
                if(c != ' ') {
                    putchar(c);
                    state = inside;
                }
                break;
            case inside:
                if(c == ' ') {
                    state = after;
                } else {
                    putchar(c);
                }
                break;
            case after:
                break;
        }
    }
    return 0;
}

獨立的自動機步驟程式

上述程式的一個重要特點是自動機步驟的程式區塊都只使用區域變數,以下的例子將自動機步驟整合為一個獨立的函數step(),更可以突顯上述的特點:

#include <stdio.h>
enum states { before, inside, after };
void step(enum states *state, int c)
{
    if(c == '\n') {
        putchar('\n');
        *state = before;
    } else
    switch(*state) {
        case before:
            if(c != ' ') {
                putchar(c);
                *state = inside;
            }
            break;
        case inside:
            if(c == ' ') {
                *state = after;
            } else {
                putchar(c);
            }
            break;
        case after:
            break;
    }
} 
int main(void)
{
    int c;
    enum states state = before;
    while((c = getchar()) != EOF) {
        step(&state, c);
    }
    return 0;
}

此例清楚的呈現自動機編程程式的基本特點:

  1. 各自動機步驟程式的執行時間不互相重疊。
  2. 前一個步驟和下一個步驟之間所交換的資料只有標示為「自動機狀態」的變數(此例中為變數state)。

顯式的狀態轉換表

自動機編程可以用顯式的狀態轉換表來表示。以下的程式中的the_table陣列即為狀態轉換表,其列表示三個不同的狀態,其每一欄對應輸入的字元(從左到右分別是空白、換行字元及其他字元)。

對於每一種可能的狀態及輸入字元的組合,表中有其對應的新狀態及一個決定是否否顯示輸入字元的旗標。在實務的專案中狀態轉換表可能更為複雜,例如可能包括所有可能條件組合下需呼叫的函數指標。

#include <stdio.h>
enum states { before = 0, inside = 1, after = 2 };
struct branch {
    unsigned char new_state:2;
    unsigned char should_putchar:1;
};
struct branch the_table[3][3] = {
                 /* ' '         '\n'        others */
    /* before */ { {before,0}, {before,1}, {inside,1} },
    /* inside */ { {after, 0}, {before,1}, {inside,1} },
    /* after  */ { {after, 0}, {before,1}, {after, 0} }
};
void step(enum states *state, int c)
{
    int idx2 = (c == ' ') ? 0 : (c == '\n') ? 1 : 2;
    struct branch *b = & the_table[*state][idx2];
    *state = (enum states)(b->new_state);
    if(b->should_putchar) putchar(c);
}
int main(void)
{
    int c;
    enum states state = before;
    while((c = getchar()) != EOF)
        step(&state, c);
    return 0;
}

自動化技術和自動機

自動機編程相當類似自動化技術領域需要的程式。

製造週期一般會用以下的方式定義:

  • 一串依輸入資料決定狀態的程式。
  • 依目前狀態輸出對應資料的程式。

許多程式語言可以用類似的方式撰寫程式。

上述程式可以用此觀點改寫,以下是改寫後程式的偽代碼,其使用關鍵字和符號說明如下:

  • 'set'是指設置變數(此處為狀態)的數值
  • ':'為設置變數,'='是判斷是否相等
SPC : ' '
EOL : '\n'

states : (before, inside, after, end)

setState(c) {
    if c=EOF then set end
    if before and (c!=SPC and c!=EOL) then set inside
    if inside and (c=SPC or c=EOL) then set after
    if after and c=EOL then set before
}

doAction(c) {
    if inside then write(c)
    else if c=EOL then write(c)
}

cycle {
    set before
    loop {
        c : readCharacter
        setState(c)
        doAction(c)
    }
    until end
}

上述程式中將更新狀態的程式獨立為setState函數,另外將依狀態和輸入更新輸出的程式獨立為doAction函數,此作法可以產生較清楚及簡單的程式碼。

自動化技術及事件

在自動化領域中,步驟之間的切換是依照機器本身的輸入資料,在本例中為讀到的輸入字元,在實務上可能是位置、速度、溫度等機器的關鍵資料。

自動化領域有些設計方式類似圖形化使用者介面的程式設計,機器狀態的改變可以視為由事件而造成,由於事件使機器由一個狀態變為下一個狀態,直到到達最後的狀態為止。可能出現狀態的組合可以產生許多的事件,因此可以定義較複雜的製造週期,其產生的製造週期一般會比線性循序流程複雜許多。一般常常會有一些同時執行的平行路徑,以及依不同事件決定執行方式的路徑,如下圖:

   s:狀態   c:條件
   
   s1
   |
   |-c2
   |
   s2
   |
   ----------
   |        |
   |-c31    |-c32
   |        |
  s31       s32
   |        |
   |-c41    |-c42
   |        |
   ----------
   |
   s4

物件導向程式

若程式語言支援物件導向程式設計,就可以將自動機封裝為一個物件,隱藏自動機實現的細節。一種稱為「狀態模式」的設計模式即包括了此作法。上述的程式可以改為為以下的物件導向程式,利用C++來實現:

#include <stdio.h>
class StateMachine {
    enum states { before = 0, inside = 1, after = 2 } state;
    struct branch {
        enum states new_state:2;
        int should_putchar:1;
    };
    static struct branch the_table[3][3];
public:
    StateMachine() : state(before) {}
    void FeedChar(int c) {
        int idx2 = (c == ' ') ? 0 : (c == '\n') ? 1 : 2;
        struct branch *b = & the_table[state][idx2];
        state = b->new_state;
        if(b->should_putchar) putchar(c);
    }
};
struct StateMachine::branch StateMachine::the_table[3][3] = {
                 /* ' '         '\n'        others */
    /* before */ { {before,0}, {before,1}, {inside,1} },
    /* inside */ { {after, 0}, {before,1}, {inside,1} },
    /* after  */ { {after, 0}, {before,1}, {after, 0} }
};
int main(void)
{
    int c;
    StateMachine machine;
    while((c = getchar()) != EOF)
        machine.FeedChar(c);
    return 0;
}

註:為了減少和此主題不直接相關的修改,此處的輸入輸出函數使用C語言的標準函式庫,另外,其中的三元運算子?:也可以用if-else來實現。

應用

自動機編程常用在詞法分析語法剖析器[1]

此外,用自動機的方式處理問題(將執行的程式分為自動機的步驟,以及各步驟間只透過顯式的狀態傳遞資訊)是事件驅動程式設計中必要的一部份,否則就要使用平行程式或是多線程的作法。

狀態及狀態機的表示法常用在形式規格英語formal specification的領域。例如以統一塑模語言為基礎的軟件架構開發,會使用狀態圖表示程式的行為,許多通訊協定也利用顯式的狀態來加以定義,例如RFC 793[4]

狀態機的思維也可以用來描述一些程式語言的語義,例如執行一個Refal語言的程式就可以描述為在抽象Refal機器上執行一連串的步驟,機器的狀態稱為view(任意的Refal表示式,其中沒有變數)。

Scheme程式語言不是一個和狀態機有關的程式語言(Scheme為遞歸式的),但其中的續體(Continuation)需要以自動機的步驟及狀態的方式來思考。若要使call/cc英語call/cc的機能有效,需要可以記錄整個執行程式的狀態,只有在所有狀態都是顯式,不存在隱式狀態的情形下才可能達到。此處的「記錄完整狀態」即為延續性,可以視為一個較複雜自動機的狀態,自動機的步驟是由以前的延續性資料推斷下一個的延續性資料,而所執行的程式就是這些步驟的循環。

亞歷山大·奧隆格羅(Alexander Ollongren)在其著作[5]中解釋了一種稱為「維也納方法」(Vienna method)的程式語言語義描述,完全以形式自動機為基礎。

UCSB的STAT(狀態轉移分析技術)系統[1]是一個使用自動機編程的範例,此系統還包括一種稱為「STATL」的嵌入式語言,是完全自動機導向的語言。

和指令式編程及程式編程的比較

狀態不是自動機編程特有的概念[6]

一般來說,狀態可視為所有在執行時會更改的資訊的結合,任何電腦程式執行時都有其對應的狀態。一傳統指令式編程程式的狀態包括:

  1. 所有變數的值及動態記憶體中的資訊。
  2. 暫存器的內容。
  3. 堆疊的內容(包括區域變數的值及回傳地址)。
  4. 程式計數器中的內容。

上述的狀態可分為顯式(變數的內容)及隱式(回傳地址及程式計數器)二種。

以上述的觀點來看,自動機編程可視為一種特殊的指令式編程,其顯式的狀態減少到最少,因此二個不同時間點的程式的差異只在自動機狀態的不同,因此可以簡化程式的分析。

和物件導向程式設計的關係

物件導向程式設計的理論中,物件有其內部的狀態,而且可以接收訊息、回應訊息,傳送訊息給其他物件,且依訊息調整其內部的狀態。實際上,呼叫一個物件的方法也就是傳送訊息給此一物件。

因此,物件導向程式設計的物件也可以視為是自動機(或是自動機的模型),其狀態是內部屬性的組合,而物件的一個或多個方法可視為自動機的步驟。這些視為自動機步驟的方法不能直接或間接的互相呼叫(或呼叫本身),否則此物件就不能視為以自動機編程的方式來設計。

當用物件導向程式設計來實現自動機編程時,也可以用類別來實現自動機模型,其中狀態為其私有成員,而步驟是物件的一個公開方法,是除了建構子及解構子外,唯一可以變更物件內容的公開方法。物件的其他公開方法可以查詢狀態,但不能變更其狀態。物件的其他方法(例如不同狀態的處理程式)會是物件的私有方法,無法由外界程式來呼叫。

相關條目

參考資料

  1. ^ 1.0 1.1 Aho, Alfred V.; Ullman, Jeffrey D. The theory of parsing, translation and compiling 1. Englewood Cliffs, N. J.: Prentice-Hall. 1973. ISBN 0-13-914564-8. 
  2. ^ Johnson, W. L.; Porter, J. H.; Ackley, S. I.; Ross, D. T. Automatic generation of efficient lexical processors using finite state techniques. Comm ACM. 1968, 11 (12): 805–813. doi:10.1145/364175.364185. 
  3. ^ Naur, Peter. The design of the GIER ALGOL compiler Part II. BIT Numerical Mathematics. September 1963, 3 (3): 145–166. doi:10.1007/BF01939983. [永久失效連結]
  4. ^ RFC 793
  5. ^ Ollongren, Alexander. Definition of programming languages by interpreting automata. London: Academic Press. 1974. ISBN 0-12-525750-3. 
  6. ^ Automata-based programming. Bulletin of St Petersburg State University of Information Technologies, Mechanics and Optics. 2008. http://books.ifmo.ru/NTV/NTV_53.pdf (rus), 53. 

外部連結