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

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

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

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

Последний недостаток неустраним ни в каком языке, если проверки и действия трактовать как процедуры, которые работают в общем контексте, задаваемом в заголовке таблицы. Так что лучше сразу отказаться от описания этого контекста в рамках таблицы (задание инициализации в ней возможно). Что касается первого недостатка, то он носит почти синтаксический характер, и если бы можно было задавать тела процедур как значения полей структур, то наглядность представления можно было бы сохранить. В языковом оформлении, похожем на С/С++/C#, это выглядело бы как следующее присваивание значения Table в качестве инициализации.

Table[]= {
    {{return true;}, /* инициализация */, 1},
/*St1*/{{return 'a'<=symbol &&
    symbol <= 'z';}, {printf ("%c", symbol);
        cnt = 1;}, 4},
    {{return symbol != '\n';}, 
    /* Так как нужно печатать только слова,
       действия не заполняются */,
        1},
    {{return true}, 
    /* Переход не требует чтения, 
	   symbol == '\n' не нужно читать */, 0},
/*St2*/{{return 'a'<=symbol &&
    symbol <= 'z';}, {printf ("%c", symbol);
        cnt++;}, 4},
    {{return symbol!='\n';},
        {printf ("-%i\n",cnt);},
    1},
    {{return true }, 
	    {printf ("- %i\n",cnt);}, 0}
};
Листинг 10.3.2.

Но для С/С++/C#, а также для других языков, не ориентированных на работу с таблицами, говорить о естественности представления не приходится. По-видимому, наиболее разумно строить автоматический преобразователь (типа препроцессора, но не путать этот термин с С!), который составляет текст программы по таблице. В этом случае снимается много проблем:

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

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

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

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

  • Когда читать? Поскольку прочитанный символ используется после его распознавания, естественно "не терять" его, пока выполняется действие. Следовательно, читать очередной символ целесообразно после отработки действия.
  • Как поступать в начале процесса: должен ли быть прочитан символ перед первым вызовом функции handler? Понятно, что чтение символов является частью функциональности этой функции. Если в ней реализовать чтение до вызовов условия и действия, то это противоречит предыдущему соглашению, а противоположное решение делает нестандартным начало процесса. В предлагаемой программе принято решение об особой обработке нулевой строки. Поскольку, являясь инициализацией, она действительно особая, это соответствует сделанному соглашению.
  • Как поступать в конце процесса? Чтение заключительного символа ('\n') должно прекращать возможные попытки последующего обращения к чтению. Следовательно, необходимо позаботиться о завершающих переходах. В предлагаемой программе принято решение о завершающем переходе к нулевой строке, которая, согласно предыдущему, является нестандартной.

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

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

#include <stdio.h>
char symbol;                   // переменная для чтения потока символов
int cnt;                       // переменная для счета длин слов

int c0(), c1(), c2();          // функции-условия
void a0(), a1(), a2(), a3();   // функции-действия
int handler ( int i );         // обработчик строк таблицы
struct T_StructTableLine
{                              // поле для имени состояния избыточно
    int (*condition)();        // поле условия
    void (*action)();          // поле действия
    int ref;                   // поле перехода: индекс строки таблицы,
                               // которая будет обрабатываться следующей,
                               // или признак завершения процесса
}
    T_StructTableLine
    Table[]={ {c0,a0,1},       // таблица инициализируется статически,
    {c1,a1,2},                 // альтернативное решение - специальная
    {c2,a0,1},                 // функция задания начальных значений.
    {c0,a0,3},                 // Оно более гибкое, но менее
    {c1,a2,2},                 // эффективно.
    {c2,a3,1},                 // О наглядности см. комментарий
    {c0,a3,3};                 // в тексте.
    {c1,a2,2},
    {c2,a3,1},
    {c0,a3,0}};
void main()
{
    int iSt=0;
    do {
        iSt = handler ( iSt );
        }
    while ( iSt);
}

int handler ( int i )
{
    if (Table[i].condition()) {
        Table[i].action();
        if (Table[i].ref) { symbol = getchar ();
        return Table[i].ref; }
    } else return ++i;
}

// Описания используемых функций:
int c0(){return symbol == '\n';}
int c1(){return 'a'<=symbol && symbol <= 'z';}
int c2(){return 'a'>symbol ||symbol > 'z';}

void a0(){}
void a1(){printf ("%c", symbol);cnt = 1;}
void a2(){printf ("%c", symbol);cnt++;}
void a3(){printf ("- %i\n", cnt);}
Листинг 10.3.3. Длины слов: интерпретатор конечного автомата.
< Лекция 9 || Лекция 10: 12345 || Лекция 11 >
Федор Антонов
Федор Антонов

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

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

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

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

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

Добрый день!

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

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