Опубликован: 11.02.2005 | Уровень: специалист | Доступ: платный
Лекция 10:

Автоматное программирование: от таблицы к программе

< Лекция 9 || Лекция 10: 12345 || Лекция 11 >

Ручная трансляция таблиц переходов

В решениях 1 и 2 возникает следующий вопрос: во что транслировать таблицу переходов? Вариантов множество, ниже рассматриваются лишь некоторые из них.

Вариант 1.Можно считать St1 и St2 функциями, реализующими все действия состояний.При использовании верно подобранных стиля и инструментальных средств этот подход дает отличный результат. Рассмотрим, в частности, как может быть реализована наша задача (еще точнее, автомат таблицы 9.1) на языке Рефал.

ENTRY Go{=<Open 'r' 1 'input.txt'><Open 'w' 2 'output.txt'><Init<Get 1>>};
Letters {='abcdefghijklmnopqrstuvwxyz';};
Init{=;
    e.1=<St1 e.1>;};
St1 {
    s.1 e.3,<Letters>: e.A s.1 e.B =<St2 e.3 (s.1)>;
    = <Init<Get 1>>;
    s.1 e.3 =<St1 e.3>;
    };
St2 {(e.2)= <Outstr e.2> <Init<Get 1>>;
    s.1 e.3 (e.2),<Letters>: e.A s.1 e.B =<St2 e.3 (e.2 s.1)>;
    s.1 e.3 (e.2)=<Outstr e.2><St1 e.3 >;
    };
* St3 не нужно
Outstr {
    e.2, <Lenw e.2>: {s.1 e.2 = <Putout 2 e.2 " - " <Symb s.1>>;};
    };
*
* Вторая программа, чуть дальше от непосредственной автоматной модели
*
$ENTRY Go{=<Open 'r' 1 'input.txt'><Open 'w' 2 'output.txt'><Init <Get 1>>};
Letters {='abcdefghijklmnopqrstuvwxyz';};
Init {=;
    e.1=<Parse e.1()>;};
Parse {
    s.1 e.3 (e.2),<Letters>: e.A s.1 e.B =<Parse e.3 (e.2 s.1)>;
    (e.2)= <Outstr e.2> <Init<Get 1>>;
    s.1 e.3 (e.2)=<Outstr e.2><Parse e.3 ()>;
    };
Outstr { = ;
    e.2, <Lenw e.2>: {s.1 e.2 = <Putout 2 e.2 " - " <Symb s.1>>;};
    };
Листинг 10.2.1. Длины слов: рефал

Эти программы настолько коротки и естественны, что практически не требуют комментариев. Единственная новая возможность, использованная здесь, в принципе излишняя, но делает программу красивее. Конструкции <Letters>: e.A s.1 e.B и e.2, <Lenw e.2>: являются, соответственно, вложенной проверкой условия на выражении, порождаемом вызовом <Letters>, и вызовом определяемой дальше анонимной функции на выражении, получившемся после запятой. Стандартная функция <Lenw e.2> добавляет первым символом к выражению его длину (количество термов в нем). При проверке и при вычислении вспомогательной функции переменные, уже получившие значения, не изменяются.

Таким образом, структура управления на переходах хорошо согласуется со структурой управления в конкретизационном варианте сентенциального программирования3Это верно и для унификационного варианта. . Вновь отметим, что, хотя некоторые описания функций кажутся рекурсивными, рекурсий нет, так как исходный вызов завершается до активизации порожденных им вызовов. Каждый шаг конкретизации начинается с проверки условий, поэтому действия естественно сопоставить именно переходам.

В данном случае удалось добиться столь красивого результата потому, что действия также полностью соответствовали той области, где применение языка Рефал наиболее выигрышно. Но даже в таком простом случае после минимальной ручной доработки программа стала еще лучше.

Формально функциональное представление состояний возможно как на языке традиционного типа, так и на LISP и Prolog. Но в этом случае мы проигрываем в выразительности и естественности программы, а также еще по одному важному критерию. Во всех этих случаях то, что выглядит как рекурсия, действительно ею является, а рекурсия в данном случае лишь мешает.

Вариант 2. Считать St1, St2, St3 значениями некоторого перечислимого типа State.

// Реализация автомата с табл. 9.1
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

char symbol;
int cnt;

enum States { St1, St2, St3 } State;
void main( void )
{fstream a,b;
    a.open("input.txt",ios::in);
    b.open("output.txt",ios::out);
    State = St1;
    while (true )
    {symbol=a.get();
        switch ( State )
        { case St1: if ('a'<=symbol && symbol <= 'z')
            { b<<symbol;
                cnt = 1;
                State = St2;
        }
        else if (symbol != '\n') {State = St1;}
        else if (symbol == '\n') State = St3;
        break;
    case St2: if ('a'<=symbol && symbol <= 'z')
            {b<< symbol;
            cnt++; // State = St2;
            }
        else if (symbol != '\n')
            { b<<" - "<<cnt<<endl;
                State = St1;
            }
    else if (symbol == '\n')
        { b<<" - "<<cnt<<endl;
            State=St3;
        };
        break;
    case St3: if ('a'<=symbol && symbol <= 'z')
            { b<< symbol;
                cnt = 1;
                State = St2;
        }
        else if (symbol != '\n') {State = St1;}
        else if (symbol == '\n')
            { a.close(); b.close(); return;};
    } }
}
Листинг 10.2.2. Длины слов: явный цикл обработки потока

