自动机编程

自动机编程英语: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. 

外部链接