Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Стили и методы программирования. Учебное пособие для СПО

.pdf
Скачиваний:
0
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
☆
St1
/*(symbol<'a'|| 'z'<symbol) &&*/ symbol!='\n'
St1
symbol='\n' St3
St2
printf ("%c", symbol); Cnt++;
'a'<=symbol && symbol <= 'z' St2
/*(symbol<'a'|| 'z'<symbol)
St4
symbol='\n' St5
St3
/* Действий нет */
'a'<=symbol && symbol <= 'z' St2
/*(symbol<'a'|| 'z'<Symbol) &&*/ symbol!='\n'
St1
Второй '\n' дальше не нужно читать symbol='\n'
exit
St4
printf ("­%i\n", Cnt); Cnt=0;
'a'<=symbol && symbol <= 'z' St2
/*(symbol<'a'|| 'z'<symbol) &&*/ symbol != '\n'
St1
symbol='\n' St3
St5
printf ("­%i\n", Cnt); Cnt=0;
'a'<=symbol && symbol <= 'z' St2
/*(symbol<'a'|| 'z'<symbol) &&*/ symbol != '\n'
exit
Второй '\n' дальше не нужно читать symbol='\n'
exit
exit
/* Нет неявного чтения потока */
return 0; // Считать данную секцию таблицы состоянием или нет - дело вкуса
1)
Если Вы посмотрите точное определение, то есть еще один
компонент: преобразование состояний в выходные символы. Аналогом этого компонента в программировании может служить массив процедур, вызовы которых сопоставлены состояниям, но для
Стили и методы программированияНепейвода Н.Н.
161
принципиальных рассмотрений у нас он не важен.
2)
Конечно же, в каждом конкретном модуле нужно использовать лишь одну из структур, концептуально несовместимых, хотя формально эквивалентных.
3)
Интересно, что необходимость развитых средств поддержки модуляризации программ первоначально провозглашалась для структурного программирования, но оказалась прекрасно приспособлена совсем к другому стилю. Впрочем, в современных программах самый верхний (часто полуавтоматически генерируемый системами визуального программирования и поэтому обычно мало интересный для программистов) этаж структуры программ, как правило, записан именно в стиле автоматного программирования .
4)
Данный абзац добавлен после обсуждения с проф. А.А. Шалыто .
5)
Сделаем общее замечание насчет доказательной силы аргумента: "Это исследовано в теории". При применении теории необходимо всегда помнить, что вовсю исследуются не только разумные понятия, но и извращения.
Стили и методы программированияНепейвода Н.Н.
162
Автоматное программирование: от таблицы к программе
Трансляция таблиц состояний и переходов. Различные методы представления таблиц как программ. Интерпретация таблиц переходов.
Анализ состояния дел
Построение таблиц заканчивает этап спецификации нашей программы. Таблицы 9.1
и 9.2-другое формализованное представление рисунков 9.6 и 9.7. Как всегда, разные формализмы отличаются по практической направленности. Граф в некоторых случаях может быть автоматизированно преобразован в прототип программы (попытайтесь сами проделать это со спецификацией на языке UML ), но получающиеся программы всегда требуют ручной доработки. Табличное же представление допустимо рассматривать как мини-язык программирования. Для него возможно построение транслятора или интерпретатора, которые в частных типах таблиц давно уже используются (их широкому применению мешает привязка к конкретным предметным областям и приложениям, поскольку профессиональные системные программисты свысока смотрели на столь простые задачи).
Обратимся к семантике вычислений на языке таблиц. Рассматривается контекст некоторой программы на языке C++ (или на другом языке традиционного типа). В этом контексте определяется значение переменной Entry (множество ее значений совпадает с множеством наименований состояний), далее выполняются следующие действия:
1. Выбрать вход в таблицу, соответствующий значению Entry (текущее состояние).
2. Если Условие отсутствует, то перейти к шагу 4.
3. Вычислить совместно все Условия срабатывания переходов (клетки второй колонки для данного входа):
4.
Если среди результатов есть значения True, то выбрать одну из строк, Условие которой истинно, и перейти к шагу 4
для выбранной строки
1)
;
Стили и методы программированияНепейвода Н.Н.
163
Если все значения Условий ложны и есть строка failure, то выбрать эту строку и перейти к шагу 4 для нее; Если все значения Условий ложны и нет строки failure, то завершить вычисления.
5. Выполнить Действие.
6. В качестве значения переменной Entry установить наименование состояния-преемника (из четвертой колонки таблицы). Перейти к шагу 1.
Для рассматриваемого сейчас примера указанной семантики вычислений недостаточно. Во многих случаях полезно расширение выразительных возможностей таблиц за счет добавления действий, которые выполняются в начале и в конце обработки текущего состояния. Это достигается, например, с помощью специальных служебных слов:
start - указание действий, которые выполняются до основных действий и проверок условий перехода (здесь таким действием является чтение очередного символа из файла); finish -указание действий, которые выполняются после основных действий и проверок, но до перехода.
Можно также определять локальные данные для состояний (в примере такие данные определены только для начального состояния), но это должно быть согласовано с правилами локализации имен языка программирования и с общим стилем, в котором написана программа. Заметим, что локальные данные всех состояний конечного автомата должны быть помещены в общий контекст, а приписывание их к конкретным состояниям является ограничением видимости, подобным тому, которое используется в модульном программировании (если Вы уже программировали на Object Pascal Delphi, Java либо Oberon, то знакомы с модульностью). Это прагматически следует из того положения, что работа теоретического конечного автомата не требует
привлечения памяти
2)
.
Озаботимся теперь тем, как запрограммировать требуемые действия, когда нет возможности воспользоваться языком таблиц непосредственно. С таблицами, представляющими таблицу переходов, можно сделать следующее.
Стили и методы программированияНепейвода Н.Н.
164
1. Оттранслировать вручную.
2. Отдать препроцессору для превращения в нормальную программу.
3. Использовать интерпретатор.
Рассмотрим последовательно эти три возможности.
Ручная трансляция таблиц переходов
В решениях 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>>;};
}; * * Вторая программа, чуть дальше от непосредственной автоматной модели *
Стили и методы программированияНепейвода Н.Н.
165
$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)
. Вновь отметим, что, хотя некоторые описания функций кажутся рекурсивными, рекурсий нет, так как исходный вызов завершается до активизации порожденных им вызовов. Каждый шаг конкретизации начинается с проверки условий, поэтому действия естественно сопоставить именно переходам.
В данном случае удалось добиться столь красивого результата потому, что действия также полностью соответствовали той области, где применение языка Рефал наиболее выигрышно. Но даже в таком простом случае после минимальной ручной доработки программа стала
Стили и методы программированияНепейвода Н.Н.
166
еще лучше.
Формально функциональное представление состояний возможно как на языке традиционного типа, так и на 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;
Стили и методы программированияНепейвода Н.Н.
167
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:, приводят лишь к уменьшению наглядности по сравнению
с табличным представлением.
Есть еще один вопрос, который требуется решить при таком подходе, ­выбор типа результата функций-состояний и способа возвращения результата. Естественный результат такой функции - это новое
Стили и методы программированияНепейвода Н.Н.
168
состояние, в которое попадает автомат при переходе. Он может быть представлен, например, глобальной переменной (такой как 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 () {
Стили и методы программированияНепейвода Н.Н.
169
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 ();
Стили и методы программированияНепейвода Н.Н.
170