Программа 10.2.2 хороша только в том случае, если ограничиться автоматными моделями в достаточно узком смысле. Программа точно соответствует табличному представлению автомата, почти все дублирования действий, связанные с таким представлением, остались. Возникает некоторая неудовлетворенность тем, что появилось лишнее действие: выбор выполняемого фрагмента по значению переменной State. Если ограничиться управляющими средствами методологии структурного программирования, то это - наилучший выход. Попытки применить циклы внутри фрагментов, помеченных как case St1: и case St2:, приводят лишь к уменьшению наглядности по сравнению с табличным представлением.

Есть еще один вопрос, который требуется решить при таком подходе, - выбор типа результата функций-состояний и способа возвращения результата. Естественный результат такой функции - это новое состояние, в которое попадает автомат при переходе. Он может быть представлен, например, глобальной переменной (такой как State в программе 10.2.2, которой присваивается новое значение при выполнении функции-состояния). Но предпочтительнее не трогать State в каждой из функций, а предоставить задачу фактического изменения состояния общему их контексту. Наиболее правильной реализацией состояний при таком подходе являются функции, которые вырабатывают в качестве результата значение типа, перечисляющего все состояния.

Следующая программа 10.2.3 почти буквально реализует данную идею. В отличие от программы 10.2.2, тип States содержит четыре значения: St1, St2, St3 и Exit. Последнему из них не соответствует ни одна функция из-за тривиальности переходов и действий в данном состоянии.

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

char symbol;
int cnt;
enum States { St1, St2, St3, Exit } State;

inline States f_St1 ()
{
    if ('a'<=symbol && symbol <= 'z')
        printf ("%c", symbol);
        cnt = 1;
        symbol = getchar (); return St2;
    }
    else if (symbol != '\n') {
            symbol = getchar (); return
        }
    else {symbol = getchar (); return
}

inline States f_St2 ()
{
    if ('a'<=symbol && symbol <= 'z')
        printf ("%c", symbol);
        cnt++;
        symbol = getchar (); return St2;
    }
    else if (symbol != '\n') {
        printf (" -%i\n", cnt);
        symbol = getchar (); return St1;
    }
    else {
        printf (" - %i\n", cnt);
        symbol = getchar (); return St3;
    }
}

inline States f_St3 ()
{
    if ('a'<=symbol && symbol <= 'z') {
        printf ("%c", symbol);
        cnt = 1;
        symbol = getchar (); return St2;
    }
    else if (symbol != '\n') {
            symbol = getchar (); return St1;
        }
    else return Exit;
}

void main( void )
{
    symbol = getchar ();
    State = St1;
    for (;;)
        { switch ( State ) {
            case St1: State = f_St1 ();
                break;
            case St2: State = f_St2 ();
                break;
            case St3: State = f_St3 ();
                break;
            default: return;
        }
    }
}
Листинг 10.2.3. Длины слов: использование процедур для описания состояний.

Следует обратить внимание на то, что программа наглядна, строго соответствует таблице автомата. Можно сказать, что граф автомата определяет распределенную по состояниям схему потоковой обработки. На самом деле она гораздо лучше подходит модели вычислений в состояниях, а не на переходах. Более того, эта программа при небольшом увеличении числа состояний быстро теряет наглядность, если программировать на переходах, поскольку следующие состояния упрятаны внутри соответствующих процедур, но сохранит ее, если программировать в состояниях.

Таким образом, мы пришли к тому, что в рамках стандартной методики программирования все решения имеют некоторые недостатки. Надо искать другое, и здесь стоит вспомнить пословицу (на самом деле отражающую один из приемов творческого мышления): "Новое - хорошо забытое старое". Поищем решение среди того, что забраковано стандартными методиками.

Проанализировав граф автомата или таблицу, можно заметить, что в данном примере, наряду с циклом потоковой обработки, имеется еще один цикл: передача управления между состояниями. Это причина, из-за которой в двух предыдущих вариантах появились присваивание переменной State значения и выбор выполняемого фрагмента по этому значению.

Особенность последовательностей действий

State = <значение>;

switch ( State )

которая выполняется всякий раз в конце действий, ассоциированных с состояниями (точнее - реализаций действий в программах), в том, что в каждой точке программы, где встречается данное присваивание, можно точно указать результат динамически следующего оператора переключения, причем эта информация не зависит от обрабатываемого потока и может быть определена до вычислений статически.

А нельзя ли использовать это явно для организации управления конечным автоматом? Можно, это демонстрирует четвертый вариант решения обсуждаемой задачи.

Вариант 4. Матрица переходов и вектор-функций, соответствующих состояниям4Этот вариант сознательно запрограммирован на Object Pascal, поскольку разница между языками традиционного типа несущественна, а изменение языка заодно выделяет и тот факт, что данный вариант ориентирован на другую модель автоматного программирования.

program funcgoto;
{$APPTYPE CONSOLE} {$T+}
uses SysUtils;

type P=procedure;
type Pp= ^P;
const maxstate = 7;
const maxcond = 3;
type states = 1.. maxstate;
conds = 1.. maxcond;
type table = array [states, conds] of states;
type act = array [states] of P;
const gotos: table = ((2,2,2),(3,2,4),(3,5,6),(3,2,7),(3,2,4),(3,2,7),(1,1,1));
var Symbol: char;
var Cnt: integer;
var Inf, Outf: text;
var state: states;

procedure Start;
begin
    Cnt:=0;
    AssignFile(Inf, 'input.txt');
    Reset(Inf);
    AssignFile(Outf, 'output.txt');
    Rewrite(Outf);
end;

procedure Finish;
begin
    Closefile(Inf);
    Closefile(Outf);
    Abort;
end;

procedure St1;
begin
    {No actions}
end;

procedure St2;
begin
    write(outf,Symbol);
    Inc(Cnt);
end;

procedure St4;
begin
    writeln(outf,' - ',Cnt);
    Cnt:=0;
end;

const actions: act = (Start, St1,St2,St1,St4,St4,Finish);

begin
    state:=1;
    while true do begin
        actions[state];
        if (state <>1) and (state<>7) then begin read(inf,Symbol);
        if Ord(Symbol)=10 then read(inf,Symbol) end;
        if Symbol in ['a'..'z'] then state:= gotos[state,1];
        if not(Symbol in ['a'..'z']) and (Ord(Symbol)<>13)
            then state:=gotos[state,2];
        if Ord(Symbol)=13 then state:=gotos[state,3];
    end;
end.
Листинг 10.2.4. Длины слов: массив функций и переходов

Это решение неплохое, но оно годится лишь для вычислений в состояниях. Фактически оно резко расходится с канонами структурного программирования, где даже не предполагалось переключение между функциями внутри массива функций. Оно несколько утяжелено деталями, связанными с отработкой особых состояний ввода входного потока. Зато повторяющиеся действия не дублируются.

Рассмотрим последний вариант.

Вариант 5. Использование статической информации о разветвлениях вычислений.

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

char symbol;
int cnt;
void main( void )
{
    symbol = getchar ();
St1: if ('a'<=symbol && symbol <= 'z') {
                printf ("%c", symbol);
                cnt = 1;
                symbol = getchar (); goto St2;
            }
            else if (symbol != '\n') {
                symbol = getchar (); goto St1;
        }
        else /* (symbol == '\n') */ {symbol = getchar (); goto St3;};
St2: if ('a'<=symbol && symbol <= 'z') {
                printf ("%c", symbol);
                cnt++;
                symbol = getchar (); goto St2;
            }
            else if (symbol != '\n') {
                printf (" -%i\n", cnt);
                symbol = getchar (); goto St1;
            }
            else {
                printf (" -%i\n", cnt);
                symbol = getchar (); goto St3;
            };
St3: if ('a'<=symbol && symbol <= 'z') {
                printf ("%c", symbol);
                cnt = 1;
                symbol = getchar (); goto St2;
            }
            else if (symbol != '\n') {
                symbol = getchar (); goto St1;
            }
            else /* (symbol == '\n') */ return;
}
Листинг 10.2.5. Длины слов: состояния - метки в программе.

В данном варианте исчезает необходимость вычисляемого перехода (результат внедрения статической информации в текст программы), и, как следствие, становятся избыточными описания типа States и переменной State этого типа. В программе появляются операторы безусловного перехода, из-за этого структура управления программы полностью расходится с канонами структурного программирования, что дает повод догматически мыслящим программистам и теоретикам подвергать критике такой вариант программы. Но в данном случае отступление от канонов структурного программирования полностью оправдано, поскольку за счет специального расположения фрагментов текста вся программа оказалась похожа на таблицу конечного автомата, а структура передач управления копирует граф конечного автомата. Более того, такое представление нейтрально по отношению к моделям Мура и Мили. Таким образом, лишь после полного отхода от канонов структурности программа стала адекватна своей спецификации.

< Лекция 9 || Лекция 10: 12345 || Лекция 11 >
Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?

Илья Ардов
Илья Ардов

Добрый день!

Я записан на программу. Куда высылать договор и диплом?

Феофан Миков
Феофан Миков
Россия, Томск, ТПУ, 2013
Виталий Лусников
Виталий Лусников
Россия, Киров, ВятГГУ, 2001