Вы находитесь на странице: 1из 919

Н. Н. Непейвода, И. Н.

Скопин

ОСНОВАНИЯ
ПРОГРАММИРОВАНИЯ
УДК 519.682

Непейвода Н. Н., Скопин И. Н.

Основания программирования

Книга представляет собой первое издание в серии, предназначенной для


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

c Н. H. Непейвода, И. Н. Скопин, 2002


Разработка поддержана Фондом содействия развитию малых форм пред-


приятий в научно-технической сфере (договор № 1296р/3029 от 27 апреля
2001г.).
Предисловие

Данное учебное пособие предназначено в первую очередь для тех, кто


пришел в вуз, уже имея понятие о программировании (либо изучал его в
школе, либо был самоучкой) и намерен стать профессионалом в области ин-
формационных технологий. Такой студент часто имеет массу предрассудков,
связанных с теми разрозненными и обрывочными сведениями и навыками,
которые он успел накопить. Заложить прочную базу основных понятий и вы-
работать умение ориентироваться в системе понятий программирования —
основная цель пособия.
Это пособие доступно и для начинающего, если он параллельно изучает
какой-то конкретный язык программирования.
В данном пособии мы старались следовать стратегии опережающего обу-
чения, сформулированной В. В. Милашевичем [56]. Суть ее в том, чтобы
максимально выявить высокоуровневые структуры, лежащие за изучаемыми
конкретными понятиями, и тем самым обеспечить обучающемуся, облада-
ющему достаточным интеллектуальным потенциалом, возможность быстро
осваивать новые концепции, и, более того, до некоторой степени предуга-
дывать возможное изменение концептуальной базы и работать с заделом на
перспективу.
В данной книге одна из целей изложения состоит в том, чтобы показать
читателю понятия программирования в их взаимосвязи и взаимозависимо-
сти. Отсюда, в частности, следует, что мы вынуждены многократно обра-
щаться к одним и тем же понятиям, показывая их под разными углами зре-
ния. Этим мы стремимся достичь понимания имеющейся на высшем уров-
не общности многих, по виду различных, методов и приемов деятельности
программиста, а также показать понятия не в плоской картине их уже сло-
жившихся взаимосвязей, а многомерно и многоаспектно, что позволяет осу-
ществить переход на высшие слои сфер знаний и умений. В конечном итоге
это, как проверено опытом, дает плоды: высокую квалификацию обучаемо-

i
ii

го, умение быстро творчески осваивать новые системы понятий и находить


новые эффективные решения.
Эту книгу правильнее всего хоть один раз прочитать с начала и до конца
подряд. Тогда Вы сможете наиболее четко выстроить систему понятий. Не-
которые фрагменты изложения могут показаться сведущему читателю слиш-
ком простыми, другие — сложны для новичка. Тем не менее, мы советуем не
пропускать материал, возможно, уделяя меньше времени простым вопросам,
а к не до конца понятому — обращаясь повторно, когда накопится необходи-
мый опыт. Конечно же, она приспособлена и к фрагментарному чтению, но
в этом случае следует контролировать однозначность понимания тех поло-
жений, которые определяются ранее1 . Отчасти этому помогут приложения и
глоссарий в конце текста. При чтении полезно не только знакомиться с при-
водимыми программами, но и пытаться выполнять их на своем компьютере.
При этом полезно не буквальное повторение написанного, а вариации на за-
данную тему. Так, если у вас получится более эффективная программа, по-
старайтесь понять, почему авторы предпочитают свой вариант; если под ру-
кой нет системы программирования, на языке которой представлен пример,
постарайтесь перевести его в привычную для вас обстановку, по возможно-
сти сохраняя идеи моделируемой программы.
Пособие не затрагивает тренинга, на котором отрабатываются типовые
приемы программирования и методы их применения. Место таким занятиям
в разделах курсов, посвященным конкретным языкам программирования и
алгоритмике как таковой, менее связанной с программированием на языках.
По конкретным языкам есть множество отличных пособий, по алгоритмике
можно рекомендовать книгу [42], соответствующую по уровню изложения
данному курсу и отлично сочетающуюся с ним по материалу. Отчасти мы
неизбежно затрагиваем и эти две темы. Отличие нашего изложения в следу-
ющем.
• Языковые аспекты. Подчеркиваются существенные черты, анализиру-
ются с показом достоинств, недостатков и существенных особенностей
их различные реализации; порою полностью игнорируются случайные
детали конкретных (хотя бы и самых распространенных) реализаций;
если какие-то интересные аспекты были отброшены из-за неудачной
реализации либо еще не реализованы как следует, они все равно ана-
лизируются.
Не стоит пытаться переносить на эту книгу опыт чтения энциклопедий, поскольку при
нынешнем понимании этого слова они представляют из себя базу данных, а не базу знаний.
1
iii

• Алгоритмические аспекты. Мы сосредоточиваемся на том, как потреб-


ности алгоритмики влияют на развитие соответствующих стилей про-
граммирования, а через них — на языковые средства.

Иными словами, как конкретные языки, так и алгоритмы служат для предла-
гаемого учебника в первую очередь иллюстративным материалом.

Внимание!

При описании языков мы не всегда следуем традиционно при-


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

Еще одна особенность данного изложения. Любое конкретное положение


и конкретное решение считается неуниверсальным, и поэтому освещают-
ся его существующие либо потенциальные альтернативы. Таким образом,
здесь показываются на практике инструменты многоуровневого критическо-
го мышления (не путать с одноуровневым, когда некая позиция идолизиру-
ется и все критикуется с точки зрения выбранного «Священного писания»!)
В учебнике представлен ряд заданий для контроля усвоения материала.
Эти задания не являются задачами с одним-единственным правильным от-
ветом (засилье таких задач и являющейся дальнейшим логическим шагом
за ними системы тестов — один из инструментов блокировки критического
мышления). Мы считаем, что гораздо перспективнее разбудить мысль чита-
теля, чем давать готовые рецепты и оценивать следование им. Ход рассужде-
ний при решении задачи представляется более полезным, чем получаемые
результаты: умную и интересную ошибку порою стоит в процессе обучения
оценивать выше, чем лобовое решение.
iv
Часть I

Базовые понятия

1
2

Базовый характер данной части книги вовсе не означает ее элементарно-


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

Введение в систему понятий


программирования

Назначение данной главы — ввести обучающегося в систему понятий


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

§ 1.1. ЯЗЫКИ И СИСТЕМЫ ПРОГРАММИРОВАНИЯ

1.1.1. Сравнение программ на разных языках

Начнем с рассмотрения нескольких примеров. Всем данным текстам про-


грамм при исполнении соответствует действие, состоящее в распечатке стро-
ки «Hello World!».
Программа 1.1.1

/*Язык C.*/
#include <stdio.h>
int main(void)
{printf("Hello World!");
return 0;}

3
4 ГЛАВА 1. ПОНЯТИЯ

Программа 1.1.2
//Java
public class HelloWorld {
public static void main (String[] args) {
System.out.println("Hello World!");
}}

Программа 1.1.3
(*Паскаль*)
program First (Output);
begin
writeln(’Hello World!’)
end.

Программа 1.1.4
comment Algol 68 comment
begin
println(‘Hello World!’)
end

коммент Русский Алгол 68 коммент

начало
печатать(‘Hello World!’)
конец

comment Еще два представления comment

(println(‘Hello World!’))

(печатать(‘Hello World!’))
1.1. ЯЗЫКИ И СИСТЕМЫ ПРОГРАММИРОВАНИЯ 5

Программа 1.1.5
Лисп:
( PRINT “Hello World!”)

Программа 1.1.6
Рефал
$ENTRY GO{=<Prout ’Hello World!’>};

Программа 1.1.7
Prolog
:-Print(’Hello World!’);
Сравним все эти программы. Что у них является общим?
1. Все они представлены в виде текстов: последовательностей символов,
размещенных на нескольких строчках.
2. В каждом языке имеется понятие строки (последовательности симво-
лов нефиксированной длины). Строки оформляются во всех языках при-
мерно одинаково: с использованием кавычек как обрамляющих симво-
лов. Правда, вид конкретных кавычек меняется от языка к языку.
3. Каждая из этих программ имеет конструкцию, выполнение которой при-
водит к распечатыванию строки.
4. Все они при их выполнении делают одно и то же: печатают строку
«Hello World!»
В чем же их отличия? Видны некоторые различия в записи, а также в пра-
вилах построения. Таким образом, напрашивается вывод, что, освоив один
язык программирования, можно понимать тексты программ на большинстве
других языков практически так же легко, как образованный русский человек
может читать украинский либо, в крайнем случае, польский (это чуть труд-
нее) текст.1
Казалось бы, что все эти программы, естественно, после преобразования
каждого текста в исполняемый код, приводят к одной и той же машинной
программе, исполняемой компьютером. Как мы дальше увидим, это не так.
Конечно же, и здесь могут быть исключения, но это верно практически для всех т. н.
языков высокого уровня
1
6 ГЛАВА 1. ПОНЯТИЯ

1.1.2. Язык, его реализация и среда программирования

Начнем с анализа понятия программы. Существуют различные взгляды


на то, что такое программа. Например:
1. Запись алгоритма в виде, однозначно понятном для человека.

2. Последовательность символов из заданного (конечного) множества сим-


волов V , построенная в соответствии с определенными правилами (фор-
мально это записывается L ⊆ V ∗ , где L — множество правильно по-
строенных выражений, которое в данном контексте называется языком,
а V ∗ — множество всех возможных последовательностей символов).

3. Последовательность команд некоторого абстрактного вычислителя, ко-


торый специально определен для данного языка. То есть сам текст про-
граммы может рассматриваться как изображение команд этого вычи-
слителя.

4. Текст, который может быть преобразован в исполнимый код на машин-


ном языке.

Мы видим, что программа должна удовлетворять следующим требова-


ниям:

1. Должна быть однозначно понятна для человека.

2. Должна удовлетворять точно определенным правилам построения (на-


зываемым формальным синтаксисом языка).

3. Должна быть понятна для исполнителя (абстрактного или конкретного


вычислителя), что может достигаться различными средствами.

Понятия исполнитель и вычислитель в данном тексте (точно так же, как и в


абсолютном большинстве работ по информатике) понимаются как синони-
мы.
Минимальная осмысленная для исполнителя последовательность симво-
лов называется лексемой. Например, в языке С "Ник — не лексема, поскольку
такая последовательность символов не осмыслена, а "Ник" — уже лексема.
Заметим, что понятие лексемы зависит от контекста. В операторе x=x1; сим-
вол x лексемой является только в первом своем вхождении, но не во втором,
где он всего лишь часть лексемы x1.
1.1. ЯЗЫКИ И СИСТЕМЫ ПРОГРАММИРОВАНИЯ 7

Минимальная последовательность лексем, которая способна вызывать дей-


ствия вычислителя, называется конструкцией языка. Во многих языках про-
граммирования среди конструкций языка выделяются так называемые опе-
раторы, обозначающие законченные действия.2
Понятие оператора характерно для т. н. операционных языков, к числу
которых относятся языки FORTRAN, ALGOL 60, Алгол 68, C/C++/C#, Pascal,
Object Pascal, Ada, Modula-2. Только что перечисленные языки имеют, по-
мимо общей основы, целый ряд общих второстепенных признаков, которые
делают их настолько же близкими, насколько близки естественные языки од-
ной и той же группы (например, славянские), и очень многие замечания мож-
но высказать сразу для всех данных языков. Поэтому перечисленные выше
языки в данной книге будут называться языками традиционного типа или
просто традиционными языками.3
Пример 1.1.1. В языках традиционного типа переменная x является кон-
струкцией, поскольку она может вызвать действия чтения ее значения либо
записи значения в нее. Вызов функции sin(x) означает вычисление соответ-
ствующего значения имеющейся в системе программирования реализации
математической функции sin . Этот вызов оператором не является, а вот при-
сваивание
y=sin(x); или y:=sin(x); или Y=SIN(X) или y:=sin(x)
уже оператор.
Конец примера 1.1.1.
Имея в виду операторы, осмысленно говорить и о порядке действий вы-
числителя, и о его соответствии или несоответствии порядку следования опе-
раторов-конструкций в тексте программы; о том, какие команды конкретного
вычислителя выполняются при исполнении оператора как команды абстракт-
ного вычислителя.
Определение 1.1.2. Реализация языка — это комплект программ, которым
обеспечиваются:
Конечно же, это не формальные определения, а указания на то, что в языке программи-
рования, как и в естественном языке, выделены структурные составляющие, за которыми
2

закрепляется их смысл.
Не все операционные языки мы относим к языкам традиционного типа. В частности, язык
Java является операционным, но из-за некоторых принципиальных отличий данного языка
3

и языков, перечисленных выше, мы его к традиционным языкам не относим.


8 ГЛАВА 1. ПОНЯТИЯ

— Поддержка операций с исходной программой: ввод, редактирование и


сохранение текста, анализ синтаксических ошибок.

— Подготовка синтаксически правильной программы к исполнению на


конкретном вычислителе.

— Поддержка на конкретном вычислителе всех возможных действий аб-


страктного вычислителя.

Кроме того, в комплект реализации языка могут включаться другие програм-


мы, удовлетворяющие требованиям, логически связанным с вышеперечислен-
ными.
Конец определения 1.1.2.
Реализация языка неразрывно связана с окружающей ее операционной
средой, т. е. с базовыми средствами, доступными при работе на данном ком-
пьютере в данной системе. Вместе они составляют систему программиро-
вания для данного языка. Система программирования обязательно включает
следующие компоненты:

1. Файловая система для хранения текстов программ — как правило, это


общая часть программного обеспечения для различных систем на дан-
ном компьютере.

2. Редактор для ввода текста программы как последовательности симво-


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

3. Транслятор для преобразования текста программы к виду, в котором


она может исполняться, и указания ошибок, если преобразование не
удается. Транслятор — это не обязательно одна программа.

4. Библиотеки периода трансляции, которые используются в процессе


преобразования программного текста, например, для включения в него
стандартизованных фрагментов (чтобы программисту не нужно было
их повторять в своих программных текстах).
1.1. ЯЗЫКИ И СИСТЕМЫ ПРОГРАММИРОВАНИЯ 9

5. Библиотеки периода исполнения, которые содержат программы стан-


дартных действий абстрактного вычислителя (стандартная библиоте-
ка, иногда называемая библиотекой поддержки языка). Они связывают
язык с операционной средой.
6. Отладчик — программа, позволяющая отслеживать ход вычислений
программ на данном языке.
Кроме перечисленного, системы программирования обычно включают в себя
еще:
7. Пользовательские библиотеки, которые содержат программы на дан-
ном языке (в текстовом или преобразованном виде), используемые в
составляемых программах для задания специальных вычислений (за-
висят от среды программирования);
8. Средства поддержки разработки программ — разнообразные инстру-
ментальные программы, которые применяются в процессе проектиро-
вания, составления и отладки программ.
Из перечисленного выше видно, что конкретная реализация языка обычно
привязывается к операционной среде и, соответственно, к конкретному вы-
числителю.4
Теперь разберем по очереди, как подготавливается к исполнению и ис-
полняется каждый из текстов программ, рассмотренных в предыдущем па-
раграфе.
Пояснение к программе 1.1.1.

/*Язык C:*/
#include <stdio.h>
int main(void)
{printf("Hello World!");
return 0;}

Строка
#include <stdio.h>
В принципе это не исключает переносимости (т. е. возможности исполнять программу
без всяких изменений в разных вычислительных обстановках), и уже были эксперименты
4

по созданию переносимых систем программирования.


10 ГЛАВА 1. ПОНЯТИЯ

отражает то, что написанный текст программы должен быть расширен путем
вставки вместо этой строки текста, именуемого stdio.h, и уже такой расши-
ренный текст подается транслятору. одно из правил формирования текстов
программ на языке С). Таким образом, stdio.h является библиотекой периода
трансляции. Фактически stdio.h содержит все то, что нужно для организации
ввода и вывода данных, описывая компоненты библиотеки периода исполне-
ния.
Текст после #include <stdio.h> — это описание функции без параметров,
вырабатывающей целое (ее заголовок — int main (void)), которая печатает
строку:

После обработки этого текста транслятором, в частности, подключается би-


printf ("Hello World!");.

блиотечная функция периода исполнения printf, описание которой взято из

Пояснение к программе 1.1.2.


stdio.h.

// Java:
public class HelloWorld {
public static void main (String[] args) {
System.out.println("Hello World!");
}}

Строка

в тексте на языке Java указывает на то, что программа является публичным


public class HelloWorld

(доступным всем) классом, поименованным как HelloWorld. К этому клас-


су можно будет обращаться для исполнения содержащихся в нем действий.
Внутри класса HelloWorld определяется функция static void main, с которой
начинаются вычисления. А внутри нее происходит обращение к системным
средствам вывода строк, содержащимся в классе System.out:

Это обращение делается из объявляемой функции main.


System.out.println("Hello World!");

Пояснение к программе 1.1.3.

(* Pascal:*)
program First (Output);
1.1. ЯЗЫКИ И СИСТЕМЫ ПРОГРАММИРОВАНИЯ 11

begin
writeln(’Hello World!’)
end.

(*PASCAL*)
PROGRAM FIRST (OUTPUT);
BEGIN
WRITELN(’Hello World!’)
END.

Последние два текста на стандартном5 языке Pascal — даже с точки зре-


ния синтаксиса одна и та же программа. Из этого видно, что в языке не раз-
личаются заглавные и строчные буквы. Эта особенность восходит к исполь-
зованию древних печатающих устройств, для которых не было такого разли-
чия.
Возможность печатать что-либо для языка Pascal обусловлена указанием
Output в заголовке программы, который подключает соответствующую би-
блиотеку периода исполнения и инициализирует ее.
Пояснение к программе 1.1.4.
Алгол 68:

begin
println(‘Hello World!’)
end

comment Русский Алгол 68 comment

начало
печатать(‘Hello World!’)
конец

comment Еще два представления comment


(println(‘Hello World!’))

Мы специально не воспользовались здесь расширениями, предоставляемыми, например,


Delphi, чтобы четко видеть разницу между эталонным, или стандартным языком, и его
5

диалектами. Диалекты существуют лишь в контексте данной машины и транслятора.


12 ГЛАВА 1. ПОНЯТИЯ

(печатать(‘Hello World!’))

Алгол 68 демонстрирует четыре текста одной и той же программы. В язы-


ке предусмотрены и варианты нотации для национальных алфавитов (срав-
ните первый и второй тексты), и возможности скорописи (сравните первый
и третий тексты).
Для этого языка постулируется существования стандартного вступле-
ния и заключения, которые окружают написанный текст. Считается, что ис-
ходным текстом для трансляции является то, что получится в результате со-
единения текстов вступления, текста, написанного программистом, и текста
заключения. Здесь прослеживается аналогия с #include языка С, но название
файла, который должен расширять написанный текст, явно не задается.
Пояснение к программе 1.1.5.

Программа на языке Lisp представляет собой функцию PRINT с аргументом


( PRINT "Hello World!")

"Hello World!". Вычисление этой функции — так называемое S-выражение,


представляющее аргумент самой функции. В данном случае это "Hello World!".
При вычислении PRINT происходит побочный эффект, т. е. действие, кото-
рое сопровождает получение значения. В данном случае это печать аргумен-
та функции, т. е. требуемое действие. Приведенная программа распечатает
строку дважды: в первый раз, когда выполняется указанный побочный эф-
фект, а во второй — из-за следующей причины. Лисп-программа всегда за-
вершает свои вычисления распечаткой значения функции, полученного в ка-
честве результата. S-выражение "Hello World!" и есть тот самый результат.
Пояснение к программе 1.1.6.

Программа на Рефале представляет из себя функцию Go. Эта функция ра-


$ENTRY GO{=<Prout ’Hello World!’>};

ботает с полем зрения, которое уже не может быть прямо представлено как
совокупность ячеек вычислителя. Она проверяет, что поле зрения пусто, и
подставляет вместо пустого выражения то, что идет справа от знака равен-
ства: вызов стандартной функции, печатающей строку и опять очищающей
поле зрения. Поскольку в поле зрения функций больше не осталось, програм-
ма заканчивает работу, а поскольку поле зрения пусто, больше ничего не пе-
чатается.
Пояснение к программе 1.1.7.
:-Print(’Hello World!’);
1.1. ЯЗЫКИ И СИСТЕМЫ ПРОГРАММИРОВАНИЯ 13

Программа на языке Prolog представляет собой цель, которая должна быть


достигнута. В типичной ситуации описываются еще и данные, необходимые
для достижения цели, в нашем простейшем случае таких данных не нуж-
но. Вызывается стандартная функция, которая печатает строку и исчезает.
Поскольку не недостигнутых целей более не остается, программа завершает
работу. Prolog так же, как Рефал, имеет дело сразу со сложными данными и
так же не имеет прямой связи с физическим строением машинной памяти.
Теперь мы видим, что действия, предписываемые языком, совершенно
по-разному достигают одних и тех же целей. С чем это связано? Ответ в том,
что каждый язык определяет свою модель вычислений. Иногда эти модели
довольно близки, несмотря на существенные различия в изобразительных
средствах языков. Для таких языков программист по существу пишет од-
но и то же, и функции систем программирования весьма близки. Различия в
оформлении связаны, например, с тем, как соотносится программа с ее окру-
жением и как задаются общие для всех программ действия. Именно поэтому
мы внесли наиболее распространенные из языков, имеющих близкие модели
вычислений, в список традиционных языков.
Существенные различия моделей вычислений возникают в случае разно-
го устройства данных, с которыми работают программы (сравните, напри-
мер, C и Рефал). Но стоит помнить, что одна и та же модель вычислений на
разных вычислительных машинах и в разных операционных средах реализу-
ется по-разному. Могут быть различны разрядные сетки, способы предста-
вления чисел, способы вызова процедур. Явно позаботились об учете опера-
ционной среды, чтобы обеспечить переносимость программного обеспече-
ния, пожалуй, лишь в языке Ada, являющемся стандартом для Министерства
Обороны США.
В свою очередь, операционная среда оказывает обратное влияние на язы-
ки, и сформировались следующие понятия, их связывающие.
Определение 1.1.3.
Эталонный (стандартный) язык программирования — язык, задавае-
мый безотносительно операционной среды и без привлечения знания об устрой-
стве конкретного вычислителя (компьютера). Именно эталонным языком фик-
сируется абстрактный вычислитель для данного языка.
Стандартные библиотеки, или стандартное библиотечное расширение
языка — комплект библиотечных средств, которые стандарт языка предпи-
сывает для всех его реализаций.
Прагматические расширения языка — конструкции, предписывающие
14 ГЛАВА 1. ПОНЯТИЯ

изменение поведения абстрактного вычислителя с теми или иными целями,


например, для ускорения выполнения программы. Различаются эталонные
прагматические расширения, задаваемые в его стандартном описании (руко-
водстве), и конкретные прагматические расширения, появляющиеся в связи с
реализацией языка на данном вычислительном оборудовании, в данной опе-
рационной системе.
Библиотеки конкретной системы программирования — комплект библио-
течных средств, которые реализованы в данной среде программирования (при
правильной реализации они включают в себя стандартные библиотеки).
Конкретный, или реализованный язык — диалект стандартного языка вме-
сте с его конкретной реализацией на данной вычислительной машине.
Конец определения 1.1.3.
К сожалению, иногда в описаниях языков, а в особенности в руководствах
для программиста сведения о разных аспектах языка и системы перемешаны,
и в результате трудно оценивать предлагаемые средства, сравнивать возмож-
ности различных языков.
Применительно к языку С, необходимыми и достаточными составляю-
щими реализации являются:

1. Препроцессор — программа, подготавливающая текст к обработке. Текст,


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

2. Компилятор — программа, переводящая (транслирующая) текст про-


граммы в машинный код, который затем по отдельному указанию мо-
жет быть исполнен. В результате компиляции строится так называемый
загрузочный модуль. Альтернативой компилятору является интерпре-
татор — программа, которая одновременно и анализирует, и испол-
1.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ ФОН НЕЙМАНА И ТРАДИЦИОННЫЕ ЯЗЫКИ 15

няет текст. Для реализации языка С обычен компилятор, транслирую-


щий подготовленный препроцессором текст, и в результате получается
загрузочный модуль.
3. Редактор связей — специальная программа, обеспечивающая возмож-
ность работы загрузочного модуля совместно с другими программами
данной операционной среды. В ряде случаев (например, в ОС UNIX) ре-
дактор связей не требуется, поскольку решение соответствующих задач
возлагается на операционную систему.
4. Стандартная библиотека, которая объединяет в себе библиотеки пе-
риода трансляции (статические библиотеки) и периода исполнения (ди-
намические библиотеки).
5. Отладчик — программа, обеспечивающая отслеживание хода вычи-
слений путем пошагового исполнения исходной программы в режиме
диалога. Обычной для С и для большинства других систем программи-
рования является организация отладки, которая связывает шаг испол-
нения со строкой исходного текста.

МОДЕЛЬ ВЫЧИСЛЕНИЙ ФОН НЕЙМАНА И ТРАДИЦИОННЫЕ ЯЗЫ-


КИ
§ 1.2.

Из материалов предыдущего раздела видно, что подходы к решению про-


граммистских задач при использовании различных языков отличаются друг
от друга. Иногда эти различия значительны, иногда сводятся лишь к тексто-
вому представлению программы, а потому их можно считать непринципи-
альными. Если различия считаются непринципиальными, то мы говорим о
том, что языки имеют сходную модель вычислений.
В дальнейшем мы часто будем пользоваться термином “традиционные
языки”, понимая под этим языки, модель вычислений которых унаследована
от так называемой архитектуры фон Неймановского типа, принятой в пода-
вляющем большинстве существовавших и существующих вычислительных
машин. Эта архитектура характеризуется следующими принципами:
1. Наличие трех элементов вычислительной системы:
(a) память, которая предназначена для хранения произвольных зна-
чений. Значения на аппаратном уровне представляются как после-
довательности битов;
16 ГЛАВА 1. ПОНЯТИЯ

(b) процессор, который способен выполнять команды, т. е. интерпре-


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

2. Однородность памяти и адресация. Память машины рассматрива-


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

3. Пассивность памяти и активность процессора. Ячейка памяти все-


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

4. Роль устройства управления. Управляющее устройство содержит адрес


1.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ ФОН НЕЙМАНА И ТРАДИЦИОННЫЕ ЯЗЫКИ 17

команды, назначаемой для выполнения процессором. Если эта коман-


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

5. Наличие канала связи между памятью и процессором. Для пере-


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

(a) когда требуется подать для выполнения процессором очередную


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

Конкретная схема канонической фон Неймановской машины дополняется


устройствами ввода и вывода данных, активизация которых осуществляется
при выполнении соответствующих команд процессором (возможны различ-
ные варианты взаимодействия таких устройств с остальным оборудованием:
связь с памятью автономная, через процессор или через специальную шину
данных и др.).
На рис. 1.1 показано взаимодействие частей фон Неймановской вычисли-
тельной машины. Сплошными стрелками отмечена передача информации по
каналу (двунаправленные стрелки — передача в оба направления). Пунк-
тирные стрелки обозначают действия с управляющим устройством, которые
18 ГЛАВА 1. ПОНЯТИЯ

осуществляются в связи с исполнением команды К О1 О2, размещенной в па-


мяти по адресу 3: непосредственно до нее (запрос управляющего устройства
кода команды по адресу 3) и после нее (указание на необходимость запроса
команды, следующей в памяти за исполняемой).

Память Управляющее
0 устройство

+1
3
Процессор

К О1 О2

Канал связи

Рис. 1.1. Схема выполнения двухадресной команды на фон Неймановской


машине

Классическая схема сформировалась под давлением требования миними-


зировать части системы, критичные к сбоям оборудования, поскольку суще-
ствовавшая в середине XX века техническая база не позволяла решать задачу
надежности в должной мере. В результате только процессор можно назвать
активным устройством машины. По этой же причине выбран довольно при-
митивный способ управления: переход к следующей команде или к команде
1.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ ФОН НЕЙМАНА И ТРАДИЦИОННЫЕ ЯЗЫКИ 19

по принудительно задаваемому адресу. К тому же неразвитость отрасли в


целом не позволяла даже ставить задачу повышения эффективности вычи-
слений за счет выбора подходящей модели вычислений.
По мере накопления опыта работы с вычислительными машинами появи-
лась потребность увеличения быстродействия процессора и объемов памяти.
И очень скоро выяснилось, что каноническая схема фон-неймановских вы-
числений препятствует повышению производительности. По этой причине
сегодня эта схема всегда модифицируется. Ниже приведены некоторые, но
довольно характерные модификации схемы:
a) У процессора появляются собственные ячейки со специальной адресаци-
ей, не требующие обращения к памяти. С их помощью можно уменьшать
адресность команд машины, передавать промежуточные результаты вы-
числений от команды к команде, выполнять иные локальные действия.
Они называются быстрыми регистрами процессора.

b) Уменьшается размер команд, в частности, их адресность. Наиболее упо-


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

c) Команды кодируют довольно сложные действия над операндами, которые


объединяют наиболее часто употребляемые последовательности элемен-
тарных операций.

d) Процессор снабжается памятью для команд, которые вероятно будут вы-


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

e) Память разбивается по уровням доступа: регистры процессора, область


быстрого доступа с непосредственной адресацией, область, адресация ко-
торой требует предварительного указания некоторого сегмента, ячейки
которого адресуются непосредственно, и т. д. Другой вариант той же идеи —
возможность переключения с сегмента на сегмент с сохранением более
медленного доступа к ячейкам вне текущего сегмента.
Полезность подобных модификаций очевидна. Однако есть причина, из-за
которой возможности роста эффективности фон Неймановских вычислений,
20 ГЛАВА 1. ПОНЯТИЯ

пусть даже трансформированных в указанных отношениях, ограничены. По-


вышение быстродействия процессора как активного элемента оборудования
приводит к росту скорости счета лишь в тех пределах, которые определе-
ны скоростью канала связи между процессором и памятью: если она невы-
сока, то процессор будет все равно простаивать, ожидая очередной коман-
ды, операндов или окончания выполнения присваивания6 . В свою очередь,
рост скорости канала связи ограничен из-за постоянно растущей потребно-
сти расширения объема памяти. На эти принципиально непреодолимые огра-
ничения классической модели вычислений указал Бэкус еще в середине се-
мидесятых годов, назвав канал связи памяти с процессором узким местом
(буквально bottleneck) модели фон Неймана. Уместно заметить, что много-
процессорность, а тем более кэширование и прочие модификации канони-
ческой модели являются лишь полумерами, позволяющими расширить, но
никак не ликвидировать узкое место (впервые это было отмечено в лекции
Бэкуса [82]).
В качестве альтернативы фон Неймановской модели укажем лишь на од-
ну из идей радикально новой модели, реализация которой позволяет значи-
тельно обогатить возможности параллельного счета. Для достижения парал-
лельности действий увеличивается активность элементов вычислительной
системы и изменяются принципы управления вычислениями.
Идея заключается в том, чтобы разрешить выполнение всех команд, для
которых к данному моменту готовы операнды (вычислены их значения). Цен-
трализованное управление ликвидируется и появляется возможность глубо-
кого распараллеливания вычислений, если оборудование обеспечит факти-
чески одновременное выполнение готовых команд. Прямое развитие данной
идеи наталкивается на массу тонких вопросов синхронизации. Простейший
и наиболее очевидный из конфликтов синхронизации — попытка одновре-
менной записи результатов нескольких команд в одну ячейку памяти. Поэто-
му присваивания становятся тормозом на пути программирования. Более то-
го, можно теоретически предвидеть (хотя точной оценки еще не было сдела-
но; впрочем, даже там, где такие оценки имеются, они игнорируются и замал-
чиваются), что распараллеливание программ, написанных в традиционном
стиле — мартышкин труд7 . Стиль программирования можно корректировать
Иллюстрацией интенсивности работы канала фон Неймановской машины может служить
схема выполнения команды на рис. 1.1, которая показывает, что для выполнения двухадрес-
6

ной команды К О1 О2 требуется шесть обращений к каналу.


7
И. Н. Скопин. Я считаю этот эпитет слишком сильным, но соавтор не согласился смяг-
1.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ ФОН НЕЙМАНА И ТРАДИЦИОННЫЕ ЯЗЫКИ 21

различными способами, например, путем замены хранения результата пере-


дачей его в качестве операндов ранее заблокированным командам, которые
тем самым активизируются. Какие элементы оборудования будут выполнять
такие команды, на уровне идеи не имеет значения — это задача техническая
(т. н. задача динамической коммутации).
Здесь стоит обратить внимание на два взаимосвязанных момента. Во-пер-
вых, часто нет нужды в хранении промежуточных результатов счета, и, как
следствие, потребность в пассивной памяти существенно снижается. Во-вто-
рых, ликвидируется примитивное устройство управления, а его роль прини-
мают на себя элементы оборудования, отвечающие за выяснение готовности
команд к выполнению. Это — одна из схем, которая подчиняет управление
потокам данных (data flow). Такие схемы противопоставляются управляю-
щим потокам (control flow) фон Неймановских вычислений.
Представленная идея неоднократно воплощалась в оборудовании (т. н.
машины потоков данных). Но каждый раз узким местом для таких машин
оказывалось несоответствие стиля программ, требуемых для них, тем сти-
лям, к которым привыкли программисты. Как следствие, в каждом из таких
процессоров делались грубые системные ошибки, приводившие к затрудне-
ниям в программировании, а также к понижению скорости и гибкости вычи-
слений. Тем не менее, поскольку скорость света очень скоро поставит предел
механическому увеличению пропускной способности каналов, к подобным
схемам придется вернуться на другом уровне.
Одной из сторон машин потоков данных является повышение активно-
сти памяти, в которой размещаются команды и их операнды. По этому пути
можно пойти еще дальше, сделав всю память активной. В этом случае, во из-
бежание полной анархии, обычно память имеет общее устройство управле-
ния, задающее одну команду (или несколько команд, в зависимости от како-
го-то очень быстро проверяемого условия), которую выполняют все ячейки
(ассоциативная память). История реализации этой идеи аналогична исто-
рии машин потоков данных: аппаратные предпосылки есть, но эффективных
способов программирования нет.
История с ассоциативной памятью и машинами потоков данных показы-
вает, что, несмотря на очевидные преимущества для отдельных классов задач
других моделей вычислений и на очевидную неэффективность классической
модели вычислений, не происходит переход к другим более перспективным
с точки зрения ускорения счета моделям. Почему? Это очень непростой во-
чить его.
22 ГЛАВА 1. ПОНЯТИЯ

прос, связанный как со сложившимися традициями, так и с огромным грузом


накопленного программного обеспечения. В частности, другие модели вы-
числений очевидно неуниверсальны, ориентированы на некоторые классы
задач, а иллюзия универсальности, основанная на примитивно истолкован-
ной теореме об универсальном алгоритме, сопутствует традиционной моде-
ли вычислений. Но, пожалуй, более всего консерватизм обусловлен тем, что
сегодняшняя армия программистов воспитана на фон Неймановской модели,
и она уже не в состоянии перестроиться. Одна из причин тому — так называе-
мые традиционные языки, ключевые средства которых “выросли” из модели
фон Неймана. Эти языки стали основой для выработки наиболее употребля-
емых стилей программирования8 . Ниже перечислены особенности языков,
которые унаследованы от фон Неймановской модели вычислений:
a) Присваивание значений. Языковая конструкция, предназначенная для ло-
кального запоминания результата вычислений, получившее название опе-
ратора присваивания значения переменной, или засылки значения, — пря-
мой потомок передачи значения по каналу связи от процессора к памяти.
В большинстве языков программирования понятие переменной практиче-
ски всегда рассматривается как аналог ячейки (или группы ячеек) памяти.
b) Операторы. Основной конструкцией действия в языке является опера-
тор. Выполнение одного оператора зависит от выполнения другого толь-
ко в том смысле, что более ранние вычисления могут менять память (со-
держимое некоторых ячеек), в контексте которой выполняются последу-
ющие операторы. Иными словами, зависимость операторов посредством
использования памяти в точности соответствует тому, что имеет место для
команд фон Неймановской машины.
c) Структура управления. Последовательное выполнение операторов, тек-
стуально следующих друг за другом, совершенно аналогично последо-
вательному выполнению команд процессором. Оператор перехода, т. е.
принудительное указание того оператора, который должен выполняться
следующим, ничем не отличается от машинной команды безусловной пе-
редачи управления по адресу. То же можно сказать и о ситуациях, когда
В последующих разделах мы уделим особое внимание вопросам, связанным со стилями
программирования. Здесь же следует отметь, что универсального стиля программирования
8

не существует, и действительно квалифицированный программист не будет ограничивать


себя использованием какого бы то ни было, но одного стиля, он всегда стремится применять
средства, которые соответствуют решаемой задаче в максимальной степени.
1.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ ФОН НЕЙМАНА И ТРАДИЦИОННЫЕ ЯЗЫКИ 23

вычисления определяют, какой оператор будет выполняться следующим.


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

d) Приведения. В традиционных языках обычным является употребление зна-


чения одного типа, которое присваивается переменной другого типа. Это
не только присваивание целого вещественной переменной, когда суще-
ствует “разумная” интерпретация целого как округленного вещественно-
го, или подобное преобразование в выражениях, но и неоднозначно трак-
туемые преобразования, смысл которых нельзя понять в отрыве от кон-
кретной программы. Автоматические приведения значений к типам, опре-
деляемым контекстом использования, — прямое следствие соглашения
об однородности памяти. И хотя в развитых языках программирования
можно заметить стремление избегать неконтролируемых приведений, сам
факт их осуществимости считается чуть ли не достоинством языка.

e) Подпрограммы. Возможность объявлять некоторые последовательности


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

Если в архитектурах вычислительных машин отходят от принципов фон Ней-


мана преимущественно для повышения эффективности счета, то побудитель-
ных причин для нарушения канона при конструировании языков программи-
рования гораздо больше. Это и стремление к большей выразительности, и
уже упомянутая фиксация типовых приемов программирования, и помощь в
отыскании ошибок в программах. Наиболее часто нарушается принцип одно-
родности памяти, отход от которого дает возможность содержательной трак-
товки хранимых данных, что, в свою очередь, способствует и выразительно-
24 ГЛАВА 1. ПОНЯТИЯ

сти, и надежности программ9 .


Для большинства существующих языков характерно, что программист
может (и должен!) думать о выполнении программы как о работе автомата,
имеющего активный процессор и пассивную память (возможно, разнород-
ную), связанные каналом. Появление в языке структуры памяти и так назы-
ваемых высокоуровневых конструкций, а также возможности использования
библиотек — это всего лишь повышение уровня абстрактного вычислителя
языка, который, тем не менее, трактуется как автомат фон Неймановского ти-
па, пусть даже с весьма сложными командами. Если язык предусматривает
возможность отхода программиста от фон Неймановского образа мышления,
то мы называем этот язык нетрадиционным. С этой точки зрения такие со-
временные языки, как С++ или Ada, остаются традиционными, а, к примеру,
такой “патриарх языкотворчества”, как LISP — нетрадиционен.
Данное разграничение традиционных и нетрадиционных языков нельзя
рассматривать в качестве точного определения. Достаточно развитый язык
позволяет выражать алгоритмы в разных стилях, и программист может себе
позволить строить тексты программ (фрагментов программ) в соответствии с
особенностями решаемой задачи, отходя от тех правил, которые поддержива-
ются явно языком. Но если в языке не хватает возможностей, адекватных для
такого построения, а это, вообще говоря, обычная ситуация, то приходится
прибегать к моделированию нужных возможностей подручными средствами.
Когда при этом требуется забота об эффективности, а это опять-таки обыч-
но, необходимо учитывать реалии конкретного компьютера. Как следствие,
весьма вероятны нарушение адекватности моделирования и потеря нагляд-
ности решения.
В заключение настоящего раздела приведем перечень (далеко не полный!)
языков, которые мы относим к классу традиционных, сопровождая его не-
большими комментариями.
1. FORTRAN. Самый почтенный долгожитель среди языков программи-
Как уже отмечалось, отклонение от принципа однородности памяти для повышения эф-
фективности используется и в оборудовании. Сегментация, быстрые регистры, кэширова-
9

ние — далеко не единственные решения этого плана. Нарушение принципа здесь лишь ча-
стичное: ячейка любого из видов по-прежнему способна принимать значения разного типа,
по-прежнему смысл значения раскрывается только процессором. Заслуживают упоминания
и более радикальные архитектуры, предусматривающие тегирование (см. стр. 62), а также
полное отделение в памяти областей, в которые можно записывать данные и команды (в
обычном режиме выполнения программ процессору не разрешается записывать что-либо в
область команд, в результате повышается надежность программ).
1.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ ФОН НЕЙМАНА И ТРАДИЦИОННЫЕ ЯЗЫКИ 25

рования, постепенно вбирающий в себя новые веяния из области языкотвор-


чества, но сохраняющий преемственность. FORTRAN сегодня — это эклек-
тическое собрание полезных и архаичных средств. Упоминая FORTRAN в
дальнейшем, мы чаще всего говорим о языке, сформировавшемся в середи-
не семидесятых, о так называемом FORTRAN-77. Модель вычислений языка
FORTRAN в точности соответствует представлению середины XX века о том,
что нужно для реализации вычислительных алгоритмов и что можно реали-
зовать на компьютерах фон Неймановского типа (тогда это был единствен-
ный тип компьютеров). Таким образом, это наиболее типичный представи-
тель традиционных языков. Разработчики ранних версий языка FORTRAN не
принимали в расчет полезности независимого от системы программирования
определения языка. И его формальное определение по существу задавалось
транслятором. В результате случайные решения, принятые в ранних транс-
ляторах последующие разработчики вынуждены сохранять на многие годы.
Для этого языка такое положение дел объяснимо и простительно. Удивитель-
но, что и сегодня, в XXI веке появляются языки, зависимые от реализации,
которые претендуют на универсальное применение.
2. Algol 60. Язык, сформировавшийся под давлением идеи осуществимо-
сти одновременно понятного для компьютеров и для человека представле-
ния алгоритмов. Очень скоро утопичность идеи выяснилась, и Algol 60 (до
него был еще Algol 58 — индексация по году публикации первой официаль-
ной версии, но лишь Algol 60 был утвержден в качестве стандарта и полу-
чил широкое распространение; Algol 60 часто называют в литературе просто
Алгол) стал не просто более строгим, но и более многословным аналогом
FORTRAN. В Алголе появились и принципиальные новшества, выгодно от-
личающие его от предшественника. Это, прежде всего, определение языка,
независящее от транслятора, структурность описания языка и определение
действий абстрактного вычислителя на базе понятий из такого описания.
В Алголе предложена структурная организация контекстов выполнения
конструкций, которая выводит вычислитель языка за рамки канона однород-
ности и прямой произвольной индексной адресации памяти, — так называ-
емая блочная структура программы. Этой структуре отвечает определенная
дисциплина распределения памяти в рабочих программах, которые получа-
ются в результате трансляции алголовских текстов. Эта дисциплина, полу-
чившая название стековой, с тех пор используется повсеместно при реали-
зации языков программирования. Стековая дисциплина распределения памя-
ти является ограничением доступа к векторно организованной памяти, эле-
менты которой могут указываться в произвольном порядке, но именно это
26 ГЛАВА 1. ПОНЯТИЯ

ограничение дает возможность эффективно отображать блочную структу-


ру. Более того, поскольку доступ к памяти регламентирован, можно гово-
рить о внедрении этого регламента в систему команд компьютера, хотя бы
для уменьшения адресности команд. И сегодня модернизации фон Нейма-
новской модели вычислений, связанные (в большей или меньшей степени)
с таким регламентом, являются неотъемлемыми для современных вычисли-
тельных архитектур. Можно сказать, что стековая дисциплина стала частью
модернизированной фон Неймановской архитектуры компьютера.
Если FOTRTRAN заслужил право считаться выдающимся достижением
программистского языкотворчества из-за огромного прикладного значения,
то Algol-60 также следует рассматривать как безусловное достижение в дан-
ной области, связанное в первую очередь со строгостью описания, отстра-
ненного от конкретного вычислителя, с новыми хорошо проработанными
конструкциями, со структурностью. Не случайно именно Алгол стал отправ-
ной точкой развития большинства существовавших и до сих пор существу-
ющих языков программирования. Он стал базой для многих теоретических
разработок, прояснивших основные языковые и программистские понятия.
Появились и до эры персональных компьютеров были популярными вычи-
слительные архитектуры, явно поддерживающие алголовскую организацию
памяти. Наиболее известными из них являются машины фирмы Burroughs
(США) и линия многопроцессорных вычислительных комплексов Эльбрус

3. Симула 67 характеризуется разработчиками как универсальный язык


(СССР).

моделирования. Это, как и его предшественник Симула-1, — правильное рас-


ширение Алгола 60, а потому сохраняет его достоинства и недостатки. Впро-
чем, именно недостатки этой базы стали главной причиной того, что указан-
ная пара языков не получила заслуживающее их распространение. Разработ-
чики Симулы 67 очень точно подошли к оценке программистского опыта при
воплощении его в языковые формы: не конкретные решения, а содержатель-
ные сущности, их обобщающие, воплощались в языке. В результате новые
для того времени конструкции, отражающие объектный взгляд на обрабаты-
ваемые данные, стали в будущем основой для весьма перспективных мето-
дологий программирования, в частности, для методологии объектно-ориен-
тированного программирования.
4. PL/1. Этот язык разрабатывался как попытка объединения всего, что
может в принципе потребоваться программисту. Неизвестно, так это или нет,
но вполне правдоподобно, что число “1” в названии языка есть амбициоз-
ное “единственный”, т. е. способный сделать бессмысленными любые другие
1.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ ФОН НЕЙМАНА И ТРАДИЦИОННЫЕ ЯЗЫКИ 27

языки программирования. Совсем не заботясь о чистоте объединения всех


известных программистских средств, разработчики языка предложили изна-
чально эклектичную коллекцию, которая лишь с натяжкой может быть назва-
на системой. Непознаваемость языка отмечается многими критиками, по вы-
ражению Э. Дейкстры, PL/1 — это “рождественская елка”, на которой можно
увидеть все, а не инструмент, который можно эффективно использовать. Раз-
розненность и несводимость к единым концепциям создает большие трудно-
сти и для реализации: системы программирования для PL/1 всегда выделяли
некоторый диалект, по существу определяя соответствующее подмножество
средств, зависящее от транслятора.
5. Алгол 68 — язык программирования, который, как и PL/1, претендо-
вал на всеобщность, но уже на базе математического обобщения разрознен-
ных средств программирования в фон Неймановской модели вычислений.
Можно сказать, что это PL/1 с элементами научности (С. Костер). Попытка
распространения средств, хорошо себя зарекомендовавших в одной сфере,
на уровень максимального обобщения в целом удалась, но разработчики за-
фиксировали в языковых формах лишь то, что уже было известно на момент
появления проекта языка. По этой причине в Алголе 68 нет хороших средств
поддержки модульного построения программ, в точном соответствии с фон
Неймановскими принципами перерабатываемые данные не являются актив-
ными. К недостаткам языка следует отнести весьма тяжеловесное формаль-
ное описание, совершенно недоступное для практического применения в ка-
честве руководства для программиста10 . Основная заслуга разработчиков Ал-
гола 68 в том, что они сумели реализовать на практике принцип обобщения
без потерь, продемонстрировали продуктивность этого принципа. Множе-
ство ссылок в данном курсе на фактически не используемый сейчас язык Ал-
гол 68 объясняется тем, что в этом языке, несмотря на явные недоделки и
недостатки в форме описания языка, одновременно имелись ряд блестящих
концептуально важных находок и система понятий, остающаяся до сего дня
наиболее последовательной и строгой. Некоторые из концепций Алгола 68
были восприняты в языках C и Ada, но сама система была при этом утеряна.
До сих пор в журнале «Communications of the ACM» периодически появля-
ются комментарии к опубликованным статьям, озаглавленные приблизитель-
но в следующем стиле: “Algol 68 ignorance considered harmful,” сводящиеся к
тому, что очередные «новые» предложения являются лишь ухудшенной вер-
сией того, что уже давно было реализовано в Алголе 68.
10
Такова цена, которую пришлось заплатить за полную формальную точность описания.
28 ГЛАВА 1. ПОНЯТИЯ

6. Pascal — один из самых распространенных языков программирова-


ния. По этой причине он представлен множеством диалектов и версий. Пер-
вый Pascal был предложен Н. Виртом (N. Wirth) в ответ на принципиальное
несогласие с позицией руководства рабочей группы по созданию Алгола 68,
в частности, с Ван Вейнгаарденом. Главное в критике Вирта — чрезмерная
сложность языка и особенно метода его формального описания.
Одной из побудительных причин создания языка стало осознание строй-
ной концепции системы типов, вычисляемых статически, т. е. во время транс-
ляции программы. Рассуждения о типах языка Pascal можно попытаться ре-
конструировать следующим образом. Если базовая машина допускает произ-
вольную трактовку смысла хранимых значений (принцип однородности па-
мяти), а для человека смысл значений первичен для понимания программы,
то в программе нужно фиксировать типы значений, а не только вычисления.
В результате появится возможность смыслового контроля программы, отде-
ленного от обработки данных. Что для этого нужно? Прежде всего опреде-
лить типы строго и дать точные механизмы построения одних типов через
другие. Нужно такое определение, которое позволяет, не зная о конкретных
значениях переменных, входящих в выражение, определять тип выражения.
В первом приближении тип отождествляется с множеством его значений, а
построение требуемых в программе множеств возлагается на транслятор. В
результате целый ряд потенциальных ошибок в программе может быть вы-
явлен при трансляции.
Есть и другие особенности языка Pascal, которые делают этот язык стро-
гим, есть и естественные ограничения универсальных возможностей, кото-
рые упрощают понимание языковых средств программистом. Язык создан
так, чтобы поддержать написание хороших программ (в том понимании, ка-
кое сложилось к концу шестидесятых годов). Все это позволило утверждать
Вирту, что он разработал язык для обучения студентов программированию.
Это утверждение не лишено основания, особенно если иметь в виду, что в
дополнение к языку существует методика обучения с его помощью, которую
разработал сам Вирт. Поэтому в учебных заведениях, где имеются серьез-
ные курсы информатики, Pascal остается самым распространенным языком
обучения.
Довольно скоро после своего появления Pascal становится очень попу-
лярным языком. Это породило потребность сделать на его базе язык програм-
мирования, который был бы более приближен к практическим нуждам. Сам
Вирт разрабатывает языки-наследники Pascal’я, которые в большей степени
поддерживают составление программ с независимыми модулями: Modula и
1.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ ФОН НЕЙМАНА И ТРАДИЦИОННЫЕ ЯЗЫКИ 29

Modula 2. Но это все-таки новые языки. Заслуживают внимания и более близ-


кие родственники стандартного Pascal’я, последовательно версией за верси-
ей предлагавшиеся коллективом фирмы Borland: языки и системы програм-
мирования линии Turbo Pascal. В них новые, весьма развитые возможности
органично укладываются в строй родительского языка, не нарушая концеп-
ций. К сожалению, в разработанной этой же фирмой системе Delphi язык
Object Pascal выпадает из этого ряда: концептуальной целостности сохра-
нить не удалось. В свое время мы будем иметь повод обсудить эту ситуацию
подробнее.
7. Язык С и другие машинно-ориентированные языки. Осознание то-
го, что программирование с использованием универсального языка, не отра-
жающего особенности конкретного вычислительного оборудования, не дает
возможности достичь предельной эффективности программ, возможной на
этом оборудовании, привело к появлению так называемых машинно-ориен-
тированных языков. Популярность таких языков зависит не только от рас-
пространенности оборудования, для которого они рассчитаны, но и от успеш-
ности выполненных с их помощь проектов.
В этом отношении показателен язык С, успех которого был во многом
обеспечен удачным решением операционной системы Unix. Для разработки
этой системы на машины серии PDP и был построен язык C, с помощью кото-
рого, в частности, предложен технологичный метод переноса программного
обеспечения. Справедливости ради следует сказать, что хотя вклад системы
Unix в успех С очень велик, но не менее важно и то, что этот язык предоста-
вляет достаточно удобную для составления эффективных программ оболоч-
ку, закрывающую невыразительные средства машинного уровня. Растущая
популярность этого языка повлияла на то, что компьютеры стали конструиро-
вать так, чтобы их архитектура соответствовала модели вычислений языка C.
В результате — рост популярности таких архитектур. В этом процессе наи-
более преуспела фирма Intel, процессоры которой становятся стандартом де-
факто в сфере разработки персональных компьютеров. Сегодня уже нельзя
представить массовый компьютер, который не поддерживал бы программное
обеспечение, разработанное под эти процессоры11 .
Язык С если не первый, то один из первых языков программирования, в
которым с самого начала существования провозглашалась идея рассматри-
вать в качестве вычислителя не уровень команд конкретного компьютера, а
уровень операционной системы. Иными словами, в данном языке присут-
11
Вообще говоря, это тормозит развитие машинных архитектур.
30 ГЛАВА 1. ПОНЯТИЯ

ствуют конструкции, выполнение которых не может осуществляться без со-


ответствующего обращения к средствам операционной системы. Конечно же,
это не новшество. Во всех практически используемых языках программиро-
вания такие средства есть, и всегда они расширяют программистский взгляд
на среду функционирования программы. Но чаще всего они представлены в
языке как подчиненные средства, вынесенные на уровень библиотек, и дале-
ко не всегда точно специфицированы. В С ситуация иная. В частности, наря-
ду с автоматическим распределением памяти в языке определены механизмы
предоставления участков памяти по запросам и возврата их, когда они пере-
стают быть нужными.
В конце 80-х гг. на базе языка С было создан язык C++, практически явля-
ющийся расширением С. С++ отличается прежде всего значительным усиле-
нием системы описаний (объектно-ориентированные возможности являются
одним из наиболее применяемых расширений). Но по конструкции он еще
намного сложнее, а определение его производит впечатление еще большей
эклектичности. Но С++, усугубив недостатки С с точки зрения человека, со-
хранил при колоссальном расширении возможностей языка все достоинства
С, касающиеся машинной ориентированности и эффективности. Подхватив
эстафету С, С++ в последнее время является наиболее распространенным
языком профессионального программирования сложных систем.
Еще более укрепляют позиции языка С++ многие современные инстру-
ментальные системы, создававшиеся на нем без учета потребностей других
языковых средств. В частности, системы работы с динамически подключае-
мыми программами (middleware) CORBA и COM практически требуют, что-
бы программа, к ним обращающаяся, была написана на С++, поскольку вся
система интерфейсов ориентирована на типы данных этого языка и порою
даже на их конкретные представления.
Чтобы более эффективно работать прежде всего с системами middleware,
корпорация Microsoft предложила новый диалект С, названный C#. При его
создании ставилась цель получить язык, так же относящийся к С++, как С++
относится к С. Для наших целей в большинстве случаев различия между С++
и С# несущественны.
На фоне заслуженной популярности С уместно упомянуть неизмеримо
менее распространенный язык Bliss. Этот машинно-ориентированный язык
программирования мог бы быть концептуально более выверенной альтерна-
тивой С, но отсутствие разработанного с его помощью проекта, сравнимого
по значимости с Unix, не позволило ему выделиться. И хотя в идейном пла-
не Bliss и повлиял на языкотворчество, интерес к нему не вышел за рамки
1.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ ФОН НЕЙМАНА И ТРАДИЦИОННЫЕ ЯЗЫКИ 31

академических исследований.
Отечественный опыт разработки машинно-ориентированных языков де-
монстрирует поддержку архитектуры, отличную от Intel-подобной. Укажем
на два проекта этого рода. Первый — язык Ярмо (аббревиатура: язык реа-
лизации машинно-ориентированный), построенный для ЭВМ БЭСМ-6 и от-
ражающий все современные веяния в языкотворчестве. О качестве и востре-
бованности этого языка можно судить хотя бы по тому, что было реализо-
вано несколько его версий. Второй пример — Эль-76, разработанный в ка-
честве аналога ассемблерного языка для многопроцессорного вычислитель-
ного комплекса Эльбрус. Оставаясь в целом фон Неймановской машиной,
архитектура этого комплекса далеко отходит от канонических принципов. В
частности, в ней предусмотрена аппаратная поддержка вызова процедур, сте-
ковая организация памяти и другие высокоуровневые средства программиро-
вания. Особенностью этой архитектуры является тегирование: каждое зна-
чение сопровождается тегом, т. е. описателем того, к какому типу относится
информационная часть значения. По существу это отказ от строго однород-
ной памяти. Все архитектурные особенности Эльбруса отражены в Эль-76,
что позволило рассматривать данный язык в качестве единственного инстру-
мента программирования системных программ. Конечно, нельзя говорить о
механическом переносе этого языка в архитектурную среду другого типа, а
потому время использования его, как и любого машинно-ориентированного
языка, ограничено временем жизни данной архитектуры12 .
8. Язык Ada. Он разрабатывался по заказу Министерства обороны США
на конкурсной основе с предварительным сбором и анализом требований,
с обширной международной экспертизой. По существу в нем воплощена по-
пытка определить язык программирования как экспертно обоснованный ком-
плекс средств программирования. На завершающем этапе конкурса приняли
участие около ста коллективов. В результате проверки соответствия предста-
вленных разработок сформулированным требованиям, для обсуждения об-
щественности было отобрано четыре языка, зашифрованных как Red, Green,
Blue и Yellow. В разной степени критике подверглись все четыре кандидата.
Особенно острым критиком оказался Э. Дейкстра, который камня на камне
не оставил от Red, Blue и Yellow, но чуть-чуть ‘пожалел’ Green, доказывая,
Это утверждение не противоречит долголетию С. Как уже отмечалось, данный язык сего-
дня навязывает архитектуру машин, которая бы не противоречила его эффективной реализа-
12

ции. Например, использование С в качестве базового языка для Эльбруса явно ограничивало
бы доступ ко многим развитым машинным возможностям.
32 ГЛАВА 1. ПОНЯТИЯ

что все недостатки языка связаны с несвободой в выборе решений, обусло-


вленных жестко фиксированными требованиями: там, где авторы могли бы
решать какие-либо проблемы по-своему, они были вынуждены идти на пово-
ду у априорных предписаний. Тем не менее, Green стал приемлемым вариан-
том и получил одобрение. Как оказалось, это был единственный из финали-
стов язык, предложенный не американцами. Конкурсная комиссия утверди-
ла его в качестве единого официального языка Министерства обороны США
для разработки программ для встроенных систем и дала ему имя Ada — в
честь Ады Августы Лавлейс, дочери Байрона и ученицы Бэббиджа — пер-
вой в истории программистки. Мы будем иметь возможность познакомиться
с рядом интересных особенностей этого языка в свое время. Пока же отме-
тим, что успеха разработчики языка добились благодаря заботе о концепту-
альной целостности — именно это выделяло Green среди конкурентов. В то
же время, весьма примечательно, что в ходе последующего развития Ada в
угоду появившимся пользователям в язык стали включать все новые и новые
средства. В результате к концу девяностых годов Ada по стилю построения
стала подобна PL/1: в ней есть средства поддержки всему, что можно найти
в практике работы программистов в конце XX века. Как отмечал известный
советский программист В. Ш. Кауфман, язык Ada 9х можно рассматривать
в качестве добротной энциклопедии программистского знания и опыта, но
никак не в качестве инструмента, ориентированного на пользователя.
9. Объектно-ориентированные языки. Последним достижением в обла-
сти программистского языкотворчества считается поддержка объектно-ори-
ентированной методологии. Эта сфера интересует многих разработчиков язы-
ков начиная с восьмидесятых годов. Первым проектом, провозгласившим
принцип перехода от пассивных данных к активным объектам, стал Smalltalk.
В этом языке объектам предоставляется возможность действовать в ответ на
получаемые ими сообщения без каких бы то ни было иных способов активи-
зации действий. Эта возможность реализована в рамках идеи динамической
типизации (отход от классической статической системы типов, ставшей об-
щепризнанной после Pascal’я). В качестве наглядной демонстрации мощи
идеи была предложена система программирования Smalltalk-80 с очень бога-
той библиотечной поддержкой конструирования графических интерфейсов.
Smalltalk — последний крупный проект, результаты которого были пред-
ставлены не только в виде предложения программного продукта как данного,
но и как материалы для всестороннего обсуждения программистской обще-
ственностью. В результате таких обсуждений выработалась идея того, что
нужно для поддержки объектной ориентированности в языках для промыш-
1.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ ФОН НЕЙМАНА И ТРАДИЦИОННЫЕ ЯЗЫКИ 33

ленного производства программ. Такие языки появились достаточно скоро.


Наибольшее распространение из них получил С++, по-видимому, из-за ра-
стущей популярности С. Заметными объектными языками стали также Turbo
Pascal версий с 5.5 до 7.0 и Object Pascal системы Delphi. Общим для про-
мышленного развития Smalltalk’а является возврат к статическим типам дан-
ных, повышенное внимание к вопросам защиты. В результате удалось по-
строить приемлемые по эффективности объектного кода системы програм-
мирования, удовлетворяющие требованиям технологичного программирова-
ния. Однако, как это обычно бывает с производственными системами, на сме-
ну аналитическим исследованиям роли и границ применимости языка при-
шли рекламные хвалебные обсуждения достоинств и никогда — недостатков.
Применительно к обсуждению традиционности языков уместно посмо-
треть на то, как трансформируются фон Неймановские принципы вычисле-
ний при использовании объектной модели. Очевидно, что отход от одно-
родности памяти для этой модели более радикален, нежели, к примеру в
Pascal’е. Если рассматривать объекты как хранимые в памяти данные, то за
счет связанности этих данных с программами (методами объектов) память в
объектной модели приобретает активность. На концептуальном уровне рас-
смотрения можно усмотреть, что модернизируется управление: объект сам
знает, какую программу-метод нужно активизировать, чтобы выполнить то
или иное действие. Несомненно, все это повышает гибкость программиро-
вания, способствует расширению возможности отхода от фон Неймановско-
го взгляда на программу, как на автомат, выполняющий предписания-коман-
ды. Однако на уровне реализации программ-методов все остается по-ста-
рому: все то же последовательное выполнение операторов, те же подходы
к разветвлениям вычислений и к организации циклической обработки. Бо-
лее того, последовательный характер вычислений остается и при задании
взаимодействия объектов. Следовательно, объектный подход, хотя и способ-
ствует взгляду на вычислительные процессы, отличающемуся от фон Ней-
мановского стиля, сам по себе не приводит к смене базовой модели вычи-
слений. Этот подход может быть применен и для организации вычислений
на основе иных моделей, отличных от фон Неймановских. И весьма успеш-
ный опыт такого применения имеется: разработан язык CLOS (Common Lisp
Object System), который есть надстройка над нетрадиционным языком Lisp.
Уместно отметить, что многие приемы и механизмы объектного программи-
рования на этом языке оказываются более наглядными и естественными, не-
жели в рамках традиционных моделей вычислений.
10. Язык Java. Заметным этапом в развитии объектно-ориентированно-
34 ГЛАВА 1. ПОНЯТИЯ

го подхода стало появление языка Java, который был предложен как средство
программирования не для отдельного компьютера, а сразу для всех машин,
имеющих реализацию так называемой Java-машины — исполнителя про-
грамм на промежуточном языке более высокого уровня, нежели командный
язык обычных машины. Иными словами, провозглашается система програм-
мирования с явно и точно определенным промежуточным языком. Другая
особенность Java-проекта — его ориентация на Интернет программирова-
ние: поддерживается возможность построения приложений, запускаемых на
сервере от клиентской рабочей станции и взаимодействующих с клиентом
через произвольный браузер.
Схема трансляции с выделенным промежуточным языком, независящим
от исходного языка, очень даже не новая. В шестидесятые годы ее пытались
применять для сокращения расходов на разработку трансляторов (например,
в качестве промежуточного языка в США был разработан специальный язык
Uncol, в Советском Союзе для тех же целей предлагался язык АЛМО). Чисто
умозрительно можно представить ситуацию. Требуется реализация m язы-
ков на n машинах. В схеме без промежуточного языка в этом случае нуж-
но запрограммировать m × n трансляторов, тогда как использование такого
языка позволяет сократить это число до m + n: для каждого языка пишет-
ся транслятор в промежуточный язык (m) и для каждой машины создается
транслятор или интерпретатор промежуточного языка (n). К тому же, опять-
таки умозрительно, можно предположить, что затраты на ‘половинки’ транс-
ляторов сократятся. Все получается, если удается построить промежуточный
язык удовлетворяющий следующим условиям:

a) все реализуемые языки можно вложить в промежуточный язык, т. е. их


модели вычислений совместимы, не противоречат друг другу;

b) все целевые машины можно непротиворечиво представить в одной моде-


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

Выполнить эти условия весьма сложно даже для близких языков и машин.
Точнее сказать, что затраты на решение этих задач неизмеримо и неоправ-
данно превышают стоимость пресловутых m × n трансляторов. Поэтому по-
сле серии экспериментальных проектов идея промежуточного языка была
предана забвению. В проекте Java она возродилась, правда, в урезанном до
1.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ ФОН НЕЙМАНА И ТРАДИЦИОННЫЕ ЯЗЫКИ 35

одного языка варианте, причем специально разработанном с учетом осуще-


ствимости построения Java-машины. Именно эти дополнительные условия
(точнее — квалифицированное сужение исходного языка и довольно высо-
кий уровень команд-конструкций языка Java-машины) в сочетании с практи-
чески унифицированной архитектурой массовых компьютеров (см. выше) и
с очень существенным ростом чисто технических возможностей машин по-
зволило воплотить старую идею в промышленной разработке13 .
В контексте обсуждения традиционности языков необходимо рассмотреть
вопрос о том, насколько далеко язык Java и Java-машина отходят от фон
Неймановской модели вычислений. Совместная разработка этих двух ком-
понентов системы программирования для нового языка позволила прийти к
достаточно практичным компромиссам, удовлетворить условиям выбранной
схемы реализации, о которых шла речь выше.
Условие (a) выполняется почти автоматически, и, т. к. нет нужды забо-
титься о других языках, можно сосредоточить внимание на том, чтобы обес-
печить наиболее рациональное вложение модели вычислений языка в модель
машины. Что касается условия (b), то здесь ставка делалась на фактическое
сходство архитектуры конкретных вычислителей, для которой уже накоплен
опыт программистских решений во многих типовых ситуациях. В резуль-
тате отход от фон Неймановской модели вычислений в Java-системе, хотя
и заметен, но не распространяется далее того, что уже было при разработ-
ке трансляторов и языков. Достаточно сказать, что Java-машина построена
на принципах, предложенных еще в 1963 году для организации вычислений
в рабочей программе Ветстоунского компилятора для Algol 60 [68]. Схема,
представленная в этом проекте, стала классической, и именно она воспроиз-
водится в Java-машине, естественно существенно более развитой по сравне-
нию с первоначальным прототипом14 .
Важным новым качеством Java-машины является поддержка работы про-
граммиста с потенциально неограниченной памятью. При выполнении кон-
Про новые условия данного проекта говорят обычно с неохотой или же вовсе замалчива-
ют их. К примеру в книге «Java технология» успех старой идеи в новом проекте объясняется
13

тем, что в прежние времена эту идею изучали в университетах, тогда как сегодня за нее взя-
лись промышленники.
В этой связи уместно следующее замечание. Если бы книга [68] не была бы сегодня би-
блиографической редкостью, то ее главу 2 «Рабочая программа» можно было бы рекомен-
14

довать в качестве первоначального введения для тех, кто желает изучить устройство Java-
машины. После прочтения этого изложения понимание Java-машины окажется более глубо-
ким.
36 ГЛАВА 1. ПОНЯТИЯ

кретной программы на языке Java можно не заботиться о том, что какая-то


часть памяти перестает быть нужной. Система программирования сама сде-
лает так, что та память, которая оказалась недоступной, а значит, ненужной,
возвращается для использования в новых запросах. Такие ситуации выявля-
ются в процессе вычислений, когда фактические ресурсы, предоставляемые
для размещения данных, требуется пополнить для переиспользования. В уго-
ду этому соглашению принесен в жертву ряд приемов организации ручного
переиспользования памяти, необходимых, например, при программировании
на С.
Модель вычислений Java в точности соответствует тому, что требуется от
объектно-ориентированного программирования: активность памяти на уров-
не методов объектов, совместное описание данных и программ методов, от-
деление предоставляемых средств от их реализации. Все это сочетается с
традиционной схемой управления вычислениями при описании алгоритмов
обработки. Следует отметить, что разработчики языка не стали включать в
него средства, с трудом укладывающиеся в концептуальную схему Java-ма-
шины и обычно, как, например, в С++, предоставляемые через довольно про-
извольные реализационные соглашения. Ориентация Java-машины на схему
организации вычислений, ставшую классической со времен Algol 60, повли-
яла на язык в том отношении, что все, что выходит за рамки принятой моде-
ли, представлено таким образом, чтобы это можно было вычислить в период
трансляции. К примеру, проблемы статической типизации в данном языке ре-
шены радикально: в нем просто нет средств конструирования структурных
типов, отличных от классов объектов. В результате, язык стал лаконичнее по
сравнению, например, с С++.
Благодаря совокупности отмеченных и других нововведений, которые в
разрозненном виде появлялись в различных языках, программирование на
Java, относящееся в целом к объектно-ориентированному стилю, обретает
самостоятельность. Характерным для нового стиля является принципиаль-
ный отход от учета особенностей конкретных вычислителей в предположе-
нии (далеко не всегда оправданном!) о том, что используемые машины вы-
полняют требуемые действия с приемлемым уровнем эффективности.
В значительной степени для того, чтобы перехватить инициативу у языка
Java, и был создан C#, стремящийся сохранить в новой области эффектив-
ность С/C++.
1.3. БАЗОВЫЕ КОНСТРУКЦИИ ЯЗЫКА 37

§ 1.3. БАЗОВЫЕ КОНСТРУКЦИИ ЯЗЫКА

1.3.1. Линейные программы и их компоненты

Рассмотрим несколько простых примеров программ в языке C. Последо-


вательность действий всех рассматриваемых программ линейная: оператор
за оператором, так, как это записано в тексте. Такие программы называются
линейными программами.
Программа 1.3.1

/* Печатается N -ая степень (7-ая) вводимого числа для данного N .


Прямолинейный алгоритм*/
#include <stdio.h>
int X, Y;
int main(void)
{
scanf("%d", &X );
Y = X*X*X*X*X*X*X;
printf("\n7 power of %d is %d\n", X, Y);
return 0;
}

Программа 1.3.2

/* Печатается N -ая степень (7-ая) вводимого числа для данного N .


Улучшенный алгоритм 1
*/
#include <stdio.h>
int X, Y, X23;

/* начало блока */
int main (void)
{
scanf ( "d", &X );
X23 = X * X;
X23 = X23 * X;
Y = X23 * X23 * X;
printf (“\n7 power of %d is %d\n”, X, Y);

/* конец блока */
return 0;
}
38 ГЛАВА 1. ПОНЯТИЯ

Программа 1.3.3

/*Печатается N -ая степень (7-ая) вводимого числа для данного N


Улучшенный алгоритм 2/*
/* */
#include <stdio.h>

/* начало блока */
int main (void)
{

/* Такой стиль лучше для комментариев к переменным: */


int X; /* для задания входного значения */
int Y; /* для промежуточного и окончательного результата */

scanf( "%d", &X );

Y = X * X * X;
Y = Y * Y * X;
printf ("\n7 power of %d is %d\n", X, Y );

/* конец блока */
return 0;
}

Что мы видим, анализируя эти программы? Программа содержит компо-


ненты, обсуждению которых мы посвятим следующие абзацы (порядок раз-
бора следует порядку, в котором компоненты содержатся в обсуждаемых про-
граммах 1.3.1, 1.3.2 и 1.3.3).

1.3.2. Не выполняемые вычислителем фрагменты программы

Эти фрагменты с точки зрения вычислителя языка не несут никакой ин-


формации, но они очень полезны для человека, читающего программу, изме-
няющего ее, использующего программу при составлении другой программы.
1. Комментарий. Комментарии служат:

— Для облегчения чтения и восприятия программы путем дополни-


тельного выделения и графического оформления ее структур.
— Для понимания того, зачем нужна программа, какие у нее особен-
ности, а также что делает программа или какой-либо ее фрагмент
(документирование);
1.3. БАЗОВЫЕ КОНСТРУКЦИИ ЯЗЫКА 39

При чтении программы вычислителем комментарии удаляются препро-


цессором.
2. Пробельные символы: концы строк, пробелы, табуляции.
В данном случае рассматриваются лишь явно обозначенные комментарии.
В С они начинаются с последовательности “/*” и завершаются “*/”, В С++
используются также комментарии, начинающиеся с последовательности “//”
и завершающиеся вместе с текущей строкой.
В некоторых языках могут быть не обозначенные явно комментарии15 :
куски текста, пропускаемые транслятором. Сейчас все большую популяр-
ность приобретает идея программ, вложенных в поясняющий их и описыва-
ющий общую задачу текст. В таком случае комментарии занимают основную
часть текста.
Текст программы на языке С сам по себе не нуждается ни в разбиении на
строки, ни в специальном расположении своих составляющих в строках. Но
без пробельных символов реальные программы неудобно читать человеку!16

Существуют и форматные языки, где пробельные символы являются зна-


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

1.3.3. Подключение внешней информации (библиотек)

Строка

дает возможность пользоваться операторами scanf и printf (которые позволя-


#include <stdio.h>

ют вводить данные и распечатывать результаты выполнения программы), а


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

В современных технологиях индустриального программирования значительное место от-


водится требованиям к комментариям и к оформлению программ. Одна из лучших похвал
16

тексту программы — что он самодокументирован, не требует никакого внешнего описания.


В ряде случаев принимается регламент оформления комментариев, который позволяет вы-
делять их автоматически; существуют программы, позволяющие строить из таких коммен-
тариев вполне осмысленную документацию (примером может служить библиотечное для
Java средство JavaDoc).
40 ГЛАВА 1. ПОНЯТИЯ

тексту программы (см. рис. 1.2, на котором простые стрелки изображают ука-
затели мест хранения нужных текстовых фрагментов, а жирные стрелки по-
казывают процесс включения файла в текст). Из схемы видно, что в С/С++/C#
подключение внешних библиотек — двухступенчатый процесс: в исходном
тексте программы указывается только заголовочный файл. Встраивание его
в тест позволяет транслятору корректно работать с именами, т. е. считать их
описанными и доступными в исходном тексте. Для выполнения транслируе-
мой программы нужно иметь следующую ступень: тексты или отдельно от-
транслированные фрагменты программ, представленных внутри заголовоч-
ных файлов. В С/С++ задачу обеих ступеней решает препроцессор.
В других языках подключение библиотек организовано по-разному. Так,
в Algol 60 понятие внешней библиотеки вообще никак не определялось. Счи-
талось, что достаточно говорить об отдельных процедурах. То же можно ска-
зать и про большинство других ранних языков.
Для внешних библиотек, которые определены языком, нет проблемы, как
указать их подключение — система программирования их “знает”, а потому
может считать их частью программы, которую программист писать не дол-
жен. Эта идея в законченном виде представлена в Алголе 68, в котором лю-
бая программа считается блоком, погруженным в стандартный блок, начина-
ющийся библиотечным вступлением и заканчивающийся библиотечным за-
ключением (см. § 1.1.1). Проблема может возникнуть, когда в распоряжении
программиста есть несколько библиотек, выполняющих похожие функции и
поименованных одинаково. В этом случае нужно явно указывать, какую из
библиотек подключать17 .
В ряде языков приняты более строгие соглашения: программисту пред-
писывается указывать источник подключения (подобно тому, как в С/С++/C#
указывается заголовочный файл) и конкретные подключаемые средства, в
дальнейшем используемые в программе. Если это соглашение оформляется
в виде языковой конструкции, то появляется возможность на уровне языка
конструировать библиотеки. В таком случае можно корректно подключать
не только процедуры и функции, но иные программные фрагменты (напри-
мер, описания переменных). В законченном виде этот подход представлен в
языке Modula-2, в котором программа рассматривается как набор модулей,
Именно это сделано в С/С++/C#, причем для всех, как стандартизованных языком, так
и для других доступных библиотек. С точностью до того, что подключение выполняется в
17

С/С++ с помощью препроцессора, который не может ничего знать о заказываемых файлах,


это достаточно хорошее решение.
1.3. БАЗОВЫЕ КОНСТРУКЦИИ ЯЗЫКА 41

Файлы
Библиотека stdc заголовков
stdio.h
...
scanf
...
Исходный текст программы
Реализация string.h
scanf ...
... #include <stdio.h>
...
...
mem.h scanf
... ...

...

Результат работы препроцессора


...

...
scanf
...

Рис. 1.2. Схема включения файла в текст программы (работа препроцессора


для языков семейства С)
42 ГЛАВА 1. ПОНЯТИЯ

явно содержащих разделы описания импорта — то, что используется в мо-


дуле из других модулей, в том числе и внешних, подключаемых, и экспор-
та — то, что данный модуль предоставляет для использования. В результате
единообразно описываются все связи между модулями, в том числе и связи
составляемой программы с окружением, которые естественно трактовать как
подключение библиотек (см. рис. 1.3).
Подключение внешних библиотек возможно в виде текстов на транслиру-
емом языке и/или в виде готовых к сборке модулей. Второй режим, по край-
ней мере в принципе, позволяет говорить об использовании библиотек, напи-
санных на других языках. Но он не в состоянии обеспечить совместимость
форматов данных и программ. Контроль за совместимостью, а при необходи-
мости и преобразование форматов в этом случае возлагается на окружающую
систему. Наличие этого режима ставит перед разработчиками системы про-
граммирования проблему: необходимость различения текстов и оттрансли-
рованных библиотечных программ. Если есть оттранслированная библио-
тека и ее текст, то возникает двусмысленность: какой из вариантов должен
подключаться к программе. Разрешение этой двусмысленности делается по-
разному. В С/С++/C# указание того, какого вида библиотека требуется дается
на синтаксическом уровне, а двухступенчатая схема подключения дает опре-
деленную гибкость использования: заголовочные файлы могут (и должны!)
объединять тематически связанные средства. В Алголе 68 такое объединение
не предусматривается, и это один из недостатков языка для практического
применения.
Часто для внешних (библиотечных) средств требуются специальные опи-
сания с пометкой external вместо их реализации. Это дает возможность транс-
лятору программировать их использование без обращения к фактическому
описанию, а также упрощает организацию составления сведений о том, какие
внешние средства надо будет подключить на этапе сборки. Такой метод сего-
дня стал почти стандартом для языков программирования. Иногда он счита-
ется достаточным и дополнительных описаний для использования внешних
средств не предлагается, во многих случаях он сочетается с другими мето-
дами оперирования с библиотеками (в частности, в языках С/С++/C#, Pascal,
Алгол 68).
Следует заметить, что когда стандарт языка не распространяется на ис-
пользуемые библиотеки (так было, например, в Algol 60), возможности ис-
пользования готовых программ в других операционных обстановках резко
сужаются.
Сбалансированное решение для задания подключения внешних библио-
1.3. БАЗОВЫЕ КОНСТРУКЦИИ ЯЗЫКА 43

Формат описания модуля (условный)


module M; // Имя модуля
import Mi1: a,b; // Из модуля Mi1 импортируются a и b
Mi2: x,y,z; // Из модуля Mi2 импортируются x, y и z
export U, V; // Модуль M предоставляет U и V другим
// На схеме импорт изображается на верхней кромке
// прямоугольника, а экспорт на нижней
begin // Тело модуля
...
end.

module M1; M1.A M2.F


...
A B C module MP1;
...
A X

module M2; MP1.A M3.H


...
D E F module MP2; ...
...
M2.D R Q T
module M3;
... MP2.R MP2.Q MP1.X M1.B
G H
module MP3; ...
A B C
module M4; не используется M3.H MP1.A MP2.T
G H module MP4; ...

………………….

Модули, поставляемые системой Модули составляемой программы

Рис. 1.3. Схема взаимосвязей модулей программ на языке Modula-2


44 ГЛАВА 1. ПОНЯТИЯ

тек предложено в Turbo Pascal’е версий 5 и выше. Здесь к программе под-


ключается не просто текстовый файл заголовков — как в С/С++/C#, не какой-
то фрагмент-вступление к составляемой программе, который предлагает для
использования библиотеку — как в Алголе 68, а самостоятельная языковая
единица, конструкция, содержащая все, что нужно при работе с подключае-
мыми средствами — как в Modula-2. Эта конструкция, называемая модулем
(как в Modula-2), описывает как то, что можно использовать, так и то, что
нужно выполнить до и после использования с тем, чтобы применение би-
блиотечных средств было бы корректно. На рис. 1.4 схематически показаны
описание модуля Turbo Pascal’я (это программный текст, который начинает-
ся со служебного слова unit), программы, использующей модуль (начинается
со служебного слова program), и подключение модуля к программе как про-
цесс вставки в нее соответствующих фрагментов из модуля (показан серыми
стрелками).
В связи с задачей подключения внешней информации мы затронули очень
важный общий вопрос программирования, вопрос модульности разрабаты-
ваемой системы. К нему мы еще не раз будем возвращаться в связи с тем,
что модульность (в разном понимании этого слова) — основа технологии
разбиения решаемой задачи на подзадачи. Такое разбиение, получившее на-
звание декомпозиции программ, приходится проводить программисту посто-
янно, ибо сложность конструирования программ не позволяет охватывать
все стороны этого процесса одновременно. Искусство программирования во
многом состоит в том, чтобы добиваться такой декомпозиции, которая спо-
собствует более быстрому и эффективному пути к получению решения.
Применительно к использованию библиотек метод декомпозиции можно
продемонстрировать следующим образом. Для нашей задачи нужно выпол-
нить действие, для которого в системе программирования есть уже готовое
типовое решение (в обсуждаемых программах это две задачи: ввод данных
и вывод результатов счета). Именно его мы и используем. Иными словами,
с точки зрения декомпозиции нашей задачи библиотеки выступают в роли
заранее решенных подзадач.
1.3.4. Описания

В современных системах программирования, как правило, жестко прово-


дится в жизнь следующий принцип:
Bсе, что используется, должно быть предварительно описано!

Рассмотрим встретившиеся нам варианты описаний.


1.3. БАЗОВЫЕ КОНСТРУКЦИИ ЯЗЫКА 45

unit M // имя модуля program



interface uses M; // объявление
… // объявление того, что // подключения модуля
// модуль предоставляет … // какие-то описания
// для внешнего // здесь уже можно
// использования // использовать средства
procedure PM(a : real); // модуля M
… // вставка недоступной
implementation // части модуля
… // описание того, что begin
// • нужно для реали- // вставка инициализации
// зации модуля, // модуля
// • объявлено для
// внешнего исполь- … // действия программы
// зования (реали-
// зация процедур и
// функций) PM(7.4); // вызов процеду-
procedure PM(a : real); // ры из модуля
begin … end;
… … // действия программы
begin
… // действия, которые
// нужно выполнить до
// использования предо- // вставка завершающих
// ставляемых средств // действий
finalization end. // конец программы
… // действия, которые
// нужно выполнить после
// использования предо-
// ставляемых средств
end. // конец описания модуля

Рис. 1.4. Схема подключения модуля в языках Turbo Pascal и Object Pascal.
46 ГЛАВА 1. ПОНЯТИЯ

1. В программе 1.3.2 описание

вводит определение трех переменных, которые используются для хра-


int X, Y, X23;

нения значений целого типа. Описание действует во всем тексте про-


граммы.

2. Если поставить описания после строки

как сделано в программе 1.3.3, то будет запрещено использование X и


int main (void)

Y вне функции main, о которой говорится ниже.

3. Как показано в программе 1.3.3, можно обойтись без описания пере-


менной X23, поскольку транслятор сам создает промежуточную пере-
менную для хранения X * X (см. 4) в § 1.3.5.

В С различают объявление имени (предварительное упоминание) и собствен-


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

1.3.5. Действия

1. Заголовок функции. Выражение

определяет функцию main, которая должна вычислить целочисленное


int main (void)

значение (описатель int). Заданное в скобках служебное слово void ука-


зывает на то, что вычисление определяется как независящее от пара-
метров. Компилятор не позволяет вызывать такую функцию с параме-
трами. Для языка С принято, что любая программа должна содержать
функцию main. Выполнение этой функции начинает выполнение про-
граммы.
1.3. БАЗОВЫЕ КОНСТРУКЦИИ ЯЗЫКА 47

2. Знаки группировки { и }. Строки с фигурными скобками обрамляют


блок операторов. Внутри блока находится описание алгоритма как по-
следовательности операторов. В данном случае это алгоритм функции
main.

3. Вызов функции ввода. Чтение значения переменной осуществляется с


помощью оператора

которая задает формат ввода %d и переменную X, получающую значе-


scanf ( "%d", &X );

ние с помощью этого оператора. Здесь заслуживает внимания знак "&".


Он указывает на особый способ использования переменной: в алгорит-
ме scanf используется не значение X, а его адрес, чтобы указать, куда
должно помещаться читаемое значение.

4. Тело функции. Следующие строки


X23 = X * X;
X23 = X23 * X;

или
Y = X23 * X23 * X;

Y = X * X * X;

описывают алгоритм. Это подряд идущие операторы присваивания зна-


Y = Y * Y * X;

чений переменным. Здесь хорошо видно, что от порядка операторов


зависит результат.

5. Вызов функции вывода. Строка

определяет формат вывода \n7 power of %d is %d\n и переменные X и


printf ("\n7 power of %d is %d\n", X, Y);

Y, отформатированные значения которых вставляются вместо двух вхо-


ждений %d. "\n" — это символ завершения текущей печатаемой строки.

6. Завершение. Наконец, строка

представляет завершающий вычисления оператор. В результате его вы-


return 0;

полнения окружению станет известно, что вычисления завершились


нормально с искомым результатом (с кодом ответа 0).

***
48 ГЛАВА 1. ПОНЯТИЯ

Чем определяется последовательность действий линейной программы? Что


может делать транслятор? И как это зависит от языка программирования?
Ответы на эти и другие вопросы поможет дать разбор действий и изучение
структуры конкретной программы. Ниже этот анализ проводится для про-
граммы 1.3.3. Будет, в частности, показано, что даже в самом простом случае
линейной программы структура вычислений не во всем соответствует струк-
туре текста программы, что записанный алгоритм — это не совсем то, что
имеет в виду разработчик программы, когда он размышляет над решением
программистской задачи.

§ 1.4. СТРУКТУРА ВЫЧИСЛЕНИЙ И СТРУКТУРА ТЕКСТА ПРОГРАММЫ

Текст программы носит двойственную природу: это операционная струк-


тура, ориентированная на человека, но в то же время допускающая интерпре-
тацию абстрактным автоматом. Например, структура вычислений програм-
мы 1.3.3 представлена на рис. 1.5, а структура ее текста — на рис. 1.6. Из
этих рисунков видно, что понятия, использованные при задании текстовой
структуры, во многом обусловлены тем, как вычисления представлены в ви-
де текста, т. е. последовательности символов. Структура вычислений более
свободна в этом отношении.
Поэтому различные тексты программ часто порождают эквивалентные
с точки зрения результатов вычислений программы. Более того, традици-
онная запись программы избыточна с точки зрения абстрактных вычисле-
ний, и поэтому могут возникать недоразумения, когда сам человек при пере-
стройке программы забывает, какие именно аспекты текста для него важны,
а какие — нет. С другой стороны, запись в программе часто допускает от-
носительную свободу структуры вычислений: детали, важные для человека,
могут оказаться не важны для вычисляющего программу автомата. Все это,
в частности, порождает участки строго последовательных действий и дей-
ствий, которые можно выполнять в произвольном порядке.
1.4.1. Последовательное, параллельное и
совместное исполнение

Для того, чтобы разобраться с тем, как порядок конструкций в тексте про-
граммы соотносится с порядком их исполнения абстрактным вычислителем
и с представлением абстрактного вычислителя в конкретном, целесообразно
рассмотреть понятия последовательного, параллельного и совместного ис-
1.4. ВЫЧИСЛЕНИЯ И ТЕКСТ 49

Действия окружения

Активизация main

В любом порядке
Выделение памяти для X Выделение памяти для Y

Чтение X

Y = X * X * X;
В любом порядке
Y = Y * Y * X;

Вывод X и Y

Завершение вычислений

Рис. 1.5. Структура вычислений программы


50 ГЛАВА 1. ПОНЯТИЯ

Программа

Подключение Глобальные Объявление


внешних текстов описания главной функции

Подключение
Заголовок Блок
внешних текстов

#include <stdio.h>

int main (void)

{ Начало блока Описания Тело Конец блока


int X;
Описания Оператор Операторы
int Y;
Описания Оператор ввода Оператор Операторы
scanf( “%d”, &X );
Оператор присваивания
Y = X * X * X;
Оператор Операторы

Y = Y * Y * X; Оператор присваивания

Оператор Операторы

printf (“\n7 power of %d is %d\n”, X, Y ); Оператор вывода

Оператор

return 0; Оператор возврата


}

Рис. 1.6. Структура текста программы 1.3.3


1.4. ВЫЧИСЛЕНИЯ И ТЕКСТ 51

полнения.
При последовательном вычислении последовательность языковых кон-
струкций переходит в команды абстрактного вычислителя, исполняемые стро-
го в том порядке, который явно задан в тексте программы (именно так, в пер-
вом приближении, интерпретируется последовательность операторов, разде-
ленных ";", в языках С и Pascal).
Параллельное вычисление означает, что для выполнения двух и более ко-
манд абстрактного вычислителя выделяется соответствующее число процес-
соров, каждый из которых выполняет свою команду (в стандартном С по-
нятие параллельности отсутствует, в Алголе-68, Concurrent Pascal и других
языках параллельные участки часто выделяются отдельными программными
скобками, например, parbegin и parend; обрамляемые ими операторы могут
быть выполнены параллельно).
Даже если на машине всего один процессор, указание на параллельность
процессов часто полезно, поскольку оно позволяет организовать квазипарал-
лельное исполнение, при котором команды каждого из процессов исполняют-
ся в строгом порядке, но они могут перемежаться в абстрактном вычислителе
командами другого, квазипараллельно исполняемого, процесса. В частности,
это позволяет более производительно использовать время, которое процесс
тратит на ожидание завершения операции ввода-вывода. Например, при та-
ком исполнении отрезок программы
printf("Первый результат: %d, %d\n", X, Y);
Оператор 1;
...

printf("Второй результат: %d , %d\n", Z, V);


Oператор n;

выполняется в следующем порядке. Запускается первая печать, и, поскольку


этот процесс длительный, начинают выполняться последующие операторы.
Если они не успеют выполниться, пока печатается первая строка, то все в
порядке, если успеют, то перед печатью второй строки придется подождать,
пока допечатается первая.
Именно в данном контексте квазипараллелизм был осознан как полезная
возможность в операционных системах, и в связи с этим был введен на уров-
не библиотек практически во все современные системы программирования.
В частности, C++, Java и Object Pascal Delphi имеют операторы порождения
процессов, подобные
thread(run_this,"Новый процесс"),
52 ГЛАВА 1. ПОНЯТИЯ

которые все программисты воспринимают как исполняемые квазипараллель-


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

a, b, c, d могут вычисляться в любом порядке, перемежаясь с вычислениями


x = a*b+c*d

того из подвыражений a*b, c*d, для которого уже готовы исходные данные.
Если a, b, c, d являются простыми переменными, то действительно безраз-
лично с точки зрения результата, в каком порядке их вычислять. Но вычи-
сление операнда может иметь побочный эффект, то есть вызывать не отно-
сящиеся к использующему оператору и не обозначенные явно действия. На-
пример, побочный эффект появляется при вызове функции, в теле которой
меняются значения глобальных переменных. Распознавать такие ситуации в
общем случае невозможно. И поэтому в руководствах по языкам можно часто
увидеть загадочную фразу «побочный эффект игнорируется», смысл которой
соответствует тому, что в данном месте могут быть элементы совместного
вычисления.
Явно и достаточно четко концепция совместного исполнения была прове-
дена лишь в Алголе 68, в котором был введен другой разделитель операторов:
“,” вместе с оставшимся как признак строго последовательного исполнения
“;”.
x := 1, y := 2, z := 3;
(1.1)
a[x] := b[y], d[z] := e[x];
На примере данного текста программы можно проиллюстрировать понятие
строгого совместного вычисления. При таком исполнении результаты фраг-
мента программы одни и те же при любой реализации совместного испол-
нения. В приведенном выше фрагменте совершенно безразлично, в каком
порядке присваивать значения переменным и в каком — элементам масси-
вов, лишь бы сначала были сделаны присваивания переменным, а затем —
присваивания элементам массивов. Казалось бы, такие случаи можно было
Если изучить стандарт языка Java, то оказывается, что он специфицирует в данном случае
слабое совместное исполнение (см. ниже) в чистом виде.
18
1.4. ВЫЧИСЛЕНИЯ И ТЕКСТ 53

бы находить автоматизированно, но задача определения независимости опе-


раторов в программе алгоритмически неразрешима. Хотя ее можно решать в
частных случаях, но гораздо лучше было бы сразу при составлении програм-
мы помечать, какие действия независимы.19
Слабое совместное вычисление возникает, если результаты зависят от спо-
соба исполнения группы операторов. Как говорят, в данном случае исполне-
ние не детерминировано. Например, слабое совместное вычисление возни-
кает в той форме условного оператора, которую предложил Э. Дейкстра [30]
и интенсивно использовал Д. Грис20 [27]:

Охраняемые команды:
if
A1 → S1 ,
(1.2)
... ,
An → Sn
fi

Этот оператор исполняется следующим образом. Совместно вычисляются


все условия Ai , пока одно из них, например, Aj , не окажется истинным. Далее
берется и исполняется соответствующий выполнившемуся условию опера-
тор Sj . Если истинных условий не оказалось вообще, оператор (1.2) выдает
ошибку.21 Например, вычисление минимума двух чисел в форме Дейкстры
выглядит, как показано ниже:

if
x 6 y → z := x,
y 6 x → z := y (1.3)
fi
return(z);

Если наши данные не являются числами и лишь предупорядочены (см., напр. [34]),
то не определено, какое значение получит z в итоге, но это нам и безразлично.
19
К сожалению, в общераспространенных системах таких средств не предусмотрено.
Мы в идущем ниже примере чуть-чуть изменили обозначения, чтобы привести их в боль-
шее единообразие с тем единственным случаем, когда совместные исполнения были явно
20

реализованы на практике: с Алголом-68


Это более технологично, чем соглашение об эквивалентности пустому оператору любого
непредусмотренного случая.
21
54 ГЛАВА 1. ПОНЯТИЯ

Даже если мы рассматриваем обычные числа, то в случае x = y мы можем


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

X := 1;
parbegin
X := X + 1;
Y := 5 + X;
parend;

Это некорректное по смыслу вычисление, поскольку невозможно сказать,


какое значение получит переменная в результате счета. Если выполнение
первого из указанных операторов завершится ранее, чем начнется чтение X
во втором операторе, то Y получит значение 7, а в противном случае — 6.
Мы рассмотрели случаи, когда нелинейное исполнение разрешается, но
часто изменение порядка исполнения следует предписывать.

1.4.2. Управление порядком вычислений

Средств, использованных в линейных программах, недостаточно, чтобы


можно было создавать содержательные программы. Следующий пример ил-
люстрирует этот тезис. Пусть требуется написать программу для решения
квадратного уравнения
X 2 + pX + q = 0.
Предполагается, что коэффициенты уравнения вводятся, и, следовательно,
программа должна работать корректно при любых вводимых данных. Есте-
1.4. ВЫЧИСЛЕНИЯ И ТЕКСТ 55

ственно при составлении программы воспользоваться известной формулой


p
X1,2 = −p/2 ± p2 /4 − q

и попытаться записать ее непосредственно на языке программирования.


Прямое решение на языке С — программа 1.4.1.
Программа 1.4.1

Корни квадратного уравнения


/* Прямолинейная попытка */

#include <math.h> /* это указание препроцессору о том,


#include <stdio.h>

что нужно получить доступ к функции sqrt(), которая, как и


средства ввода/вывода, имеется в стандартной
библиотеке stdc */
int main (void)

float X1, X2; /* ВСЕ вычисления проводятся


{

с плавающей точкой! */
float P, q;
float D;

scanf ( "%f %f", &p, &q ); /* Ввод p, q. Обратите внимание на


float r;

%f — формат ввода чисел с


плавающей точкой. Не забываем о взятии адреса: & */
r = - p / 2;
D = sqrt( r*r - q );
X1 = r + D;
X2 = r - D;
printf ("\nX1 = %d; X2 = %d\n", X1, X2);
return 0;
}

Что будет, если r ∗ r − q < 0? Ошибка счета!


Ответ на этот вопрос доказывает, что программу требуется доработать.
Нужны разные вычисления в различных случаях, а значит, средства упра-
вления порядком вычислений. Простейшее из них — условный оператор. В
большинстве традиционных языков он представлен в двух формах:
56 ГЛАВА 1. ПОНЯТИЯ

1. if ( условие ) действие T — если условие истинно, то выполняется


действие T, в противном случае действие T пропускается;

2. if ( условие ) действие T else действие F — если условие истинно, то


выполняется действие T, в противном случае выполняется действие F.

Действие T и действие F — это операторы языка. За счет блоков (последова-


тельностей операторов, заключенных в фигурные скобки) можно в качестве
действий использовать несколько операторов.
Эти формы в ряде языков изображаются чуть по-другому, но с полным
соответствием указанной семантике:
1. if условие then действие T
и

2. if условие then действие T else действие F


Обе формы условного оператора восходят к устоявшимся стереотипам
управления фон Неймановских машин. Так, для формы 2 годится схема, изо-
браженная на таблице 1.1. Здесь выделены жирным шрифтом машинные ко-

Адреса Команды
//команды, вычисляющие условие и
//помещающие результат в процессорный регистр S
m
m+1

(S = false)? ПЕРЕЙТИ К m+k+i


... ...

//команды, вычисляющие действие T


m+k
m+k+1

ПЕРЕЙТИ К m+k+i+j
... ...

//команды, вычисляющие действие F


m+k+i-1
m+k+i

//команды, следующие за образом условного


... ...

//оператора в машинном коде


m+k+i+j
...

Таблица 1.1. Трансляция условного оператора

манды, записанные в некотором произвольном формате:


(аргумент-регистр = false)? ПЕРЕЙТИ К аргумент-адрес перехода

— условная передача управления по адресу перехода


1.4. ВЫЧИСЛЕНИЯ И ТЕКСТ 57

ПЕРЕЙТИ К аргумент-адрес перехода

— безусловная передача управления по адресу перехода.


m+k+i и m+k+i+j изображают адреса, участвующие в командах перехода в ка-
честве операндов.
Покажем, как надо записывать условные операторы и блоки в текстах
программ.

Программа 1.4.2

/* Корни квадратного уравнения


Попытка решения с разветвлениями */
#include <stdio.h>
#include <math.h>

int main (void)


{
float X1, X2;
float p, q;
float D;
float r;

scanf ( "%f %f", &p, &q );


r = - p / 2;
D = r*r - q;

if ( D > 0 )
{
D = sqrt ( D );
X1 = r + D;
X2 = r - D;
printf ("\nX1 = %f, X2 = %f\n", X1, X2);
}
else if ( D == 0 )
{
X1 = r;
printf ("\nX1 = X2 = %f\n", X1);

else printf ("\nДействительные корни не существуют\n");


}
58 ГЛАВА 1. ПОНЯТИЯ

return 0;
}
Оба приведенных выше представления условного оператора по сути изо-
морфны, и при каждом из них возникает одна и та же проблема. Рассмотрим
запись
if . . . then if . . . then . . . else . . . ;
К какому из if . . . then относится else? Есть два, с внешней содержательной
точки зрения совершенно равноправных ответа:
if . . . then
if . . . then . . .

и
else . . . ;

if . . . then
if . . . then . . . else . . . ;
Здесь использованы отступы и концы строк, чтобы отразить в записи, что к
чему относится. Решения данной проблемы известны следующие:
1. Запретить использование условного оператор в качестве действия T.
Такое решение, принятое в Алголе 60, приводит тому, что при необхо-
димости нужно превращать условный оператор в безусловный, обрам-
ляя его блочными скобками:
if . . . then begin if . . . then . . . else . . . end;
if . . . then begin if . . . then . . . end else . . . ;

2. Видоизменить синтаксис условного оператора, рассматривая само if


как открывающую скобку и ввести парную к ней закрывающую, как
сделано в языках Алгол 68, Modula-2 и Ada, соответственно:
if . . . then . . . else . . . fi
if . . . then . . . else . . . end if.
Пожалуй, это решение — самое лучшее.
3. Принять соглашение: считать верным один из указанных перед нача-
лом данного перечисления вариантов (чаще второй). Именно такое ре-
шение предлагается для С, С++, Pascal. Даже если такое соглашение
имеется, лучше им не пользоваться, а применять одно из тех предста-
влений, в которых структура вычислений указывается явно!
1.5. РАБОТА СО ЗНАЧЕНИЯМИ 59

§ 1.5. РАБОТА СО ЗНАЧЕНИЯМИ

В наиболее часто используемых профессионалами языках программиро-


вания предполагается устройство вычислителя, близкое к реальной аппара-
туре компьютеров. Известно, что компьютер выполняет действия в процес-
соре, а хранит данные и программы в памяти (см. § 1.2). Память может быть
представлена на самом абстрактном уровне как некоторая совокупность кон-
тейнеров, в которых размещаются значения. Реально такие контейнеры чаще
всего соответствуют ячейкам памяти, и именно на такое специальное (хотя и
практически общепринятое сейчас) устройство логической структуры памя-
ти ориентированы, в частности, традиционные языки. Из упомянутых нами
примеров к такой структуре не привязаны Lisp, Рефал и Prolog. Дальнейшее
рассмотрение в данном параграфе относится только к традиционным языкам.
В традиционных языках основными способами представлять значения и
оперировать ими является использование переменных и констант. Рассмо-
трим некоторые базовые понятия, связанные с этим.
Значения всегда принадлежат некоторому типу.
Определение 1.5.1. Тип данных, или сокращенно тип — это множество зна-
чений вместе с набором операций над этими значениями. Константное зна-
чение — это значение, которое существует независимо от выполнения про-
граммы и не может быть изменено. Константа — обозначение константного
значения. Литерал — частный случай константы, изображение, представля-
ющее в программе свое значение. Переменная — обозначение контейнера, в
котором может храниться одно значение в каждый данный момент.
Конец определения 1.5.1.
Например, число 1 — литерал со значением единица. "Что за чушь!" — лите-
рал, обозначающий соответствующую строку символов.
Типом константы является тип ее значения. Если есть некоторый тип, то
его множество значений чаще всего может рассматриваться как состоящее из
константных значений данного типа.22 Например, bool — тип данных, мно-
жеством значений которого является {false, true}, а операциями — конъюнк-
ция, дизъюнкция, исключающее или и отрицание.
Тип литерала часто определяется по контексту, и поэтому литералы, вы-
рванные из контекста, иногда могут иметь неоднозначную интерпретацию.
Но не всегда: например, в объектно-ориентированном программировании появляются ти-
пы, значения в которых порождаются лишь динамически.
22
60 ГЛАВА 1. ПОНЯТИЯ

Например, в языке Pascal "L" — изображение как литерного, так и строково-


го значений.
Значения обозначаются не только своими изображениями, т. е. литерала-
ми, но часто и именами, которые должны быть описаны как имеющие опре-
деленные значения (например в конструкции описания константы — см. ни-
же).23
Определение 1.5.2. Имя как конструкция (см. стр. 7) языка программиро-
вания — это последовательность символов, обычно букв, цифр и некоторых
дополнительных символов (например, "_"), построенная в соответствии с
определенными правилами (например, в С первый символ — буква или "_"),
которая является лексемой.
Конец определения 1.5.2.
Например, в языке Pascal имя константы вводится описанием, подобным
следующему:
const size =1000;
Изображения констант разных типов, вообще говоря, могут совпадать,
но всегда должны существовать соглашения, позволяющие, в частности, при
определении имени константы, однозначно определить еще и ее тип. Напри-
мер, в Алголе 68 константа описывается в виде

type const =< значение >;

Рассмотрим в языке C++ два частных случая:

const float shortlength = 2.2888;


(1.4)
const double length = 2.2888;

В первом из них shortlength будет интерпретировано как обычное действи-


тельное число, имеющее значение, изображаемое 2.2888. Во втором то же
изображение действительного числа понимается как константа, представля-
ющая длинное действительное число, имеющее повышенную точность и рас-
Обычно встречающимися исключениями из данного правила являются промежуточные
значения, возникающие в ходе вычисления выражений, например, значение x + y в выраже-
23

нии (x + y) ∗ z, и литералы.
1.5. РАБОТА СО ЗНАЧЕНИЯМИ 61

ширенный диапазон значений, так что число незначащих нулей,24 автомати-


чески приписываемых к изображению справа, будет гораздо больше.
В языках традиционного типа переменная — основной способ предста-
вления данных программы. Она отражает понятие памяти вычислительной
машины и интерпретируется как контейнер для размещения значений. Пе-
ременные практически всегда задаются в программе своими именами и рас-
сматриваются по сути дела как более удобное представление адресов яче-
ек компьютера. Почти всегда переменной можно присвоить значение25 , при
этом старое значение, хранившееся в контейнере, пропадает, и в нем теперь
будет новое.
Пример 1.5.3. Рассмотрим оператор
X = Y + Z;

Этот оператор обозначает в языке C следующую последовательность дей-


ствий:
Взять значение из ячейки, обозначенной как Y;
Сложить его со значением из ячейки, обозначенной как Z;
Поместить результат в ячейку, обозначенную как X.
Конец примера 1.5.3.
Пример 1.5.4. Рассмотрим оператор языка Pascal:
x := y + 1
Этот оператор дает следующую последовательность действий:
Взять значение из ячейки, обозначенной как y;
Сложить его с целым числом, имеющим значение единица;
Поместить результат в ячейку, обозначенную как x.
Конец примера 1.5.4.
То, что написано в основном тексте, является огрублением реальной ситуации. Число
будет дополнено до того количества значащих двоичных цифр, которое соответствует пред-
24

ставлению соответствующего типа действительных чисел в машине. Способ дополнения


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

пустимая операция.
62 ГЛАВА 1. ПОНЯТИЯ

Для вычислительных машин с традиционной (классической) архитекту-


рой информация о типе значения не может быть извлечена из машинного
представления самого значения (принцип однородности памяти). Поэтому
тип значения (константы или переменной) остается внешней информацией,
которая определяется тем, как оно используется в тексте программы.
Чаще всего типовая информация вычисляется до выполнения програм-
мы, т. е. статически, и принципиальной новацией объектно-ориентирован-
ного подхода явилось, в частности, широкое использование динамического
определения типа объекта и, более того, динамического его переопределе-
ния в некоторых пределах.26 В дальнейшем это было перенесено и на типы
данных, используемые в работе с распределенными системами. В частности,
в системах COM и CORBA, предназначенных для построения программ, ра-
ботающих по схеме клиент-сервер (программ, которые могут работать в ка-
честве объектов внутри других, заранее неизвестных программ (клиент) или
же, наоборот, использовать такие объекты, которые динамически подставля-
ются в момент исполнения (сервер)) широко применяется тип данных variant,
значение которого несет информацию о своем типе внутри себя.
Наряду с традиционными архитектурами вычислительных машин нашли
свое применение такие архитектуры, в которых принцип однородности па-
мяти не догматизируется (на уровне оборудования он давно потерял статус
абсолюта, например, в связи с очевидными преимуществами использования
быстрых регистров, но здесь речь идет об основной памяти). С этой точки
зрения полезно рассмотреть архитектуру, в которой предлагается так назы-
ваемое тегирование. Смысл его заключается в том, что в ячейках (основной
памяти) выделены специальные разряды, именуемые тегом, которые пред-
назначены для того, чтобы указывать тип хранимого в остальной части зна-
чения (см. рис. 1.7).
Достоинствами тегирования, реализованного аппаратно, являются:

• семантически емкие операции (в частности, поэтому время выполне-


ния программ сокращается);

• дополнительный контроль корректности вычислений.

В языковых терминах это выражается как динамическая (определяемая при


выполнении программы) типизация данных.
26
На практике впервые такие средства получили распространение в языке SMALLTALK.
1.5. РАБОТА СО ЗНАЧЕНИЯМИ 63

Тег Значение
определяет тип того, что хранится в основной части ячейки (тип значения)
<Int a> + <Float b> → <Float aF > + <Float b> Выполняется
процессором

Рис. 1.7. Структура ячейки при тегировании

В языке C тегированное данное соответствует описанию структуры, по-


добному
struct tagged
{
int type_tag;
union
{ (1.5)
int x;
float y;
}
}
Здесь второе поле структуры tagged является значением, тип которого мо-
жет быть либо целым, либо действительным. Первое поле должно (по смы-
слу идентификатора) быть значением, которое однозначно идентифицирует,
согласно некоторой системе договоренностей, тип второго поля. При этом
приходится внешним образом (например, в фирменных либо технологиче-
ских стандартах) определять множество соглашений, являющихся по сути
затыканием очевидных недоделок языка. В данном случае (как и во многих
других) в языках Pascal и Ada приняты более логичные решения. Например,
в языке Pascal говорится о вариантной части структуры, причем должен яв-
но определяться соответствующий тег и значения этого тега, приписанные
каждому варианту:
record
case b : Boolean of
true : (i : integer); (1.6)
false : (r : real)
end;
64 ГЛАВА 1. ПОНЯТИЯ

Соотношение между понятиями имя, значение, тип, константа и перемен-


ная наиболее точно и строго определено в языке Алгол 68. Эта точность от-
ражена и в языковых конструкциях описания имен. Так, описание

type Имя = выражение_вырабатывающее_значение_типа_тип;

трактуется как определение идентификатора Имя, обладающего значением,


т. е. определяется константа, но значение этой константы может быть вы-
числено в ходе исполнения программы. Таким образом, имя, обозначающее
константу, с того момента, когда оно появляется в программе и до того мо-
мента, когда оно исчезает из-за выхода из его области определения, имеет
одно и то же значение. Но при другом входе в тот же блок константа может
иметь другое значение.
Может показаться, что появление константы, обозначающей значение, воз-
никшее в ходе исполнения программы и не имеющее смысла вне контекста,
определяемого данным исполнением, противоречит самой сущности констан-
ты. Но самому математическому понятию константы это не противоречит. В
математических текстах все время встречаются конструкции типа «Обозна-
чим значение, которое существует согласно доказанному выше утверждению
(5), через a0 .» Это утверждение (5) может не иметь смысла вне данного дока-
зательства, а то и быть прямо ложным (если ведем доказательство приведени-
ем к абсурду), так что константа a0 возникает и существует лишь в контексте
данного математического рассуждения, зато в нем она имеет некоторое фик-
сированное значение все время, пока используется. Именно это последнее
свойство нужно нам и от имени, обозначающего программистскую констан-
ту.
Такая конструкция позволяет определять не только имена констант, но и
имена переменных.
type Имя1;
рассматривается как сокращение для

ref type Имя1 = loc type;

где loc type — это выражение, вырабатывающее новое значение типа ref type
(локальный генератор адреса для размещения значений типа type в качестве
содержимого по указанному адресу; это — один из исключительно редко
встречающихся случаев, когда переменная задается не именем, а значением
некоторого выражения). Таким образом, по-прежнему определяется констан-
та, значением которой обладает имя Имя1.
1.5. РАБОТА СО ЗНАЧЕНИЯМИ 65

Но это значение есть адрес (о чем говорит служебное слово ref), и появля-
ется возможность использовать Имя1 как переменную: можно присваивать
ей значение типа type.
Аналогично
ref type Имя2;
сокращение для
ref ref type Имя2 = loc ref type;
Таким образом, есть возможность присваивать Имя2 значение типа ref type.
Иными словами, Имя2 — это переменная, предназначенная для размещения
в ней адресов, или, что то же самое, имен других переменных.
Таким образом, последовательность предложений
real A = 53/5;
real B;
описывает константу A со значением, равным результату деления числа 53 на
5, и переменную B вещественного типа. Можно записать и выполнить при-
сваивание B:=A, но не наоборот. Если же записать
real A := 53/5;
real B;
то A оказывается переменной и присваивание A:=B становится законным.
В языке С все, казалось бы, проще. Понятие констант и переменных опре-
деляется на уровне соглашений об их отношении к памяти вычислителя. Опи-
сание
int X, Y;
определяет имена X и Y как переменные, способные принимать целочислен-
ные значения, а
float X, Y;
как вещественные переменные. Но, как и обычно, иная простота хуже воров-
ства. За недоработку в базовых понятиях приходится многократно платить
целой серией соглашений, которые сейчас будут описаны для простейшего
случая, и которые отдельно и достаточно сложно вводятся для более сложных
случаев, возникающих, в частности, в объектно-ориентированном програм-
мировании.
Для описания соглашений об отношении между переменными и памятью
вычислителя в C вводится понятие класса памяти, отражающее различные
66 ГЛАВА 1. ПОНЯТИЯ

возможности привязки переменных к видам памяти конкретного вычислите-


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

• auto — автоматические переменные (слово auto можно не указывать).


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

• external — внешние переменные: имеют глобальную область действия,


т. е. доступны для использования вне программной единицы, в кото-
рой определена внешняя переменная. Внешними также считаются пе-
ременные, описанные (без служебных слов external) вне описаний всех
функций, и тогда они оказываются доступными для всех функций (внеш-
ние переменные формируют общий контекст функций программы).
Внимание!
Имя, описанное в двух и более функциях как внешняя переменная, определя-
ется как одна и та же переменная, входящая в общий контекст всех функций
программы.
• static — статические переменные. Этим термином определяются пере-
менные, которым приписано фиксированное место в памяти. Они со-
здаются при входе в программу и никогда не уничтожаются. В част-
ности, статические переменные функций сохраняют свои значения от
вызова до вызова функции.

• register — регистровые переменные. Это те переменные, которые для


оптимизации вычислений размещаются в специальных (быстрых) ре-
гистрах процессора (если это возможно). Во всем остальном они пол-
ностью аналогичны автоматическим переменным.
1.5. РАБОТА СО ЗНАЧЕНИЯМИ 67

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

/* Вычисление среднего арифметического значения


двух вводимых чисел */
#include <stdio.h>
#define two 2
int X, Y;
int main (void)
{
scanf ( "%d, %d ", &X, &Y );
printf ("\n average of %d and %d is %d\n", X, Y, (X + Y) / two );
return 0;
}

Поскольку логически понятие константы весьма полезно и ничему не проти-


воречит, в С++ оператор описания константы введен и имеет обычную форму
(показанную в фрагменте (1.4)).
Тому, чему в Алголе 68 соответствовали значения типа ref ref type и так
далее, в С соответствуют указатели, например:

и ссылки:
int **a;

int &r;
68 ГЛАВА 1. ПОНЯТИЯ

Задания для самопроверки

1. Возьмите книгу по программированию на неизвестном вам языке (же-


лательно выбрать первый доступный из списка Ada, Perl, Modula, Java,
FORTRAN) и, не читая текста, поймите некоторые из приведенных в
ней программ. Повторяйте это упражнение раз в несколько месяцев по
мере овладения новыми понятиями программирования.

2. Перепишите одну из понравившихся вам программ, прочитанных в хо-


де предыдущего контрольного упражнения, на используемый вами язык
программирования.

3. Приведите пример программы, в которой выражения вида

(A + B) + C и B + (C + A)

дают различные результаты.

4. Приведите пример предложения естественного языка, которое выгля-


дит замкнутым в себе, но интерпретация которого полностью меняется
в зависимости от контекста.

5. Постройте трансляцию формы условного оператора из п. 1 на стр. 56 в


команды фон Неймановского вычислителя.

6. Постройте программу вычисления значения ex при целых значениях x


в диапазоне от −1 000 000 000 до 1 000 000 000, выдающую порядок
числа и 10 точных значащих цифр. Проверьте свою программу, вос-
пользовавшись какой-либо математической системой высокого уровня
(например, Maple).
Глава 2

Синтаксис, семантика и
прагматика языка
программирования

Назначение этой главы — дать введение в проблему точного определения


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

b) Нужно понимать, как представлены средствами языка сущности, закоди-


рованные в программе. Это включает в себя ясное понимание того, как
будут исполняться предусмотренные нами действия над данными сущно-
стями.

c) Знание конкретных команд вычислителя необязательно, но требуется по-


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

стируемого переполнения диапазона целых чисел сталкиваются не только программисты.


Например, во многих играх это — одна из самых коварных ошибок.

69
70 ГЛАВА 2. ССП

d) Все вышеизложенные аспекты соединяются воедино в проблеме перено-


симости программ, возникающей перед каждым профессиональным про-
граммистом. Например, Вы написали программу для Windows, а исполь-
зовать ее оказалось необходимо для машин с операционной системой Unix,
и так далее. Эта задача является очень сложной и в практически важных
случаях почти всегда теоретически неразрешимой.2 Некоторое время ми-
ровое программистское сообщество интенсивно занималось проблемой
переносимости, но затем, так и не добившись серьезных результатов, пе-
решло на «более интересные» темы освоения новых архитектур машин
и новых операционных систем, тем более, что фирма Sun объявила про-
блему решенной, представив язык Java. Тем не менее, проблема стоит,
и новое поколение программистов столкнется с ней уже в гораздо бо-
лее запущенном состоянии. Связь данной проблемы со всеми сторонами
строгого и точного определения языка очевидна. Для ее успешного ре-
шения, в частности, необходимо выработать четкое понимание абстракт-
ных и конкретных аспектов системы программирования. На нынешний
момент единственно практичное решение проблемы переносимости — с
самого начала писать программу так, чтобы она была переносимой. До-
полнительные затраты труда при этом обычно в пределах 5–10%, а пере-
нос непереносимой программы часто требует ее полного переписывания
с самого начала, что означает не менее чем 100% дополнительных затрат.

§ 2.1. СИНТАКСИС, СЕМАНТИКА

Для того, чтобы строить системы программирования, проверять и пре-


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

тельно чем-то существенным пожертвовать, и что любое предложенное практическое реше-


ние будет либо весьма частным, либо неточным, либо (чаще всего) и частным, и неточным.
2.1. СИНТАКСИС, СЕМАНТИКА 71

программы — то, как исполняется ее текст после перевода транслято-


ром).
Именно так пытались поступать на заре программирования, когда, ска-
жем, легендарный язык FORTRAN создавался вместе и одновременно
с первым транслятором с данного языка. Но это — совершенно неудо-
влетворительный путь, поскольку всякое случайное изменение в про-
грамме-трансляторе может полностью изменить смысл некоторых кон-
струкций языка со всеми вытекающими из этого последствиями.
Рассмотрим пример, как пытаются самоучки изучать язык (в данном
случае C) непосредственно на практике, без всякой теории, основыва-
ясь на этом подходе. Эксперименты с первой программой показывают
ряд свойств программ на этом языке. Так, можно вставлять и удалять
пробелы почти в любом месте текста. Но в некоторых местах это не до-
пускается (например, внутри последовательностей букв). Нельзя (ина-
че ничего не напечатается) удалять строчку

Транслятор выдаст сообщение об ошибке, если нарушена парность ско-


#include <stdio.h>

бок (удалены или добавлены лишние круглые или фигурные скобки).


Таким образом, мы получаем некоторую информацию и о том, какие
программы считаются правильными или неправильными, и о том, как
действуют правильные программы. Ясно, что такой способ очень по-
верхностный и малоэффективный, но самое главное, что он не дает
представления о возможностях языка.
• Считать определением языка формальную лингвистическую систему
(грамматику).
Это соответствует взгляду на язык как на множество правильных по-
следовательностей символов, изложенному на стр. 6. Мы выделяем не-
которое множество правильных (синтаксически) программ L и описы-
ваем его формально. Если это описание явное, оно может быть понято
человеком и использовано для построения либо для контроля правиль-
ности работы транслятора. Но синтаксическая правильность не гаран-
тирует даже осмысленности программы. Таким образом, здесь опреде-
ляется лишь одна сторона языка — синтаксис.
• Считать определением языка соответствие между структурными
единицами текста и правилами интерпретации. Это также односто-
ронний взгляд, поскольку структурные единицы должны соединиться
72 ГЛАВА 2. ССП

в синтаксически правильную систему, но он раскрывает вторую сторо-


ну языка — семантику.

Определение 2.1.1.
Синтаксис алгоритмического языка — совокупность правил, позволяю-
щая формально проверить текст программы, выделив тем самым множество
синтаксически правильных программ, и разбить эти программы на составля-
ющие конструкции и в конце концов на лексемы.
Семантика алгоритмического языка — соответствие между синтаксиче-
ски правильными программами и действиями абстрактного исполнителя, по-
зволяющее определить, какие последовательности действий абстрактного ис-
полнителя будут правильны в случае, если мы имеем данную программу и
данное ее внешнее окружение.
Под внешним окружением понимаются характеристики машины, на кото-
рой исполняется программа (точность представления данных, объем памяти,
другие программы, которые можно использовать при выполнении данной, и
т. д.), и потоки входных данных, поступающие в программу в ходе ее испол-
нения.
Конец определения 2.1.1.
Задаваемое семантикой соответствие между входными данными, програм-
мой и действиями, вообще говоря, определяется лишь полным текстом про-
граммы, но для понимания программы и работы над ней необходимо, что-
бы синтаксически законченные фрагменты программы могли интерпретиро-
ваться автономно от окружающего их текста.
При создании описания языка следует стремиться к выполнению следу-
ющих требований:

• К понятности для программиста и умеренной сложности описания.

• К точности и полной определенности для построения транслятора или


интерпретатора.

Эти цели концептуально противоречивы. В принципе ничто не мешает их


совмещать, но на практике, чем точнее и чем лучше для построения трансля-
тора описан язык, тем, как правило, такое описание более громоздко и менее
понятно для обычного человека. Поэтому точные описания существуют да-
же не для всех реальных языков программирования, а если они и имеются,
то в виде стандартов, к которым обращаются лишь в крайних случаях.
2.1. СИНТАКСИС, СЕМАНТИКА 73

Если какая-то задача не решается целиком, надо пытаться выделить те ее


части, которые легче поддаются решению и позволяют хотя бы приблизиться
к поставленным общим целям. В данном случае оказывается, что синтаксис
описывается во всех разумных случаях намного легче семантики. А синтак-
сис можно разделить на две части: контекстно-свободную грамматику3 (КС-
грамматика) языка, которая представляется достаточно обозримым образом
даже для таких монстров, как C++ или Ada; и правила задания контекстных
зависимостей, дополняющие контекстно-свободное описание.
Содержательно можно охарактеризовать КС-грамматику языка как ту часть
его синтаксиса, которая игнорирует вопросы, связанные с зависимостью ин-
терпретации лексем от описаний имен в программе. На уровне КС-граммати-
ки можно также достаточно точно описать некоторые семантические аспекты
языка.
Контекстные зависимости сужают множество программ, формально до-
пустимых КС-грамматикой. Например, правило «все идентификаторы долж-
ны иметь свои описания в программе» указывает на то, что программа с не-
описанными именами не принадлежит данному языку (хотя она вполне мо-
жет быть допустимой с точки зрения контекстно-свободного синтаксиса).
Неоднократные попытки формально описывать контекстные зависимо-
сти при определении языков показали, что эта задача гораздо более сложная,
чем задание контекстно-свободного синтаксиса. Вдобавок ко всему, даже та-
кие естественные правила, как только что представленное, при формальном
описании становятся громоздкими и весьма трудно понимаемыми челове-
ком. По этой причине в руководствах редко прибегают к формализации опи-
саний контекстных зависимостей (одним из немногих исключений такого ро-
да является Алгол 68, за что разработчики языка были подвергнуты весьма
серьезной критике; позитивным результатом этой критики явилось создание
языка Pascal; см. стр. 28).
В практических описаниях языков и в курсах программирования обычно
довольствуются неформальным, но достаточно точным описанием контекст-
Понятие контекстно-свободной грамматики стало первым строгим понятием в описаниях
практических алгоритмических языков. Что это такое, в базовом курсе программирования
3

определять нет смысла, поскольку за понятием КС-грамматики, при внешней его простоте,
стоит достаточно серьезная теория. Эта грамматика представляется во многих формах (син-
таксические диаграммы, металингвистические формулы Бэкуса-Наура либо расширенные
металингвистические формулы), и, как правило, сопровождает любое систематизированное
изложение конкретного языка. Любое такое конкретное представление КС-грамматики до-
статочно понимаемо.
74 ГЛАВА 2. ССП

ных зависимостей. Приведем пример такого описания.


Пример 2.1.2. Дадим следующую характеризацию налагаемого (в частно-
сти языком С) требования, что каждое имя должно быть описано.
Для каждого имени должно быть описание, в котором оно встречается.
Это описание должно стоять либо в данном блоке, либо в охватывающем
его, и предшествовать в тексте программы использованию данного имени.
Два описания одного и того же имени в одном и том же блоке не считаются
ошибкой лишь в случае, если первое из них является предварительным упо-
минанием, а второе — полноценным описанием. Если есть несколько описа-
ний одного и того же имени в разных блоках, то действующим считается то из
них, которое стоит в самом внутреннем блоке. Если действующее описание
определяет переменную как глобальную, то оно не должно противоречить
никакому другому глобальному описанию той же переменной, встречающе-
муся в программе.
Такая совокупность требований достаточна для того, чтобы человек мог
проверить по тексту программы, как в данном месте понимается данное имя.
Конец примера 2.1.2.
Грамматика определяется системой синтаксических правил (в данном па-
раграфе чаще всего называемых просто правилами). На уровне граммати-
ки определяются понятия, последовательное раскрытие которых, называе-
мое выводом, в конце концов дает их представление в виде последователь-
ностей символов. Символы называются также терминальными понятиями, а
все остальные понятия нетерминальными. Понятия бывают смысловые, т. е.
языковые конструкции, для которых определено то или иное действие аб-
страктного вычислителя, и вспомогательные, которые нужны лишь для по-
строения текста, но самостоятельного осмысливания не имеют. Минималь-
ные смысловые понятия (как уже упоминалось) соответствуют лексемам. Не-
которые понятия вводятся лишь для того, чтобы сделать текст читаемым для
человека. Минимальные из них (они подобны знакам пунктуации) естествен-
но считать вспомогательными лексемами.
Действия абстрактного вычислителя, как правило, описываются в связи
с синтаксическими определениями.
Следующие пункты поясняют тот способ определения контекстно-сво-
бодного синтаксиса, который используется в данном курсе:

a) Понятия языка (языковые конструкции — определяемые и входящие как


составляющие в определение) задаются как слова и сочетания слов рус-
2.1. СИНТАКСИС, СЕМАНТИКА 75

ского языка, обрамляемые угловыми скобками < и >. Эти слова и сочета-
ния слов называются наименованиями понятий.

b) Символы и (непрерывные) последовательности символов, из которых стро-


ятся языковые конструкции, изображают сами себя и обрамляются кавыч-
ками “ и ”. Такие последовательности, в частности, представляют изобра-
жения некоторых лексем.

c) Каждое правило в определении грамматики размещается с нового абзаца.

d) Определяемое понятие отделяется в синтаксическом правиле от своего


определения знаком ::= (читается: «определяется как»).

e) Вариантные фрагменты правила, т. е. те части определяемой конструк-


ции, которые в тексте программы могут иметь различное представление,
обрамляются скобками [ и ]; между собой варианты разделяются знаком
|.

f) Если за фрагментом, заключенным в скобки [ и ], следует знак +, то в


текстовом представлении определяемого понятия фрагмент повторяется
произвольное число раз.

g) Если за фрагментом, заключенным в скобки [ и ], следует знак *, то фраг-


мент повторяется (произвольное число раз) либо отсутствует.

h) Если за фрагментом, заключенным в скобки [ и ], следует знак ?, то фраг-


мент может либо присутствовать, либо отсутствовать.

Имеется ряд достаточно широко применяемых соглашений, облегчающих


понимание металингвистических правил за счет снижения уровня их фор-
мальной строгости (которую, впрочем, легко восстановить):

1. Некоторые из правил объявляются лексическими, т. е. предназначенны-


ми для описания лексем языка. (Для компонент таких правил содержа-
тельный смысл уже не определяется).

2. Следующие фрагменты текстов считаются комментирующими:

(a) пробелы, знаки табуляции и перехода к следующей строке текста;


(b) последовательности символов, заключенные в /* и */, и не содер-
жащие в себе эти пары символов;
76 ГЛАВА 2. ССП

(c) последовательности символов, начинающиеся с "//"и заканчива-


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

<имя . . . > ::= <имя>


<буква> ::= “A” | “a” | “B” | “b” | “C” | “c” | “D” | “d” | “E” | “e” | “F” | “f” |
“G” | “g” | “H” | “h” | “I” | “i” | “J” | “j ” | “K ” | “k ” | “L ” | “l” | “M” | “m” | “N”
| “n” | “O” | “o” | “P” | “p” | “Q” | “q” | “R” | “r” | “S” | “s” | “T” | “t” | “U” | “u” |
“V” | “v” | “W” | “w” | “X” | “x” | “Y” | “y” | “Z” | “z” | “_”
<цифра> ::= “0” | “1” | “2” | “3” | “4” | “5” | “6” | “7” | “8” | “9”

В дальнейшем правила порождения имен понимаются в указанном только


<имя> ::= <буква> [ <буква> |<цифра>]*

что смысле.
Используя контекстно-свободную грамматику, можно следующим обра-
зом записать определения для конструкций, уже показанных к настоящему
моменту в примерах программ:
<программа /* на языке С*/> ::= <заголовок> [ <описание функции /* одна
из которых имеет имя main */> ]+
<заголовок /* это определение упрощено4 */> ::= [<объявление подключа-
емого файла>]*
<объявление подключаемого файла> ::= “#include” <указание имени фай-
ла>
<указание имени файла> ::= “<” <наименование файла> “>” |<наимено-
вание файла>
<наименование файла> ::=
<имя файла> [ “.” <расширение> ]?
Комментарий /*это определение упрощено*/ здесь и далее означает, что в стандарте языка
данное понятие определено более полно. Некоторые из таких понятий в дальнейшем будут
4

дополняться.
2.1. СИНТАКСИС, СЕМАНТИКА 77

<описание функции> ::= [ <тип функции >]?


<расширение> ::=<имя >

<имя функции > “(” [ <параметры> [“,” <параметры> ]* ]? “)” “{” <описания>

<тип функции> ::= <имя типа> |“void”


<операторы> “}”

<параметры> ::= <описатель> < имя параметра > [ “,” <имя параметра>

<описатель /* это определение будет дополняться*/> ::= <имя типа>


]*

<описание /* пока это определение дается только для группы перемен-


<описания> ::=[ <описание> ]*

ных*/> ::= <описатель> <имя переменной> [ “,” <имя переменной> ]*

<оператор /* это определение упрощено*/> ::= <оператор присваивания>


<операторы> ::=[<оператор> ]*

|<оператор вызова функции> |<условный оператор> |<блок> |<пустой опера-


тор>
<оператор присваивания> ::= <адрес> “=” <выражение > /* определяется
позже */“;”
<оператор вызова функции> ::= <имя функции> “(” [ <аргумент> [“,” <ар-
гумент> ]* ]? “);”

<условный оператор> ::= “if (” <условие> “)” <оператор> [ “else” <опера-


<аргумент> ::= <выражение >

тор> ]?
<условие> ::= <выражение >

<пустой оператор> ::= /* пустая последовательность лексем */


<блок> ::= “{” [ <оператор> ]* “}”

Вывод понятия в КС-грамматике можно представить как дерево, в верши-


нах которого стоят терминальные символы, а в корне — выводимое понятие.
Каждая вершина такого дерева представляет собой используемое при выводе
правило. Рассмотрим пример.
Пример 2.1.3. Вывод описания функции main смотри на рис. 2.1. С помо-
щью данного вывода получается простейшая программа

Конец примера 2.1.3.


void main(void){}

Для усвоения материала настоятельно рекомендуется построить вывод


какой-либо из предъявленных выше программ на основе приведенных пра-
вил.
78 ГЛАВА 2. ССП

<пустой оператор>

void main void <оператор>

<имя функции>

<тип функции>
{<описания> <операторы>}
(<параметры>)

<описание функции>

Рис. 2.1. Функция main

Мы определили семантику как соответствие между синтаксически пра-


вильными программами и действиями абстрактного исполнителя. Но остает-
ся вопрос, как задавать это соответствие. В параграфе 2.3 ему будет уделено
внимание специально, а сейчас лишь заметим, что при определении семанти-
ческого соответствия между текстом программы и действиями вычислителя
по существу задается сам вычислитель, т. е. набор его команд и структуры
перерабатываемых им данных.
Если семантическое соответствие определено, то задача трансляции язы-
ка —это построение двух программ:

1. Вычисление функции соответствия: построение по тексту программы


(аргументу функции) его образа как последовательности команд аб-
страктного вычислителя и

2. Вычисление функции моделирования абстрактного вычислителя коман-


дами и структурами данных конкретного вычислителя.

Взаимодействие этих двух вычислений может определяться по-разному, раз-


2.1. СИНТАКСИС, СЕМАНТИКА 79

личны также критерии качества (эффективности) их реализации, не всегда


даже требуется точность, т. е. строгое следование в трансляторе стандарт-
ному определению языка, но всегда необходимо, чтобы программист четко
представлял, как на конкретном оборудовании достигается реализация запи-
санной им программы.
Если вычисления программы на конкретном оборудовании рассматри-
вать как цель деятельности программиста, то, имея в виду задачу трансляции,
можно сказать, что действия абстрактного вычислителя, игнорирующие осо-
бенности конкретных вычислителей, есть модель вычислений программы.
Можно занять и другую точку зрения: считать, что конкретные вычисле-
ния программы моделируют абстрактные. Эта точка зрения больше подходит
для тех, кто занимается реализацией языка, нежели для программистов, по-
скольку именно им приходится решать задачу трансляции.
Для программиста предпочтительнее первая точка зрения, поскольку он,
составляя программу, думает о ней как об абстрактной сущности и совсем
не хочет знать о регистрах, процессоре и других объектах конкретного обо-
рудования.
В соответствии с позицией программиста моделью вычислений языка про-
граммирования более естественно считать то, какой абстрактный вычисли-
тель задается описанием языка, а не то, что получается в результате конкрет-
ной трансляции. Эта позиция обосновывается следующими особенностями
оперирования с программами:

• образ мышления программиста при составлении программы характе-


ризуется как абстрагирование;

• трансляция может осуществляться на разных конкретных вычислите-


лях;

• исследование общих свойств программ математическими методами воз-


можно только при отвлечении от особенностей конкретных вычисли-
телей.

Следуя этой точке зрения, мы, говоря о модели программы, всегда имеем в
виду ее образ в виде команд абстрактного, а не конкретного вычислителя.
Понятие модели вычислений языка естественно распространяется на слу-
чаи, когда используются библиотеки программ. Библиотеки, стандартизован-
ные описанием языка, можно считать частью реализации языка независимо
от того, как реализуются библиотечные средства: на самом языке или нет.
80 ГЛАВА 2. ССП

Иными словами, библиотечные средства — дополнительные команды аб-


страктного вычислителя языка. Независящие от определения языка библио-
теки можно рассматривать как расширения языка, т. е. как появление новых
языков, включающих в себя исходный язык. И хотя таких расширений может
быть много, хотя вполне вероятна противоречивость совместного использо-
вания нестандартных библиотек, рассмотрение модели вычислений для язы-
ка вместе с его библиотеками продуктивно, поскольку это хорошо соответ-
ствует стилю мышления человека, конструирующего программу.
К сожалению, говорить об идентичности трансформации абстрактных
вычислений в конкретные не приходится. Дело здесь не только в том, что
конкретные вычислители имеют, к примеру, фиксированную разрядную сет-
ку и не позволяют выполнять арифметические операции с вещественными
числами точно5 . Гораздо существеннее то, что при программировании для
конкретного оборудования нельзя ни на минуту забывать о времени счета:
программа окажется неприемлемой, если ее вычисление с реальными дан-
ными выходит за определенные временные границы (для каждой программы
свои!). Об этом вынуждены заботиться и трансляторы, а потому они никогда
не следуют модели вычислений языка точно. Другая возможная причина рас-
хождений абстрактной и конкретной моделей вычислений — неоправданная
сложность алгоритмов трансляции, из-за которой приходится идти на те или
иные ограничения, делать (может быть) разумные предположения о том, ка-
кими будут реальные программы и т. д.6
Иными словами, реализованный язык всегда демонстрирует прагматиче-
ский компромисс между абстрактной моделью вычислений и возможностя-
ми ее воплощения. Как влияет такой компромисс на языки программирова-
На самом деле даже в принципе ни один вычислитель не может точно работать с такими
идеальными и актуально бесконечными объектами, как действительные числа.
5

Показательный пример такого рода дает язык FORTRAN. В этом языке разрешается ис-
пользование идентификаторов с пробелами (причем пробелы внутри идентификаторов про-
6

сто игнорируются), в том чисел и таких:


IF A

Транслятор должен отличить присваивания таким переменным от соответствующих начал


DO I

условного и циклического оператора, например


DO I=1

Как он это ухитряется делать, рассматривать нет смысла, но гораздо практичнее и безопас-
DO I=1,. . .

нее запретить пробелы в идентификаторах, что сегодня можно считать стандартом почти
для всех языков программирования.
2.2. ПРАГМАТИКА 81

ния — предмет обсуждения следующего параграфа.

§ 2.2. ПРАГМАТИКА

До сих пор речь шла об определении языка его абстрактным вычислите-


лем. Конкретные условия реализации дополняют такое определение в следу-
ющих направлениях:
• Все определения становятся явными (изгоняются такие понятия, как
«не определено», «определяется реализацией» и т. п.)7 .
• Появляются дополнительные конструкции, описатели и др., обусловлен-
ные реализацией. Они обязательно учитывают:
– особенности вычислительной машины и среды вычислений,
– особенности принятой схемы реализации языка,
– обеспечение эффективности вычислений,
– ориентацию на специфику пользователей.
Эти дополнения называются прагматикой языка. Прагматика иногда пред-
писывается стандартом языка, иногда нет, она зависит от того, для каких це-
лей предназначены язык и его реализация.
Пример 2.2.1. В языке Pascal есть так называемые прагматические коммен-
тарии, например, {$I+}, {$I-} (включение/выключение контроля ввода-выво-
да). Многие из таких комментариев практически во всех версиях одни и те
же. В самом стандарте языка явно предписана лишь их внешняя форма: {$. . . }.
Конец примера 2.2.1.
Рассмотрим крайний случай. Пусть данный язык ориентирован на реали-
зацию лишь в единственной операционной обстановке (например, это какой-
нибудь язык скриптов для микропроцессора, встроенного в собачий ошей-
ник). Есть ли надобность в этом случае выделять прагматику специально?
Надобности нет, но для понимания сути того, что относится к действиям, а
что — к их оптимизации, ее можно выделить.
Принципиально различаются два вида прагматики языка программиро-
вания: синтаксическая и семантическая. Синтаксическая прагматика — это
Нельзя абсолютизировать это требование. Совместные вычисления могут оставаться про-
изводимыми в неизвестном программисту порядке.
7
82 ГЛАВА 2. ССП

правила сокращения записи, можно сказать, скоропись для данного языка.


Пример такого рода — скоропись для определения переменных и констант в
Алголе-68 (см. стр. 64).
Другой пример более интересен. Это команды увеличения и уменьшения
на единицу. В С/С++ они представлены операторами
<переменная>++; или ++<переменная>;
и
<переменная>- -; или --<переменная>;
В Turbo-Pascal:

и
Inc (<переменная>)

соответственно. В С/С++ команды такого рода следует относить к модели вы-


Dec (<переменная>)

числений языка, так как для нее постулируется, что язык является машинно-
ориентированным и отражает в себе особенности архитектуры вычислитель-
ного оборудования, а команды увеличения и уменьшения на единицу предо-
ставляются программисту на уровне оборудования достаточно часто.
Если же рассматривать Turbo Pascal как правильное расширение стан-
дартного языка Pascal, не содержащего обсуждаемые команды из-за стрем-
ления к минимизации средств, то эти команды есть не что иное, как подсказка
транслятору, как надо программировать данное вычисление. Следовательно,
указанные операторы для данного языка нужно относить к прагматике. 8
Семантическая прагматика — это уже упомянутые определения того,
что в описании языка оставлено на усмотрение реализации или предписыва-
ется в качестве вариантов задания вычислений.
Пример 2.2.2. Стандарт языка Pascal утверждает, что при использовании
переменной с индексом на уровне вычислений контролируется выход индек-
са за диапазон допустимых значений. Однако в объектом коде постоянные
проверки этого свойства могут оказаться накладными и избыточными (на-
пример, когда можно гарантировать соответствующие значения индексов).
Стандарт языка для таких случаев предусматривает сокращенный, т. е. без
Вообще говоря, данная подсказка избыточна для современных трансляторов, поскольку
распознать шаблон вида
8

<переменная> “:=” <переменная /* та же самая */> “+” 1


и им подобные не представляет труда не только в тех случаях, когда <переменная> указана
явно, но и тогда, когда она вычисляется (индексирование, косвенная адресация и т. д.). Таким
образом, обсуждаемые операторы можно рассматривать и как прагматику-скоропись.
2.2. ПРАГМАТИКА 83

проверок, режим вычислений. Выбор режимов управляется пользователем с


помощью прагматических указаний для транслятора.
Конец примера 2.2.2.
Очень часто прагматические указания в языках программирования задаются
неявно, иногда даже без соответствующих разъяснений в руководствах. При-
мер операторов увеличения и уменьшения на единицу из Turbo Pascal демон-
стрирует ситуацию наглядно. Гораздо реже языковая прагматика оформляет-
ся явно. В качестве исключения, которое показывает, как можно определять
прагматику в языке, стоит еще раз обратить внимание на то, как указывается
в Turbo Pascal задание контроля значений индексов, другие режимы транс-
ляции и исполнения (см. пример 2.2.1).
Определение модели вычислений языка дает рецепт, как отделять праг-
матику от семантики: прагматика — это то, что органично не вписывается в
абстрактные вычисления программы. Но это весьма неконструктивный ре-
цепт, и разработчики языка должны прилагать специальные усилия, чтобы
обеспечить явное выделение прагматического уровня. Зачем это нужно? От-
вет прост: без этого может сложиться превратное представление как о пред-
лагаемой модели вычислений, так и о ее реализации. Вдобавок к тому, рез-
ко ограничиваются возможности программиста применять в своей практике
методы абстрагирования. Следующие иллюстрации обосновывают данное
утверждение.
Пример 2.2.3. Стандарт языка С предписывает, что системы программиро-
вания на нем должны предусматривать специальный инструмент для обра-
ботки программных текстов, который называется препроцессором. Препро-
цессор делает массу полезных преобразований. Как уже упоминалось, он бе-
рет на себя решение задачи подключения к программе внешних (библиотеч-
ных) файлов (см. 1.2), с его помощью можно скрывать утомительные детали
программирования, достигать ряда нужных эффектов, не предусмотренных в
основных средствах языка (например, именованные константы — см. 1.5.1).
Постулируется, что программа на языке С есть то, что получается после ра-
боты препроцессора с текстом (разумеется, если результат такой работы ока-
жется корректным). Следовательно, использование препроцессора — син-
таксическая прагматика языка. Но это противоречит практике работы про-
граммиста: он просто не в состоянии написать содержательную программу,
которая могла бы выполняться без использования при своей трансляции пре-
процессора. Если при программировании на С ограничиваются употребле-
84 ГЛАВА 2. ССП

нием препроцессорных команд подключения файлов определений и опре-


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

if (x > 0) Firstmacro else PerformAction;

Внешне это выглядит как то, что действие выполняется, если x 6 0, но


первый макрос раскрывается как

PrepareAction; if (x <= 0) CancelAction

Даже сам автор данной программы через некоторое время не поймет, почему
же она у него так себя ведет. . .
Конец примера 2.2.3.
Как это ни странно, подобные построения используются в практике програм-
мирования на С: они применяются, чтобы в одном общем тексте задать не-
сколько вариантов выполняемых программ, которые разграничиваются при
работе препроцессора, т. е. до выполнения.
Наложение команд препроцессора на текст программы — это смешение
двух моделей вычислений: одна из них — модель базового языка С, другая —
модель препроцессора.9 В результате программист при составлении и изу-
чении программ вынужден думать на двух уровнях сразу, а это и трудно, и
провоцирует ошибки.
Модель вычислений препроцессора можно охарактеризовать следующими свойствами.
Исходные перерабатываемые данные — это текст (любой структуры, не обязательно на язы-
9

ке С), в котором имеются строки, начинающиеся с символа ‘#’. Такие строки представляют
команды, управляющие работой препроцессора. Например,

заставляет препроцессор вставить некоторый файл в перерабатываемый текст (не следует


#include . . .

понимать это буквально — нужно просто обеспечить соответствующий эффект). Другой


пример:

дает указание препроцессору на то, что в оставшейся части текста идентификатор two дол-
#define two 2

жен заменяться числом 2. Результат вычислений препроцессора — текст, который не содер-


жит его команд.
2.2. ПРАГМАТИКА 85

Во многих языках, в частности, в Object Pascal, для подобных целей ис-


пользуется более концептуально подходящее средство, так называемая услов-
ная компиляция. Суть его в том, что программисту дана возможность указать,
что некоторый фрагмент компилируется, если при компиляции задан соот-
ветствующий параметр (опция), и не компилируется в противном случае.
При этом оказывается исключенной ситуация, приведенная в предыдущем
примере, поскольку чаще всего способ, которым задается фрагмент, привязан
к синтаксическим конструкциям языка, т. е. условную компиляцию можно
задавать не для произвольных фрагментов, а лишь для тех, которые являют-
ся языковыми конструкциями. При условной компиляции проверяется кор-
ректность синтаксиса и, что не менее существенно, зримый образ программы
явно отражает ее вариантность (именно это свойство нарушено в примере
2.2.3). Конечно, программист на С может писать тексты, в которых не по-
является незаметная смесь вариантов, но, к сожалению, проверить, что это
условие выполнено, препроцессор не в состоянии.
Разработчики С с самого начала мало заботились о концептуальной це-
лостности языка. Это привело к тому, что при развитии языка концептуаль-
ная эклектика множилась: надо было заботиться о стихийно складывающих-
ся традициях использования языка, чтобы не терять активно работающих
пользователей. Ситуация типичная, она многократно повторялась в истории.
В этой связи поучительно будет обсудить ход развития другого языка — Pascal
линии Turbo. Изначально целостная модель вычислений стандартного язы-
ка Pascal не во всем удовлетворяла практических программистов. Это стало
стимулом для развития языка, которое среди прочих вариантов демонстри-
рует последовательность версий Turbo Pascal. Разработчики данной линии
смогли сохранить стиль прародительского языка вплоть до версии 7, несмо-
тря на значительные расширения. В целом они успешно решали очень труд-
ную задачу отделения прагматики от развивающейся модели вычислений.
Однако с ней не удалось справиться тем же разработчикам, когда они взя-
лись за конструирование принципиально новой системы программирования
Delphi и ее языка Object Pascal (причины могут быть разные, в том числе
банальная конкурентная гонка, которая, безусловно, заставляла чем-то жерт-
вовать). Одним из многих отрицательных следствий явилась принципиаль-
ная неотделимость языка от системы программирования, а потому — бес-
смысленность их самостоятельного раздельного развития. А далее история
Delphi с точностью до деталей повторяет то, что было с языком С/С++. В
последовавших версиях системы, вынужденных поддерживать преемствен-
ность, все более переплетаются модель вычислений и прагматика.
86 ГЛАВА 2. ССП

АБСТРАКТНОЕ И КОНКРЕТНОЕ
ПРЕДСТАВЛЕНИЯ ПРОГРАММЫ
§ 2.3.

2.3.1. Абстрактное представление

Обычное текстовое представление программы имеет синтаксическую струк-


туру, в которой каждая структурная единица — языковая конструкция —
является строкой, выводимой из некоторого нетерминального символа грам-
матики языка. Грамматика, таким образом, задает конкретные синтаксиче-
ские правила построения программы как строки символов и, вместе с тем,
определяет, какие структурные элементы могут быть выделены в правиль-
ном тексте программы, который естественно считать конкретным предста-
влением программы. Здесь нет речи о вычислительном аспекте программ, ко-
торый появляется тогда, когда грамматическое, или, что то же, конкретно-
синтаксическое определение языка дополняется заданием модели вычисле-
ний.
Действия абстрактного вычислителя определяются на структурном пред-
ставлении программы.
Можно описывать вычисления на структуре, которая непосредственно за-
дается грамматикой языка: для каждой грамматической (конкретно-синтак-
сической) конструкции определяется, какие действия абстрактный вычисли-
тель должен осуществить с ее составляющими, т. е. с сыновними вершина-
ми в дереве конструкции. Эти составляющие, являясь также конструкциями
программы, в свою очередь, инициируют соответствующие вычисления над
составляющими их конструкциями. Однако такое использование конкретной
синтаксической структуры не совсем адекватно, в частности, по следующим
причинам.
a) Есть элементы, вообще никаким действиям не соответствующие и необ-
ходимые только для обеспечения представления программы в виде текста:
служебные слова, не несущие операционной нагрузки, комментарии и др.

b) Программы, записанные по-разному, но очевидным образом эквивалент-


ные с точки зрения эффекта вычислений, дают разную конкретную син-
таксическую структуру (см. пример).
Пример 2.3.1. Три оператора

X = a ∗ b + c ∗ d;
X = (a ∗ b) + (c ∗ d);
X = ((a ∗ b) + (c ∗ d));
2.3. ПРЕДСТАВЛЕНИЯ ПРОГРАММЫ 87

и подобные им содержательно полностью эквивалентны, тогда как с точки


зрения текстового представления различны.
В то же время, приведенные выше фрагменты не эквивалентны следую-
щему:
X = a ∗ (b + c) ∗ d;
Конец примера 2.3.1.
Таким образом, нужна структура синтаксических понятий, которая соответ-
ствует некоторому понятию эквивалентности программ. От выбранного кон-
кретного понятия эквивалентности зависит выбор структурного представле-
ния синтаксиса, используемого для задания абстрактного вычислителя, ко-
торое называется абстрактно-синтаксическим представлением.
Возможен и другой, чаще всего применяемый на практике, ход. Вместо
явного задания вычислительной эквивалентности задают понятие синтакси-
ческой эквивалентности, которое очевидным образом согласуется с функци-
ональной эквивалентностью. Так, например, предложения, перечисленные
в примере 2.3.1, могут описываться следующим понятием синтаксической
эквивалентности: скобки вокруг подвыражений, связанных операцией бо-
лее высокого приоритета, чем операция, примененная к их результату, могут
опускаться. В данном смысле присваивание рассматривается также как опе-
рация, имеющая более высокий приоритет, чем любая из арифметических
операций.
Еще одна возможность, открываемая переходом к абстрактно-синтакси-
ческим определениям, может быть усмотрена, если мы определим, напри-
мер, эквивалентность подвыражений для сложения и умножения

A + B ↔ B + A.

Здесь абстрактная эквивалентность выражает не просто соглашение об опус-


кании скобок, а свойство самой операции. Опыт показывает, что дальше ассо-
циативности и коммутативности в абстрактном синтаксисе двигаться весьма
опасно.
Рассмотрим, в частности, предельный случай: если считать эквивалент-
ными все программы, которые на одних и тех же входных данных приводят к
получению одних и тех же выходных данных (функциональная эквивалент-
ность программ), то данное понятие является просто алгоритмически не-
разрешимым, и использовать его для определения абстрактного синтаксиса
абсурдно.
88 ГЛАВА 2. ССП

Для понимания сущности вычислений и отделения ее от конкретно-син-


таксических деталей ниже приводится ряд иллюстраций, которые показыва-
ют, как можно определять вычисления в языке программирования на основе
абстрактно-синтаксических структур.
Пример 2.3.2. [Соответствие конкретного и абстрактного] В качестве раз-
вития приведенных примеров эквивалентных и неэквивалентных фрагмен-
тов программ на рис. 2.2 вверху приведены две (неэквивалентные!) абстракт-
но-синтаксические структуры арифметических выражений и их текстовые
представления. Для выражений, получающихся дописыванием лишних ско-
бок, деревья абстрактно-синтаксических представлений будут в точности те
же самые. Другой вариант абстрактно-синтаксической структуры для выра-
жений может быть предложен, если допустить переменное число ветвей у
вершин дерева (см. третий вариант на рис. 2.2, приводится дерево только для
первого фрагмента, поскольку для второго оно совпадает с предыдущим ва-
риантом). Здесь уже используются конкретные знания о свойствах сложения:
его ассоциативность.
Следующий пример иллюстрирует вызов функции:

printf ("\nX1 = %f, X2 = %f\n", X1, X2);

Приведенная на рис. 2.3 структура абстрактного синтаксиса для данного опе-


ратора показывает, как можно ликвидировать привязку конструкции языка к
конкретному синтаксису: остались только имя функции, строка и два пара-
метра.
Конец примера 2.3.2.
Теперь перейдем к построению структуры, порождающей подобные пред-
ставления.

2.3.2. Структура абстрактного синтаксиса

Следует проводить четкое различие между схемами (шаблонами, прави-


лами и т. п.), определяющими строение какого-либо математического объек-
та, и их применением для построения объектов. Представленные примеры
относятся к применению специальных схем для порождения структур уже
известных языковых конструкций. Рассмотрим такие структуры подробнее.
Оператор присваивания в конкретном синтаксисе задается следующим
правилом:
2.3. ПРЕДСТАВЛЕНИЯ ПРОГРАММЫ 89

+ *

a + + +

* d a b c d

b c

a+b*c+d (a + b)*(c + d)

a b c d

a + (b*c) + d
Рис. 2.2. Абстрактно-синтаксическое представление выражений
90 ГЛАВА 2. ССП

printf

\nX1 = %f, X2 = %f\n X1 X2

Рис. 2.3. Оператор печати

<оператор присваивания> ::= <адрес> “=” <выражение> “;”


Языковая суть этой конструкции состоит в том, что имеются две соста-
вляющие оператора присваивания, называемые источником значения (<вы-
ражение>) и получателем значения (<адрес>, это понятие пока можно свя-
зывать просто с переменной), для которых в качестве результата выполнения
присваивания происходит передача значения, выработанного источником, в
память, на которую указывает получатель. Таким образом, после исполне-
ния присваивания имеет место совпадение значения источника и значения,
содержащегося по адресу получателя.
Знаки “=”, “;”, а также порядок следования источника и получателя в тек-
сте — это атрибуты конкретного синтаксиса. Схема задания абстрактно-син-
таксической структуры для этого оператора представлена на рис. 2.4.
Здесь использована следующая нотация:
a) Вершина, имеющая вид — заголовок, ее внутренняя пометка — на-
именование, а внешняя — указание на те типы вершин, к которым мо-
жет присоединяться заголовок (в ней отражено, что присваивание явля-
ется оператором и может быть использовано везде, где есть возможность
употребить оператор).

b) Две ветви, оканчивающиеся вершинами вида , имеют имена: “Полу-


чатель” и “Источник”, а пометки под ними указывают на типы вершин,
которые могут быть подсоединены к данным ветвям (отражено, что в ка-
честве Получателя значения может служить Адрес (в частности, адрес пе-
ременной), а в качестве Источника значения — выражение, которое при-
сваивается Получателю). Вообще говоря, допускаются наборы типов под-
соединяемых вершин, состоящие более чем из одного элемента.
2.3. ПРЕДСТАВЛЕНИЯ ПРОГРАММЫ 91

Оператор
присваивания

Получатель Источник
Адрес Выражение

Рис. 2.4. Абстрактный синтаксис оператора присваивания

Алгоритм работы абстрактного вычислителя для присваивания определяется


следующим образом:
1. Вычислить совместно ветви <Получатель> и <Источник>. В резуль-
тате должны быть получены адрес (например, переменной) и значение
<Выражения>.
2. Записать по полученному адресу вычисленное значение.
3. Завершить вычисление присваивания.
Видно, что, определяя вычисления для присваивания, приходится предпо-
лагать, что определены вычисления для каждой из ветвей: для <Адреса> и
<Выражения>; таким образом, данное определение, как и большинство опре-
делений в программировании, носит индуктивный (или, с операциональной
точки зрения, рекурсивный) характер.
<Выражение> на уровне абстрактной синтаксической структуры — это
не конструкция языка, а совокупность схем для констант и переменных, а
также для сложения, вычитания, умножения и других операций. Констан-
ты и переменные являются терминальными конструкциями языка (лексема-
ми) — для каждой такой конструкции считается известным, как извлекаются
ее атрибуты, в данном случае — значения.
92 ГЛАВА 2. ССП

В качестве характерных примеров раскрытия <Выражения> на рис. 2.5


приводятся схемы задания абстрактного синтаксиса для сложения (a), вы-
читания (b) и переменной (c). Здесь фигурными и квадратными скобками

Выражение,
Выражение Выражение Адрес
Сложение Вычитание Переменная

+ + Имя
Операнд Операнд
Выражение Выражение

(a) (b) (c)

Рис. 2.5. Абстрактный синтаксис арифметических выражений

обрамлены вершины, которые в абстрактно-синтаксической структуре пред-


ставляют одну или более ветвь (пометка “+” у скобок), если ветви могут от-
сутствовать, то используется пометка “*” — см. ниже. Если представлена
единственная ветвь, то это означает использование унарного плюса (<Сло-
жение>) или минуса (<Вычитание>). Различие между схемами с фигурными
и квадратными скобками связано с тем, что в первом случае ветви не упоря-
дочены, а во втором — упорядочены (это соответствует смыслу сложения и
вычитания как арифметических операций).
Схема (c) демонстрирует две новые возможности. Во-первых, есть не од-
на, а две позиции, в которых она может быть применена, причем с разным
смыслом: в одном случае извлекается атрибут адрес, а в другом — значе-
ние. Соответственно, данная конструкция может быть использована для вы-
числения значения, и тогда она представляет собой один из вариантов <Вы-
ражения>, или для вычисления адреса, и тогда она представляет собой один
из вариантов <Адреса>. Во-вторых, связь между наименованием <Перемен-
ная> и ветвью <Имя> изображена пунктиром, отражающим тот факт, что для
абстрактного вычислителя <Переменная> является лексемой, а ее атрибут
2.3. ПРЕДСТАВЛЕНИЯ ПРОГРАММЫ 93

<Имя> — порожден из лексемы конкретно-синтаксического уровня (<имя


переменной>), которая идентифицирует соответствующую лексему абстракт-
но-синтаксического уровня (<Переменную>). Правила идентификации, как
уже говорилось на стр. 73, не сводятся к контекстно-свободным граммати-
кам и определяются особо. Для конструкции-лексемы считается известным,
как извлекаются ее атрибуты (в данном случае адрес или значение <Пере-
менной> по ее атрибуту <Имя>).
Имея схемы, подобные приведенным, можно показать, как строятся аб-
страктно-синтаксические структуры, изображенные на рис. 2.2 и 2.3. К при-
меру, третье выражение на рис. 2.2 получается при однократном примене-
нии схем выражения-сложения и выражения-умножения, а также четырех-
кратным применением схем выражения-переменной с разными значениями
атрибута <Имя> (см. рис. 2.6). Верхняя вершина на рисунке помечена много-
точием. Это означает, что данная структура является частью другой, не рас-
сматриваемой здесь, абстрактно-синтаксической структуры.
Составление подобных абстрактно-синтаксических структур аналогично
тому, как выстраиваются кости при игре в домино, поэтому обсуждаемые
схемы можно назвать абстрактно-синтаксическим домино. Но составление
структур, разумеется, не является самоцелью. Назначение их в том, чтобы
точно определять вычисления, разграничивать текстовую основу языка и его
семантическое содержание. В конечном итоге благодаря такому разграниче-
нию появляется возможность сравнивать семантики различных языков бе-
зотносительно их синтаксиса и реализации в системах программирования.

2.3.3. Абстрактный вычислитель и абстрактная структура

Алгоритм работы абстрактного вычислителя для <Выражения-Сложения>


определяется следующим образом:
1. Вычислить совместно все ветви <Операнд>. В результате должны быть
получены значения всех Выражений-Операндов;

2. Если набор ветвей состоит только из одной ветви, то


объявить значением <Выражения-Сложения> значение этого <Операн-
да>.
Перейти к п. 4;

3. В противном случае просуммировать значения всех <Выражений-Опе-


рандов> и объявить результат значением <Выражения-Сложения>;
94 ГЛАВА 2. ССП

...
Выражение
Сложение

Операнд Операнд
Операнд Операнд

Выражение Выражение Выражение

Переменная Умножение Переменная

Имя = a Операнд Имя = d


Операнд

Выражение Выражение
Переменная Переменная

Имя = b Имя = c

Рис. 2.6. Сложное выражение


2.3. ПРЕДСТАВЛЕНИЯ ПРОГРАММЫ 95

4. Завершить вычисление <Выражения-Сложения>.


Следует обратить внимание на то, что абстрактное вычисление выраже-
ния предполагает объявление результата-значения, тогда как для оператора
присваивания этого не требуется. Вычисление других выражений, использу-
ющих операции, определяется подобным образом. Если порядок операндов
операции (как, к примеру, для вычитания) существенен, то он наследуется от
текстового представления и сохраняется в синтаксической структуре, а пото-
му есть возможность использования его при задании вычислений. В против-
ном случае (как при сложении) текстовый порядок операндов игнорируется.
Конкретно-синтаксическое понятие <операторы> может представлять по-
следовательно выполняемые операторы, параллельно выполняемые опера-
торы, либо совместно выполняемые операторы. Соответственно, в абстракт-
но-синтаксическом представлении должны поддерживаться три варианта ис-
пользования этой конструкции. Однако, применительно к языку С, конструк-
ция <операторы> всегда задает последовательное выполнение. Поэтому для
данного языка можно ограничиться одним из абстрактно-синтаксических ее
представлений. Тем не менее, для полноты картины полезно определить еще
совместно выполняемые последовательности операторов. Это может исполь-
зоваться, например, когда транслятор в состоянии определить, что эффект
выполнения некоторой серии операторов не зависит от их порядка, а потому
допустимо задание такой последовательности вычислений, которая удобна в
данной ситуации.
Таким образом, на рис. 2.7 показаны две схемы для образования абстракт-
но-синтаксической структуры операторов:
• операторы, выполняемые последовательно (схема используется в пози-
ции, называемой далее <Операторы>), и
• совместно выполняемые операторы (схема предназначена для исполь-
зования в позиции <Совместные операторы>).
В обеих схемах скобками выделены наборы, состоящие из нуля и более ве-
твей (на это указывает пометка “*”). Представленные схемы отличаются толь-
ко тем, что в первой из них набор упорядочен, а во второй — нет (исполь-
зование квадратных и фигурных скобок, соответственно). Новое в них то,
что демонстрируется возможность не указывать наименование ветвей, когда
в этом нет необходимости.
Алгоритм работы абстрактного вычислителя для <Последовательно вы-
полняемых операторов> задается следующим образом:
96 ГЛАВА 2. ССП

Операторы Совместные
операторы
Последовательно Совместно
выполняемые выполняемые

* *
Оператор Оператор

Рис. 2.7. Две схемы исполнения

1. Если ветвей у вершины нет, то


завершить вычисление <Последовательно выполняемых операторов>.
2. Вычислить последовательно все ветви вершины.
3. Завершить вычисление <Последовательно выполняемых операторов>.
Как уже отмечалось, разветвление вычислений — необходимое средство язы-
ков программирования. В большинстве языков для этого используется кон-
струкция условного оператора. Эта конструкция, как правило, представлена
в двух видах: с вариантом “иначе” и без него. В абстрактно-синтаксической
схеме можно обойтись одним видом, т. к. нужный эффект дает пустой на-
бор операторов в качестве одной из ветвей. На рис. 2.8 представлена требуе-
мая схема. Здесь <Действия T> — это операторы, выполняемые, когда ветвь
<Условие> дает истину, а <Действие F> — когда оно дает ложь.
Стоит отметить, что на абстрактно-синтаксическом уровне ничего не го-
ворится о порядке ветвей вершин операторов присваивания и условного опе-
ратора. Это чисто текстовая информация, хотя в иных случаях, например, при
задании последовательно выполняемых операторов, операции вычитания и
др., порядок ветвей существенен.
2.3. ПРЕДСТАВЛЕНИЯ ПРОГРАММЫ 97

Оператор

условный

Условие Действия T Действия F


Выражение Операторы Операторы

Рис. 2.8. Условный оператор

Алгоритм работы абстрактного вычислителя для <Условного оператора>


сводится к следующему:

1. Вычислить ветвь <Условие>.

2. Если результат вычисления 1 — истина, то


Вычислить ветвь <Действие T>.

3. Если результат вычисления 1 — ложь, то


Вычислить ветвь <Действие F>.

4. Завершить вычисление <Условного оператора>.

Для определения условного оператора может быть предложен альтернатив-


ный способ, который не предполагает вычисления <Действие F> с пустым
набором операторов. Но в этом случае ветвь “иначе” должна быть факульта-
тивной, т. е. допускающей отсутствие ее в абстрактно-синтаксической струк-
туре. По существу, это отказ от совмещения двух вариантов <Условного опе-
ратора>: с “иначе” и без него. Новые средства, нужные для такого предста-
вления условного оператора, демонстрируются на рис. 2.9, где факультатив-
ность ветви отмечается фигурными скобками с вопросительным знаком (ветвь
может появиться один раз или отсутствовать). Естественно, что при исполь-
зовании альтернативного представления необходимо переопределить алго-
ритм работы абстрактного вычислителя.
98 ГЛАВА 2. ССП

Оператор

условный

?
Условие Действия T Действия F
Выражение Операторы Операторы

Рис. 2.9. Альтернативная форма условного оператора

Подобным образом описываются все другие абстрактно-синтаксические


схемы языковых конструкций и действия абстрактного вычислителя с ни-
ми. Перечислим различные варианты связей между элементами абстрактной
древовидной синтаксической структуры:
a) число ветвей данной вершины фиксированное, вариантное или произволь-
ное;
b) ветви упорядочены или нет;
c) идентификация ветвей посредством имен или иным способом;
d) наличие или отсутствие у вершины атрибута, используемого для вычи-
слений.
Что дают абстрактно-синтаксические представления?
1. Позволяют разделить представления о языке, как о системе построения
текстов и как о способе задания программ для вычислений;
2. Точнее указывают на то, что существенно, а что нет с разных позиций,
точек зрения: текста, семантики трансляции и семантики вычислений;
3. Облегчают сравнение структур языков.
Следует подчеркнуть, что дерево абстрактного синтаксиса — это дерево струк-
туры, т. е. оно отражает отношение между конструкциями, которое можно
охарактеризовать глаголом «содержит»: вершина-конструкция содержит в ка-
честве своих составляющих конструкции ветвей.
2.3. ПРЕДСТАВЛЕНИЯ ПРОГРАММЫ 99

2.3.4. Вызовы как пример синтаксических схем

Особый интерес представляют схемы, которые изображают использова-


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

b) динамическое определение (построение) схемы активизации вычислений


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

Оба варианта имеют право на существование: первый проще с точки зрения


задания абстрактных вычислений, второй более нагляден, т. к. дает возмож-
ность не различать встроенные в язык и определяемые программистом сред-
ства. В качестве иллюстрации на рис. 2.10 приводятся два варианта абстракт-

Выражение, Выражение,
Оператор Оператор
Вызов Имя
функции функции

Имя * *
функции Параметр Параметр
Имя
Выражение Выражение
Имя
(a) (b)

Рис. 2.10. Два варианта вызова функции

но-синтаксических схем, представляющих конструкцию <вызов функции>


для языка С.
100 ГЛАВА 2. ССП

Алгоритм действий абстрактного вычислителя для схемы (a) предпола-


гает определение того, какая именно функция должна быть вызвана (рабо-
та с ветвью Имя функции). Во втором варианте при обработке определения
функции должна быть подготовлена схема и задействован соответствующий
шаблон действий абстрактного вычислителя, который активизируется при
обработке вершины Имя функции в синтаксической структуре. Указанные
различия нашли свое отражение и в наименованиях схем.
Выбор варианта определения семантики вычислений обусловливается осо-
бенностями языка, а точнее, возможностями, предоставляемыми программи-
сту для организации оперирования с создаваемыми им конструкциями.
Пример на рис. 2.11 демонстрирует древовидное представление фрагмен-
та текста программы 1.4.2:
Программа 2.3.1

...
if ( D > 0 )
{
D = sqrt ( D );
X1 = r + D;
X2 = r - D;
printf ("\nX1 = %f, X2 = %f\n", X1, X2);
}
else
...

и возможности использования представленных выше схем.


Если вы захотите переписать фрагмент, приведенный в программе 2.3.1,
скажем, на язык Pascal, и построите его абстрактно-синтаксическую струк-
туру (понятно, что для этого нужна предварительная работа: описание со-
ответствующих шаблонов!), то вы убедитесь, что с точностью до оператора
вывода результатов счета, получится то же, что и для языка С. Это ли не под-
тверждение родственности моделей вычислений двух языков!

§ 2.4. ПРИНЦИПИАЛЬНЫЕ ТРУДНОСТИ, СВЯЗАННЫЕ С СЕМАНТИКОЙ

Любой алгоритмический язык является формальным языком10 . Поэтому


10
Точнее, должен являться в теории и во всякой своей конкретной реализации является.
2.4. ПРИНЦИПИАЛЬНЫЕ ТРУДНОСТИ, СВЯЗАННЫЕ С СЕМАНТИКОЙ 101


Усло вный

Условие Действия T Действия F


> Последовательно Последовательно
выполняемые выполняемые

Операнд Операнд ...


Присваи- Присваи- Присваи- Имя
Переменная Константа
вание вание вание функции

Имя = printf
Имя = D Значение = 0

Параметр Параметр
Получатель Источник Переменная
Константа
Получатель Источник Переменная —
Адрес Имя
функции
Имя = X2
Операнд Операнд
Имя = D Переменная Переменная
Получатель Источник Параметр
Имя = sqrt
Переменная + Имя = X2 Переменная
Имя = r Имя = D
Операнд
Переменная Операнд Операнд Имя = X1
Переменная Переменная

Значение = "\nX1 = %f, X2 = %f\n"


Имя = D Имя = X1
Имя = r Имя = D

Рис. 2.11. Представление фрагмента текста программы


102 ГЛАВА 2. ССП

для его точного определения нужно задать его синтаксис и семантику. Мето-
ды определения синтаксиса рассматриваются во вводных курсах программи-
рования для будущих профессионалов. Хотя полноты определения синтакси-
са обычно не достигают, но изложение этих методов достаточно последова-
тельное и строгое (как Вы могли увидеть в предыдущих главах данной кни-
ги). Даже если нечто объясняется на пальцах вместо кропотливого точного
определения (как, например, соглашения о том, какое описание соответству-
ет какому вхождению идентификатора), об этом говорится прямо.
Семантику же обычно рассказывают на поверхностном уровне, без те-
ни точных определений, в выражениях типа «При исполнении присваивания
x:=S старое значение, содержавшееся в памяти, обозначенной именем x, ис-
пользуется для вычисления S, после чего заменяется на полученное значе-
ние.» Это скорее беда, чем вина начальных (да и профессиональных) курсов
программирования. Ведь смысл — многоаспектное, крайне сложное и не-
формализуемое понятие. Это означает, что любое его уточнение приемлемо
лишь для некоторых целей и отказывает в других случаях. Но это не озна-
чает, что формализовывать семантику не нужно. Ведь если мы откажемся от
ее формализации, мы тем самым сползем на позицию первых языков типа
FORTRAN: смысл выражений языка определяется тем, как они переводятся
транслятором и исполняются компьютером.11
Далее, поскольку алгоритмические языки делались прежде всего не для
удобства определения их свойств, а для удобства их использования, работа-
ет концептуальное противоречие, которое вы могли бы уже заметить в кур-
се, например, логики: чем удобнее представление формального понятия для
применения, тем тяжелее его строгое формальное определение. Определе-
ние семантики реальных алгоритмических языков намного сложнее семан-
тик математических языков (хотя и намного проще семантик естественных
языков). Оно находится как раз на той грани, когда считанные по пальцам
профессионалы еще могут создать точное и полное определение и восполь-
зоваться им, но при этом практически все их интеллектуальные ресурсы ухо-
дят на выработку и освоение данного понятия. Соответственно, данное точ-
ное определение задает лишь значение либо действие конструкций языка,
Конечно, такое определение семантики алгоритмического языка в принципе является не-
погрешимым для каждой конкретной его реализации, применяемой в конкретной среде про-
11

граммирования. Но оно, мало того, что ничего не дает для осмысленного применения чело-
веку, еще и меняется непредсказуемо при изменениях версии или настроек транслятора и
всей вычислительной системы.
2.4. ПРИНЦИПИАЛЬНЫЕ ТРУДНОСТИ, СВЯЗАННЫЕ С СЕМАНТИКОЙ 103

совершенно игнорируя их содержательный смысл. Так что тот, кто пользует-


ся алгоритмическим языком на практике, прибегает к точному определению
(если оно имеется; в частности, у языка Pascal его нет, есть лишь стандар-
ты, которые практически не применяются) лишь в самом крайнем случае, и
обычно при помощи посредника-трансляторщика. И даже те, кто реализуют
язык программирования, обычно вовсю нарушают формальное определение,
если только они не будут проходить официальную аттестацию на строгое со-
ответствие принятому стандарту (как, например, разработчики трансляторов
для языка Ada, сертифицируемых Министерством обороны США).
Рассмотрим, какие же стороны смысла нужно принять во внимание при
анализе семантик алгоритмических языков.
I) Интерпретационная семантика. Прежде всего, программа задает ал-
горитм, который должен исполняться вычислительным устройством. Та-
ким образом, нужно для всех осмысленных конструкций языка (грубо
говоря, для всех лексем) задать, какие действия либо вычисления они
должны вызывать.

II) Функциональная семантика. Далее, программа нам часто нужна как


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

III) Логическая семантика. Далее, программа решает некоторую задачу.


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

IV) Трансляционная семантика. Далее, программа должна быть обрабо-


тана нашей вычислительной системой и переведена в исполняемый код.
Поэтому нас интересует соотношение между понятиями программы и
104 ГЛАВА 2. ССП

внутренними понятиями вычислительной системы. Это необходимо пре-


жде всего для построения транслятора либо интерпретатора. Соответ-
ственно, данный вид семантик подробнее всего разработан, но реже все-
го применим для программистов-практиков.

V) Алгебраическая семантика. И, наконец, нам часто нужно преобразо-


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

В каждый момент практической деятельности нам нужна лишь часть из этих


аспектов семантики программ, но зато конкретные проблемы часто требу-
ют аккуратного выбора конкретного формализма. А в некоторых из перечи-
сленных выше областей различающихся по своим целям и возможностям
формализмов уже достаточно много. Поэтому для успешного применения
накопленных мощных методов и орудий необходимо комплексное видение
сложнейшей проблемы семантического описания алгоритмических языков и
программ. Далее, часто, как и бывает в столь сложных и основательно про-
анализированных областях, приносит пользу не столько само по себе теоре-
тическое решение, сколько подсказанные им идеи либо выявленные при его
анализе ограничения.
Но для корректного применения аппарата и идей приходится преодолеть
достаточно много трудностей, поскольку математический аппарат, использу-
емый теорией семантик алгоритмических языков, весьма изощрен и исполь-
зуется по существу. В частности, приведем таблицу знаний, необходимых для
изучения различных типов семантик.
Задача часто усложняется еще и тем, что разные виды семантик тесно
взаимодействуют. Так, алгебраическая и трансляционная и алгебраическая и
функциональная семантика тесно взаимосвязаны, а логическая вообще по-
чти никогда не ходит в одиночку.
Задания для самопроверки

1. Вернитесь на стр. 70. Подумайте, в каких случаях и почему затраты на


переписывание программы могут превысить 100% ее исходной стои-
мости?
2.4. ПРИНЦИПИАЛЬНЫЕ ТРУДНОСТИ, СВЯЗАННЫЕ С СЕМАНТИКОЙ 105

Интерпретационная Теория алгоритмов


Теория автоматов
Функциональная Топология
Теория моделей
Алгебра (теория структур)
Логическая Теория моделей
Логика (теория доказательств)
Логика (неклассическая)
Трансляционная Теория графов
Теория автоматов
Формальные грамматики
Алгебраическая Теория категорий
Теория моделей
Универсальная алгебра

Таблица 2.1. Семантики и требуемые знания

2. Приведите пример на используемом Вами языке программирования,


когда программа корректна при одних значениях прагматических па-
раметров (прагматических комментариев, параметров трансляции или
чего-то другого, что используется в Вашей стандартной системе про-
граммирования), а при других — успешно транслируется, но дает не-
верный результат.

3. Семантика каких понятий языка задается без ссылок на семантику дру-


гих понятий языка?

4. Возьмите какой-нибудь официальный стандарт описания языка (жела-


тельно Алгола 68 [65] или Ada) и попытайтесь прочесть хотя бы три
страницы определений.

5. Приведите пример графически разных операторов с одинаковым аб-


страктным синтаксисом.

6. Реализуйте приведенные на рис. 2.11 схемы вызова процедуры при по-


мощи действий с указателями.
Глава 3

Стили программирования,
или программирование с
птичьего полета

В данной главе программирование рассматривается, что называется, с


птичьего полета, выявляя прежде всего общие направления и стили и спуска-
ясь до рассмотрения подробностей лишь в необходимых, наиболее важных
либо наиболее характерных, случаях. Наша главная цель — дать рекомен-
дации по выбору того или другого стиля программирования, показав как его
сильные стороны, так и известные ограничения (в особенности те, когда при-
менение стиля практически недопустимо, и те, когда его в широкой практике
упрямо применяют, несмотря на очевидные несообразности).
Стили программирования можно сопоставить со стилями одежды. Клас-
сический стиль приемлем в любой ординарной обстановке, хотя иногда мо-
жет быть и функционально неудобным, и не очень подходящим в других от-
ношениях. Но представьте себе последствия появления в классической евро-
пейской одежде среди талибов или красных кхмеров!
Рассмотрению различных стилей программирования мешала иллюзия без-
условной пользы от универсальности, пронизывающая современную теорию
и практику. До сих пор слово ‘универсальный’ используется в качестве хва-
лебного эпитета. Но универсальная система не всегда является удобным ин-
струментом в конкретных обстоятельствах. Воспользоваться такой системой
целесообразно, в частности, если задача достаточно простая и освоение ин-
струмента для ее решения потребует больше сил, чем само решение. Если
затем этой задачей придется заниматься дальше и дальше, затрата сил на под-

106
3.1. СТИЛИ ПРОГРАММИРОВАНИЯ 107

бор подходящего инструмента многократно окупится.


Беда в том, что порой в качестве универсальной рекламируется большая
специализированная система, плохо спроектированная, плохо проанализи-
рованная или переросшая в результате «добавления новых возможностей»
рамки своей разумности. Поэтому необходимо настораживаться, встречая ре-
кламу очередной панацеи, очередного универсального средства. К тому же
известно, что, как правило, чем сложнее система, тем ниже ее надежность.
А для решения нашей задачи необходимо сначала разобраться в самом
понятии стиля программирования.

§ 3.1. СТИЛИ ПРОГРАММИРОВАНИЯ

Поскольку стиль программирования — неформализуемое понятие очень


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

• Запрещение (или ограничение) применения некоторых других базовых


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

• Требования и рекомендации по оформлению и документированию про-


грамм.

• Совокупность инструментальных и организационных средств, поддер-


108 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

живающих все вышеперечисленные требования и рекомендации.

Предупреждение!1
То, что в данном тексте называется “методикой программирования”, в ли-
тературе традиционно называется “методологией”, а то, что у нас обознача-
ется этим словом, называется “парадигмой программирования”.
Остановимся на методиках. Они направлены на поддержку деятельности
коллектива, работающего с программой, в том числе и ее автора (авторов).
Поскольку методики включают в себя организационные и административ-
ные аспекты, они чаще всего даются в виде рецептов, следование которым, по
мнению их разработчиков, приводит к хорошим результатам. Обычно пред-
полагается (иногда об этом говорится явно), что методика фиксирует опыт
практической разработки хороших и неудачных программ. Однако разные
методики чаще противоречат друг другу, чем согласуются, а потому появля-
ется конкуренция между ними за сферы влияния. В рекламных целях опыт,
на базе которого возникла методика, затушевывается, и утверждается, что
методика годится на все случаи жизни. То же самое, хоть и не столь регуляр-
но, происходит и с методологиями. Это ведет к фетишизации, которая про-
воцирует применение методологий и методик в неподходящих ситуациях, а
следствием этого является их дискредитация. В результате эти действитель-
но полезные методологии и методики порой отвергаются на некоторое время.
Важнейший аспект стиля и соответствующей ему методологии: запре-
щения и самоограничения. Необходимо твердо усвоить, что многие базо-
вые концепции плохо согласуются друг с другом. В результате, будучи со-
единенными вместе, они утрачивают свои лучшие стороны, зато многократ-
но усиливают худшие (как говорят, они концептуально противоречат друг
другу). Поэтому каждый стиль отгораживается от тех конструкций, которые
концептуально противоречат ему (если уж вам такая конструкция необходи-
ма, выносите ее в под-, над- или сопрограмму)2 . Большой бедой является то,
что, запрещая не согласующиеся со стилем конструкции, практически все-
гда представляют их как абсолютно плохие, хотя на своем месте они часто
Заметим, что в программировании происходит систематическая ‘возгонка’ понятий. То,
что в других местах называется орудием, здесь называется методом; то, что называется мето-
1

дом или методикой, называется методологией; то, что называется методологией, возводится
в ранг парадигмы, и т. д. В данном пособии мы придерживаемся выражений, более адекват-
но передающих смысл используемых понятий и согласующихся с общечеловеческой прак-
тикой.
2
На худой конец, используйте комментарии, чтобы явно выделить отступления от стиля.
3.1. СТИЛИ ПРОГРАММИРОВАНИЯ 109

великолепно работают.
Понятие стиля программирования требует глубокой концептуальной про-
работки. При этом требуется обобщение большого количества практическо-
го материала и работа на стыке нескольких сложных теоретических напра-
влений. Из-за трудности задачи и во многих случаях из-за недостаточности
имеющегося теоретического и практического аппарата, анализ ряда стилей
вообще не делался. Поэтому в данном курсе мы приводим только извест-
ные нам результаты анализа, излагая их в как можно более доступной фор-
ме. Знать эти результаты особенно важно из-за того, что они дают не толь-
ко оптимистические заключения, но еще и серьезные предупреждения. Эти
предупреждения гораздо важнее для практика, чем гладенькие результаты о
принципиальной возможности что-то сделать (можно сравнить с прохожде-
нием программы через транслятор: то, что она успешно оттранслировалась,
почти ничего не говорит о ее правильности, а вот выданные при трансляции
предупреждения часто содержат ценнейшую информацию).
Так что в отношении стилей возникает та же ситуация, которая была с по-
нятием программы до появления цельной системы концепций и разделения
конкретных и абстрактных аспектов: несущественные особенности конкрет-
ного представления часто мешают понять идейное единство. С другой сто-
роны, формальное внешнее сходство затушевывает серьезнейшие концепту-
альные различия. Как говорится, за деревьями не видно леса.
Следует учесть, что разные стили ориентированы на разные модели вы-
числений, и, соответственно, система программирования выглядит по-раз-
ному для тех, кто привык к одному или к другому стилю, причем одни и те
же слова, как правило, приобретают разные значения, и могут возникнуть
серьезнейшие трудности при передаче опыта, при общении программистов,
при взаимодействии созданных ими систем.
Обратим внимание на другую сторону стиля. Составление программы —
процесс, гораздо в большей степени аналогичный составлению текста на
естественном языке, чем это может показаться на первый взгляд. Конечно,
речь идет не о художественных текстах, а о тех, которые читают, чтобы по-
лучить информацию. При работе над такими текстами понимание того, кто
именно будет его читать, просто необходимо для качественного изложения3 .
Поэтому рассмотрим читателей программы.
Самый очевидный читатель программы — это вычислитель, для которого
А непонимание и неучет читателя, соответственно, один из распространеннейших недо-
статков.
3
110 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

она составляется. С узко программистской точки зрения, ориентированной в


основном на производителя, можно, более того, говорить лишь о конкретном
вычислителе. Тогда критерии качества программного текста укладываются
в задачу повышения эффективности результирующей программы. Но такой
путь немедленно ведет в тупик. Так, ошибки при программировании (а это
правило; программист чувствует на себе, что атрибут человека — ошибка),
делают необходимым рассмотрение, по крайней мере, еще одного читате-
ля: автора программы, который вынужден исправлять составленный им же
текст. Далее, в реальной ситуации часто появляется и несчастный, который
вынужден исправлять текст другого программиста. Следовательно, появля-
ется ряд критериев качества программы: ее понятность, приспособленность
для модификаций представленного программой алгоритма и его модерниза-
ций. Заметим, что обеспечить выполнение значительной части этих крите-
риев (но далеко не всех) можно, рассмотрев в качестве читателя абстрактно-
го, а не конкретного вычислителя, поскольку именно абстрактные вычисле-
ния являются предметом деятельности программиста. Более того, чем глубже
мы погружаемся в предмет программирования, тем больше становится ясно,
что задача эффективного исполнения программы конкретным вычислите-
лем на самом деле одна из наиболее низкоприоритетных при составлении
программного текста4 .
К сожалению, наши языки программирования очень далеки от того, что-
бы дать возможность обеспечить понятность записи любой разумной про-
граммы, а значит, приходится прибегать к специальным приемам адаптации
текстов программ к восприятию человеком. Среди них в первую очередь нуж-
но указать на комментирование текстов, в том числе, на вид программного
текста (использование пробелов, отступов, переносов между строками и др.).
Уже само по себе наличие комментариев говорит о том, что программа
предназначена для чтения человеком, (путь даже единственным человеком —
ее автором). На практике же всегда приходится иметь дело и с другими чита-
телями программы, которые в силу разных причин вынуждены разбираться
в том, как она работает. Для поддержки этого процесса необходимы соглаше-
ния между разработчиком и читателем, способствующие пониманию. Здесь
Низкий приоритет задачи не влечет, что ей всегда можно пренебречь, но практически все-
гда означает, что ею можно заняться в последнюю очередь. “Практически всегда”, в свою
4

очередь, понимается как: ‘всегда, если задача не заставляет нас явно заняться этим вопро-
сом’. А задача определяется как цель в заданных ограничениях, и тот компонент задачи, ко-
торый чаще всего заставляет заняться вопросами эффективности — это ограничения.
3.1. СТИЛИ ПРОГРАММИРОВАНИЯ 111

прослеживается еще одна аналогия программирования с коммуникациями


на естественном языке: наличие стиля языка общения. Мы говорим о кан-
целярском стиле, о стиле деловой прозы, о стиле того или иного писателя.
При этом всегда понимается, что следование определенному стилю накла-
дывает определенные ограничения на словоупотребление, на используемые
выражения и т. д. С другой стороны, правомерно суждение о стиле языка че-
ловека: хороший стиль, выразительный стиль, бедный стиль и др. Подобные
характеристики свидетельствуют о соответствии способа изложения данно-
го индивидуума нашему представлению о том, как он должен изъясняться.
С точностью до мелочей понятие стиля может быть распространено на упо-
требление языков программирования. Так же, как и для естественных язы-
ков, здесь правомерно говорить об ограничениях на употребление слов (кон-
струкций) языка, о хорошем или плохом стиле программирования.
При анализе стилей программирования важно ответить на вопрос о том,
может ли быть стиль единым для разных языков. Мы не будем заострять
внимание на тех аспектах стиля, которые обусловлены конкретным синтак-
сисом — к синтаксису можно приспособиться и, тем самым, выработать тот
способ, которым реализуется стиль на данном языке. Но что касается модели
вычислений, то здесь все не так просто. Понятно, например, что операцион-
ные и функциональные языки требуют разного программистского подхода,
что нельзя, к примеру, использовать запрещенный оператор, если его язык
не предоставляет. Но и в рамках одного класса моделей вычислений вполне
могут уживаться различные стили. Правомерно ли при программировании
сочетание стилей? Всегда ли оно будет приводить к эклектике5 ? Эти вопро-
сы — предмет ближайшего рассмотрения.
Наиболее распространенной (благодаря соответствующей архитектуре вы-
числительного оборудования и традициям программирования) является опе-
рационная модель фон Неймановского типа, базовая концепция которой —
однородная пассивная память, обслуживаемая активным процессором (см.
§ 1.2). Следствием базовой концепции является то, что основной оператор
традиционных языков программирования — присваивание. Какие дополни-
тельные операции хорошо согласуются с присваиванием, а какие противопо-
казаны ему, как часто должны встречаться присваивания в программе — вот
простейшие из вопросов, ответы на которые необходимы для традиционного
Эклектика — соединение по сути несовместимых вещей. Продукт эклектики часто на-
зывают химерой. Пример эклектики в одежде — фрак с джинсами или телогрейка с туфлями
5

на шпильках.
112 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

языка. Кроме них, есть еще круг вопросов, ответы на которые устанавливают
связи стиля программирования с методами проектирования программы:
• Как структурировать априори однородную память?
• На какие логические и физические части должна разбиваться програм-
ма?
• Как преодолевать сложность процесса разработки программного обес-
печения?
• За счет чего можно достичь повторного использования накопленных
программных решений и облегчать модификации программ?
Ответы на подобные вопросы формирут методологии и методики, реализу-
ющие стили программированния.
Из анализа опыта работы с фон Неймановской моделью вычислений вы-
являются свойства операционных программ, которые при надлежащей ин-
терпретации применимы также для характеристики стилей программирова-
ния в нетрадиционных моделях вычислений. В частности, для этих моделей
можно говорить об уровнях абстрактности описания данных и действий, о
глубине структурированности действий и данных, о связи структурирован-
ности с содержательным смыслом переработки данных, о степени развет-
вленности вычислений (аналоги рекурсии, условных конструкций и циклов
операционных языков). Возникает контекст вычисления, можно оценивать
степень глобальности и локальности действий и данных. Поэтому возмож-
но проникновение стилей, сформировавшихся в рамках фон Неймановской
модели, в нетрадиционное программирование.
Более того, привязанность стиля к базису часто переоценивается. Напри-
мер, в книгах [37, 77] приведены методики, как структурно программиро-
вать для столь неструктурных языков, как старые ассемблеры, FORTRAN и

Но некоторые из характеристик стилей, связанные с традиционной моде-


COBOL.

лью, являются специфичными для нее и не поддаются интерпретации вне ее


рамок. В первую очередь это касается операторов присваивания, в частно-
сти, присваивания глобальным или локальным объектам. Другая специфич-
ная характеристика фон Неймановской модели — централизованное форми-
рование последовательности приказов для задания вычислений и тесная вза-
имосвязь этого порядка с порядком записи операторов в программе. Эта мо-
дель принципиально императивна, т. е. все действия, которые возможны при
3.1. СТИЛИ ПРОГРАММИРОВАНИЯ 113

выполнении программы, задаются в повелительной форме. А можно ли стро-


ить программы, используя изъявительное наклонение? Как показывает опыт
нетрадиционных языков, ответ на этот вопрос утвердительный: да, причем
достаточно красиво и эффективно!
Вместо приказа выполнить то или иное действие в неимперативном язы-
ке должны быть записаны условия, ограничения и предпочтения, которые вы-
являют подходящие для исполнения действия. Следовательно, нет нужды в
разбиении цели на приказы, последовательность которых приводит к реали-
зации цели, — система сама выстроит действия, нужные для достижения це-
ли и сама гибко перестроит их в зависимости от имеющихся и появляющих-
ся соотношений между данными. В одной из первых монографий по языку
Рефал [72] В. Ф. Турчин выдвигает любопытное положение: мерой развито-
сти естественного языка служит доля в нем изъявительного наклонения (чем
меньше приказов, тем язык богаче). Это утверждение выглядит еще более
правдоподобным, если применять его не к языку народа, а к языку индиви-
дуума. Турчин пытается перенести его в сферу искусственных языков и, в
частности, языков программирования. И хотя в те годы (начало 70-х) опыт
языков программирования, построенных на идее использовать соотношения
вместо приказов, т. е. изъявительность вместо повелительности, был очень
мал, продуктивность их использования, разумеется, в своих сферах прило-
жения, уже тогда стала очевидной.
В теории разницу между изъявительным и повелительным наклонением
великолепно иллюстрирует разница между алгоритмами и исчислениями.
Если чрезмерно выделившуюся операционную модель вычислений фон
Неймана поставить на свое место в ряду других моделей, то становится яс-
ной уже высказанная по поводу стилей программирования мысль о том, что
каждой из них есть своя область адекватного применения, что попытки пе-
ренесения практики работы с одной модели на другую не приводят к наивно
ожидаемому росту производительности труда.
Реализация идеи внедрения изъявительного наклонения в языки програм-
мирования возможна по следующим направлениям:

• Системы продукций. Соотношения записываются как правила вывода


предложения в некоторой грамматической или логической форме. Об-
рабатываемые данные сопоставляются с шаблонами, задаваемыми ча-
стями правил, отвечающими за распознавание ситуации, когда правило
может быть применено. Соответствие данных шаблону трактуется как
разрешение применить данное правило. Применение правила состоит
114 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

в замене выделенного при сопоставлении фрагмента данных на что-то


другое. Однократное выполнение такой замены трактуется как атомар-
ный акт вычисления.

• Системы функций. Программа есть соотношение между функциями,


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

• Коммутационные системы. Элемент системы — вершина графа, име-


ющая входные и выходные места. Входные места служат концами дуг, а
выходные, соответственно, их началами. Дуги — это каналы передачи
значений. Вершины, в свою очередь, могут иметь внутреннюю графо-
вую структуру, и так далее по рекурсии. Программа есть граф с дву-
мя выделенными вершинами, одна из которых не имеет входных мест
(генератор перерабатываемых данных), а вторая не имеет выходных
мест (получатель результата). Элементарное вычисление на графе мо-
жет активизироваться, когда ко всем входам вершины поступают зна-
чения, и в этом случае либо производятся предопределенные языком
действия (когда вершина атомарна), либо значения передаются по вну-
тренней структуре к вложенным вершинам. Результатом вычисления
является появление значений на выходных местах (появление полного
результата может растягиваться во времени).
Способ передачи данных и активизации вычислений в коммутацион-
ной системе может рассматриваться как одна из реализаций потока дан-
ных в системе функций. В частности, если граф не имеет циклов, то
коммутационная система становится формой представления нерекур-
сивной системы функций.
Если граф коммутационной системы содержит циклы, то он может быть
проинтерпретирован как рекурсивная система фукнций. Такая систе-
ма может быть теоретически развернута в бесконечный ациклический
граф, но не каждый бесконечный ациклический граф задает рекурсив-
ную систему функций. Но функции, связанные (возможно, косвенной и
взаимной) рекурсией — не единственная вычислительная модель ком-
мутационной системы. Имеются и другие модели, которые совмещают
параллелизм и системы состояний и переходов. Наиболее популярны
из таких моделей сейчас сети Петри.
3.1. СТИЛИ ПРОГРАММИРОВАНИЯ 115

• Ассоциативные системы. Элементы системы — активные данные, пред-


ставляющие собой пары: hзначение, ключi. Пары, имеющие одинако-
вые ключи, соединяются и используются в качестве аргументов дей-
ствия, закодированного ключом. Алгоритм действия может быть задан
в любом стиле (например, в рамках стилей фон Неймановских вычи-
слений), его результатом для системы является набор пар, порождае-
мых в ходе локального действия, а исходные аргументы при этом уни-
чтожаются. Легко заметить, что ассоциативная система может рассма-
триваться как иная форма коммутационной системы, и с точки зрения
возможностей, предоставляемых для программирования, они теорети-
чески эквивалентны. Однако эта форма соответствует иному взгляду на
описываемые вычисления, который лучше подходит, в частности, для
работы с базами знаний6 .

• Аксиоматические системы. Если система отождествлений и замен фик-


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

Даже говорить о цепочке преобразований здесь слишком скромно, поскольку в развитых


аксиоматических системах вывод является сложной иерархической структурой, а алгоритмы
7

поиска вывода планируют сразу эту структуру.


116 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

поскольку он не является элементарной операцией). Программиста в


такой системе обычно интересует не способ вывода, а лишь его осуще-
ствимость.

Не все из этих направлений представлены одинаково в экспериментальных


разработках языков, экспертных систем и, тем более, оборудования. Напри-
мер, в полном сооветствии с теоретическими результатами об окольных пу-
тях в доказательствах и парадоксом изобретателя (см. [63] или краткий очерк
в Приложении А), прямая реализация аксиоматических систем, несмотря на
их явную привлекательность, заведомо неалгоритмична, а потому в чистом
виде они могут быть основой лишь для весьма узких классов описаний при-
кладных областей.
Аксиоматические системы могут служить иллюстрацией того, как попыт-
ки неоправданного распространения методов за сферу их адекватной при-
менимости губят идею. Пусть у нас есть потребность в вычислении значе-
ний формул исчисления высказываний. Для этой области можно указать кон-
структивное решение, в точности соответствующее потребности и даже до-
пускающее реализацию в оборудовании. Имея гипотетическую систему, ко-
торая позволяет устанавливать факты (теоремы) из данной области, пользо-
ватель очень скоро может захотеть большего, пусть даже внешне не очень от-
личающегося от формул исчисления высказываний. Иначе говоря, предпола-
гается желание распространить “хорошо себя зарекомендовавшую систему”
на исчисление предикатов. Даже оставляя без внимания, что это должно при-
вести к радикальной переделке внутренних механизмов (в частности, преж-
няя реализация на уровне оборудования оказывается непригодной), видно,
что мы переходим в область алгоритмической неразрешимости. Но на уров-
не пользовательской модели абстрактных вычислений изменения на первый
взгляд не очень заметны: как в старом, так и в новом случае мы бы хотели,
задавая формулы, поручать системе делать заключение об их истинности, со-
вершенно не заботясь о том, как такие заключения будут получены. В резуль-
тате стремления к распространению в систему включаются инородные эле-
менты, компенсирующие, например, невозможность полного перебора вари-
антов. Но проблемы неразрешимости все равно дают о себе знать: система не
справляется с, казалось бы, корректно поставленными задачами. И хотя она
по-прежнему в состоянии выполнять задания из первоначально очерченной
области, в отношении к ней, как минимум, появляются элементы недоверия.
Вот что случается, когда нарушен принцип обобщения без потерь.
Примерно по такой схеме (с учетом различия моделей вычислений) раз-
3.1. СТИЛИ ПРОГРАММИРОВАНИЯ 117

вивалось логическое программирования в языке PROLOG, который, стре-


мясь немедленно учесть то, что казалось необходимостью, обусловленной
реализацией, стал вбирать в себя повелительное наклонение, чужеродное для
идеи логического вывода в системах продукций.
А вот с языком Рефал, принадлежащим, как и PROLOG, к системам про-
дукций, ситуация прямо противоположна. В. Ф. Турчин, разработчик языка,
понимая безнадежную неэффективность идеи прямого воплощения в языке
системы правил алгоритма Маркова, предпринял попытку поиска специаль-
ного представления перерабатываемых данных, на котором описание соотно-
шений оказывается эффективным. В результате только за счет этого специ-
ального представления удалось выделить значимую с прикладной точки зре-
ния область применения идеи: обработка структурно организованных дан-
ных. Далее, осознавая, с одной стороны, что для принятого способа задания
такой обработки арифметические вычисления оказываются чужеродными, а
с другой — что они нужны, Турчин вместо прямолинейного их внедрения в
язык предложил согласованный с моделью вычислений ‘дозированный’ спо-
соб активизации вычисления любых внешних функций. В результате обоб-
щение без потерь было достигнуто, и эта ситуация сохраняется вплоть до
нынешней версии языка Рефал-5.
В этом отношении поучителен пример функционального языка LISP, в
который встроен чужеродный оператор присваивания. В результате появи-
лась возможность писать программы на LISP как на императивном языке. К
счастью, другие, чисто функциональные возможности языка не забыты —
это означает в точности то, что в отличие от Prologа программист все еще в
состоянии решать содержательные задачи, игнорируя присваивания и другие
элементы повелительного наклонения. Таким образом, чужеродная импера-
тивность LISPа отделима от его функциональной сути.
Неимперативные системы должны в современных компьютерах модели-
роваться императивно. Но конкретный алгоритм построения модели на осно-
ве неимперативной системы должен быть как можно лучше упрятан от про-
граммиста. Наличие одного из таких алгоритмов, достаточно хорошо реали-
зованного, не означает, что все остальные должны отвергаться для данного
класса неимперативных систем (как de facto произошло с логическими си-
стемами после взрывообразного распространения языка Prolog). Напротив,
использование конкретных особенностей реализации быстро и практически
безнадежно портит программы неимперативного стиля (что и произошло с
Prolog-программами), и поэтому альтернативные реализации просто необ-
ходимы. Именно отсутствие признанных и хороших альтернативных реали-
118 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

заций является признаком незрелости данного класса неимперативных про-


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

• Программирование от состояний;

• Структурное программирование;

• Сентенциальное программирование;

• Программирование от событий;

• Программирование от процессов и приоритетов;

• Функциональное программирование;

• Объектно-ориентированное программирование;

• Программирование от переиспользования;

• Программирование от шаблонов.

Это перечисление не претендует на теоретическую полноту и даже на пол-


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

§ 3.2. ПРОГРАММИРОВАНИЕ ОТ СОСТОЯНИЙ

Это — пожалуй, самый старый стиль программирования. Он соответ-


ствует теоретическому понятию конечного автомата (изучаемому в курсе
дискретной математики, см., напр., Минский [57] и Приложение А). На этот
3.2. ПРОГРАММИРОВАНИЕ ОТ СОСТОЯНИЙ 119

стиль программирования наталкивает само устройство существующих вы-


числительных машин, которое представляют собой гигантские конечные ав-
томаты.
В общераспространенных языках C, Pascal, Ada есть все средства для
того, чтобы воспользоваться таким стилем, но писать прямо на алгоритми-
ческом языке программы в этом стиле не рекомендуется, поскольку их пред-
ставление получается ненаглядным, и, соответственно, их модификация за-
труднена. В значительной степени ориентированы на такой стиль машинные
языки и, соответственно, языки ассемблера. Имеется много систем, в кото-
рых такие программы автоматически или полуавтоматически генерируются
по графовому или табличному представлению, более органичному для тако-
го стиля. Например, в системе UML [89, 50] одна из моделей дает возмож-
ность преобразовать диаграмму состояний и переходов в заготовку програм-
мы.
Суть программирования от состояний можно охарактеризовать следую-
щим образом. Определяются:
• множество так называемых состояний, которые может принимать ко-
нечный автомат;

• переходы между состояниями, которые осуществляются под внешним


воздействием (например, под воздействием перерабатываемых данных).

Программа, написанная в таком стиле, является перечнем команд, фиксиру-


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

hсостояние, внешнее воздействиеi

указывается очередное состояние. Описание такой программы может быть


произведено разными способами. Многие из них хорошо изучены теорети-
чески и поддерживаются развитыми методиками программирования.
Современные методики программирования от состояний (напр. [81]) ба-
зируются на таблицах состояний, подобных таблице состояний конечного
автомата. Эти таблицы часто также представляются в виде графов, что осо-
бенно удобно, когда не все возможные переходы между состояниями реали-
зуемы. См., напр., рис. 3.1, где представлены и таблица, и граф.
На таблице состояний или на ее графовом аналоге все действия предпо-
лагаются по умолчанию глобальными, и каждое действие соединено с рас-
познаванием, какое из перечисленных на выходящих из него ребрах условий
120 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

a b c d
4
1 2 3 4 1 Q P
2 3 4 1 2
3 4 1 2 3 R
1 3
Здесь состояния идентифици-
4 1 2 3 4

руются порядковыми номера-


P Q

ми, а воздействия — буквами.


2

Рис. 3.1. Таблица состояний. Граф переходов.

выполнено. В зависимости от того, какое из них выполнено, автомат пере-


ходит в соответствующее состояние, которому опять-таки, если оно не за-
ключительное, сопоставлено действие и распознавание.
Прежде всего, отметим вариацию, отражающую различие автоматов Ми-
ли и Мура в § A.4. Действия в диаграмме состояний и переходов могут ас-
социироваться либо с состояниями (с вершинами графа), либо с переходами.
Ниже Вы увидите примеры, когда при программировании естественно воз-
никают обе эти ассоциации.8
Заметим, что естественно рассматривать таблицы состояний и перехо-
дов как недетерминированные, поскольку после выполнения действия впол-
не может оказаться истинно сразу несколько условий, соответствующих вы-
ходящим ребрам.
Внимание!
В данном случае мы видим один из неистощимых источников ошибок в про-
граммах, который впервые заметил Д. Грис. Если по сути задачи нам все рав-
но, какое из действий будет выполнено, а язык (как те стандартные языки, на
которых обычно работают) заставляет детерминировать переходы, либо ран-
жировав их по порядку, либо принудительно сделав их условия логически
противоречивыми, то при изменении программы очень часто можно натолк-
нуться на трудности, связанные с тем, что после изменения детерминиро-
вать-то надо было по-другому, но уже нельзя различить, какие из условий
существенны, а какие вставлены лишь для детерминации.
Таблицы переходов и состояний являются естественным способом про-
граммирования для модуля, имеющего дело с глобальными операциями над
Конечно же, в каждом конкретном примере нужно использовать лишь одну из них! Эти
две структуры концептуально несовместимы, хотя формально эквивалентны.
8
3.2. ПРОГРАММИРОВАНИЕ ОТ СОСТОЯНИЙ 121

некоторой средой (эти глобальные операции сами, как правило, программи-


руются в другом стиле). Для программирования от состояний характерно go
to, и здесь оно на месте.
Исторически первой моделью программирования от состояний, исполь-
зованной и на практике, и для теоретических исследований, явилось пред-
ставление программы в виде блок-схемы (см., напр., рис. 3.2), узлы которой
представляют собой состояния. Узлы блок-схемы делятся на пять типов:

a) начальная вершина, в которую нет входов и в которой производится ини-


циализация переменных либо состояния вычислительной системы;

b) действия, в которых исполняется вызов процедуры либо оператор, и по-


сле которых авитомат однозначно переходит в следующее состояние;

c) распознаватели, которые проверяют значение переменной либо предика-


та и затем передают управление по разным адресам;

d) соединения, в которые имеется несколько входов и один выход;

e) выход, попав в который, программа заканчивает работу.

A

A∨B

A B
f g B
B ◦ C
B∨C

Рис. 3.2. Блок-схема

Представление программ в виде блок-схем было целесообразно для мно-


гих классов программ, писавшихся в машинных кодах без средств автомати-
зации программирования. Блок-схемы являлись тогда основным средством
122 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

планиравания разработки программ и их документирования. Традиционные


блок-схемы являются, в частности, предметом изучения в теоретическом про-
граммировании (см. книги Котова [44, 45]).
Таблицы переходов концептуально противоречат такому фундаменталь-
ному понятию программирования, как присваивание. В блок-схеме произ-
вольной формы исключительно трудно проследить, как будет изменяться зна-
чение переменной, какие существуют зависимости между данными перемен-
ными, и так далее. Попробуйте, например, разобраться в такой уродине, как
схема на рис. 3.3.
Заметим, что, операции в программировании от состояний глобальны, а
условия локальны. Проверка условия не изменяет состояния всей системы
(ни одного из ее параметров или характеристик), она лишь ведет нас в то или
иное состояние самой программы.
Это подтверждает и анализ практических систем, для моделирования ко-
торых удобно применять программирование от состояний. Например, откры-
вание или закрывание одного вентиля в трубопроводной системе изменяет
все потоки в системе, а проверка, открыт ли вентиль, является чисто локаль-
ной операцией. Изменение температуры рабочего вещества в системе опять-
таки влияет на все ее характеристики, а измерить эту температуру можно,
сняв показания всего одного датчика.
Здесь мы сталкиваемся с необходимостью четко различать внешние поня-
тия, описывающие систему, которая связана с решаемой программой зада-
чей, и внутренние понятия самой программы. Для системы в целом безраз-
личны состояния автомата, который ее моделирует либо взаимодействует с
ней. Для нее важно, какие изменения в ней самой происходят. Таким образом,
состояние памяти вычислительной системы вполне может рассматриваться
как внешняя характеристика по отношению к программе, которая в ней ра-
ботает.
Необходимость одновременного и согласованного рассмотрения внеш-
них и внутренних характеристик приводит к тому, что, когда внутренние ха-
рактеристики раздробляются и детализируются (например, при соединении
стиля программирования от состояний с присваиваниями), программист на-
чинает путаться, согласованность понятий исчезает и возникают ошибки.
Внимание!
Если пользоваться произвольными таблицами переходов, то надо позаботить-
ся о том, чтобы присваивания встречались как можно реже, в идеале обой-
тись без них совсем, либо присваивать лишь значения переменным, которые
немедленно после этого используются в качестве основания для выбора в
3.2. ПРОГРАММИРОВАНИЕ ОТ СОСТОЯНИЙ 123

x:=a
y:=a

x:=f(x)

P(x)
1 0
x:=f(x) stop(x,y)

P(x)
1 0
y:=f(y)
x:=f(x)
P(y)
P(x) 1 0
1 0
y:=f(y)
x:=f(x)
P(y)
P(x) 1 0
1 0
y:=f(y)
x:=f(x)
P(y)
P(x) 1 0
1 0
y:=f(y)
x:=f(x)
P(y)
P(x) 1 0
1 0
Error

Рис. 3.3. А что здесь вычисляется?


124 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

операторе типа case.


В теории данный эффект концептуальной противоречивости проявляется
в неразрешимости большинства естественных свойств схем программ. Од-
нако все основные свойства оказываются разрешимыми, если все функции
одноместные и переменная всего одна (схемы Янова). Как же проинтерпре-
тировать такой теоретический результат?
Поскольку в схеме Янова все присваивания имеют вид x:=f(x), где меня-
ется лишь функция, можно воспринимать переменную x как глобальное со-
стояние среды вычислений, что еще раз подтверждает, что стиль програм-
мирования от состояний эффективен, если все действия глобальные (или, по
крайней мере, рассматриваются лишь с точки зрения их глобальных эффек-
тов), а присваивания по самой своей природе — локальные действия.
Подводя итоги сказанного, дадим рекомендации по выбору представле-
ния. Использование схем Янова целесообразно тогда, когда переходов в про-
грамме относительно немного, условия многообразны, и использование та-
бличного или графового представления лишь затемняет структуру. Если же
условия однообразны, а переходов много, то лучше подходит графовое пред-
ставление. Если же граф переходов практически является полным, то лучше
всего воспользоваться табличным представлением.
Важно подчеркнуть, что для стиля программирования от состояний ха-
рактеристическим качеством является не конкретное представление автома-
та, а то, что программист заставляет себя думать о разрабатываемой или ис-
следуемой программе, как о таком автомате. Во многих случаях эта позиция
оправдана и приносит существенные плоды (например, когда требуется пе-
рерабатывать потоки данных). Но далеко не всякая обработка соответствует
автоматному мышлению. В частности, когда сложность алгоритма превыша-
ет определенные пределы и обозримость автомата не может быть достигну-
та, данный стиль мышления и, как следствие, стиль программирования ста-
новится неприемлемым, в чем мы очень скоро сможем убедиться даже на
простых примерах.

СТРУКТУРНОЕ ПРОГРАММИРОВАНИЕ —
САМЫЙ РАСПРОСТРАНЕННЫЙ СТИЛЬ
§ 3.3.

В теории схем программ было замечено, что некоторые случаи блок-схем


легче поддаются анализу [44]. Поэтому естественно было выделить такой
класс блок-схем, что и сделали итальянские ученые С. Бем и К. Джакопи-
3.3. СТРУКТУРНОЕ ПРОГРАММИРОВАНИЕ 125

ни в 1966 г. Они доказали, что любую блок-схему можно преобразовать к


структурированному виду, использовав несколько дополнительных булевых
переменных. Э. Дейкстра подчеркнул, что программы в таком виде, как пра-
вило, являются легче понимаемыми и модифицируемыми, так как каждый
блок имеет один вход и один выход. Эти наблюдения послужили основой сти-
ля программирования, который, начиная с 70-х гг. XX века, занимает прак-
тически монопольное положение в преподавании и исключительно широко
используется в практике программирования. Этот стиль называется струк-
турным программированием.
В качестве методики структурного программирования Э. Дейкстра пред-
ложил пользоваться лишь конструкциями цикла и условного оператора, из-
гоняя go to, как концептуально противоречащее этому стилю.
Поскольку на практике и в основной части данного пособия речь идет
именно о структурном программировании, здесь мы остановимся на данном
стиле лишь бегло, поставив его на то равноправное место, которое он в прин-
ципе занимает среди альтернативных ему друзей-соперников9 .
Структурное программирование основано главным образом на теорети-
ческом аппарате теории рекурсивных функций. Программа рассматривается
как частично-рекурсивный оператор над библиотечными подпрограммами и
исходными операциями. Структурное программирование базируется также
на теории доказательств, прежде всего на естественном выводе. Структу-
ра программы соответствует структуре простейшего математического рас-
суждения, не использующего сложных лемм и абстрактных понятий.10
Средства структурного программирования в первую очередь включаются
во все языки программирования традиционного типа и во многие нетради-
ционные языки. Они занимают основное место в учебных курсах програм-
мирования ([3, 27, 37, 51]).
При структурном программировании присваивания и локальные действия
становятся органичной частью программы. Достаточно лишь внимательно
следить, чтобы каждая переменная повсюду в модуле использовалась лишь
для одной конкретной цели, и не допускать «экономии», при которой ненуж-
Если нечто в данный момент используется практически монопольно, то из этого не сле-
дует, что оно будет занимать столь же исключительное положение и в будущем и что оно
9

окажется лучшим средством для решения Ваших конкретных задач.


На самом деле традиционным вычислительным программам соответствует не классиче-
ская, а интуиционистская логика, но структура доказательств и большинство правил вывода
10

в них совпадают.
126 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

ная в данном месте переменная временно используется совсем под другое


значение. Такая «экономия» запутывает структуру информационных зависи-
мостей, которая при данном стиле должна быть хорошо согласована со струк-
турой программы.
Структурное программирование естественно возникает в многих классах
задач, прежде всего в таких, где задача естественно расщепляется на подза-
дачи, а информация — на достаточно независимые структуры данных. Для
подобных задач — это, безусловно, лучший стиль программирования, в слу-
чае, если соблюдается основной его инвариант:
Действия и условия локальны

Необходимой чертой хорошей реализации структурного стиля програм-


мирования является соблюдение согласованности, а в идеале и единства, сле-
дующих компонент программы:

1) Структура информационного пространства. Содержательно любую за-


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

2) Структуры управления. Стиль структурного программирования предпо-


лагает использование строго ограниченного набора управляющих конструк-
ций: последовательность операторов, условные, выбирающие и цикличе-
ские конструкции, все вычислительные ветви которых (для циклов — ите-
В этой системе требований без труда распознается так называемая блочная структура
языков программирования, появившаяся еще в Algol 60 и ставшая в настоящее время факти-
11

ческим стандартом.
3.3. СТРУКТУРНОЕ ПРОГРАММИРОВАНИЕ 127

рации) сходятся в одной точке программы, а также процедуры, вычисле-


ния которых всегда заканчиваются возвратом управления в точку вызова;

3) Потоки передачи данных. Разбивая задачу на подзадачи, программист пре-


дусматривает их взаимодействие по данным: одни подзадачи передают
другим данные для переработки;

4) Структуры данных. Данные объединяются в логически связанные фраг-


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

5) “Призраки”. Часто даже сама программа не может быть объяснена через


понятия, которые используются внутри нее. Еще чаще это происходит для
ее связей с внешним миром. Понимание программы возможно лишь после
сопоставления ‘реальных’ внутрипрограммных объектов с ‘идеальными’
внепрограммными. Эти идеальные внепрограммные объекты (призраки)
часто не просто не нужны, но даже вредны для исполнения программы.12
Первым обратил внимание на неоходимость введения призраков для логи-
ческого и концептуального анализа программ Г. С. Цейтин в 1971 г. Впо-
следствии в Америке это ‘независимо’ переоткрыли в 1979 г., хотя упомя-
нутая статья Цейтина была опубликована на английском языке в общедо-
ступном издании. Даже название сущностям было дано то же самое. . .
Для структурного программирования весьма важным является следующее
требование:
Все структуры подчиняются структуре информационного
пространства.
Это общее требование конкретизируется в следующие.

1. Необходимо, чтобы структура управления программы была согласова-


на со структурой ее информационного пространства. Каждой структу-
ре управления соответствуют согласующиеся с ней структуры данных
И, кстати, программистам стоит почаще вспоминать, что с точки зрения пользователя ли-
бо заказчика их внутрипрограмные объекты как раз являются такими же ‘призраками’, ко-
12

торые не имеют никакого отношения к реальной действительности. Так что при переходе от
уровня рассмотрения программы самой по себе к уровню программы как части архитекту-
ры реальной человеко-машинной системы идеальные и реальные объекты порою меняются
местами.
128 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

и часть информационного пространства. Это условие позволяет чело-


веку легко отслеживать порядок выполнения конструкций в программе
2. Подзадачи могут обмениваться данными только с помощью обращения
к объектам из общей части их информационных пространств
3. Информационные потоки должны протекать не ортогонально, а соглас-
но иерархии структур управления; мы должны четко видеть для каждо-
го блока программы, что он имеет на входе и что он дает на выходе. Та-
ким образом, свойства каждого логически завершенного фрагмента
программы должны ясно осознаваться и в идеале четко описываться
в самом тексте программы и в сопровождающей ее документации.13
4. Описание переменных, представляющих перерабатываемые объекты,
а также других, вспомогательных переменных при структурном про-
граммировании строго подчиняется разбиению задачи на подзадачи.
5. Все признаки действуют на своем структурном месте и соответству-
ют идеальным сущностям, которые, согласно парадоксу изобретателя,
должны вводиться для эффективного решения задачи.

Структурное программирование лучше всего описано теоретически, но


частные описания не сведены в единую систему. Одни книги трактуют его
с точки зрения программиста, другие — с точки зрения теоретика. Так что
даже здесь единой системы взглядов еще нет, хотя, видимо, все основания
для ее формирования уже имеются.
Необходимо помнить, что структурное программирование, как правило,
плохо подходит для описания систем глобальных действий. Это выявилось
уже в теории. А именно, в теореме о структурировании схем программ, до-
казанной Бемом и Джакопини, вводились дополнительные локальные булев-
ские переменные, в совокупности которых, по сути дела, запоминалось со-
стояние программы. Без введения дополнительных переменных невозможно
структурировать даже следующий цикл с двумя выходами (см. рис. 3.4).
В общее употребление структурное программирование вошло после по-
пуляризировавшей его работы Э. Дейкстры, в которой, к сожалению, на ука-
занные нами ограничения не было даже намека, так же, как и на ограничения,
вытекающие из самой теоремы Бема-Джакопини. Применение структурных
Как видим, программа должна составляться после того, как программист хорошенько
подумал, а не до того.
13
3.3. СТРУКТУРНОЕ ПРОГРАММИРОВАНИЕ 129

◦ f

Q g P

Рис. 3.4. Цикл Бема-Джакопини

переходов, которые ввел в практику и теорию Д. Кнут (откопавший ориги-


нальную работу Бема-Джакопини и четко выделивший ограничения дейкс-
тровского структурного подхода14 ) избавляет от многих недостатков, прису-
щих методике Дейкстра. Структурные переходы — переходы лишь вперед
и на более высокий уровень структурной иерархии управления, ни в каком
случае не выводящие нас за пределы данного модуля.

Программа 3.3.1

/*Примеры структурных goto.*/


...
do {y = f( x ,y);
if(y>0) break;
x=g(x,y);
} while ( x > 0 );
...
{...
{
if (All_Done) goto Success;
}
...
Success: Hurra;}

Структурные переходы в настоящее время также включаются в общераспро-


страненные языки программирования. Их использование понемногу прони-
кает и в учебные курсы.
Так что, применяя теоретический результат, всегда интересуйтесь не только его форму-
лировкой, но и доказательством, желательно в работе авторов.
14
130 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

Структурные переходы являются паллиативом. Они возникли из-за не-


обходимости выразить мысль, что успех либо неудача глобального процесса
может выявиться внутри одной из решаемых подзадач, и дальнейшая работа
и над текущей задачей, и над всей последовательностью вложенных подзадач
становится просто бессмысленной. В этом случае нужны даже не переходы, а
операторы завершения. Но они в распространенных языках действуют лишь
на один уровень иерархии вверх, а даже теоретически этого недостаточно.15
Еще один вопрос — о совместимости структурного программирования с
рекурсиями. Опыт показывает, что процедура, в которой есть ее рекурсивный
вызов внутри цикла, практически почти всегда неправильна, теоретически
же она выходит за рамки примитивной рекурсии (см. курс логики и теории
алгоритмов) и, как следствие, становится практически невычислимой. Что-
бы здесь не попасть впросак, соблюдайте простое правило.
Внимание!
Не используйте рекурсивный вызов процедуры внутри цикла! Рекурсия и ци-
клы должны быть «территориально разделены»!
Хотя данное правило пригодно в подавляющем большинстве случаев, но
тем не менее иногда бывают исключения. Рассмотрим, например, схему по-
иска вглубь на дереве.

Программа 3.3.2

int search (ELEMENT x)


ELEMENT y; int result; {
if (good(x)) {
return id(x)}
else for(int i=0; i<100; i++)
{y=get_successor(x,i); result=search(y);
if (result>0) return result;
}
return 0;
}

Здесь рекурсии вместе с циклом задают обход дерева возможностей и гибель-


ного размножения рекурсивных вызовов не происходит.
В связи с этим стоит помянуть добрыс словом отечественные разработки в области алго-
ритмических языков, в частности, язык ЯРМО [25], в котором необходимость многоуровне-
15

вых средств завершения конструкций была осознана еще в начале 70-х гг.
3.3. СТРУКТУРНОЕ ПРОГРАММИРОВАНИЕ 131

Для стиля мышления при структурном программировании характерно стрем-


ление к локальности и иерархическое разбиение задачи на подзадачи — так
называемое нисходящее программирование. Нисходящее программирование
часто рассматривали (и рассматривают) как общий способ декомпозиции дей-
ствий. Причины тому следующие:
• так проще, поскольку можно ограничиваться операционной моделью
вычислений;
• раннее программирование — это поиск способов вычислений, т. е. на-
работка типовых алгоритмов, приемов и методов (рекурсия, динамиче-
ское программирование, поиск в глубину и др.);
• нисходящее построение структур данных стало появляться (оформлять-
ся явно) только тогда, когда сформировалась идея абстракции данных;
• давление теории сложности. Задача программистской оптимизации ча-
ще всего формулируется так: требуется найти минимальное по времени
вычислений решение в заданных ограничениях по памяти (а иногда и
без них).
Последнее сомнительно, т. к. наиболее существенными критическими участ-
ками программ очень часто являются алгоритмы доступа к данным, но до
появления концепции абстракции данных понять это было трудно.
Невозможность при нисходящем структурном программировании увидеть
тождественность процедур, работающих на разных ветвях декомпозиции, а
тем более унифицировать несколько формально различных процедур на раз-
ных ветвях в одну общую, показывает необходимость дополнения подхода
средствами, позволяющими “поднять” уровень понятий. Первым выраже-
нием стремления разумно обобщить понятия можно считать привлечение к
разработке структурных программ подхода, получившего название восходя-
щего программирования. К примеру, когда строят библиотеку, занимаются
обобщением, а не делением какой-то задачи. Части, выделяемые в виде би-
блиотечных средств, выбираются исходя из их применимости в различных
контекстах. Таким образом, это построение снизу вверх!
Создание глобального контекста, каковым и является восходящее постро-
ение, требует такого стиля, который ему соответствует. Например, здесь мо-
жет применяться программирование от состояний. Если разделение стилей
по модулям, пакетам и другим единицам декомпозиции проведено строго, то
можно избежать эклектического их смешения.
132 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

Реальный проект — это смесь нисходящего и восходящего подходов:

• сначала сверху вниз для выяснения крупных “строительных блоков”


(при любой технологии: от данных или от действий);

• затем попытка движения снизу вверх, чтобы спроецировать понятия,


оформившиеся ранее, на абстрактные структуры, допускающие аде-
кватную реализацию;

• далее проверка соответствия, углубление нисходящей декомпозиции и


обобщение понятий, выделенных восходящими приемами.

§ 3.4. СЕНТЕНЦИАЛЬНОЕ ПРОГРАММИРОВАНИЕ

Теорией, вдохновившей создателей сентенциального программирования,


являются такие формализации понятия алгоритма, как алгоритмы Маркова и
Колмогорова, и логические исчисления с крупноблочными правилами выво-
да (например, метод резолюций).
Схема сентенциального программирования получила две различные реа-
лизации, появившиеся практически одновременно (Рефал, Россия, В. Ф. Тур-
чин, 1968, Prolog, Р. Ковальски, М. ван Эмден, Великобритания, А. Колмероэ,
Канада, 1971–1974 гг.) и активно используемые до настоящего времени. Обе
они концептуально интересны, и поэтому мы их рассматриваем в соответ-
ствующей главе.
В. Ф. Турчин предложил термин “сентенциальное программирование”,
и он наиболее четко проанализировал методологические особенности этого
стиля на том уровне, на котором это было возможно в 70-х гг. Его анализ лег
в основу нашего рассмотрения, но еще более важно то, что высочайшая науч-
ная и общая культура создателя языка позволила ему избежать эффектных,
но необоснованных заявлений, которыми кишат многие методологические
работы. Поэтому ни один из компонентов анализа Турчина не был отверг-
нут здесь, хотя, конечно же, он был существенно пересмотрен и дополнен с
позиций современной теории и практики.
Сентенциальное программирование отличается следующими особенно-
стями.
• Память представляется как единая большая структура данных. Состо-
яние программы (поле зрения) задается как выражение либо совокуп-
ность выражений.
3.4. СЕНТЕНЦИАЛЬНОЕ ПРОГРАММИРОВАНИЕ 133

В некоторых из вариантов сентенциального программирования эти выраже-


ния могут содержать переменные, а в других — нет. Например, в языке Prolog
поле зрения — конечная система скобочных выражений, подобная
Boss(Commercial)(x,Kovalyov),Father(Mihail,Ivan),
Boss(Formal)(x,YuErshov);
где x — переменная.
• В поле зрения по некоторым правилам может быть выделена активная
часть, т. е. рассматриваемая в данный момент.
Например, в приведенном выше выражении активная часть — первое ско-
бочное выражение

На каждом шаге выполнения программы глобальные свойства активной ча-


Boss(Commercial)(x,Kovalyov).

сти памяти подвергаются анализу. Выполняется не тот оператор, который


стоит следующим в программе либо на который явно передано управление,
а тот, который соответствует условию, выполненному для данного состояния
внутреннего мира программы.
• Каждое действие программы состоит в замене одного выражения на
другое, в традиционной для таких языков записи, левой его части на
правую.
И левая, и правая части оператора могут при этом содержать переменные,
значения которых станут известны лишь в момент применения оператора.16
• Выполняется тот оператор, для которого активная часть выражения по-
лучается из левой части оператора некоторой подстановкой значений
переменных, их конкретизацией либо унификацией17 . Полученные при
конкретизации (унификации) значения переменных используются для
конкретизации правой части (а при унификации — и всего поля памя-
ти). Активная часть выражения заменяется на правую часть правила.
Традиционно в сентенциальных языках избегают называть операторы этих языков опе-
раторами. Например, в Prolog их называют (различные русские варианты транслитерации и
16

перевода) дизъюнктами, предложениями, кл(ау,яу,о)зами.


Термин конкретизация обычно употребляется тогда, когда переменные могут содержать-
ся лишь в правилах, но не в рассматриваемом выражении, а унификация — тогда, когда пе-
17

ременные могут содержаться и в обрабатываемом выражении тоже, и задача согласования


их значений становится симметричной.
134 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

Например, если у нас есть правило

то вся активная часть поля памяти будет проверена на возможность предста-


X*(Y+Z)=>X*Y+X*Z,

вления форме X*(Y+Z), при этом переменные X, Y, Z получат значения, после


чего будет произведена соответствующая замена выражения с использовани-
ем найденных значений переменных.
При сентенциальном стиле образ мышления программиста подобен вы-
воду предложения по грамматике (отсюда и наименование стиля). Этот стиль
удобен, если перерабатываемые данные естественно представлять в виде слож-
ной структуры единиц, которые достаточно глобально распознаются и доста-
точно регулярно, но глобально, меняются. В отличие от структурного про-
граммирования, когда структурируются и локализуются прежде всего дей-
ствия, здесь делается упор на локальности активной (преобразуемой) части
данных, а действия глобальны.
Такой стиль плохо сочетается со структурной моделью вычислений, но в
принципе сочетаем с операционной моделью вычислений от состояний. Ко-
гда действия глобальны и не смешиваются с распознаванием, то противоре-
чия не возникают. Поэтому, к примеру, оказался успешным язык SNOBOL,
который в начале 60-х гг. прошел полпути к сентенциальному программиро-
ванию. В качестве основного действия в нем было отождествление и замена
по образцу, а структура управления была взята из тогдашних языков про-
граммирования и сответствовала программированию от состояний. Но когда
тот же SNOBOL попытались переложить на схему операторов структурного
программирования (в языке SNOBOL-A, разработанном в Ленинграде в на-
чале 80-х гг.), сразу исчезла наглядность языкового представления SNOBOL-
машины, и распространения структурный SNOBOL не получил. На эту по-
пытку можно было бы не тратить усилий, если бы разработчики обратили
внимание на несочетаемость разнородных стилей, о которой уже было из-
вестно в то время.
Сентенциальный стиль программирования до сих пор обсуждался как
стиль программирования на конкретных языках с сентенциальной моделью
вычислений. Однако программировать в сентенциальном стиле можно не толь-
ко на этих языках. Уже сам факт реализации их на фон Неймановской архи-
тектуре доказывает принципиальную возможность моделирования сентен-
циальных примитивов операционными средствами. Поскольку здесь возни-
кают любопытные методы и структуры, такому моделированию посвящен
отдельный параграф 13.4.2.
3.5. ПРОГРАММИРОВАНИЕ ОТ СОБЫТИЙ 135

§ 3.5. ПРОГРАММИРОВАНИЕ ОТ СОБЫТИЙ

Есть довольно обширный круг задач, которые естественно описывать как


совокупность реакций на события, возникающие в среде выполнения про-
граммы. Вообще говоря, так можно трактовать любую программу, обрабаты-
вающую данные: поступление очередного данного — это внешнее событие,
требующее реакции, которая, как минимум, должна быть связана с вводом
этого данного. Понятно, что такая трактовка далеко не всегда продуктивна.
Но она оправдана, например, когда есть много событий, порядок которых не
определяет логику обработки, когда реакция на каждое событие автономна,
т. е. не зависит от реакции на другие события. Общая характеристика по-
добных ситуаций сводится к трем условиям, которые можно считать опре-
деляющими для целесообразного применения стиля программирования от
событий, или событийно-ориентированного стиля программирования:
• процессы генерации событий отделены от процессов их обработки;

• процессы отработки разных реакций не зависят друг от друга;

• можно определить единый механизм установления контакта между со-


бытием и реакцией на него, никак не связанный с обработкой.

Косвенным признаком полезности событийного стиля могут служить труд-


ности декомпозиции решаемой задачи, при которой генерация и обработка
рассматриваются объединенно. Например, в схеме программирования от со-
стояний вполне можно рассматривать в качестве событий появление данных,
приводящих к смене состояний. Однако здесь в каждом состоянии свои со-
бытия, пусть даже они и одинаковы с точки зрения генерации. Знание после-
довательности поступления событий на обработку необходимо для органи-
зации вычислений. Область эффективного применения событийного стиля
программирования начинается именно там, где становится неудобным ис-
пользовать граф перехода между состояниями.
Стиль событийного программирования — это создание для каждого со-
бытия собственной процедуры-обработчика. Порядок, в котором обработчи-
ки описываются в программе, не имеет никакого значения. Более того, они
могут, а во многих случаях и должны быть приписаны к разным структур-
ным единицам программы, в рамках которых только и осмыслена реакция
на событие. Как следствие, продуктивно разбивать реакцию на событие на
части, за которые отвечают такие структурные единицы, и иметь несколько
реакций (разных) структурных единиц на одно событие.
136 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

Фактическую реакцию на событие, иными словами, вызов обработчика,


нужно связывать не со структурными единицами текста программы — с ни-
ми связаны программы обработчиков, а не их вызовы, а с теми динамически-
ми сущностями, которые порождаются при активизации этих структурных
единиц. По такой схеме организуются событийный механизм в рамках объ-
ектно-ориентированного программирования. Здесь обработчики, как и дру-
гие методы, приписаны к классам — структурным единицам текста програм-
мы, а вызовы обработчиков, включая проверку необходимости реагирования,
осуществляют объекты, которые являются экземплярами классов, т. е. струк-
турными единицами процесса выполнения программы.
Когда имеется несколько реакций на одно и то же событие, встает вопрос
о порядке, в котором они должны выполняться. Постулат автономности ре-
акций здесь уже не действует, поскольку речь идет не о разных событиях, а о
распределении объединенной реакции по программным компонентам. Прак-
тические потребности работы в таких ситуациях мотивируют особый стиль
программирования: программирование от приоритетов (см. следующий па-
раграф). В то же время выделяется важный с практической точки зрения
класс событийных программ с обособленными реакциями, который харак-
теризуется тем, что разные реакции на одно событие не конкурируют между
собой: каждая динамическая структурная единица (например, объект) дела-
ет свое дело, связанное с событием, никак не пересекаясь с делами соседей.
Для этого класса может быть построен очень простой механизм установле-
ния контакта между событием и реакцией, теперь — реакциями! Суть его в
том, что организуется цикл опроса всех динамических структурных единиц,
которые в принципе могут отвечать за реакцию на события. В каждой такой
единице активизация реакции на событие задается следующей схемой:

если для данного события должна быть выполнена реакция


то обработчик этой реакции вызывается для исполнения
// иначе в связи с событием данная структурная единица ничего не делает

Понятия, введенные при объяснении событийного программирования, ил-


люстрирует схема на рис. 3.5, на котором блоками с загнутыми углами изо-
бражены фрагменты текстов программ, а жирными стрелками — порожде-
ние экземпляров и событий. Из схемы видно, что одна и та же программа
обработчика может быть активизирована из разных динамических структур-
ных единиц, а также то, что одна и та же динамическая структурная едини-
ца может в разные моменты вычислений реагировать или не реагировать на
3.5. ПРОГРАММИРОВАНИЕ ОТ СОБЫТИЙ 137

Описание структурных единиц, предусматривающих реакцию на соб ытия:


S1 S2
... ...
if(rE1)hE1; условия if(rE1)hE1; условия
активизации активизации
if(rE2)hE2; if(rE3)hE3; реакций
if(rE3)hE3; реакций
rEi, hEi (i =1, 2, 3) rEi, hEi (i =1, 2)
... ... локальны в S2
локальны в S1
hE1 hE2 hE3 hE1 hE

Программы обработчиков событий

Порождение динамических экземпляров структу рных единиц


O1(S1) O2(S1) O3(S2)
rE1=true rE1=true rE1=true
rE2=true rE2=false rE3=true
rE3= false rE3=true

Генерация событий и активизация реакций:


Одна и та же программа из S1 Программа из S2

Генератор E1 ⇒ {O1.hE1, O2.hE1, O3.hE1}


событий E2 ⇒ {O1.hE2}

E3 ⇒ {O2.hE3, O3.hE3}

Изменение rE1 из O1 до повторной генерации E1:


O1.rE1 := false;
E1 ⇒ {O2.hE1, O3.hE1}

Рис. 3.5. Схема активизации обработчиков событий


138 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

событие. В то же время, если при программировании не предусмотрен об-


работчик события, то в динамике вычислений он не может возникнуть ‘из
ничего’.
Примечательно, что цикл опроса может быть без труда распараллелен, и
разные структурные единицы, реагирующие на события, могут активизиро-
вать свои обработчики для совместного исполнения. Более существенно то,
что для обсуждаемого класса возможно построение универсального, т. е. не
зависящего от решаемой задачи цикла опроса. В таком случае от програм-
миста не требуется даже знать о цикле опроса, и он вполне может сосредо-
точиться на обработчиках, описываемых в контекстах структурных единиц
текста программы.
Когда составляемая программа целиком укладывается в схему событий-
но-ориентированного программирования с обособленными реакциями, а не
является частью другой, охватывающей программы, то управление во всех
таких программах может быть унифицировано: содержательные части, ка-
сающиеся обработки событий встраиваются (в разных смыслах: вызывают-
ся, подключаются как внешние библиотеки и др.) в стандартный программ-
ный текст, обычно называемых проектом. Получается, что схема составле-
ния программы как последовательного текста заменяется более адекватной
задаче схемой, основанной на встраивании (см. рис. 3.6). Именно такая схема
событийно-ориентированного программирования приобрела сегодня широ-
кую популярность после того, как она была предложена в языке Visual Basic.
В замкнутом относительно средств объектно-ориентированного программи-
рования виде впервые она была реализована в системах Think C++, Think
Pascal и Delphi. Именно эту конкретную реализацию сегодня чаще всего на-
зывают событийно-ориентированным программированием. Как естественно
предположить, в этой объектно-ориентированной системе в качестве струк-
турных единиц программы, способных реагировать на события, выступа-
ют классы, а в качестве динамических экземпляров таких структурных еди-
ниц — объекты. Обработчиками событий служат методы этих объектов.
До сих пор мы ничего не говорили о том, какие события возможны при
программировании в событийно-ориентированном стиле. Исторически этот
стиль как определенный “художественный прием” сформировался в области
разработки операционных систем, где естественно связывать понятие собы-
тия с прерываниями. Прерывание — это сигнал от одного из устройств (мо-
жет быть, и от самого процессора), который говорит о том, что произошло не-
что, на что нужно обратить внимание. Когда происходит прерывание, опера-
ционная система распознает, какая причина его вызвала, и далее формирует
3.5. ПРОГРАММИРОВАНИЕ ОТ СОБЫТИЙ 139

Текст программы (основной), разрабатываемой программистом

Встраивание,
вызов и т.д.
(любой смысл)

Модули (библиотеки)
a) составление последовательного текста программы

Стандартизованный текст
программы (проект)

Встраивание
(любой смысл)

Библиотечные мод ули Фрагменты, разрабатываемые программистом

b) встраивание разрабатываемых модулей

Рис. 3.6. Две схемы составления программы

событие как информационный объект, вызывающий реакцию программной


системы. Возможны разные способы реагирования на события, в том числе
и передача его для обработки той программе, при выполнении которой воз-
никло прерывание, породившее это событие.
Из потребности такой обработки, собственно говоря, и сформировался
событийно-ориентированный стиль программирования. Наиболее очевидная
область его адекватного применения — реализация интерактивных взаимо-
действий программы с пользователем и решение других подобных задач (на-
пример, тех, которые требуют опрос датчиков состояния каких-либо техно-
логических процессов).
В частности, при взаимодействии с пользователем других событий, кроме
нажатия на клавишу, перемещения курсора мыши, указания световой кнопки
и т. п., не требуется. И не случайно именно те прерывания, которые способ-
на перенаправить операционная система для обработки на уровень пользова-
тельской программы, стали основой для выработки систем событий, реакция
на которые задается в событийно-ориентированном стиле. События этого ро-
140 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

да можно передать от одного обработчика к другому, не нарушая условий


адекватного применения данного стиля: для этого достаточно объявить та-
кое перенаправление события как генерацию нового события. Но требуется
также генерация событий, не связанных с прерываниями.
К примеру, программируя в событийно-ориентированном стиле, естествен-
но объявить событием ситуацию, когда значение одной переменной стано-
вится больше другой. Однако такого рода свойства вычислительного про-
цесса никак не отражаются в системе прерываний, а потому обычные языко-
вые средства событийно-ориентированного программирования (например, в
Delphi), часто провозглашаемые как универсальные, здесь не помогут. Нуж-
ны иные механизмы, которые сразу же уводят программиста из области ша-
блонов проектов, стандартизующих обработку событий.
Об этом ограничивающем применимость подхода обстоятельстве пред-
почитают умалчивать, и, как это обычно бывает в подобных ситуациях, при
беглом знакомстве с соответствующими средствами могут возникнуть ни на
чем не основанные ожидания с последующими разочарованиями и даже от-
торжением стиля, хорошо работающего на своем месте.
Примером языка, ориентированного на собственно событийно-ориенти-
рованное программирование, может служить Perl.
Событийный стиль может оказаться удобным не только на уровне кон-
кретного программирования. Часто концептуальное осмысление задачи по-
лезно проводить в терминах системы событий, а уж затем решать, как ре-
ализовывать систему: непосредственно использовать события как средство
управления, моделировать событийное взаимодействие имеющимися сред-
ствами или вообще отказаться от этого механизма в данной разработке.
В качестве иллюстрации этого тезиса ниже с позиций событийного сти-
ля программирования описываются “взаимоотношения” между синтаксиче-
ским анализом и вычислениями семантики программы. Реализация этих вза-
имоотношений — обычная задача, решаемая при разработке любого транс-
лятора. В событийном описании этой задачи в качестве событий системы
естественно рассматривать распознавание во входном потоке (транслируе-
мом тексте) синтаксических единиц, т. е. конструкций языка. Такое событие
требует в качестве обработчика семантическую подпрограмму, связанную
с распознаваемой конструкцией. Следовательно, выявлены два автономных
процесса: синтаксический анализ, задающий генерацию событий, и семан-
тическая обработка, осуществляемая как последовательность вызовов се-
3.5. ПРОГРАММИРОВАНИЕ ОТ СОБЫТИЙ 141

мантических подпрограмм-реакций, упорядоченная событиями.18 Коль ско-


ро есть концептуальное разделение процессов, его можно воплотить в реаль-
ной системе в событийном стиле. Это решение влечет за собой следующее:
• Необходимость рассмотрения в качестве событий не одного факта рас-
познавания конструкции, а двух: начало и завершение распознавания
конструкции, поскольку именно с ними, а не с конструкцией в целом
связываются семантические действия;
• Для обсуждаемой задачи существенно, что события возникают не в
произвольном порядке, их множество имеет вполне определенную струк-
туру. Нисколько не удивительно, что эта структура в точности соответ-
ствует синтаксической структуре текста: например, не может возникать
событие завершения распознавания конструкции раньше, чем возник-
нет событие начала конструкции;
• Следствием этой структуры является понятие ожидания вполне опре-
деленных, а не произвольных событий. Если ожидание не оправдалось,
то это можно считать событием еще одного вида: ошибочная ситуация.
Количество таких событий в точности равно числу наборов ожидаемых
событий.
При событийном программировании разделение процессов генерации и об-
работки событий влечет за собой их полную независимость. В практике про-
граммирования транслирующих систем это качество считается не очень удоб-
ным, и вместо событийного механизма обычно используется схема сопро-
граммного взаимодействия с так называемым синтаксическим управлением:
синтаксический анализатор сам вызывает нужные семантические програм-
мы, когда распознает необходимость сделать это (в событийном стиле это
означает генерацию соответствующего события). Сопрограммную схему и
синтаксическое управление нам предстоит еще изучить в дальнейшем, здесь
же отметим только то, что нет никакого противоречия между двумя схемами,
и единственное различие между ними в том, что при синтаксическом упра-
влении ожидание события оказалось возможным подменить прямым вызо-
вом подпрограммы. Иными словами, в схеме трансляции удается статически,
Точно также в событийном стиле можно трактовать взаимодействие двух других процес-
сов транслирующей системы: лексического анализа — генератора событий-лексем и синтак-
18

сического анализа — процесса, реагирующего на возникающие события-лексемы. Но здесь


применение такого стиля не дает заметных преимуществ по сравнению с традиционной схе-
мой сопрограммного взаимодействия.
142 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

до выполнения программы вычислить события, наступление которых требу-


ет вызова их обработчиков (семантических подпрограмм). Вообще, сопро-
граммное взаимодействие можно трактовать как статически вычислен-
ную событийность19 .
Разумеется, событийное программирование много шире случаев, в кото-
рых можно статически определять, когда будет нужно активизировать обра-
ботчики, т. е. когда полезно объединение генерации событий с их обработкой
и за счет такого объединения растворять события в программе. В частности,
и по этой причине его следует рассматривать как самостоятельный стиль. Но
интересны и такие случаи, когда объединение, хотя и возможно, но не дела-
ется. Конкретный пример — XML/XSL технология, для которой разделение
структуры и интерпретации текста считается принципиальным: это позво-
ляет строить ‘съемные’ системы обработки, иметь несколько независимых
таких систем для разного назначения. В своей сфере применения в такой
многовариантности усматриваются большие преимущества, но, как всегда,
перенос принципов данной технологии “куда угодно” чреват уже не раз от-
меченной неадекватностью.

§ 3.6. ПРОГРАММИРОВАНИЕ ОТ ПРИОРИТЕТОВ

Обсуждая событийное программирование, мы упомянули о том, что при


программировании в этом стиле может возникнуть потребность упорядочи-
вания выполнения нескольких конкурирующих между собой реакцийна со-
бытия. Достаточно универсальным средством удовлетворения этой потреб-
ности является приписывание реакциям приоритетов: сначала выполняют-
ся те реакции, которые имеют больший приоритет. Этот прием, как выяви-
лось на практике, пригоден для решения достаточно широкого круга задач.
Он стал основой для формирования самостоятельного стиля программиро-
вания от приоритетов.
Этот стиль порожден практическими потребностями. Он предназначен
для организации работы многих взаимодействующих процессов, динамиче-
ски порождаемых и динамически исчезающих. Фундаментальных теорети-
Статическое вычисление некоторых частей программ — очень важный и имеющий в
потенциале исключительно широкую область применения прием программирования. Если
19

программист предусматривает, что его программа будет частично вычислена до основного


выполнения программы, то он следует еще одному стилю: специализирующему программи-
рованию, который рассматривается в § 3.9.3.
3.6. ПРОГРАММИРОВАНИЕ ОТ ПРИОРИТЕТОВ 143

ческих исследований в области программирования от приоритетов почти нет.


В частности, в классической работе Хоара [76], в которой рассматривает-
ся управление взаимодействующими процессами, предполагается, что про-
грамма написана в традиционном структурном стиле, и никаких попыток
приспособить ее к обстановке, где есть много процессов, нет.20
Стиль программирования от приоритетов не реализован систематически
в виде законченного языка (в качестве некоторой попытки можно привести
проект Joule, см. ???), но часто используется в прикладных языках скриптов
для управления процессами или событиями либо в распределенных систе-
мах, либо на полуаппаратном уровне (так называемые встроенные програм-
мы, являющиеся частью специализированного прибора или устройства).
В программировании от приоритетов, как и в сентенциальном програм-
мировании, порядок расположения операторов в программе имеет малое зна-
чение, зато имеет значение его приоритет, т. е. некоторое значение, принад-
лежащее в самом общем случае частично-упорядоченному множеству и по-
чти всегда рассматриваемое как элементарное. После завершения очередно-
го оператора среди оставшихся выбирается оператор с максимальным прио-
ритетом. Если таких операторов с равными или несравнимыми приоритета-
ми несколько, то, вообще говоря, в идеале надо было бы выбирать один из
них недетерминированно.
Об операторах, имеющих приоритеты, можно говорить, когда програм-
мирование от приоритетов реализовано в специально предназначенном для
этого стиля языке. Если же приходится моделировать программирование от
приоритетов в обычном языке, то в качестве таких операторов могут высту-
пать иные структурные единицы текста программы. В объектно-ориентиро-
ванном языке, как правило, это методы объектов. Для краткости, в рамках
настоящего параграфа мы будем говорить об операторах, имея в виду только
что сделанное замечание.
При назначении приоритетов важно различать случаи, когда они задаются
для операторов как элементов текста программы, и когда приоритеты вводят-
ся для динамических структурных единиц, т. е. для тех элементов процесса
вычислений, которые возникают в ходе выполнения программы, но связа-
ны с тем или иным оператором, приоритет которого определяется в рамках
конкретного экземпляра (например, объекта). Таким образом выстраиваются
По этой причине область применения языка OCCAM. непосредственно реализующего
идеи Хоара, оказалась неоправданно суженной до тех ситуаций, когда процессы жестко при-
20

вязаны друг к другу статически описываемыми каналами передачи данных.


144 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

разные системы приоритетов, причем во втором случае система приоритетов


вынуждено оказывается динамической.
На практике обычно используют жесткую дисциплину, базирующуюся на
порядке операторов в программе. Часто управление по приоритетам соеди-
няется с т. н. системой демонов, т. е. процедур, вызываемых при выполне-
нии некоторого условия, а не в тот момент, когда при движении по тексту
программы мы подошли к их явному вызову (сидит такой демон в засаде и
выжидает момент, когда можно будет начать исполняться). И текущий «нор-
мальный» процесс, и проснувшиеся демоны имеют свои приоритеты, и де-
мон начнет исполняться, даже если он активизирован, лишь в том случае,
если среди активных не осталось процесса с большим, чем у демона, прио-
ритетом. Эта схема реализована, в частности, в системе UNIX.
Уместно отметить, что событийно-ориентированное программирование
с обособленными реакциями (см. предыдущий параграф) есть вырожден-
ный случай стиля программирования от приоритетов: для каждого экземпля-
ра структурной единицы, способной реагировать на события, выставляется
(бесконечно большой) приоритет, если этот экземпляр фактически должен
активизировать реакцию, и не выставляется приоритет (выставляется беско-
нечно малый приоритет), если экземпляр не должен реагировать на собы-
тие. Общий случай стиля событийно-ориентированного программирования
также близок к стилю программирования от приоритетов, если считать, что
всем обработчикам события, которые должны быть выполнены при его по-
явлении, присвоены приоритеты, соответствующие фактическому порядку
их выполнения. Когда такой порядок неизвестен, можно считать, что всем
активизируемым обработчикам присвоены равные приоритеты.
Кардинальное различие между событийным программированием и про-
граммированием от приоритетов состоит в принципах задания управления.
В случае событийного программирования установлена прямая связь между
событием и реакцией: если условие срабатывания открывает для обработ-
чика возможность быть выполненным, то он обязательно будет выполнен в
ответ на соответствующее событие. При программировании от приоритетов
ситуация иная: задавая демону даже наивысший приоритет, можно лишь на-
деяться на то, что он сработает раньше других, причем, возможно, даже без
появления какого бы то ни было события. Если абстрагироваться от меха-
низма управления при оперировании с приоритетами и событиями, то можно
считать эти два стиля вариантами одной сущности.
Стили программирования от событий и приоритетов хорошо совмеща-
ются, взаимно дополняя друг друга, когда целесообразна следующая архи-
3.6. ПРОГРАММИРОВАНИЕ ОТ ПРИОРИТЕТОВ 145

тектурная схема разработки программного проекта. В рамках событийного


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

3: Цикл пока не будет все сделано

Подготовка данных;
{

Обработка текущих изменений;


5:
10:

Демон {Если Поступили данные То


};

Прием рутинных данных};


4:

Демон {Если Поступили экстренные данные То


Запоминание экстренных изменений};
8:

12: Демон {Если Авария То Аварийные действия};

Программа 3.6.2

a: PRIO 3 {Prepare Data;SLEEP};


b: PRIO 10{Process Changes;SLEEP};
c: PRIO 8 DAEMON IF Extra Data
THEN Store Extra Data;
AWAKE b FI;
d: PRIO 12DAEMON IF Alert
THEN Emergency; FI;
e: PRIO 2 DAEMON IF Idle
THEN AWAKE a; FI;

Видно, что в данном примере все приоритеты задавались статически. Имен-


но к этому стремятся почти во всех случаях современной практики вычисле-
ний с приоритетами. Но полной статичности можно добиться лишь в про-
146 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

стейших случаях. Например, если процесс долго ждет своей очереди на ис-
полнение, то естественно постепенно поднимать его приоритет. Но тогда при-
оритет такого «пользовательского» действия может превзойти приоритет си-
стемного процесса, и остается либо статически задавать каждому процессу
допустимый максимальный приоритет и мы вновь попадаем в ту же ловушку,
либо задавать разным классам процессов приоритеты просто несравнимые
по порядку, чтобы ни при каком нормальном исполнении приоритет пользо-
вательского процесса не дорос, скажем, до 109 , за которым начинаются при-
оритеты срочных системных демонов.
Заметим, что назначение приоритетов операционной системой, «сверху»,
принципиально ничего в данной картине не меняет. С точки зрения пользо-
вательских программ эти приоритеты все равно статические.
Рассмотрим теоретически, какие классы программ можно описать при
помощи статических и динамических приоритетов. Оказывается, при помо-
щи статических приоритетов, являющихся натуральными числами, невоз-
можно описать даже примитивно-рекурсивные композиции исходных дей-
ствий21 .
Таким образом, чисто концептуально натуральных чисел не хватает для
описания приоритетов, но оказывается, что для них хватает ординалов (см.
курс математической логики). Поскольку ординалы до ε0 имеют хорошую
вычислительную модель (см., напр. [63]), можно рассматривать целочислен-
ные приоритеты как конкретную реализацию абстрактного класса таких ор-
диналов.
А в случае реально распределенных или реально параллельных действий
проблема приоритетов еще важнее и еще сложнее. Часто управление по при-
оритетам — единственный шанс справиться со сложностью такой нелиней-
ной во времени системы. Тут уже и линейно упорядоченного множества при-
оритетов не хватает.
В целом программирование от приоритетов является мощным, но спе-
цифическим орудием для описания глобальных совместных, параллельных
или распределенных процессов. Его элементы проникают и в традиционные
системы программирования в виде обработки исключительных ситуаций в
программах. Здесь, как и в событийном программировании с обособленными
реакциями, всего два приоритета, которые налагаются на структуру исполь-
зующей конструкции. Противоречивость этого средства со структурным про-
Правда, здесь мы отвлекаемся от реальных ограничений, и считаем 1010 столь же легко
10

достижимым числом, как и 10.


21
3.7. ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ 147

граммированием только в том, что не всегда ясно, что делается далее, когда
обработка исключения завершается. В сравнении со структурными перехо-
дами такое расширение представляется более искусственным.

§ 3.7. ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ

При функциональном программировании основным понятием является


функция. Мы интересуемся определением и переопределением функции как
целого. Соответственно, тогда сами функции становятся значениями. По-
этому здесь необходим серьезный теоретический анализ. Нужно разобрать,
какие действия мы можем производить над функцией как над единым объ-
ектом, какие мы можем получить от этого преимущества и в каких случаях
целесообразно так работать, а в каких нет.
Функциональное программирование полностью22 реализовано в ряде до-
статочно широко используемых систем, например, в языках LISP и ML. На-
коплен громадный багаж ценного программного обеспечения на этих языках.
Но применяются они, в основном, в академической среде. Для понимания
того, что затрудняет использование функционального программирования в
производственной практике, также необходим теоретический анализ. Функ-
ции оказываются столь абстрактными и высокоуровневыми объектами, что
непосредственная интуиция и попытки действовать чисто экспериментально
немедленно ведут в тупики.
Функциональное программирование базируется на λ-исчислении, на те-
ории рекурсивных схем и на денотационной семантике, дающей модели этих
понятий.
Элементарное изложение λ-исчисления, представление о котором дол-
жен иметь любой квалифицированный программист, можно найти, напри-
мер, в [63]. Для лучшего представления дадим его сверхкраткий набросок.
В λ-исчислении любой объект является функцией, которая может приме-
няться к другим объектам, в том числе и сама к себе. Так что законно вы-
ражение f (f ). По любому выражению t, содержащему переменную x, мож-
но определить функцию λx.t, принимающую в качестве аргумента значение
x и подставляющую его в выражение t. Если в выражении нет других сво-
бодных переменных, то после такого определения получается постоянный
22
И даже более, чем полностью; см. дальнейший текст этого параграфа.
148 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

объект. Основное правило преобразования очень естественно:

λx.t(a) → t[x | a].

Оно сводится к обычному вызову функции с заменой ее параметра на кон-


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

λx.x(x)(λx.x(x)).

Попытайтесь его преобразовать и убедитесь, что оно зацикливается.


Если запретить самоприменимость, приписав всем переменным и кон-
стантам типы и разрешив применять функцию лишь к аргументам правиль-
ного типа (это обозначается следующим требованием: функция f должна
иметь тип (τ → ρ), ее аргумент t — тип τ , и тогда выражение f (t) имеет
тип ρ). В этом типизированном λ-исчислении зацикливание уже исключает-
ся, если его нет в исходных функциях.
В денотационной семантике области значений типов данных представля-
ются как сложные топологические и алгебраические структуры, а вычисли-
мые функционалы описываются как непрерывные операторы.
Основной метод определения новых функций, принятый в программиро-
вании — рекурсия.
Пример 3.7.1. Дж. Маккарти в 1960 г. сформулировал определение вычи-
слимости, в котором все функции строятся из исходных при помощи схем
типа

Mult(x, y) ⇐ if y = 0 then 0 else Add(Mult(x, Pd(y)), x) fi

(рекурсивные схемы по Маккарти [57]). Здесь Pd — функция, находящая пре-


дыдущее натуральное число, Add — функция сложения. А что определяется
такой схемой, читатель может разобраться и сам.
Конец примера 3.7.1.
Соответственно, в функциональном програмировании рекурсия использует-
ся на каждом шагу. Но статического определения новых функций мало для
функциональности. Функции нужно уметь порождать динамически.
3.7. ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ 149

Полностью проявить свою силу функциональное программирование мо-


жет, если разрешается строить не только функции, но и функционалы произ-
вольных конечных типов, преобразующие функционалы более низких типов.
Дело в том, что логически23 эти функционалы соответствуют абстрактным
математическим понятиям, а, как показано Оревковым (см. [63]), примене-
ние таких понятий может сократить рассуждение в башню экспонент раз.
Соответственно, и длина программы может быть сокращена во столько же
раз, а потери эффективности практически не будет.
Поэтому возникает вопрос о том, какие минимальные операции нужны
для работы с функциями? Конечно же, это композиция функций (эту опера-
цию не позволяет в общем виде естественно запрограммировать ни C++, ни
Pascal, ни Ada, ни Алгол-68).
В 60-х гг. была выделена еще одна естественная и в принципе исключи-
тельно просто реализуемая операция над функциями. Это частичная пара-
метризация. При частичной параметризации мы фиксируем часть аргумен-
тов процедуры и получаем процедуру, выдающую результат после получения
оставшихся аргументов. Например, частичной параметризацией деления на-
цело является функция (в обозначениях C)

third(i) = i%3.

Доказано, что композиции и частичной параметризации достаточно для мо-


делирования всего типизированного λ-исчисления. Соответственно, эти опе-
рации приводят к эффективным функциональным программам.
Надо сказать, что при работе с функционалами высших типов возникают
большие сложности, и для понимания их теории нужно хорошо знать логику,
современную алгебру и топологию. Специалистов, владеющих и этой теори-
ей, и практикой программирования, практически нет. Поэтому, хотя экспери-
ментальных систем для работы с понятиями высших типов довольно много,
все они наследуют первородный грех языка LISP: их создатели просто не
могут удержаться и добавляют какую-либо возможность (концептуально не-
совместимую), которая разрушает эффективность системы. Соответственно,
это препятствует применению систем на практике.
Логика, особенно неклассическая, является мощнейшим средством анализа программ. Ее
освоение позволяет намного эффективнее решать аналитические проблемы и сознательно
23

выбирать соответствующие средства. А это — высшие уровни профессионального мастер-


ства.
150 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

Функциональное программирование дает возможность проиллюстриро-


вать тезис о вредности концептуально несогласованных пополнений систе-
мы понятий на примерах, которые проработаны и на практике, и в теории.
Рассмотрим один такой пример.
Динамическое порождение функций можно понимать следующим обра-
зом. Пусть основными структурами данных нашего языка являются списки
(поскольку исторически действия над списками определялись рекурсивно,
именно таковы базовые данные практически всех нынешних функциональ-
ных языков). Тогда можно список, произвольным способом порожденный
в нашей программе, просто прочитать как программный текст и объявить
определением динамически вычисленной процедуры. Именно так подошли
к функциональному программированию в языке LISP, созданном Дж. Мак-
карти. Этот язык уже 40 лет живет в академической среде, на нем написано
множество исследовательских и экспериментальных систем, и богатый опыт
его применения позволяет подвести некоторые итоги.
Порождение функций с помощью динамической генерации их определе-
ний оказалось крайне опасным средством. Даже при его правильном при-
менении оно почти всегда приводит к потрясающей неэффективности про-
грамм. Впрочем, это можно было бы оценить и чисто теоретически. Такой
стиль программирования практически эквивалентен λ-исчислению, в кото-
ром универсальная функция пишется легче всего. А универсальная вычи-
слимая функция безнадежно неэффективна по сложности вычислений, она
даже не может быть продолжена до всюду определенной.
Любое расширение указанного нами минимального множества операций
над функциями с большой вероятностью ведет в ту же пропасть неэффектив-
ности.
Рассмотрим пример. Показано, что функций, получающихся путем вы-
числения с помощью циклов типа пересчета типизированных λ-выражений,
базирующихся на элементарных вычислимых функциях, достаточно для до-
казательства непротиворечивости арифметики. Таким образом, внешне безо-
бидный цикл, в котором набирается композиция функций, может на выходе
дать потрясающе ресурсоемкую функцию.
Это теоретическое рассмотрение в совокупности с другими, подобными
ему, приводит к выводу: функциональное программирование и циклы кон-
цептуально противоречат друг другу.
Функциональное и сентенциальное программирование поддержано в на-
стоящий момент глубокими теоретическими рассмотрениями, которые вы-
годно отличают его от операционных моделей. Есть и опыт, и практические
3.8. ОБЪЕКТНО-ОРИЕНТИРОВАННЫЙ ПОДХОД 151

результаты, связанные с его применением. Базируясь на серьезной матема-


тической основе, языки функционального программирования гораздо более
устойчивы к влиянию чужеродных новаций, чем операционные языки. Функ-
циональный и сентенциальный стили программирования часто демонстри-
руют очень высокую выразительность, быть может, именно в связи с тем, что
область применения их практически всегда хорошо очерчена.
Наиболее богатый опыт и развитые традиции функционального програм-
мирования имеет язык LISP. Методы составления LISP-программ вполне сло-
жились, и можно подвести некоторые итоги. Стиль программирования на
LISP’е не удалось погубить даже внедрением совершенно чуждого функци-
ональности присваивания. В то же время, этот стиль оказался гибок по от-
ношению к освоению новых концепций, когда они не противоречат базовым
средствам языка: и абстрактные типы данных, и объектная ориентирован-
ность вполне успешно прививаются на стройное дерево списочной структу-
ры LISP’а.
Как показывает опыт LISP’а, взаимопроникновение стилей возможно, и
особенно успешно в случае концептуально хорошо проработанной базы. Но
далеко не всегда оно приводит к ожидаемому росту выразительности. Мно-
гочисленные примеры демонстрируют обратное. И всегда неудачи заимство-
ваний обусловлены нечеткостью проработки базовых концепций, отходом от
того, что логически и теоретически предопределено для данного стиля. Вот
почему крайне важно проанализировать существующие стили и выявить их
сущность, а не поверхностные сходства и различия.
Опыт показал, что владение функциональным стилем программирования
является элементом фундаментального образования программиста и мощ-
ным средством проектирования программ на концептуальном уровне. Тот,
кто осмыслил концепции функционального программирования, гораздо глуб-
же овладевает современными высокоуровневыми методами объектно-ориен-
тированного проектирования и дизайна и гораздо эффективнее их применяет.
Таким образом, мы естественно переходим к следующему стилю.

§ 3.8. ОБЪЕКТНО-ОРИЕНТИРОВАННЫЙ ПОДХОД

В современном программировании объектно-ориентированный подход


(ООП) является одним из популярнейших. Он превратился в стандарт во
многих фирмах и во многих сообществах программистов. Средства для под-
держки ООП вошли практически во все профессиональные системы про-
152 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

граммирования.24 Этот стиль вместе с соответствующими организационны-


ми нововведениями позволил резко повысить эффективность коллективной
работы программистов.
Конечно же, при этом ООП рекламируется как универсальное средство.
Но читатель уже должен понимать, что любое хорошее средство должно иметь
и ‘хорошие’ недостатки. Бесспорно только бесполезное. Поскольку профес-
сионалы в информационных технологиях должны пройти серьезный курс
объектно-ориентированного программирования, здесь данный подход будет
описан эскизно, тем более, что, как станет ясно ниже, он и не предназначен
для использования в малых учебных примерах.
ООП базируется на интенсивном использовании сложных структур дан-
ных: объектов. Объекты соединяют внутри себя данные и методы. Чаще
всего объекты организуются таким образом, что к данным прямого доступа
извне нет, мы можем обращаться к ним лишь через методы. Это позволя-
ет быстро заменять внутреннее представление данных, что исключительно
важно для обеспечения перестраиваемости сложных программных систем.
Пример 3.8.1. Имея тип объектов TPoint, выражающий точки двумерного
пространства, мы можем даже и не знать, в какой системе координат пред-
ставлены эти точки. Важно, что у нас должен быть метод вычисления декар-
товых координат точки и метод задания точки ее декартовыми координатами.
А внутри пусть они будут хоть барицентрические, если так удобно состави-
телю программы.
Конец примера 3.8.1.
Более того, для замены внутреннего представления данных в объектно-
ориентированном программировании имеется механизм наследования. Мож-
но описать новый тип объектов, базирующийся на уже имеющемся, и пе-
реопределяющий его методы. Как следствие, при присваивании переменной-
объекту могут замениться не только значения данных, но и обрабатываю-
щие их функции.
Присваивание функций вместо присваивания данных выводит лишь на
второй уровень функциональной абстракции, но каждый из этих уровней да-
ет экспоненциальный выигрыш в выразительных средствах. Правда, этот вы-
игрыш в полной мере проявляется лишь для достаточно больших и сложных
Поэтому мы не приводим примеров. В частности, системы Delphi и Visual C++ просто на-
вязывают элементы ООП уже при создании заготовки новой программы, что обучающиеся
24

наверняка уже видели на собственном опыте.


3.8. ОБЪЕКТНО-ОРИЕНТИРОВАННЫЙ ПОДХОД 153

систем и при хорошо продуманной системе понятий, их описывающих. Та-


ким образом, объектно-ориентированный подход позволил облегчить и про-
блему работы со сложными программными системами.
Одним из выводов теории систем и системного анализа является то, что,
когда сложность систем превосходит некоторую границу, появляются новые
трудности и новые эффекты, которые были практически неразличимы для
малых систем. Лучше всего заметны появляющиеся неприятности, но для
людей, владеющих достаточно высокоуровневой системой понятий, откры-
ваются и новые возможности. Объектно-ориентированный подход смог под-
нять планку сложности программных систем, совладав с частью трудностей
и открыв новые возможности, характерные именно для больших систем и
для их целостного анализа.25
Объектный подход является прежде всего структурой достаточно высо-
коуровневых понятий, которые надстраиваются над базисом программных
конструкций какого-то стиля первого уровня. В частности, объектная про-
грамма может с равным успехом базироваться и на структурном программи-
ровании, и на программировании от состояний, и на программировании от
приоритетов.
Отметим, что нынешняя литература по объектно-ориентированному про-
граммированию грешит тем, что то, что представляется как теория данно-
го стиля программирования, попросту неадекватно его практике. Причины
этого стоит разобрать, поскольку в курсах, специально посвященных ООП,
производится систематическая подмена понятий и даже истории вопроса.
Необходимо заметить, что в ООП нет никаких ограничений на измене-
ние наследником смысла операций предка. Конечно, благими пожеланиями
кишат все руководства, но, поскольку ограничений нет, на практике един-
Как ни парадоксально, именно эти принципиальные преимущества ООП труднее всего
показать в практике формального обучения в вузе.
25

Для преподавания надо найти небольшую предметную область, для которой можно было
бы проделать системный анализ и построить обозримую открытую модель. Это еще возмож-
но (например, в качестве таковой можно рассматривать игру в ‘очко’ с учетом различных
стратегий игроков и их психологии.) Но на таких задачах удается показать лишь некоторые
преимущества и некоторые стороны объектной модели, которые почти уравновешиваются
недостатками, связанными с резким увеличением объема сопровождающей моделирующую
программу информации.
Можно привести содержательную аналогию. Если построен мощный танк, то даже для
того, чтобы его разместить, требуется значительное пространство, а уж чтобы ему развер-
нуться и показать свои возможности. . . Естественно, что его нельзя опробовать на столе в
комнате директора или в аудитории, рассчитанной на 10 человек.
154 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

ственное, что с гарантией сохраняется — имена понятий. Когда говорят о


конкретизации объекта, фактически речь идет о смене ролей.26
Система программирования, основанная на объектах и их ролях, была ре-
ализована в языках SMALLTALK и PLASMA. В последнем из них объекты так
и назывались актерами. Эта система и была воспринята ООП. Поскольку си-
стема ролей очень хорошо подходит для описания взаимодействия человека
и программной системы и для описания поведения программной системы
в человеческой среде, ООП оказалось успешным методом проектирования
систем по заказу, для нужд непрофессионалов, и позволило повысить произ-
водительность труда в данной отрасли программирования порою в десятки
раз. Далее, поскольку при изменении задачи меняются роли, ООП позволило
быстро перестраивать достаточно сложные программные системы при изме-
нении требований заказчика. Уже эти два преимущества показывают прак-
тическую ценность ООП.
А в качестве теории (это показывает еще раз, что часто к теоретику обра-
щаются как к научному аналогу священника, который должен освятить ре-
шение, прочтя над ним свою молитву, но к сути дела никакого отношения не
имеет) ООП восприняло то, что развивается под именем абстрактных типов
данных (АТД). АТД сохраняет не только имена методов, но и их смысл. Цель
теории — дать способ автоматизированной конкретизации понятий, когда в
них подставляются другие понятия. Проблема оказалась крайне тяжелой и
сложной, непосредственного практического выхода получить не удалось, и,
как часто бывает, теория вышла из моды, так и не дойдя до точки зрелости.
Конечно же, сама проблема осталась и лишь обострилась. Вернуться и раз-
вивать теорию дальше все равно придется, но произнесенные слова о конкре-
тизации понятий и связанной с ней заменой представлений и методов были
перенесены туда, где нет ни конкретизации, ни сохранения смысла при заме-
не.
Внимание!
В ООП конкретизацией понятий называется переход от класса к
его наследнику, который содержит все функции и значения, опре-
В данном случае роль — это не профессиональный термин из актерской деятельности
или из программирования, а психологическое понятие. Например, придя в аудиторию, Вы
26

принимаете на себя роль обучаемого, а авторы данной книги — роль наставников. Человек,
выступающий в разных ролях, на одни и те же раздражители реагирует по-разному. Если
же человек играет не ту роль, которую ожидают от него окружающие, то он ведет себя (с
точки зрения окружающих) неадекватно, что зачастую рассматривается как один из видов
асоциального поведения.
3.9. ТРИ ТЕХНОЛОГИЧЕСКИХ СТИЛЯ ПРОГРАММИРОВАНИЯ 155

деленные в его предке (хотя некоторые из них он может переопре-


делить), и новые функции и значения. С точки зрения логики и
алгебры, это не конкретизация, а, в лучшем случае, обогащение
структуры. Старые методы вполне могут оказаться неадекватны-
ми для нового класса. В результате потребовалось разрабатывать
специальные способы борьбы с подобными затруднениями, кото-
рые описываются в курсе объектно-ориентированного програм-
мирования.

Очень важной стадией развития ООП является объектно-ориентирован-


ное проектирование (дизайн) (ООД). Здесь система описывается с несколь-
ких сторон, сначала со стороны пользователя (диаграммы использования),
потом со стороны данных (диаграммы классов) и с других сторон реализа-
ции (диаграммы состояний и т. п.) Поддерживать целостность системы опи-
саний и переходить от диаграмм к прототипам программ позволяет систе-
ма UML (Unified Modelling Language). Это уже не язык программирования, а
язык формализованных спецификаций программ.

§ 3.9. ТРИ ТЕХНОЛОГИЧЕСКИХ СТИЛЯ ПРОГРАММИРОВАНИЯ

Рассмотренные в предыдущих разделах стили программирования отно-


сятся к уровню методик (см. сноску 1 на стр. 108). Для следования этим сти-
лям необходимы определенные условия, связанные со спецификой решае-
мой задачи, выбранного языка или что-либо иное, характеризующее методи-
ку данного стиля. Это необходимые, но, разумеется, недостаточные условия,
выполнение которых делает возможным адекватное применение стиля. На-
стоящий параграф посвящен несколько иным стилям, зависящим не столько
от языков и от задач, сколько от программного и технологического окруже-
ния программиста.
Эти стили рассчитаны не на составление единственной программы, а на
серийное их производство (в том или ином смысле).
В одном случае в качестве серии рассматривается набор программ, в ко-
торых повторяется применение одних и тех же фрагментов. Это стиль про-
граммирования от переиспользования.
В другом случае серия — это набор различных программ, которые в прин-
ципе могут быть построены автоматически (что в реальности означает либо
полуавтоматически, либо вручную, как правило, по жестко фиксированному
методу) из одной общей программы с учетом особенностей применения. Это
156 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

стиль специализирующего программирования.


Наконец, третий случай, для которого серийность вырождается. Она под-
меняется парой, один компонент которой — макет, образец, демонстраци-
онная разработка или же что-то отличное от программы, выполняющей ре-
шение задачи, но способное показать некоторые ее особенности либо в дру-
гом смысле приблизить разработку к цели. Это нечто называется образцом,
макетом, шаблоном либо прототипом, по которому может строиться про-
грамма. Второй компонент пары — производственный вариант программы,
изготавливаемый на основе информации о макетном образце, о его разра-
ботке или путем технологической процедуры работы с первым компонентом
пары (например, это могут быть правила заполнения шаблона). Это стиль
программирования от образца.
Три стиля программирования, обсуждаемые в данном параграфе, связы-
вает между собой общее качество. Идея каждого из них возникла в самый на-
чальный период развития информатики как отрасли человеческой деятельно-
сти. Однако форм представления идей, которые можно было бы предложить
в качестве практического руководства, пришлось ждать десятилетия. Но и се-
годня нельзя считать каждый из этих стилей окончательно оформившимся.
Почти все конкретные реализации этих идей ограничиваются использовани-
ем фон Неймановской модели вычислений, традиционных языков и техно-
логий, сложившихся в рамках этих традиций.
Внимание!

Иногда накопленный опыт может быть перенесен на другие


модели без потерь и даже с ощутимыми выгодами, ищите эти воз-
можности!

3.9.1. Программирование от переиспользования

Может показаться странным, что в обзоре стилей мы не выделили спе-


циально модульное программирование. В самом деле, в программистском
обиходе часто употребляется выражение «модульный стиль программирова-
ния». Однако если разобраться, то становится очевидным следующее.
Требование разрабатывать программы, в которых выделены
автономные и, соответственно, легко заменяемые другими, фраг-
менты, решающие логически замкнутые задачи, является обще-
логическим и общетехнологическимым, а не исключительным,
присущим какой-либо специальной методике.
3.9. ТРИ ТЕХНОЛОГИЧЕСКИХ СТИЛЯ ПРОГРАММИРОВАНИЯ 157

Именно это требование обычно трактуется как модульность программно-


го изделия. Преимущества модульности, понимаемой в общем смысле, это:
возможности замены модуля без изменения всего остального, и осуществи-
мость повторного использования программных фрагментов. Вот последнее
уже можно рассматривать как особый стиль программирования, для которого
модульность является основным из средств.
Конкретные реализации модульности в системах программирования раз-
личны, но общий смысл понятия остается. Средства поддержки модульно-
сти характеризуют даже не стиль, а конкретный язык, поддерживающий этот
стиль. И поэтому правомерно говорить не о стиле, а о конкретном языке, в ка-
кой мере он поддерживает модульное построение программ в рамках своего
стиля. К вопросу о языковой поддержке модуляризации мы еще возвратимся
при обсуждении процедур как средства декомпозиции программ.
Стиль от переиспользования характеризуется тем, что при составлении
программы стремятся максимально использовать то, что уже сделано — са-
мим программистом, его коллегами или же вообще где-либо. В идеале на
смену программированию как кодированию алгоритмов должно прийти про-
граммирование как сборка из заранее заготовленных блоков — сборочное
программирование. Этот термин введен одним из соавторов в статьях [58,
59, 60]. Г. С. Цейтин отделил это понятие от теоретических рассмотрений
и представил его в работе [80] с чисто программистской стороны. Заметим,
что практика математики уже давно отказалась от построения новых теорем
(соответствующих в информатике программам) и новых понятий (соответ-
ствующих абстрактным типам данных) с самого начала. Она весьма профес-
сионально переиспользует ранее полученные результаты. Но в математике
из-за концептуального единства системы понятий и строгих критериев обо-
снованности не возникает проблемы совместимости новых версий, которая
зачастую губит попытки не просто переиспользования, но и просто исполь-
зования старых программ.
Понятно, что такое стремление далеко не всегда можно реализовать на
практике, что при использовании готовых строительных блоков возможны
потери. Тем не менее, потенциальная выгода за счет переиспользования все-
гда обращает на себя внимание.
Рассмотрим две реальные ситуации, которые демонстрируют разработку,
не ориентированную и ориентированную на переиспользование. В первой
ситуации действует программист, работающий с очень развитой системой, в
которой “есть все”. Тем не менее, он пишет свою процедуру лексикографиче-
ского упорядочивания строк, потому что «легче самому написать, чем найти,
158 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

а потом, ведь еще и подгонять придется». Вывод: во-первых, здесь начисто


отсутствует стремление к переиспользованию, а во-вторых, переиспользова-
нию могут препятствовать затруднения поиска того, что требуется включить
в составляемую программу и проблемы совместимости версий.
Вторая ситуация — другая крайность. Программисту на Java потребо-
валось построить синтаксический анализ. На вопрос о том, как он это де-
лает, получен ответ: «Зачем это знать? У меня есть пакет JavaCC, который
все делает, как надо!». Вместе с тем дальнейшие расспросы показали, что
этот программист не представляет себе даже того, какого типа метод анализа
поддерживает JavaCC, и, следовательно, ничего не может сказать о том, как
задание грамматики для данного пакета связано с эффективностью анализа.
Узнав возможные варианты, программист призадумался, но ничего менять
не стал. Почему? Ответ простой: «Так ведь все уже работает!». Короче го-
воря, качество использования готовых компонентов системы зависит от
знания о них. Ситуация опять-таки та же самая, что в математике, особенно
в прикладной. Квалифицированное использование теоретических результа-
тов требует знания соответствующей теории, а порою и идей доказательств
результатов (поскольку в реальной ситуации предположения теории никогда
не выполняются точно). Глубина требуемого знания различна: иногда доста-
точно общего представления, как, например, при использовании математи-
ческих функций, иногда нужны сведения о принципах реализации, но всегда
можно указать необходимый уровень знакомства с переиспользуемым.
Сравнение ситуаций показывает, что одних пожеланий и директивных
указаний, что надо что-то переиспользовать, мало. Нужны знания о том, что
можно воспользоваться переносимыми компонентами и как именно ими вос-
пользоваться, получение которых часто требует существенных трудозатрат.
Нужно, чтобы само переиспользуемое программное обеспечение было при-
способлено для этого, в частности, чтобы оно в гораздо большей степени, чем
сейчас, следовало накопленной в математике хорошей практике, не игнори-
руя ее как чистую теорию. Расшифровка предыдущего предложения позво-
лит вдумчивому читателю самому вывести все условия, необходимые для
обеспечения переиспользования в конкретной обстановке.
Конечно, переиспользованию способствует применение развитых и вы-
разительных языков. В частности, экранирование в них реализационных де-
талей позволяет легче переносить программы из одной операционной об-
становки в другую (перенос и переиспользование — различные задачи, но
в некоторых аспектах они пересекаются). Способствует переиспользованию
повышение уровня понятий языка, хотя бы до второго-третьего типа (объект-
3.9. ТРИ ТЕХНОЛОГИЧЕСКИХ СТИЛЯ ПРОГРАММИРОВАНИЯ 159

ная ориентированность языка). Иногда ООП помогает также возможностя-


ми наследования свойств и методов объектов (но часто в самых критических
ситуациях плохо концептуально продуманная концепция наследования столь
же сильно и мешает). Все это — зародыши поддержки стиля программирова-
ния, нацеленного на переиспользование. Его можно придерживаться или нет
в зависимости от ситуации, от собственных склонностей, от уровня знаний о
среде программирования, уровня знаний и умений самого программиста (тот,
кто способен подняться до уровня метода, склонен к переиспользованию, а
тот, кто не может подняться выше тактического планирования, обычно избе-
гает его) и других обстоятельств. С другой стороны, сложившаяся практика
в значительной степени препятствует переиспользованию (обстоятельный
обзор препятствий, не потерявший актуальности до сих пор, выполнен Г. С.
Цейтиным в уже указанной работе [80]), а уровень систем поддержки еще
недостаточно высок и неадекватен общей задаче применения данного стиля.
Если же ограничиться деятельностью программиста и с этой точки зре-
ния, в противовес средствам поддержки, говорить о переиспользовании, то,
прежде всего, нужно указать на два аспекта данного стиля: применение су-
ществующих компонентов и разработка переиспользуемых компонентов.
Применение переиспользуемых компонентов характеризуется следующи-
ми особенностями стиля:
a) главная характеристика стиля от переиспользования: предпочтение по-
иска кандидатов на внедрение в программу самостоятельной разработке;

b) стремление к исследованию существующих кандидатов, к выявлению в


них особенностей, полезных или вредных для решаемой задачи;

c) попытки адаптации своего решения для осуществимости внедрения (здесь


могут появляться навязанное структурирование данных, конверторы и др.);

d) сопоставление и оценка вариантов внедрения и самостоятельной гипоте-


тической разработки;

e) адаптация внедряемого в программу, если она требуется (это сближает


обсуждаемый стиль с некоторыми аспектами другого технологического
стиля, который мы назвали программированием от образцов).
При разработке переиспользуемых компонентов необходимо учитывать, что
подобная разработка всегда требует дополнительных затрат, которые связаны
со следующими требованиями:
160 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

a) необходимо уделять особое внимание документированию (лучше всего,


самодокументированию) переиспользуемых компонентов;

b) необходим серьезный анализ того, что кандидаты на переиспользование


могут быть отнесены к типовым приемам программирования, т. е. имеют
достаточно широкую для использования в дальнейшем область примени-
мости;27

c) нужно прорабатывать варианты не только варианты полного переисполь-


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

d) необходимы спецификации как области адекватного применения компо-


нента, так и границ применимости (вторая часть почти всегда отстутсвует
в нынешних спецификациях);

e) необходима оценка эффактивности применения компонента;

f) необходима специальная забота о предъявлении компонента потенциаль-


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

В характеристиках обоих аспектов программирования от переиспользования


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

ре, в трех разработках.


3.9. ТРИ ТЕХНОЛОГИЧЕСКИХ СТИЛЯ ПРОГРАММИРОВАНИЯ 161

мы в отрыве от этого набора, вообще говоря, лишено смысла. Это указывает


на естественные рамки переиспользования для данного стиля.
Во-первых, если фрагмент может быть выделен как черный ящик, т. е.
нас интересует лишь соотношение между его входными и выходными дан-
ными, то он на уровне программы может рассматриваться как самостоятель-
ный узел переработки данных и тем самым получает независимость от со-
стояний программы. В свою очередь, именно это позволяет использовать та-
кой фрагмент как самостоятельный узел переработки другой программы, т. е.
переиспользовать его. На этом принципе строятся все библиотечные матема-
тические функции, реализацию которых достаточно часто не требуется даже
знать при использовании.
Обычными единицами переиспользования при любом стиле программи-
рования являются процедуры. Именно они автономно описываются и, если
оказываются независимыми от общего с другими компонентами контекста,
то по сути дела становятся теми самыми черными ящиками, о которых только
что шла речь.
Еще один возможный случай, когда часть, ясно осознанная программи-
стом, состояний внешней программы может быть интерпретирована как го-
моморфный образ части (опять же, явно выделенной) состояний компонен-
ты (серый ящик). Тогда компоненту можно даже модифицировать, т. е. воз-
можно переиспользование как фрагмента либо шаблона. Но приемы устано-
вления гомоморфизма между состояниями — та высокоуровневая надстрой-
ка над программированием от состояний, которая пока еще не создана, и по-
этому здесь программист в высшей степени зависит от качества содержатель-
ного концептуального анализа.
Как ни странно, немногим лучше приспособлено к сочетанию со стилем
переиспользования структурное программирование. Требования к структуре
информационного пространства задачи и к согласованию с ним других ком-
понентов программы обеспечивают регламентированные связи между под-
задачами, а значит, облегчается (но не исчезает) задача выделения самосто-
ятельных компонентов. В дополнение к полностью самостоятельным пере-
используемым компонентам здесь можно указать на переиспользование, при
котором в новом применении обеспечивается необходимая часть контекста
(т. е. переиспользуется компонент и эта часть контекста; смотри модули язы-
ков Modula 2 и Object Pascal). Модули можно рассматривать как серые ящики
для структурного программирования. Возможности модуляризации доста-
точно хорошо исследованы и корректно реализованы в упомянутых выше
языках.
162 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

Следует отметить, что для рассмотренных случаев задача переисполь-


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

Внимание!

На самом деле никакой крамолы в многовариантности нет.


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

Неимперативность по своей сути лучше приспособлена для переисполь-


зования и, в частности, для шаблонного переиспользования, поскольку соот-
ношения вместо приказов легче автоматически трансформировать или даже
просто переинтерпретировать в другой обстановке (отсутствие императив-
ности — еще один фактор, обусловливающий уже не раз упоминавшуюся
практически абсолютную переносимость математических результатов). По-
этому неудивительно, например, что базы данных-фактов в программах на
языке Prolog часто становятся их общими частями.
При использовании сентенциального и функционального стиля возмож-
ности перестройки переиспользуемого компонента под ситуацию гораздо вы-
ше за счет более высокоуровневых принципов вычислений. Однако точных
практических оценок этого пока что нет. Как правило, повторно используют-
ся крупные и содержательно полные фрагменты программ, чаще всего имен-
но те, которые являют собой базовые механизмы, развивающие модель вычи-
слений языка. Недостаточность опыта разработки больших производствен-
3.9. ТРИ ТЕХНОЛОГИЧЕСКИХ СТИЛЯ ПРОГРАММИРОВАНИЯ 163

ных проектов с существенным использованием этих стилей вынуждают рас-


суждать о них лишь предположительно.
Следующие два стиля, рассмотренные выше, программирование от со-
бытий и приоритетов с точки зрения переиспользования интересны, пре-
жде всего, потому, что для них заранее предписана определенная декомпози-
ция программы: выделение в ней уровней генерации и обработки. Уже само
это наталкивает на мысль осуществимости общего генератора для однотип-
ных программ. Можно сказать, что именно эта общность и предопределяет
продуктивность использования данных стилей. Нельзя забывать и о том, что
событийный механизм или управление с помощью приоритетов неизбежно
повышают автономность обработчиков: например, они могут не обязатель-
но рассчитывать на определенную последовательность вызовов и, как след-
ствие, их гибкость повышается.
На сегодняшний день наиболее перспективными с точки зрения переис-
пользования являются программы в объектно-ориентированном стиле. Об-
щая причина тому — гибкие средства абстракции объектных языков и четкое
отделение интерфейсов классов от реализаций. Это повышает потенциаль-
ные и реальные возможности переиспользования. Вместе с тем данный стиль
эффективен лишь для достаточно больших систем и тем самым вдохновля-
ет программистов на построение больших систем классов и объектов, кото-
рые сильно взаимосвязаны. А это, в свою очередь, заставляет технологизи-
ровать разработки. Технология в настоящее время связывается с наработкой
типовых моделей фрагментов объектно-ориентированных систем. Создают-
ся шаблоны проектирования (так называемые паттерны), которые предпи-
сано использовать, чтобы минимизировать связи в системе, обеспечивать ее
развиваемость [22].
Проектирование с учетом повторного использования результатов в буду-
щем возможно осуществлять на трех уровнях:
1. Уровень приложений. В ходе ведения проекта заботятся о том, чтобы
при декомпозиции и разработке компонентов системы выявлялись ком-
поненты-кандидаты на переиспользование. Эти компоненты выделя-
ются в самостоятельные единицы и оформляются независимо от про-
екта;
2. Уровень инструментов. Разработка проекта практически всегда включа-
ет в себя создание инструментальных средств, поддерживающих уни-
фикацию: единые библиотеки общедоступных для проекта средств, об-
щий контекст и единообразные средства доступа к нему, средства под-
164 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

держки выполнения технологических соглашений и регламентов, ша-


блоны проектирования и др. Этот инструментарий или часть его во
многих случаях может быть оформлен независимо от проекта для воз-
можного переиспользования в виде библиотек;
3. Уровень решений. Ценность для переиспользования может представлять
архитектурный уровень проекта. Хорошие архитектурные решения, как
правило, допускают распространение за рамки конкретного проекта, в
котором они появились. Это могут быть фрагменты, которые пригод-
ны для использования в качестве образцов для других проектов, и то-
гда их переиспользование требует оформления соответствующего ша-
блона. Другой вариант независимого решения — каркас проекта, т. е.
набор взаимосвязанных компонентов, требующих доопределения, в ре-
зультате чего может быть построено приложение, подсистема, модуль и
др. Использование шаблонов или каркасов в другом проекте связывает
стиль переиспользования со стилем программирования от образцов.
Приведенный перечень упорядочен по степени значимости уровней для пе-
реиспользования. Наибольшая эффективность достигается, когда удается при
проектировании выйти на уровень решений, но одновременно этот уровень
является наиболее сложным и трудоемким для разработки.
В заключение отметим, что условия применения стиля переиспользова-
ния в реальном программировании должны включать в себя оценку и эко-
номических показателей, и уровня квалификации исполнителей. А это уже
функции менеджера проекта, который должен решать, ориентироваться ли на
переиспользование (в обоих аспектах) или нет, и, если да, то решить многие
финансовые и организационные проблемы, выходящие за рамки собственно
программирования.
Предупреждение!
Здесь необходима трезвая оценка уровня своих специалистов
и уровня организованности работ, поскольку на действительно
выгодное для переиспользования решение способны, как прави-
ло, лишь хорошие специалисты в хорошей инфраструктуре.

3.9.2. Программирование от образцов

Программирование от образцов — стиль, часто высокомерно игнорируе-


мый специалистами, но тем не менее он является весьма распространенным
3.9. ТРИ ТЕХНОЛОГИЧЕСКИХ СТИЛЯ ПРОГРАММИРОВАНИЯ 165

(даже сами профессионалы им пользуются). Это — подход к разработке про-


грамм по заданным заранее шаблонам. Это скорее целый набор “вырожден-
ных” стилей, каждый из которых выделяется в связи с тем, что в той или иной
ситуации скрывается под понятием образец, фрейм, шаблон.
Выше отмечалась связь между программированием от переиспользова-
ния и программированием от образцов. Но не всякое переиспользование умест-
но считать программированием от образцов. Так, когда повторно использу-
ется фрагмент, который можно рассматривать как черный ящик, то ничего,
кроме результатов вычислений фрагмента не внедряется в новую програм-
му, следовательно, фрагмент не предписывает метода построения програм-
мы. Противоположная ситуация с переиспользованием уровня проектных ре-
шений. Эти решения диктуют, как будет построена программа. Шаблоны и
каркасы, появившиеся в какой-либо разработке либо построенные специаль-
но, становятся образцами для нового программного проекта. При этом со-
всем не обязательно, чтобы образец и конструируемая программа были бы
написаны на одном языке. Напротив, можно извлечь определенную выгоду
из того, что образец не привязывается к модели вычислений разрабатывае-
мой программы. Если образец специально создается для данной программы,
например, чтобы лучше понять решаемую задачу, то для него целесообразно
выбирать язык повышенного уровня или другой модели вычислений и за счет
этого иметь возможность быстрее реализовать пробную версию, макет и т. д.
Подобные соображения мотивируют разновидность стиля программирова-
ния от образца, получившую название макетирования (см. ниже). В данном
случае четко понятно, что имеет место не переиспользование, а собствен-
но программирование от образца. Но такой чистый случай является скорее
исключением, чем правилом: чаше всего трудно провести границу между са-
мостоятельным стилем и технологическими приемами переиспользования.
Можно указать следующие характерные случаи применения программи-
рования от образцов:
• Дается программа, написанная в каком угодно стиле, в которой нужно
кое-что изменить. Точно известно, в каких местах нужно это изменять.
В результате получается новая программа. Этот случай часто в про-
фессиональном языке называется патчем программы. И весьма квали-
фицированные программисты пользуются набором таких образцов, их
стали включать и в руководства по ООП.

• Дан набор программных инструментов, разработанный специалиста-


ми, и методика их применения, которая включает в себя схему соста-
166 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

вления требуемой программы. В идеальном случае происходит приме-


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

• Предоставляется среда разработки новой программы, как и в преды-


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

• Программирование от макета. Разработчики быстро готовят прототип,


который рассматривается как макет, который затем доводится до реаль-
ного программного изделия. От макета в программной системе часто
остается лишь система понятий, сам метод разработки полностью ме-
няется (например, макет был написан на языке Prolog, а окончательная
программа на Java). Макет (особенно в системах, поддержиывающих
его представление в графической форме, таких, как UML [50]) часто
становится частью документации готовой программы.

• Предоставляется технологический фрейм: нечто, для чего известны сло-


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

– семантические сети искусственного интеллекта;


– фирменная методика и технология, которая погружает один из пред-
шествующих случаев в систему стандартизованных форм и доку-
ментов. Пример RUP [88];
– табличное программирование, примеры которого приведены в дан-
ном пособии;
– компонентное программирование, например, с использованием XML
или иного языка разметки (которая задает фрейм), и языка обра-
ботчиков разметки (разделение, как говорят на програмистском
3.9. ТРИ ТЕХНОЛОГИЧЕСКИХ СТИЛЯ ПРОГРАММИРОВАНИЯ 167

жаргоне, на парсер и обработчик). Это как раз то, что дает объ-
ектная модель документа.

• Предоставляется технический фрейм, т. е. то, что нужно заполнять. Он,


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

Использование технологического и технического фреймов демонстрирует воз-


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

3.9.3. Специализирующее программирование

Основная идея специализирующего программирования заключается в по-


строении универсальных программ, которые, как правило, не годятся для
частных применений, но они могут быть трансформированы в специали-
зированные версии, предназначенные для частных случаев. Трансформация
происходит за счет знания того, какие входные данные характеризуют это
168 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

применение и каковы их дополнительные взаимосязи. Эффективность вы-


числения повышается путем использования различного рода специализиру-
ющих и оптимизирующих преобразований. Заметим, что в данном случае
оптимизирующие преобазования, против которых мы все время предосте-
регали программистов, безвредны, поскольку исходными документами слу-
жат общая программа и дополнительные знания.
Эта идея в точности соответствует анализу работы специалиста, овла-
девшего уровнем метода. Он освоил некоторую высокоуровневую структуру
и накопил багаж приемов ее специализации и применения в самых разных
частных случаях. Фантастическая производительность и эффективность экс-
пертов (а только высококлассные эксперты поднимаются до такого уровня),
пользующихся данным уровнем, наводит на мысль смоделировать их стра-
тегию работы в информационной системе.
Несмотря на внешнюю привлекательность идеи, реализовать ее в прак-
тическом плане в виде системы программирования с автоматическим транс-
форматором не удается. Главная трудность здесь связана с тем, что суще-
ствует целый набор полезных для данной задачи трансформаций, а возмож-
ная глубина специализации программы, а значит, и получаемый результат
зависят от многих неопределенных факторов. В частности, при примене-
нии трансформаций в разном порядке получаются различные программы.
Это соответствует известным теоретическим результатам о том, что методы,
действительно достойные такого названия, начинаются с четвертого уровня
иерархии типов знаний, а проблема унификации и даже конкретизации не-
разрешима уже для понятий второго порядка.28 Тем не менее даже частные
успехи на данном пути сулят столь громадный выигрыш, что идея появляется
вновь и вновь, и, судя по всему, ее практическая реализация — дело вашего
поколения, ученики!
Рассмотрим подход к специализации, принятый в нынешней теории. Он
показывает и возможности, и подводные камни.
Программа, у которой нас интересует лишь результат, а не конкретные
действия в ходе ее выполнения (именно такой случай соответствует струк-
турному программированию), может быть описана логической формулой
∀x (A(x) ⇒ ∃y (C(y)&B(x, y))).
Здесь x — входная структура данных программы, y — выходная структура
Не говоря уже о том, что, как неоднократно отмечалось, корректной реализации понятий
выше первого типа в современных программных системах нет.
28
3.9. ТРИ ТЕХНОЛОГИЧЕСКИХ СТИЛЯ ПРОГРАММИРОВАНИЯ 169

данных, A — условие на вход (предусловие), C — область выходных данных,


B(x, y) — связь вход-выход. C(y)&B(x, y) называется выходным условием
(или постусловием) программы. При специализации программы мы можем
преобразовать данное условие тремя способами.
Попущения специализации

a) Усилить условия на вход, чтобы программа применялась к лучше опреде-


ленному множеству входных данных.

b) Расширить область выходных данных, чтобы дать возможность искать ре-


шение в более либеральных условиях.

c) Ослабить связь между входом и выходом.

Все эти три попущения могут применяться вместе. Частными случаями их


являются добавление входных данных и удаление части выходных данных.
Как показано в работе [61], при специализации у нас возникают следую-
щие основные преобразования:

1. Cпециализация элементарных действий: замена вызова некоторой


процедуры либо метода на вызов более эффективной и лучше подходя-
щей в данном частном случае процедуры.

2. Удаление вариантов и (или) действий, которые стали избыточны из-


за лучше обусловленного условия или более либерально поставленной
цели.

3. Спрямление вычислений, когда выясняется, что в частном случае мы


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

4. Раскрытие вычислений, когда в рассмотренном частном случае при-


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

Если программа при ее создании проанализирована настолько, что ее специ-


фикацию можно представить как конструктивное доказательство, действую-
щие части которого порождают операторы и описания программы, то общая
задача специализации может решаться композицией известных методов пре-
образования доказательств (чистки и нормализации). Но даже теоретически
170 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

нормализация доказательств имеет суперэкспоненциальную сложность вы-


полнения, не говоря уже о том, что исключительно редко удается достичь
точности и детальности спецификаций, превращающих программу в дока-
зательство. Далее, специализация элементарных действий и следующие три
преобразования вполне могут мешать друг другу, и результат специализации
существенно зависит от их порядка. Поэтому задачу специализации необхо-
димо решать в частных случаях.
Частный случай специализации, лучше всего исследованный в современ-
ной информатике, это смешанные вычисления, заключающиеся в следую-
щем.
Попущение специализации состоит в уточнении (обычно в полной фикса-
ции) части входных данных. Считается, что универсальный алгоритм содер-
жит в себе все возможные частные вычисления, которые относятся ко всем
его специализациям. Все множество таких вычислений записано в неявном
виде, т. е. дается лишь способ генерации множеств частных вычислений по
совокупности специализирующих условий. Следовательно, требуется поро-
дить адекватное конкретной задаче множество частичных вычислений, выде-
лить из общего решения ту и только ту часть алгоритма, которая относится к
нужной специализации, и оптимизировать выделенную часть в соответствии
с возможностями, предоставляемыми конкретной задачей. Таким образом,
при смешанных вычислениях мы, насколько возможно, сохраняем не только
результат программы, но и структуру ее вычислений.
Классическая постановка задачи смешанных вычислений связана с рас-
смотрением программы как функции, отображающей вектор входных дан-
ных в вектор выходных (см. [33]). Если входные данные обозначить как ~x,
выходные как ~y, а область значений переменной x обозначить Dx, то можно
записать Y Y
f: D~x → D~y.

Если D~x = D~x × D~x , причем значения аргументов из D~x фиксированы (для
0 00 00

определенности, ~x = ~A), то можно попытаться автоматически построить


00

программу, вычисляющую функцию


 0 00
fA (~x) = f(~x , ~x )
~x00 = ~A.
Более строго задача смешанных вычислений формулируется следующим обра-
зом.
3.9. ТРИ ТЕХНОЛОГИЧЕСКИХ СТИЛЯ ПРОГРАММИРОВАНИЯ 171

Пусть для языка L имеется частично-определенный алгоритм вычисли-


теля V, который для любой программы P и начального состояния памяти X,
если заканчивает работу, то приводит к заключительному состоянию памяти

Y = V(P, X).

Иными словами, существует вычислительный автомат, исполняющий про-


граммы на данном языке.
Смешанным вычислением в языке L называется любой частично-опре-
деленный алгоритм M, который, если он определен для программы P и на-
чального состояния памяти X, приводит к некоторому промежуточному со-
стоянию памяти Y0 = MC(P, X) и к некоторой остаточной программе P0 =
MG(P, X), где MC и MG удовлетворяют следующему равенству:

V(P, X) = V(MG(P, X), MC(P, X)).

(Принцип частичного вычисления).


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

MC(P, X) v X

всегда, когда определено. Сравните этот подход с подходом к определению


рекурсии в Придожении А!
Заметим, что состояния памяти в данном случае не обязательно тракто-
вать просто как значения, хранящиеся в памяти. Они могут включать и наши
знания о значениях. Таким образом, в отличие от того, что принято в тео-
рии рекурсивных схем, порядок и топология на значениях могут быть нетри-
виальны. Мы можем, например, не знать конкретного значения некоторого
поля, но иметь информацию о нем, например, что оно положительно.
Пусть у нас есть отношение пополнения между данными ⊆v29 . Напри-
мер, список данных может быть пополнен до другого списка, если в нем все
места, на которых стоят реальные данные, совпадают с соответствующими
местами другого списка, и в случае, если некоторые из неизвестных значений
Эта мистически выглядящая формула означает просто то, что пополнение должно содер-
жать больше информации, чем исходное данное.
29
172 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

оказались заменены на конкретные, эти конкретные значения удовлетворяют


тем условиям, которые уже были известны. Так что, если мы уже знали, что
данное поле находится в интервале между 0 и 5, то число, подставленное
в качестве его значения, обязано принадлежать этому интервалу. Данные X
назовем частичными, если для любых максимальных по отношению v дан-
ных Y, таких, что X v Y, выполнено X  Y и имеются такие данные Z,
что X ∪ Z = Y. Таким образом, частичные данные могут быть пополнены до
любых полностью заданных, их расширяющих.
Пусть дан класс X частичных данных. Смешанное вычисление является
равномерным относительно класса программ P, отношения между данными
X  Y и класса частичных данных X, если есть алгоритм PEV(P, X, Y), опре-
деленный для каждой программы P ∈ P и для каждой пары данных X  Y,
X ∈ X, такой, что выполнены равенства:

V(P, Y) = V ((PEV(P, X, Y)1 , (PEV(P, X, Y)2 )

и
X ∪ (PEV(P, X, Y)2 = Y.
А вот это математическое определение прочитайте и разберитесь в нем сами!
Русский программист обязан хорошо владеть математическим языком, иначе
он неконкурентоспособен на мировом рынке, особенно после 30 лет.
В данном определении нет никакой привязки к модели вычислений язы-
ка: смешанные вычисления можно определить везде, где определено поня-
тие алгоритма абстрактного вычисления языка. Ссылка на память не озна-
чает действий в фон Неймановской модели, сравните наше определение с
описанием теории рекурсивных схем в § A.6. Задача специализации состоит
в том, чтобы реализовать требуемое деление алгоритма на остаточные дан-
ные и остаточную программу и предварительное выполнение той его части,
которая обусловлена сведениями о перерабатываемых данных.
Пример 3.9.1. Пусть требуется построить программу, вычисляющую функ-
цию λX. X n . Это означает, что программа должна работать для любых зна-
чений X и для конкретного фиксированного значения n. Начнем со следу-
ющей более универсальной программы, реализующей функцию λX, n. X n
(для простоты иллюстрации здесь и далее приводится лишь фрагмент про-
граммы, касающийся существа дела):
Программа 3.9.1
3.9. ТРИ ТЕХНОЛОГИЧЕСКИХ СТИЛЯ ПРОГРАММИРОВАНИЯ 173

Y := 1;
while n>0 do
begin if n MOD 2 = 1
then begin Y :=Y * X; n := n - 1 end;
n := n DIV 2; X := X * X
end

Задача в том, как автоматически получить из программы 3.9.1, например, сле-


дующую программу, вычисляющую функцию Y = X 5 , т. е. частичную про-
грамму для конкретного значения n = 5 и неопределенного значения X:

Y := X; X := X * X; X := X * X; Y := Y * X (3.1)

Для этого можно попытаться рассмотреть так называемую операционную исто-


рию вычисления программы 3.9.1 для n = 5:

{ 2} ( n > 0 ) равно TRUE:


{ 1} Y := 1;

{ 3} n MOD 2 = 1 равно TRUE:

{ 5} ( n > 0 ) равно TRUE:


{ 4} Y := Y * X; n := n - 1; n := n DIV 2; X := X * X;

{ 6} n MOD 2 = 1 равно FALSE:

{ 8} ( n > 0 ) равно TRUE:


{ 7} n := n DIV 2; X := X * X;

{ 9} n MOD 2 = 1 равно TRUE:

{ 11} ( n > 0 ) равно FALSE


{ 10} Y := Y * X; n := n - 1; n := n DIV 2; X := X * X;

Для дальнейшего удобно считать программу 3.9.1 только генератором опе-


рационной истории, полагая, что “фактическое” вычисление производится
операторами линейной программы (строки 1, 4, 7, 10 истории, в которой
остальные строки отражают вычисления управляющих условий). При этом
полагается, что операторы универсальной программы выполняются либо обыч-
ным образом, если значения, входящих в них переменных определены (за-
ранее фиксированы), либо, в противном случае, выдают в качестве резуль-
татов вычисления самих себя, т. е. выполняются ‘литерально’. Иными сло-
вами, операторы универсальной программы подразделяются на обычно вы-
полняемые и задержанные. Выполнение последних не обязательно приводит
к тождественной их выдаче в качестве результата: если оператор содержит
174 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

терм, значение которого может быть вычислено (входящие в него перемен-


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

( n > 0 ) равно TRUE:


Y := 1;

NOT ODD (n) равно TRUE:


( n > 0 ) равно TRUE:
n := n - 1; n := n DIV 2;

NOT ODD (n) равно FALSE:


( n > 0 ) равно TRUE:
n := n DIV 2;

NOT ODD (n) равно TRUE:


( n > 0 ) равно FALSE
n := n - 1; n := n DIV 2;

и следующей остаточной программе

Y := 1 * X; X := X * X; X := X * X; Y := Y * X; X := X * X (3.2)

очень близкой к программе (3.1).


Первый оператор программы (3.2) отличается от своего прообраза заме-
ной терма Y на его значение. Следует заметить, что замена его на Y:= X воз-
можна, но как результат трансформации программы, природа которой совсем
иная, нежели обсуждаемые здесь механизмы преобразований. Эта замена
правомерна в силу аксиоматики кольца целых чисел, а не как следствие ча-
стичного вычисления. Последний оператор является избыточным. Это мож-
но обнаружить вполне регулярным образом, но опять не как следствие ча-
стичного вычисления.
Конец примера 3.9.1.
Таким образом, даже столь простой пример, показывает, что специализа-
ция требует применения различных механизмов преобразования программ.
Остаточная программа может оказаться короче или длиннее исходной, но она
всегда будет, по крайней мере, не хуже исходной по времени выполнения.
Еще одно замечание. Решение ‘заморозить’ X в программе 3.9.1 было про-
извольным, но коль скоро оно принято, все остальные операторы, попавшие
в остаточную программу по индукции, были задержаны ‘вынужденно’, т. к.
их термы зависели от замороженной переменной X.
3.9. ТРИ ТЕХНОЛОГИЧЕСКИХ СТИЛЯ ПРОГРАММИРОВАНИЯ 175

Что изменилось бы в этих построениях, если бы они исходили из неимпе-


ративной модели вычислений? Прежде всего, изменилось бы понятие исто-
рии вычислений, а вслед за ним и то, что понимается под остаточной про-
граммой. К примеру, в исходной программе на языке Prolog мы бы увидели
соотношения, рекурсивно задающие последовательность утверждений, ло-
гическая подстановка которых ведет к цели: к получению значения данной
функции от двух переменных X и n в качестве запрашиваемого утверждения.
Анализ истории подстановок при n = 5 покажет, что некоторые из утвер-
ждений становятся лишними, раньше распознаются тупики. Это дает осно-
вание для сокращения программы, но здесь практически ничего не достичь
без объединения утверждений и их эквивалентных преобразований.
В функциональном языке история вычислений — это последовательность
подстановок аргументов функций и применения функций к аргументам. Не-
которые из этих действий приводят к выбору конкретного варианта продол-
жения процесса в зависимости от перерабатываемых данных, иногда они мо-
гут быть вычислены заранее, если имеются сведения о данных. Анализируя
историю, как и в операционном случае, можно определить, как нужно пре-
образовать программу, чтобы были учтены эти сведения, и, таким образом,
построить остаточную программу. Программа функции F(x,n), реализующая
тот же алгоритм, что был представлен выше, при n = 5 за счет ликвидации
разветвлений и раскрытия рекурсии приобретет примерно такой вид:

F(x, 5) = F5(x) = (x2 )2 ∗ x

На Лиспе, например, этот вид превращается в

(LAMBDA (x)(LAMBDA (t)(t*t*x)*(x*x)))

Здесь специализация программы в первом приближении сводится к вычисле-


нию функций с частично определенными параметрами.
Для функционального программирования этот метод достаточно перспек-
тивен с точки зрения вычленения вычислений, зависящих от заданных аргу-
ментов, но для оформления остаточной программы опять-таки нужна систе-
ма алгебраических и логических преобразований.
Не следует думать, что при написании универсальной программы мож-
но совсем не заботиться о качестве. Так, если ваша программа вычисления
функции Y = X n будет реализовывать прямолинейный алгоритм последо-
вательного умножения на X, то бессмысленно ожидать, что с помощью уни-
версального специализатора из нее получится остаточная программа с мини-
176 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

мальным числом умножений. Скорее всего, при n = 5 у вас получится что-то


вроде ((((x*x)*x)*x)*x).
Для операционных языков задача специализации оказывается еще труд-
нее, чем при следовании неимперативным стилям. Проблема прежде всего
в неоднозначности результата при применении разных трансформаций про-
граммы и в отсутствии даже элементов хорошей теории, которые уже суще-
ствуют для функционального стиля и чистого структурного программирова-
ния. Поэтому уместно говорить об автоматизированных системах поддержки
специализации, которые либо показывают пользователю варианты преобра-
зований, требуя подсказок, либо ориентируются на частные ‘хорошие’ слу-
чаи. Не стоит исключать из рассмотрения и варианты ручной специализации,
и тогда универсальная программа может рассматриваться в качестве образца,
т. е. задача получения остаточной программы смыкается с использованием
стиля программирования от образцов. Между прочим, это путь, который ре-
ализуется в большинстве транслирующих систем, ориентирующихся на ге-
нерацию кода для разных операционных обстановок. Специализация здесь
задается указанием опции для транслятора, по которой он отбирает нужный
вариант.
И смешанное вычисление, и опциональная специализация — примеры
статического вычисления программы. Это общее понятие включает и многие
другие возможности. Рассмотрим их от простого к сложному.
Простейшая статическая специализация в языках, подобных С/С++, ис-
пользует возможности препроцессора. Для него можно задавать значения
констант, тем самым при переходе от одного варианта программы к друго-
му требуется лишь в одном месте выставить нужные значения. Немногим
сложнее развитие этого метода, связанное с так называемыми транслятор-
ными вычислениями. По существу это просто задание соотношений между
константами.
Более сложные статические вычисления требуют привлечения более глу-
боких знаний о задачах, решаемых универсальной программой, о методах ре-
шения и др. Наглядный пример — использование в универсальной програм-
ме функции вычисления квадратного корня, которая в универсальном случае
должна включать в себя проверку аргумента на отрицательность. Если ока-
зывается возможным доказать, что в данном контексте при обработке кон-
кретных данных проверка будет избыточна, то использование варианта функ-
ции без проверки есть специализация, но уже не обязательно рассматривае-
мая как частичное вычисление. Поэтому для обоснования и проведения та-
ких преобразований может потребоваться весьма серьезный анализ.
3.10. ОБЩИЕ ВЫВОДЫ 177

Иногда из-за трудности описания связи между универсальной програм-


мой и ее специализированной версии или по иным причинам нет смысла
явно составлять универсальный алгоритм. В этом случае можно говорить о
ручном способе специализации “идеальной” (несуществующей) программы,
которую можно рассматривать как призрак. Рассмотрение такого метода ра-
боты в связи с событийным программированием и сопрограммным механиз-
мом, в частности, на примере задач трансляции, показывает, что при такой
ручной специализации появляется возможность лучше узнать сущность про-
цессов, реализуемых в программе. Здесь в полной мере применим принцип
обобщения без потерь, а то, что фактическое обобщение остается програм-
мой-призраком, позволяет говорить об осознанном прагматическом сужении
задачи (в другом подобном случае программист, знающий об общем реше-
нии, вполне может пойти другим путем). В данном случае, как и всегда, когда
мы имеем дело с призраками, идеальная несуществующая программа играет
роль источника системы понятий, направляющей разработку.
Наряду со статической специализацией программы вполне продуктив-
но говорить и о динамической специализации, когда выбор варианта вычи-
слений определяется состоянием перерабатываемых данных, сложившим-
ся к определенному моменту. Наглядно ситуацию иллюстрирует следующий
пример30 , связанный с параллельными вычислениями. Пусть универсальная
программа представляет несколько альтернативных алгоритмов, каждый из
которых более эффективен для данных, удовлетворяющих определенным усло-
виям. Если эти условия проверять накладно, то можно параллельно запу-
стить на счет все алгоритмы и выполнять их до тех пор, пока не выявятся
преимущества одного из них.

§ 3.10. ОБЩИЕ ВЫВОДЫ

Как мы уже говорили, представленный анализ стилей не претендует ни на


полноту охвата вопроса, ни на окончательность систематизации. Стоит упо-
мянуть о ряде вариаций стилей, которые не нашли своего отражения выше.
Самостоятельные стили формируются вокруг программирования, близ-
кого к аппаратуре. Здесь традиционно и оправдано применяются програм-
мирование от состояний, событий и приоритетов. Однако требования учета
временных характеристик ввода/съема данных, соотношений между ними, а
также жестких ограничений по памяти приводят к тому, что в дополнение
30
Основанный на практических наблюдениях одного из соавторов.
178 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

этим стилям накапливаются собственные типовые решения.


Продолжая рассмотрение влияния аппаратуры и окружения на стили, за-
метим, что особенности систем реального времени, распределенных систем,
задач криптографии и иных специфичных приложений также приводят раз-
работчиков к самостоятельным вариантам стилей программирования, кото-
рые формируются как уточнения существующих стилей.
При работе в коллективе стандартизованные стили всегда прогружают-
ся в окружение фирменных стандартов, соглашений и регламентов. Часто
на этапах проектирования программной системы вводятся нормы, наруше-
ние которых просто запрещено. Все подобные дополнения вводятся с целью
способствовать взаимопониманию и облегчить взаимодействия31 . Продол-
жительная деятельность коллективов приводит к тому, что принимаемые со-
глашения и регламенты становятся стилевыми элементами технологии про-
граммирования для данного коллектива.
Наконец, каждый профессиональный программист имеет собственные при-
вычки, предпочтения и сложившиеся стереотипы. Они определяют его ин-
дивидуальный стиль настолько, что очень часто по тексту программы можно
узнавать ее автора32 .
Большая часть таких стилей с трудом может быть систематизирована. Да
это и не требуется! Нужно только хорошо осознавать, что особенности сти-
лей не только неизбежны, но и желательны.
Мы рассмотрели основные существующие на практике стили программи-
рования. Для каждого из них мы проследили теории, с которыми непосред-
ственно связана его методология, практические преимущества и ограниче-
ния каждого стиля, и в результате наметилась некоторая классификация.
Прежде всего, стили делятся на два уровня. В стилях первого уровня мы
имеем дело с конкретными понятиями и действиями, а в стилях второго уров-
ня — с абстрактными сущностями.
На первом уровне все стили укладываются в следующий морфологиче-
ский ящик по координатам действия-условия, локальность-глобальность, вы-
являющий назначение каждого стиля.
31
Другой вопрос, насколько они этой цели достигают.
Но авторам программ стоило бы помнить о том. что в Ватиканской канцелярии высшей
похвалой документу является: «Он так хорошо написан, что невозможно понять, кто его
32

автор.»
3.10. ОБЩИЕ ВЫВОДЫ 179

Действия Условия Стиль


Локальные Локальные Структурный
Глобальные Локальные От состояний
Локальные Глобальные Сентенциальный
Глобальные Глобальные От событий
Второй уровень, представленный функциональным программированием
и ООП, при разработке конкретных систем использует модули, написанные
в стилях первого уровня.
Что касается событийного программирования, то после появления разви-
тых систем поддержки визуального отображения программ, поддерживаю-
щиъх структуру многооконного интерфейса (прежде всего, таковыми стали
системы Think Pascal и Think C++ для Macintosh, а затем их эстафкету подхва-
тили Visual C++ и Delphi) его зачастую рассматривают как обязательный ком-
плекс средств объектно-ориентированной системы программирования. Как
мы могли убедиться, это все-таки дополнительные стилевые возможности.
Рассмотрим теперь, каким классам практических задач лучше всего со-
ответствуют различные стили программирования.
Счету по физическим либо математическим формулам (если брать это
в наиболее общей форме методов вычислений) великолепно соответствует
структурное программирование. Более того, методам вычислений соответ-
стуют чаще всего циклы, а не рекурсия.33
Чистая лексика — автоматы и, соотвественно, программирование от со-
стояний.
Статика синтаксиса — примитивная рекурсия или сводящиеся к ней сен-
тенциальные средства. Здесь часто также целесообразны табличные сред-
ства, сентенциальное и структурное программирование могут соперничать
за эту область.
Для преобразования текстов общего вида и структурное программирова-
ние, и ООП резко проигрывают в технологичности сентенциальному про-
граммированию.
Распознавание образов — автоматы.
То, что имеется значительный багаж вычислительных программ, написанных в стиле от
состояний на языке FORTRAN, не является аргументом против данной позиции. Если кто-
33

то из-за отсутствия удобных средств привык чесать левой ногой за правым ухом или спать
на гвоздях, он зачастую будет делать это, даже когда необходимость пройдет, а если имеет
власть, то и других заставлять делать то же. Оглянитесь вокруг, и Вы увидите целую кучу
таких примеров.
180 ГЛАВА 3. ПРОГРАММИРОВАНИЕ С ПТИЧЬЕГО ПОЛЕТА

Статика графики — рекурсия в объектно-ориентированной форме. Дина-


мика графики — автоматы в объектно-ориентированной форме либо собы-
тийное программирование.34
***

Опыт показывает, что достаточно сложная реальная программная систе-


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

1. Что представляет собой первый цикл в программе 3.3.1? Какую схему


программ он моделирует?

2. В программе 3.9.1 есть ошибка (по традиции появляющаяся с момента


ее разбора А. П. Ершовым). Где ошибка и почему ее не обязательно
исправлять для иллюстрации смешанных вычислений?

В свзязи с этим можно заметить, что популярная в настоящее время технология RUP явля-
ется средством автоматного представления динамики графики и следующей из нее динамики
34

программы для типичных пользовательских задач, где форма важнее содержания.


Глава 4

Понятие жизненного цикла


программного обеспечения и его
модели

§ 4.1. ВВЕДЕНИЕ

Понятие жизненного цикла программного обеспечения появилось, когда


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

181
182 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

альными объектами, и на самом деле единственными чисто искусственны-


ми объектами, кроме математических конструкций, с которыми имеет дело
человек. Например, машина сделана из реальных материалов, наследует их
свойства, и уже по этой причине не может создаваться чисто логически, си-
лами одной лишь мысли. А математический объект и программа сосотоят из
информационных сущностей. В принципе они могут быть порождены чисто
логически. Но и в том, и в другом случае чистая логика творения натыкается
на реальные либо конвенциональные ограничения. Математический объект
должен быть признан сообществом математиков, и поэтому должен вписать-
ся в систему существующих математических объектов. Программа же созда-
ется на базе других программ и должна работать в их окружении. Сложность
программного окружения такова, что разобраться в нем до конца невозмож-
но, да оно вдобавок и меняется все время. Так что программное окружение
играет сейчас для программ ту же роль, что конструкционные материалы
и окружающая среда — для технических. И, конечно же, неустраним фак-
тор пользователя. Все равно, делаете Вы программу для квалифицированных
программистов либо для конечных пользователей, пользователь перепутает
все, что возможно, и даже то, что невозможно, и затруднительно предсказать,
что он может сотворить с программой. Но, тем не менее, программа наибо-
лее близко, за исключением математическх структур, подходит к понятию
настоящего искусственного объекта.
Программы не подвержены физическому износу, но в ходе их эксплуа-
тации обнаруживаются ошибки (неисправности), требующие исправления.
Ошибки возникают также от изменения условий использования программы.
Последнее же является принципиальным свойством программного обеспече-
ния, иначе оно теряет свой смысл. Поэтому правомерно говорить о старении
программ, правда, не о физическом, а о моральном.
Необходимость внесения изменений в действующие программы (как из-
за обнаруживаемых ошибок, так и по причине развития требований) приво-
дит по сути дела к тому, что разработка программного обеспечения продол-
жается после передачи его пользователю и в течение всего времени жизни
программ. Деятельность, связанная с решением довольно многочисленных
задач такой продолжающейся разработки, получила название сопровожде-
ния программного обеспечения (см. рис. 4.1).
Исторически развитие концепций жизненного цикла связано с поиском
для него адекватных моделей. Как и всякая другая, модель жизненного цикла
является абстракцией реального процесса, в которой опущены детали, несу-
щественные с точки зрения назначения модели. Различие назначений приме-
4.1. ВВЕДЕНИЕ 183

Использование
Разработка
Продолжающаяся разработка
(сопровождение)

Рис. 4.1. Разработка, использование и сопровождение программ

нения моделей определяет их разнообразие.


Основные причины, из-за которых нужно изучать вопросы моделирова-
ния жизненного цикла программного обеспечения, можно сформулировать
следующим образом.
Во-первых, это знание даже для непрофессионального программиста по-
могает понять, на что можно рассчитывать при заказе или приобретении про-
граммного обеспечения и что нереально требовать от него. В частности, не-
удобные моменты работы с программой, ее ошибки и недоработки обычно
устраняются в ходе продолжающейся разработки, и есть основания ожидать,
что последующие версии будут лучше. Однако кардинальные изменения кон-
цепций программы — задача другого проекта, который совсем необязательно
будет во всех отношениях лучше данной системы.
Во-вторых, модели жизненного цикла — основа знания технологий про-
граммирования и инструментария, поддерживающего их. Программист все-
гда применяет в своей работе инструменты, но квалифицированный програм-
мист знает, где, когда и как их применять. Именно в этом помогают понятия
моделирования жизненного цикла: любая технология базируется на опреде-
ленных представлениях о жизненном цикле, выстаивает свои методы и ин-
струменты вокруг фаз и этапов жизненного цикла.
В-третьих, общие знания того, как развивается программный проект, да-
ют наиболее надежные ориентиры для его планирования, позволяют эконом-
нее расходовать ресурсы, добиваться более высокого качества управления.
Все это относится к сфере профессиональных обязанностей руководителя
программного проекта.
В настоящей главе модели жизненного цикла представлены в виде, позво-
ляющем рассматривать их, абстрагируясь от специфики разработки конкрет-
ных программных систем. Описываются традиционные модели и их разви-
тие, приспособленное к потребностям объектно-ориентированного проекти-
рования.
184 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

МОДЕЛИ ТРАДИЦИОННОГО ПРЕДСТАВЛЕНИЯ


О ЖИЗНЕННОМ ЦИКЛЕ
§ 4.2.

4.2.1. Общепринятая модель

Вероятно, самым распространенным мотивом обращения к понятию жиз-


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

• сопровождение.

Фазы разбиваются на ряд этапов (см. рис. 4.2).


Разработка начинается с идентификации потребности в новом приложе-
нии, а заканчивается передачей продукта разработки в эксплуатацию.
Первым этапом фазы разработки является постановка задачи и опреде-
ление требований. Определение требований включает описание общего кон-
текста задачи, ожидаемых функций системы и ее ограничений. На этом этапе
заказчик совместно с разработчиками принимают решение, о создании си-
стемы. Особенно существенен этот этап для нетрадиционных приложений.
В случае положительного решения начинается этап спецификации систе-
мы в соответствии с требованиями. Разработчики программного обеспече-
ния пытаются осмыслить выдвигаемые заказчиком требования и зафиксиро-
вать их в виде спецификаций системы. Важно подчеркнуть, что назначение
этих спецификаций — описывать внешнее поведение разрабатываемой си-
стемы, а не ее внутреннюю организацию, т. е. отвечать на вопрос, что она
должна делать, а не как это будет реализовано. Здесь говорится о назначении,
а не о форме спецификаций, поскольку на практике при отсутствии подхо-
дящего языка спецификаций, к сожалению, нередко приходится прибегать к
описанию «что» посредством «как»1 . Прежде чем приступать к созданию
Проблемы языка спецификаций не в том, что нельзя (или трудно) строго и четко описать,
что требуется в проекте. В большей степени они связаны с необходимостью добиваться и
1

поддерживать соответствие описания «что» нечетким, неточным и часто противоречивым


требованиям со стороны внешних по отношению к проекту людей. Нет оснований полагать,
что эти люди будут знакомы с «самым хорошим языком спецификаций», что они будут забо-
титься о корректности своих требований. Задача этапа спецификаций в том и состоит, чтобы
4.2. ТРАДИЦИОННОЕ ПРЕДСТАВЛЕНИЕ 185

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

Спецификации

Проектирование Фаза разработки

Реализация

Тестирование

Сопровождение
Фаза эксплуатации и
сопровождения
Развитие

Рис. 4.2. Общепринятая модель жизненного цикла ПО

проекта по спецификациям, они должны быть тщательно проверены на со-


ответствие исходным целям, полноту, совместимость (непротиворечивость)
и однозначность.
Разработка проектных решений, отвечающих на вопрос, как должна быть
реализована система, чтобы она могла удовлетворять специфицированным
требованиям, выполняется на этапе проектирования. Поскольку сложность
системы в целом может быть очень большой, главной задачей этого этапа
является последовательная декомпозиция системы до уровня очевидно реа-
лизуемых модулей или процедур.
описание программы выстроить в виде логически выверенной системы, понятной как для
заказчика данной разработки, будущих пользователей, так и для исполнителей проекта.
186 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

На следующем этапе реализации, или кодирования, каждый из этих мо-


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

4.2.2. Классическая итерационная модель

Общепринятая модель жизненного цикла является идеальной, т. к. только


очень простые задачи проходят все этапы без каких-либо итераций — воз-
вратов на предыдущие шаги технологического процесса. При программиро-
вании, например, может обнаружиться, что реализация некоторой функции
очень громоздка, не эффективна и вступает в противоречие с требуемой от
системы производительностью. В этом случае требуется перепроектирова-
ние, а может быть, и переделка спецификаций. При разработке больших не-
4.2. ТРАДИЦИОННОЕ ПРЕДСТАВЛЕНИЕ 187

традиционных систем необходимость в итерациях возникает регулярно на


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

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

Спецификации

Проектирование

Реализация

Тестирование и отладка

Эксплуатация и сопровождение

Рис. 4.3. Классическая итерационная модель

этапам, квалифицируемые как требование повторить этап для исправления


обнаруженной ошибки. В этой связи может показаться странным переход от
этапа «Эксплуатация и сопровождение» к этапу «Тестирование и отладка».
Дело в том, что рекламации, предъявляемые в ходе эксплуатации системы,
часто даются в такой форме, которая нуждается в их перепроверке. Чтобы
понять, о каких ошибках идет речь в рекламации, разработчикам полезно
предварительно воспроизвести пользовательскую ситуацию у себя, т. е. вы-
полнить действия, которые обычно относят к тестированию.
Классическая итерационная модель абсолютизирует возможность возвра-
тов на предыдущие этапы. Однако это обстоятельство отражает существен-
188 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

ный непреодолимый аспект программных разработок: стремление заранее


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

4.2.3. Каскадная модель

Более строгой разновидностью классической модели является так назы-


ваемая каскадная модель, которую можно рассматривать в качестве показа-
тельного примера того, какими методами можно минимизировать возвраты.
Характерные черты каскадной модели:
• завершение каждого этапа (они почти те же, что и в классической моде-
ли) проверкой полученных результатов с целью устранить как можно
Данное противоречие является типичным примером т. н. проблемного противоречия,
противоречия между желаемым и действительным. Решение проблемных противоречий
2

является стимулом творческой деятельности человека. Более того, задачу, которая требует
творческого подхода, лучше всего формулировать в виде проблемных противоречий, макси-
мально обостряя их. Именно тогда можно надеяться на то, что будет найдено действитель-
но хорошее и адекватное решение. Методология использования проблемных противоречий
в творческих рассуждениях была развита Г. С. Альтшуллером и советской школой теории
решения изобертательских задач (ТРИЗ). Программистам и особенно преподавателям про-
граммирования настоятельно рекомендуется прочитать книгу Г. С. Альтшуллера [4]. Хотя
методы ТРИЗ в большинстве своем прямо не могут быть применены при решении програм-
мистских задач, поскольку они в основном направлены на поиск элементарных решений,
преодолевающих «сопротивление материала», методология подхода исключительно полез-
на для любого творческого человека, занимающегося созданием реальных систем (именно
этим программисты, ученые и инженеры отличаются от гуманитариев).
4.2. ТРАДИЦИОННОЕ ПРЕДСТАВЛЕНИЕ 189

большее число проблем, связанных с разработкой изделия;

• циклическое повторение пройденных этапов (как в классической мо-


дели).

Мотивация каскадной модели связана с так называемым управлением каче-


ством программного обеспечения. В связи с ней уточняются понятия эта-
пов, некоторые из них структурируются (спецификация требований и реа-
лизация). На рис. 4.4 приведена схема каскадной модели, построенная как
модификация классической итерационной модели. В каждом блоке, обозна-
чающем этап, указано действие, которым этап завершается (наименования
этих действий отмечены серым фоном). Из рисунка видно, что в этой мо-
дели тестирование не выделяется в качестве отдельного этапа, а считается
лишь порогом, через который нужно перейти, чтобы завершить этап, точно
так же, как и другие подобные действия.
В соответствии с каскадной моделью завершение этапа определения си-
стемных требований включает фиксацию их в виде специальных докумен-
тов, называемых обзорами того, что от системы требуется (описание функ-
ций), а спецификация требований к программам — подтверждением выпол-
нения зафиксированных в обзорах функций в планируемых к реализации
программах. Кроме того, подтверждение предполагается и на первом этапе,
т. е. после определения требований. Это отражает тот факт, что полученные
требования необходимо согласовывать с заказчиком.
Результат проектирования верифицируется, т. е. проверяется, что приня-
тая структура системы и реализационные механизмы обеспечивают выпол-
нимость специфицированных функций.
Реализация контролируется путем тестирования компонент, а после ин-
теграции компонент в систему и комплексной отладки проводится аттеста-
ция, т. е. проверка-фиксация фактически реализованных функций системы,
описание ограничений реализации и т. п.
В ходе эксплуатации и сопровождения изделия устанавливается, насколь-
ко хорошо система соответствует пользовательским запросам, т. е. осуще-
ствляется переаттестация.
Каждая из указанных проверок может отослать разработчиков системы к
повторению любого из ранее пройденных этапов, что иллюстрируется стрел-
ками на рис. 4.4. В то же время, каскадная модель разработана в ответ на
требование практики разработки программных проектов, в которых за счет
преодоления проверочных барьеров достигается минимизация возвратов к
190 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

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

Спецификация требований:
• системные требования

Проектирование
Чем заканчиваются этапы

Реализация:
• кодирование и автономная отладка

Эксплуатация и сопровождение

Рис. 4.4. Каскадная модель


4.2. ТРАДИЦИОННОЕ ПРЕДСТАВЛЕНИЕ 191

пройденным этапам. Такая минимизация возможна не только в плане коли-


чества откатов по схеме: за счет ужесточения проверок разработчики пы-
таются ликвидировать прямые возвраты через несколько этапов. Соответ-
ствующая схема, называемая строгой каскадной моделью, представлена на
рис. 4.5. Поучительно проследить, как в строгой каскадной модели испра-
вляются ошибки ранних этапов. В соответствии с данной схемой разработ-
чики любого этапа в качестве исходных материалов для своей деятельности,
т.е. задания на разработку, получают результаты предыдущего этапа, прошед-
шие соответствующую проверку (в идеале исполнители этапа могут вовсе не
знать о более ранних этапах). При проведении работ этапа может быть выяс-
нено, что задание невыполнимо по одной из следующих причин:
• оно противоречиво, т. е. содержит несовместные или невыполнимые
требования;
• не выработаны критерии для выбора одного из возможных вариантов
решения.
Обе ситуации квалифицируются как ошибки задания, т. е. как ошибки
предыдущего этапа. Для исправления обнаруженных ошибок работы преды-
дущего этапа возобновляются. В результате ошибки либо ликвидируются,
либо констатируется невозможность их непосредственного исправления. В
первом случае работы этапа, вызвавшего возврат, возобновляются с откор-
ректированным заданием. Второй случай квалифицируется как ошибка бо-
лее раннего этапа.
Строгая каскадная модель фиксирует два важных момента жизненного
цикла:
• точное разделение работ, заданий и ответственности разработчиков эта-
пов и тех, кто, проверяя работы, инициирует переход к следующему
этапу;
• малые циклы между соседними этапами, в результате которых дости-
гается компромиссное задание.
Первый момент — это шаг к осознанию фактического разделения труда, из
которого вполне осуществимо явное выделение технологических и органи-
зационных функций, выполняемых на каждом этапе. В результате появляется
возможность постановки задачи автоматизированной поддержки этих функ-
ций. Второй момент можно трактовать как совместное выполнение работ со-
седних этапов, т. е. их перекрытие. Однако в рамках каскадной модели эти
192 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

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

Спецификация требований:
• системные требования

Проектирование
Чем заканчиваются этапы

Реализация:
• кодирование и автономная отладка

Эксплуатация и сопровождение

Рис. 4.5. Строгая каскадная модель


4.2. ТРАДИЦИОННОЕ ПРЕДСТАВЛЕНИЕ 193

обстоятельства отражаются лишь косвенно. Продуктивность явного включе-


ния их в качестве элементов модели жизненного цикла демонстрируется в
следующем разделе.

4.2.4. Модель фазы-функции

Чрезвычайно важным мотивом развития моделей жизненного цикла про-


граммного обеспечения является потребность в подходящем средстве для
комплексного управления проектом. По существу, это утверждение указыва-
ет на то, что модель должна служить основой организации взаимоотношений
между разработчиками, и, таким образом, одной из ее целей является под-
держка функций менеджера. Это приводит к необходимости наложения на
модель контрольных точек и функций, задающих организационно-времен-
ные рамки проекта.
Наиболее последовательно такое дополнение классической схемы реали-
зовано в модели Гантера [23] в виде матрицы «фазы — функции». Уже из
упоминания о матрице следует, что модель Гантера имеет два измерения:
• фазовое, отражающее этапы выполнения проекта и сопутствующие им
события, и
• функциональное, показывающее, какие организационные функции вы-
полняются в ходе развития проекта и какова их интенсивность на ка-
ждом из этапов.
В модели Гантера отражено то, что выполнение функции на одном этапе мо-
жет продолжаться на следующем. На рис. 4.6 представлено фазовое изме-
рение модели. Жирной чертой (с разрывом и стрелкой, обозначающей вре-
менное направление) изображен процесс разработки3 . Контрольные точки и
наименования событий указаны под этой чертой. Они пронумерованы. Все
развитие проекта в модели привязывается к этим контрольным точкам и со-
бытиям.
В данной модели жизненный цикл распадается на следующие перекры-
вающие друг друга фазы (этапы):
Для моделей реальных проектов целесообразно длины отрезков между контрольны-
ми точками выбирать пропорционально оценкам временных соотношений между этапами.
3

Если фактическое время выполнения этапа оказывается не соответствующим соотношениям


на схеме, то это свидетельствует об ошибке планирования работ: неудовлетворительны либо
предварительная оценка, либо темпы работы. Таким образом, хорошая модель жизненного
цикла может рассматриваться в качестве важного инструмента планирования.
194 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

Рис. 4.6. Фазовое измерение модели фазы — функции

• исследования — этап начинается, когда необходимость разработки при-


знана руководством проекта (контрольная точка 0), и заключается в
том, что для проекта обосновываются требуемые ресурсы (контроль-
ная точка 1) и формулируются требования к разрабатываемому изде-
лию (контрольная точка 2);

• анализ осуществимости — начинается на фазе исследования, когда


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

• конструирование — этап начинается обычно на фазе анализа осуще-


ствимости, как только документально зафиксированы предваритель-
ные цели проекта (контрольная точка 2), и заканчивается утверждени-
ем проектных решений в виде официальной спецификации на разра-
ботку (контрольная точка 5);
4.2. ТРАДИЦИОННОЕ ПРЕДСТАВЛЕНИЕ 195

• программирование — начинается на фазе конструирования, когда ста-


новятся доступными основные спецификации на отдельные компонен-
ты изделия (контрольная точка 4), но не ранее утверждения соглаше-
ния о требованиях (контрольная точка 3). Совмещение данной фазы с
заключительным этапом конструирования обеспечивает оперативную
проверку проектных решений и некоторых ключевых вопросов разра-
ботки. Цель этапа — реализация программ компонентов с последую-
щей сборкой изделия. Он завершается, когда разработчики заканчива-
ют документирование, отладку и компоновку и передают изделие служ-
бе, выполняющей независимую оценку результатов работы (независи-
мые испытания начались — контрольная точка 7);

• оценка — фаза является буферной зоной между началом испытаний


и практическим использованием изделия. Она начинается, как толь-
ко проведены внутренние (силами разработчиков) испытания изделия
(контрольная точка 6) и заканчивается, когда подтверждается готов-
ность изделия к эксплуатации (контрольная точка 9);

• использование — начинается в ходе передачи изделия на распростране-


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

На протяжении фаз жизненного цикла разработчики выполняют следую-


щие технологические (организационные) функции (классы функций):

1. планирование,

2. разработка,

3. обслуживание,

4. выпуск документации,

5. испытания,

6. поддержка,
196 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

7. сопровождение.

Перечисленные функции на разных этапах имеют различное содержание,


требуют различной интенсивности, но, что особенно важно для модели, со-
вмещаются при реализации проекта. Это функциональное измерение моде-
ли, наложение которого на фазовое измерение дает изображение матрицы
фаз-функций в целом (см. рис. 4.7, на котором интенсивность выполняемых
функций отражается густотой закраски клеток матрицы).
Состав организационных функций и их интенсивность могут меняться
от проекта к проекту в зависимости от его особенностей, от того, что ру-
ководство проекта считает главным или второстепенным. К примеру, если
исходная квалификация коллектива не очень высока, в список функций мо-
жет быть добавлено обучение персонала. Иногда бывает важно разграничить
планирование и контроль (по Гантеру контрольные функции явно не выделя-
ются). При объектно-ориентированном проектировании роль моделирования
возрастает настолько, что его целесообразно перевести из разряда методов
проектирования в явно выделенную технологическую функцию, о чем речь
впереди.
Модель учитывает соотношение технологических функций и фаз жизнен-
ного цикла, чем она выгодно отличается от ранее рассмотренных «идеаль-
ных» первопорядковых моделей. По-видимому, простота и ограниченность
«идеальных» моделей есть следствие отождествления выделяемых этапов с
технологической операцией, преобладающей при их выполнении. В то же
время задача отражения итеративности в модели Гантера в явном виде не
предусматривается. Хотя само по себе перекрытие смежных фаз проекта и
выпуск соответствующей событиям документации — путь к минимизации
возвратов к выполненным этапам, более содержательные средства описания
итераций в модель не закладываются.

§ 4.3. ИТЕРАТИВНЫЕ МОДЕЛИ ЖИЗНЕННОГО ЦИКЛА

Итеративность неизбежна при разработке сложных программных изде-


лий, а потому ее целесообразно внести в основу жизненного цикла и техно-
логии разратоки. Однако вплоть до появления объектно-ориентированного
проектирования (ООП) и Сообщества Free Soft, в настоящее время фигури-
рующего под девизом «Открытое программное обеспечение» (Open Source)
(оба эти события произошли практически одновременно) подходы к разви-
тию проектов не пытались использовать итеративность в качестве метода
4.3. ИТЕРАТИВНЫЕ МОДЕЛИ ЖИЗНЕННОГО ЦИКЛА 197

Рис. 4.7. Матрица фазы — функции модели Гантера


198 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

проектирования и стремились лишь к минимизации возвратов. ООП сразу


поставило итеративность в основу своих технологий и своих моделей жищ-
ненного цикла, по этой причине ныне часто итеративные методы органи-
зации развития программных систем отождествяют с объектно-ориентиро-
ванными, ставя (как мы уже отмечали для сентенциального и «логического»
программирования) второстепенные признаки на главное место и абсолюти-
зируя конкретную реализацию4 . Здесь выделяются те особенности итератив-
ной разработки программ, которые (хоть и были в большинстве своем в ООП
впервые либо рассмотрены и использованы, либо развиты и доведены до тех-
нологических решений) не зависят от нынешних конкретных реализаций и
могут быть перенесены, в частности, на функциональный и сентенциальный
стили, также допускающие итеративное развитие программ, но менее задей-
ствованнные в современных технологиях.
Функциональный стиль соответствует абстрактному (математическому)
мышлению, объектно-ориентированный — образному (визуальному), сен-
тенциальный — вербальному (словесному). Большие и успешные проекты,
использующие итеративное развитие, имеются на базе каждого из этих сти-
лей. Если ООП является стандартным методом индустриального программи-
рования, то функциональное программирование является одним из главных
методов сообщества Open Source, и в качестве итеративного проекта, реа-
лизованного на этой базе, можно сослаться хотя бы на EMACS (или, если
Вы признаете лишь коммерческое программное обеспечение, на AUTOCAD).
Сентенциальный стиль в его PROLOGовском варианте послужил основой ря-
да крупных баз знаний.
Если развить модель Гантера с целью учета итеративности, то, очевидно,
придется предусмотреть расщепление линии жизненного цикла, как это пред-
ставлено на рис. 4.8. Но это влечет и расщепление матрицы интенсивностей
выполняемых функций: было бы необоснованно считать, что интенсивно-
сти при возвратах сохраняются. В целом, по мере продвижения разработки
к своему завершению, они должны уменьшаться. Таким образом, матрица
интенсивностей превращается в последовательность матриц, отражающую
Сообщество Open Source также немедленно занялось разработкой технологической под-
держки итеративного стиля, и некоторые их находки, в частности, система депозитария CVS,
4

вовсю используются индустриальными фирмами. Но ООП имело преимущество в мощно-


сти и бесстыдстве рекламной кампании среди широких кругов прежде всего бизнеса, в то
время как Open Source делало ставку на специалистов и энтузиастов и занималось скорее
созданием общественного движения и общественного мнения. Поэтому практически все на-
ходки итеративной разработки в данный момент ассоциируются с ООП.
4.3. ИТЕРАТИВНЫЕ МОДЕЛИ ЖИЗНЕННОГО ЦИКЛА 199

Рис. 4.8. Учет итеративности в модели фазы-функции (фазовое измерение,


показаны лишь некоторые возвраты)

итеративный характер развития проекта.


В технологическом плане итеративные подходы к развитию проекта ко-
ренным образом отличаются от всех последовательных технологий. Для тра-
диционных подходов итерация — это исправление ошибок, т. е. процесс, ко-
торый с трудом поддается технологическим нормам и регламентам. При ите-
ративном подходе итерации в принципе не отменяют результаты друг друга,
а только дополняют и развивают их.
4.3.1. Базовые технологические принципы итеративного проектиро-
вания

Рассмотрим принципиальные моменты, которые стали трактоваться в ите-


ративном подходе к развитию проектов по-иному в сравнении с последова-
тельными методологиями.
I) Итеративность развития
Начиная с фазы анализа и до завершения реализации, процесс проекти-
рования строится как серия итераций, отбрасывая иллюзию о построе-
нии программы сразу и до конца, пронизывающую традиционные под-
200 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

ходы. Каждая итерация является результатом разрешения противоречий


между желаемым и достижимым, частично реализуя цели.
Возможно, этим итерациям предшествует период изучения самой пред-
метной области и задач проекта в целом (этапы определения требований
и начального планирования).
II) Изменение функциональности
Поскольку в ходе развития проекта требования почти всегда пересма-
триваются, и обычно требуется изменение старых функций, необходи-
мо реализовывать функциональность таким способом, чтобы пересмотр
ее производился с минимальными затратами.
III) Глоссарий проекта
В качестве инструмента поддержки целостности системы понятий в про-
цессе пересмотра требований и трассировки изменений, индуцирован-
ных пересмотром функциональности, необходимо создание и ведение
базы понятий и их взаимосвязей, фиксирующую в каждый момент по-
нимание используемыъ понятий и прослеживающую историю концеп-
туальных изменений. Изменения в тексте программной системы долж-
ны следовать за изменениями концепций, а не предшествовать им.
IV) Наращивание функциональности
Наращивание функциональности проектируемого изделия представля-
ется как развитие сценариев, которые соответствуют описаниям (в раз-
витых технологиях диаграммам) взаимодействия высокоуровневых объ-
ектов и понятий и отражают отдельные стороны функционирования.
Эти описания предписывают развитие на этапе программирования опе-
рационной базы проекта: она вырабатывается, исходя из сценариев уров-
ня проектирования (конструирования). Полная функциональность со-
стоит из функциональностей всех сценариев. Таким образом, данная
стратегия довольно близка классическому методу пошаговой детали-
зации, при использовании которого функциональность наращивается
путем уточнения (доопределения) модулей нижнего уровня. Однако в
отличие от этого метода итеративное наращивание требует, чтобы в ре-
зультате каждой итерации изделие получало полностью готовую функ-
циональность, планируемую реализуемым сценарием. Последующие
итерации чаще всего добавляют уже другую функциональность, кото-
рая планируется другим сценарием.
4.3. ИТЕРАТИВНЫЕ МОДЕЛИ ЖИЗНЕННОГО ЦИКЛА 201

V) Ничто не делается однократно.


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

VI) Оперирование на размножающихся фазах подобно.


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

При итеративном проектировании в ходе наращивания функциональности


обыкновенно выполняются вполне традиционные этапы:

1. Определение требований, или планирование итерации, — фиксирует-


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

2. Анализ — исследуются условия выполнения планируемых требований,


проверяется полнота отобранных сценариев с точки зрения реализации
требуемой функциональности;

3. Моделирование пользовательского интерфейса — коль скоро итерация


должна обеспечивать функционально законченную реализацию, требу-
ется определить правила взаимодействий, необходимые для активиза-
ции требуемых функций. Модель интерфейса представляет пользова-
тельское представление поведения объектов данной итерации;

4. Конструирование — обычная декомпозиция проекта. Конструирова-


ние включает построение или наращивание иерархии понятий (напри-
мер, системы классов, описания событий и определения реакции на
них и т. д.) В ходе конструирования определяются понятия и их про-
граммные реализации, реализуемые и/или доопределяемые на данной
202 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

итерации, и набор функций, которые обеспечивают решение задачи дан-


ной итерации;
5. Реализация (программирование) — программное воплощение решений,
принятых для данной итерации. Необходимым компонентом реализа-
ции здесь считается автономная проверка соответствия составляемых
модулей их спецификациям (в частности, должно быть обеспечено тре-
буемое поведение объектов);
6. Тестирование — этап комплексной проверки результатов, полученных
на данной итерации;
7. Оценка результатов итерации — этап включает работу, связанную с
рассмотрением полученных результатов в контексте проекта в целом. В
частности, должно быть выяснено, какие задачи проекта можно решать
с учетом результатов итерации, на какие ранее поставленные вопросы
получены ответы, какие новые вопросы возникают в новых условиях.

4.3.2. Итеративная модификация модели фазы-функции

Этапы итеративного развития проекта остаются практически традицион-


ными внутри одной итерации. Это позволяет описывать процесс итеративно-
го наращивания как модификацию существующих моделей жизненного ци-
кла. В настоящем разделе такая модификация осуществляется для модели
фазы-функции Гантера.
В сравнении с моделью Гантера фазовое измерение жизненного цикла
при итеративном проектировании почти не изменяется: появляется лишь один
дополнительный этап: «Моделирование пользовательского интерфейса», ко-
торый в старой схеме можно рассматривать как часть этапов анализа и/или
конструирования. Однако это весьма существенное дополнение, характери-
зующее подход в целом. Главный мотив явного рассмотрения моделирования
в жизненном цикле при итеративном развитии проектов связан со следующи-
ми двумя особенностями:
1. Распределение реализуемых требований по итерациям.
Совокупность сценариев, реализуемых на очередной итерации, и набор
ранее реализованных сценариев всегда образуют законченную, хотя и
неполную версию системы, предлагаемую пользователям. По разным
причинам, в том числе для исключения двусмысленностей в понима-
нии, необходимо представление планируемого для реализации в виде
4.3. ИТЕРАТИВНЫЕ МОДЕЛИ ЖИЗНЕННОГО ЦИКЛА 203

моделей, согласующих взгляд на систему со стороны пользователей,


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

2. Конструирование системы с учетом будущего ее развития, в первую


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

В приведенном выше перечне этапов жизненного цикла при итеративном


подходе явно выделяется моделирование уровня анализа, которое сводится к
построению модельного представлению сценариев. Но это только один аспект
проектного моделирования. Как было только что показано, другой, не ме-
нее существенный аспект моделирования, проявляется при конструирова-
нии. Наконец, есть еще третий аспект моделирования, связанный с предъ-
явлением каждой версии программного изделия пользователю, представле-
ние которого о системе, разумеется, не имеет отношения к моделям уровня
конструирования и лишь косвенно связано с моделями уровня анализа. Та-
ким образом, если следовать гантеровскому стилю описания жизненного ци-
кла, то правильнее будет выделять не только этап моделирования (как это,
следуя уже сложившейся традиции, чаще всего делают в ООП), а технологи-
ческую функцию моделирования, пронизывающую весь процесс разработки
проекта.
В новой схеме жизненного цикла появляется строго регламентированное
расщепление, единственное для всей последовательности работ (см. рис. 4.9).
Но этот маршрут отражает не корректировку ошибочно принимаемых реше-
ний, а вполне запланированный акт, фиксирующий то, что в ходе выполнения
итераций происходит наращивание возможностей изделия.
Любой итеративно развивающийся сложный проект строится на базе уже
204 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

Начальная Итеративное зацикливание Завершение проекта


фаза
проекта

Пополнение базового
окружения проекта
Окончание работ

Общие требования и общий


план составлены, ближайшая
и перспективные задачи,
критерии оценки результатов
итераций определены

Рис. 4.9. Фазовое измерение модели жизненного цикла при итетаривном раз-
витии проекта
4.3. ИТЕРАТИВНЫЕ МОДЕЛИ ЖИЗНЕННОГО ЦИКЛА 205

существующей среды компонентов. Это базовое окружение проекта интен-


сивно используется и, в свою очередь, пополняется средствами, возникаю-
щими в результате итеративного наращивания. Полезность сохранения та-
ких средств для текущего проекта и для последующих разработок очевидна.
Таким образом, необходимо планирование и реализация переиспользования
программного обеспечения. Конкретной реализацией планирования переис-
пользования обычно является выделение при выполнении этапа оценки об-
щеполезных компонент и сохранение их в депозитарии проектов.
По вполне понятным причинам в итеративном проектировании несколько
изменяется содержание ряда этапов, что нашло свое отражение в количестве
и наименованиях событий на рисунке.
Необходимо указать работы, которые выходят за рамки стандартизован-
ного итерационного процесса. Это — начальная фаза проекта, которая вы-
полняется на старте в ходе исследований и анализа осуществимости, и фаза
завершения проекта (итерации), с выполнением которой работы над проек-
том (над итерацией) заканчиваются.
Смысл работ начальной фазы — общее планирование развития проек-
та. Помимо традиционного содержания, вкладываемого в этапы определения
требований к проекту в целом, общий план должен стать основой разработки
еще в двух отношениях:
• требуется определить ближайшую задачу и перспективные задачи про-
екта.
Первая из них — задача первой итерации, в ходе которой, в частности,
готовится первый рабочий продукт, предъявляемый заказчику. С точки
зрения развития проекта решение ближайшей задачи должно обеспе-
чить осуществимость последующего итеративного наращивания воз-
можностей системы. От этих двух результатов зависит судьба проекта
в целом.
Перспективные задачи — это планируемое развитие. Часто они кор-
ректируются в дальнейшем, особенно по результатам оценки решения
ближайшей задачи;
• требуется выбрать критерии оценки результатов итераций. Эти крите-
рии могут варьироваться в зависимости от направленности проекта,
прикладной области и других обстоятельств.
Фаза завершения проекта (итерации) охватывает часть жизненного цикла, ко-
торая отражает деятельность разработчиков, связанную с рабочими продук-
206 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

тами итерации, после получения результатов. Она аналогична традиционной


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

• поставку, или пакетирование изделия для потребителя (контрольная


точка 9 на рис. 4.9);

• сопровождение программного продукта (по причине разнообразия ва-


риантов организации этих работ они редко описываются структурно,
т. е. с разбиением на этапы);

• этап окончания работ (контрольные точки 11, 12): оповещение о пре-


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

• выделение общих (т. е. не привязанных к проекту) переиспользуемых


компонентов (обычно эти работы связываются с событием передачи си-
стемы на распространение — контрольная точка 10).

Одним из существенных моментов итеративного проектирования явля-


ется отказ от традиционного постулата о том, что все требования к системе
сформулированы заранее6 . Следовательно, при моделировании жизненного
Этап окончания работ мог бы быть представлен во всех традиционных моделях, но в
то время, когда эти модели разрабатывались, ему не придавали особого значения. Вместе с
5

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

собственно ООП.
4.3. ИТЕРАТИВНЫЕ МОДЕЛИ ЖИЗНЕННОГО ЦИКЛА 207

цикла вообще и его фазы завершения в частности нужно учитывать обра-


ботку потока внешних требований на всех этапах. Этому вопросу еще бу-
дет уделено внимание, а пока можно считать (как чаще всего и бывает), что
требования, поступающие на фазе завершения итерации, рассматриваются
как относящиеся к следующим итерациям, т. е. к следующим версиям си-
стемы. В таком случае завершение итерации означает сопровождение про-
граммного изделия, а затем окончание работ с данной версией. Пожелания
к развитию проекта в этот период учитываются как требования к последую-
щим (возможно, еще не начатым) итерациям. Окончание проекта рассматри-
вается как отказ от сопровождения всех версий системы. Стоит сопоставить
это положение с традиционными подходами к проектированию, когда учет
пожеланий к системе в процессе ее эксплуатации чаще всего означает одно:
организацию нового проекта (быть может, специального), цель которого —
учет новых требований.
Несколько слов о функциональном измерении в модифицированной для
итеративного подхода матрице фазы — функции. Как было показано выше,
целесообразно список технологических функций расширить за счет модели-
рования. Соответственно, следует определить в матрице Гантера строку ин-
тенсивностей для этой функции. В предположении о сохранении распределе-
ния интенсивностей других функций (см. рис. 4.7) распределение интенсив-
ности для модифицированной модели жизненного цикла можно задать так,
как это сделано на рис. 4.10, который показывает новый вид модели целиком
(на рисунке контрольные точки жизненного цикла указаны своими номерами
без пояснений).
Представленные распределения интенсивностей нельзя абсолютизировать.
Наивно было бы предполагать стабильность интенсивностей технологиче-
ских функций по итерациям. Следовательно, весь цикл развития проекта в
матричном, двумерном представлении модифицированной гантеровской мо-
дели изобразить не удастся: оно не может показать изменение интенсивно-
стей технологических функций при переходе от одной итерации к другой. По
этой причине предлагается распределение интенсивностей технологических
функций рассматривать как «среднестатистическую» интегральную по ите-
рациям тенденцию. Практическая полезность рассмотрения функционально-
го измерения — не в конкретном распределении интенсивностей технологи-
ческих функций в реальных проектах, а в том, что оно заставляет руковод-
ство проекта думать о расстановке сил в коллективе разработчиков и вообще
о правильном распределении кадровых ресурсов проекта.
208 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

Начальная Итеративное зацикливание Завершение проекта


фаза
проекта

Пополнение базового
окружения проекта

Окончание работ

Контрольные
точки
(события): 0 1 2 3 4 6 7 8 9 10 11 12

Рис. 4.10. Модель фазы - функции, модифицированная для объектно-ориен-


тированного развития проекта
4.3. ИТЕРАТИВНЫЕ МОДЕЛИ ЖИЗНЕННОГО ЦИКЛА 209

4.3.3. Параллельное выполнение итераций

Любой программный проект, заслуживающий привлечения менеджера для


поддержки разработки, — процесс, развиваемый коллективно. Следователь-
но, уместно ставить вопрос, как должна отражаться в модели жизненного
цикла одновременность деятельности исполнителей коллектива. По вполне
понятным причинам, это является одним из мотивов разработки моделей.
В модели, следующей гантеровской схеме ‘фазы — функции’, это каче-
ство процесса разработки программного изделия отражено с помощью функ-
ционального измерения, показывающего, какие технологические функции
выполняются одновременно. В рамках итеративного подхода явно выделяет-
ся еще один вид технологического параллелизма: одновременная разработ-
ка нескольких итераций разными группами исполнителей (словосочетание
«разные группы» не надо понимать буквально — по существу, это группо-
вые роли, и конкретная группа исполнителей вполне может одновременно
отвечать за разработку сразу нескольких итераций).
Технологический параллелизм означает принципиальную осуществимость
одновременной разработки нескольких итераций. Однако это не означает раз-
решения механического их слияния, поскольку итерации зависят одна от дру-
гой. К примеру, невозможно наращивание еще не построенной системы клас-
сов, нельзя использовать функцию с неизвестными условиями ее корректно-
го выполнения. Говоря о совмещении работ, нужно всегда знать подобные и
другие виды зависимостей. Следует различать:
• область недопустимого совмещения, когда выполнение одной работы
непосредственно зависит от результатов другой работы;

• область возможного совмещения, когда зависимость ослаблена тем, что


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

• область рационального совмещения, когда зависимость работ фактиче-


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

Одновременность выполнения разных итераций можно представить в виде


схем, показанных на рис. 4.11. На рис. 4.11а) приведена расшифровка этапов
итераций. По сравнению с общей моделью (см. рис. 4.10), здесь представлено
210 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

Планирование Анализ Конструиров Программир Тестирова Оценка


итерации (1-2, 7-8) (2-3) ание (3-5) ование (4-7) ние (6-7) (7-8)

Рис. 4.11. Распараллеливание выполнения итераций проекта


4.3. ИТЕРАТИВНЫЕ МОДЕЛИ ЖИЗНЕННОГО ЦИКЛА 211

более мелкое дробление этапов: явно выделены планирование, которое для


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

4.3.4. Моделирование итеративного наращивания возможностей си-


стемы

В предыдущих моделях итеративного жизненного цикла программного


обеспечения не был наглядно выделен важный аспект подхода: постепенное
наращивание возможностей системы по мере развития проекта. Для его отра-
жения можно предложить представление жизненного цикла в виде спирали
212 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

развития, которая показана на рис. 4.127 .


Предоставляемые
возможности …

V
Пл| Ан| Ко | Пр| Те | Оц

IV
Пл| Ан| Ко | Пр| Те | Оц

III
Пл| Ан| Ко | Пр| Те | Оц

II
Пл| Ан| Ко | Пр| Те | Оц

I
Пл| Ан| Ко | Пр| Те | Оц
Время

Рис. 4.12. Спираль развития объектно-ориентированного проекта

На рисунке горизонтальные отрезки с пометками, имеющими тот же смысл,


что и в предыдущей модели, — это итерации. Они помещены в пространство
предоставляемых в зависимости от времени возможностей системы. Линии,
параллельные временной оси, отображают уровни пользовательских возмож-
ностей, реализуемых на итерациях (римскими цифрами справа указаны но-
мера итераций). Стрелки-переходы между итерациями учитывают условия
совмещения работ, о которых шла речь выше. Этой моделью подчеркивает-
ся тот факт объектно-ориентированного развития проектов, что возможно-
сти, предоставляемые очередной итерацией, никогда не отменяют уровня,
достигнутого на предшествующих итерациях.
Постепенное наращивание возможностей системы по мере развития про-
екта часто изображают в виде спирали, раскручивающейся на плоскости от
центра, как это показано на рис. 4.13. В соответствии с этой грубой моде-
В несколько модернизированном виде здесь приводится ставшая классической модель Г.
Буча [16], ставшая основой ООП.
7
4.3. ИТЕРАТИВНЫЕ МОДЕЛИ ЖИЗНЕННОГО ЦИКЛА 213

Оценка Планирование

Реализация Анализ
(программирование и
тестирование)

Конструирование

Рис. 4.13. Модель расширения охвата прикладной области объектно-ориен-


тированной системой

лью развитие проекта описывается как постепенный охват расширяющейся


области плоскости по мере перехода проекта от этапа к этапу и от итерации
к итерации. По существу, данная модель делает акцент на том, что объект-
но-ориентированное развитие (как и любое другое экстенсивное развитие,
ориентированное на переиспользование) приводит к постепенному расши-
рению прикладной области, для которой используются конструируемые ра-
бочие продукты.
Про объектно-ориентированное развитие проектов часто говорят: «Оно
предполагает, что традиционные этапы жизненного цикла разработки про-
граммной системы никогда не кончаются». Модель раскручивающейся спи-
рали наглядно показывает смысл этого тезиса.
В данной модели можно усмотреть еще один аспект конструирования
программных систем — типичную схему развития коллектива разработчи-
ков, который, начиная от первого своего проекта, постепенно пополняет на-
капливаемый багаж переиспользуемых в разных системах компонентов.
В отличие от предыдущих моделей, обе спиралевидные модели никак не
отражают тот факт, что у проекта есть фаза завершения. Как следствие, они
предполагают, что все модификации какой-либо версии программной систе-
мы, которые требуются после ее выпуска, будут относиться к одной из сле-
214 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

дующих версий. На практике очень часто это положение нарушается: прихо-


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

ТРЕБОВАНИЯ К ПРОГРАММНОМУ ИЗДЕЛИЮ


И ЖИЗНЕННЫЙ ЦИКЛ
§ 4.4.

Есть еще один важный мотив моделирования жизненного цикла программ-


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

Поэтому сейчас получили распространение технологии программирова-


ния, основывающиеся на методе итеративного наращивания предоставляе-
мых пользователям средств системы, в частности, на базе объектно-ориен-
тированного проектирования. Как уже было сказано, в таких технологиях
постулируется, что все этапы разработки системы рассредоточиваются по
итерациям, каждая из которых завершается предъявлением продукции, реа-
лизующей не все, а только выделенные для нее требования. Соответственно,
на следующих итерациях этапы анализа, конструирования и т. д. продолжа-
ются, а не повторяют пройденное в стиле исправления ошибок.
Понятно, что проблемы, связанные с определением и анализом требова-
ний, не исчезают и в этом случае, но благодаря специальной организации
труда преодолевать трудности можно с меньшими затратами. Именно это
обстоятельство побуждает к исследованию моделей жизненного цикла, ко-
торые более точно отражают рациональные схемы анализа и оперирования с
4.4. ТРЕБОВАНИЯ К ПРОГРАММНОМУ ИЗДЕЛИЮ И ЖИЗНЕННЫЙ ЦИКЛ 215

требованиями.

4.4.1. Проблемы определения и анализа требований

В наиболее общем виде понятие требований сводится к следующим двум


аспектам, фиксируемым для выполнения конструкторских работ:
• средства программного изделия, в которых нуждается пользователь для
решения своих проблем или достижения определенных целей;

• характеристики программного изделия, которыми должна обладать си-


стема в целом или ее компонент, чтобы удовлетворять соглашениям,
спецификациям, стандартам или другой формально установленной до-
кументации.

Даже это не очень точное определение понятия требования указывает, что


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

1. Требования имеют много источников


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

2. Требования не всегда очевидны


216 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

Смысл утверждения как минимум в том, что инициаторы работ далеко


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

3. Требования не всегда легко выразить словами


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

4. Существует множество различных типов требований и различных уров-


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

5. Требования почти всегда взаимосвязаны и взаимозависимы, и часто


противоречивы
Связи между требованиями обусловлены, в первую очередь, тем, что
пожелания к разработке даются в системе понятий, которая исходит
из предметной области и поведения пользователя, решающего задачи
из этой области. Не следует ожидать, что связи между требованиями
будут хорошо отслежены, что заранее будет сформулирована система
объектов, которые воплощаются в программном изделии. Все, на что
можно рассчитывать, получая сведения о требованиях, — это нефор-
мальное представление о том, кто будет работать с системой и зачем
ему это нужно. Как следствие, в задачу анализа входит выявление взаи-
мосвязей и взаимозависимостей и устранение противоречий (в дости-
жимом, но практически не встречающемся в современном индустри-
альном программировании идеале путем преобразования их в идеи ре-
шений);

6. Требования всегда уникальны


4.4. ТРЕБОВАНИЯ К ПРОГРАММНОМУ ИЗДЕЛИЮ И ЖИЗНЕННЫЙ ЦИКЛ 217

При формулировке требований как регламента разработки всегда долж-


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

7. Набор требований чаще всего является компромиссом


Поскольку в данный момент средства выявления требований и запро-
сов во всех имеющихся технологиях и методиках первопорядковые (они
даже не учитывают опыта перехода к надсистемам и принципиальных
переформулировок, накопленного ТРИЗ, синектикой, школой де Боно
и другими методиками творческого решения задач), они направлены на
нахождение механического компромисса между пожеланиями инициа-
торов работ и других заинтересованных лиц8 и выявление “усреднен-
ных” требований;
Заметим, что в методиках творческого мышления компромисс рассматривается как худ-
ший возможный выход. Противоречивость требований в них является проблемным противо-
8

речием, которое преобразуется в новое системное решение. Но в программировании такому


подходу к задачам мешают по крайней мере три фактора:
• отсутствие методик преобразования задач, аналогичных ТРИЗ или методике де Боно,
и ориентированных на программирование как деятельность;
• ориентация на жесткие и точные решения задач, которые якобы удовлетворяют всех,
а не на действительные решения проблем, как это делается по указанным методикам
• преобладание среди сотрудников программистских фирм лиц с мышлением комбина-
ционного уровня и практическое отстутствие тех, кто анализирует задачу на уровне
метода.
Кроме того, есть и субъективный фактор: система оплаты труда в индустрии программиро-
вания, ориентированная на оплату человеко-часа: программистской фирме в определенных
пределах выгодно повышать количество и долю неквалифицированных человеко-часов, по-
скольку разница в 80–100% в оплате часа эксперта и часа исполнителя не компенсирует фи-
нансовый проигрыш за счет сокращения в разы работы исполнителей в результате хорошего
решения, предложенного экспертом.
218 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

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

9. Требования зависят от времени


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

Список проблем, связанных с требованиями, легко продолжить, но уже и это-


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

4.4.2. Трассировка требований

Независимо от уровня первоначальной проработки требований к проекту,


не стоит рассчитывать, что требования всегда будут оставаться неизменны-
ми. Необходимо быть готовым к тому, что в любой момент развития появятся
новые требования, некоторые старые требования изменятся, другие — отпа-
дут. Но основная сложность управления процессом изменения требований не
4.4. ТРЕБОВАНИЯ К ПРОГРАММНОМУ ИЗДЕЛИЮ И ЖИЗНЕННЫЙ ЦИКЛ 219

в этом, а в том, что изменения одних требований влияет на другие и нужно


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

2. модифицирующее требование, которое изменяет одно или несколько


уже существующих требований;

3. отменяющее требование, принятие которого исключает одно или не-


сколько уже существующих требований.

Разные виды требований анализируются по-разному. Целью анализа явля-


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

1. требования должны быть заданы в виде, допускающем однозначное


представление в моделях уровня анализа и конструирования, и способ
такого представления должен быть унифицирован для всего проекта;

2. в проекте должны инструментально и организационно поддерживаться


связи как между требованиями, так и между требованиями и другими
компонентами рабочих продуктов.

Разберем оба этих условия.


Представление требований и пожеланий, исходящие от инициаторов ра-
бот, обычно ни в коей мере не способствует соблюдению первого условия.
Следовательно, они должны быть трансформированы, т. е. преобразованы
к виду, приспособленному для анализа. Прохождение исходного требования
220 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

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


му, сопровождающееся соответствующим анализом, называется трассиров-
кой требования. Основное назначение трассировки в том, чтобы в любой мо-
мент развития проекта сохранялась целостность и непротиворечивость кон-
струируемой системы, реализующей принятые требования9 .
Перейдем ко второму условию. В работах с меняющимися требованиями
большое место занимает отслеживание связей проекта, благодаря которому
планируется деятельность, необходимая как для непосредственной реализа-
ции требований, так и для распространения изменений, связанных с новыми
требованиями, по проекту. Для такого отслеживания служат упоминавшиеся
выше модели уровня проектирования, в которых выделяется подкласс моде-
лей уровня анализа. Важнейшим технологическим инструментом согласова-
ния понятий, используемых в программной разработке, является глоссарий
проекта. Глоссарий отражает текущее понимание проекта в целом и отдель-
ных используемых в нем понятий. Глоссарий может пополняться на любой
стадии трассировки требований, когда появляются новые понятия, смысло-
вую трактовку которых нужно зафиксировать.Важно подчеркнуть, что когда
разработчики игнорируют деятельность по ведению глоссария, система по-
нятий проекта все равно складывается, но стихийность этого процесса при-
водит к дополнительным издержкам коммуникаций работников.
Трассировка — это основной инструмент анализа, проводимого в рамках
управления изменениями требований. В первую очередь трассировке под-
вергаются требования, предъявленные первоначально, т. е. до того, как про-
ект начал развиваться. Но было бы неправильно ограничиваться только ими,
поскольку их связи с другими требованиями как явные, так и обнаруживае-
мые в ходе анализа, также требуют соответствующего анализа и других ра-
бот, связанных с реализацией требований.
В результате трансформаций строятся представления требований, вид ко-
торых приспособлен для выяснения целесообразности реализации требова-
ний. Если на некотором уровне трансформаций установлено, что данное тре-
Следует обратить внимание на то, что целостность и непротиворечивость — не характе-
ристика принимаемых требований, а качества, которыми должна обладать конструируе-
9

мая система. При построении системы, предназначенной для практического применения,


всегда решаются противоречия между требованиями. Противоречия предъявляемых требо-
ваний есть следствие различий интересов инициаторов работ, именно они обычно становят-
ся стимулом для поиска новых решений, для перехода от одной версии системы к другой,
т. е. являются источником развития системы. Таким образом, общая картина в программиро-
вании точно такая же, как и в других областях реальной творческой деятельности человека.
4.4. ТРЕБОВАНИЯ К ПРОГРАММНОМУ ИЗДЕЛИЮ И ЖИЗНЕННЫЙ ЦИКЛ 221

бование отвергается, то дальнейшие преобразования его не производятся. В


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

2) Унифицированные представления — исходное представление требования


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

3) Типизированное представление — каждое из элементарных составляю-


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

4) Модельные представления уровня анализа — образы элементарных тре-


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

Если требование принимается на уровне анализа, то трассировка продолжа-


ется на следующих уровнях, и можно говорить о продолжении последова-
тельности трансформаций вплоть до реализации требования:
5) Модельные представления уровня конструирования — образы элементар-
ных требований в диаграммах классов, состояний и других компонен-
тах архитектуры системы. На этом уровне требования трансформируют-
222 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

ся или отклоняются в зависимости от их соответствия уже разработанной


части проекта.

5) Программные представления — программные рабочие продукты и их фраг-


менты, которые рассматриваются в качестве образов требований, предста-
вленных очередной версией системы.

5) Документные представления — фрагменты документов, сопровождаю-


щих программный код и предназначенных для поддержки деятельности
пользователей.

Схема на рис. 4.14 иллюстрирует приведенную последовательность транс-


формаций. Первые три представления требований изображены в виде сово-
купностей стрелок, которые при переходе от одного представления к другому
становятся все более упорядоченными.
Иерархия типов требований представлена на рисунке следующим обра-
зом. Верхний уровень — это абстрактный тип, свойства которого присущи
требованиям всех типов (они сводятся к стандартизованному набору опера-
ций объединения, пересечения атрибутов, сравнения значений атрибутов и
др.). Можно сказать, что Табстр задает регламент, которого следует придер-
живаться при оперировании с требованиями. Следующий уровень содержит
четыре обязательных типа: Тэкон , Тфунк , Тинт и Тэфф , которые объединяют тре-
бования экономического характера (пределы стоимости, рентабельность и
пр.), функциональные требования, требования к интерфейсу и эффективно-
сти. Многоточием обозначены типы, которые добавляются из-за специфики
проекта.

Тa (a1, . . . , ana ), Тb (b1, . . . , bnb ), Тc (c1, . . . , cnc ), . . . , Тz (z1, . . . , znz ) —

это конкретные типы, к которым приписываются элементарные составляю-


щие требований (в скобках указаны их атрибуты).
Модельные представления уровней анализа и конструирования изобра-
жены в виде условных схем различных видов. Программные и документные
представления — это текстовые файлы, пиктограммы которых показаны на
рисунке.
Приведенная схема наглядно показывает то, что вынуждены делать разра-
ботчики для преодоления трудностей управления требованиями. Она может
рассматриваться в качестве проекции жизненного цикла на задачи анализа
4.4. ТРЕБОВАНИЯ К ПРОГРАММНОМУ ИЗДЕЛИЮ И ЖИЗНЕННЫЙ ЦИКЛ 223

1.Исходное представление требований

2. Унифицированные представления
требований

Табстр

Тэкон Тфунк Тинт Тэфф …


3. Типизированные
представления требований

Глоссарий проекта
4. Модельные представления
уровня анализа

5. Модельные представления
уровня конструирования

6. Программные представления

7. Документные представления

Рис. 4.14. Схема трансформации требований


224 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

требований. Каждое требование, поступающее для анализа, проходит впол-


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

4.4.3. Учет трассировки требований в модели жизненного цикла

При построении модели жизненного цикла следует указать этапы, когда


производятся шаги, связанные с трассировкой. По этой причине следует раз-
личать два варианта работы с требованиями в объектно-ориентированном
проекте:
1. Требование или группа требований обрабатываются до начала работ
над итерацией;

2. Требование или группа требований поступают, когда работы итерации


начались.

Первый вариант полностью укладывается в схему модифицированной моде-


ли фазы-функции (см. рис. 4.9, 4.10). Если требование (группа требований)
принимается для данной итерации и используется при разработке сценария,
который будет реализовываться (контрольные точки 2, 8), то указанные на
схеме трассировки работы включаются в аналитическую и конструкторскую
деятельность. В противном случае оно либо откладывается до последующих
итераций, либо отклоняется.
Второй вариант прерывает последовательный процесс выполнения ите-
рации — необходима немедленная реакция на поступающие требования, по-
сле которой (а во многих случаях и параллельно с которой) прерванный про-
цесс выполнения итерации возобновляется. По существу, выполняется мини-
цикл обработки требований, который нужно изобразить в качестве допол-
нительного элемента модели, описывающей итеративное развитие проекта
с учетом трассировки. При этом в модели, как и в первом варианте, следует
отразить возможные результаты анализа требования:

1. требование отклоняется — работа с требованием прекращается;

2. требование принимается к реализации на текущей итерации;

3. реализация требования откладывается до следующих итераций.


4.4. ТРЕБОВАНИЯ К ПРОГРАММНОМУ ИЗДЕЛИЮ И ЖИЗНЕННЫЙ ЦИКЛ 225

Начальная Итеративное зацикливание Завершение проекта


фаза
проекта

Требование реализуется на
более поздней итерации
Пополнение базового
окружения проекта
Окончание работ

Контрольные ← Использование
точки ↔
(события): 0 1 2 3 4 5 6 7 8 9 10 11 12
Анализ нового требования Требование реализуется на текущей итерации
Требование
отклоняется ← (a) Требование поступило, начало мини-цикла

← (b) Решение о требовании принято

Рис. 4.15. Фазовое измерение модели жизненного цикла при объектно-ориен-


тированном развитии проекта, дополненное обработкой требования в мини-
цикле

На рис. 4.15 показано фазовое измерение модифицированной матрицы Ган-


тера (см. рис. 4.9, 4.10), дополненное мини-циклом обработки одного тре-
бования или группы требований, обрабатываемых совместно. Контрольные
точки (события) в данной модели те же, что и в прежней матрице фазы —
функции. При построении модели используется прием, который ранее (при
учете итеративности в модели — см. § 4.3.2) был назван расщеплением ли-
нии жизненного цикла. Следует обратить внимание на прерывистую часть
линии, ведущей от точки принятия решения к линии итеративного зацикли-
вания. Она отражает, что для анализируемого требования, реализация кото-
рого отложена до одной из последующих итераций, работы этапа програм-
мирования не проводятся. Возобновление непрерывности линии указывает,
что на этапе оценки для данного требования начинаются работы по обосно-
ванию включения его в планы реализации одной из будущих итераций.
Понятно, что в этой модели отобразить поток требований, поступающих
при развитии проекта, невозможно (по этой причине на рисунке контрольные
точки (a) и (b) выделены пунктиром). Постулируется, что все они обрабаты-
ваются в четыре этапа:

• поступление требования или группы требований (контрольная точка


226 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

(a), которая может появиться в любой момент этапов конструирования,


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

4.4.4. Особенности первой итерации

Модель жизненного цикла с мини-циклами обработки требований аде-


кватно описывает процесс поставленной разработки проекта программного
обеспечения в его стационарный период. Однако она не учитывает тот факт,
что первая итерация всегда является особой. При ее выполнении заклады-
ваются основы сопутствующих проекту системы типов требований, глосса-
рия и других составляющих поддержки процесса трансформации требова-
ний, которые в стационарный период используются.
Первую итерацию обычно характеризует следующее:
1. Разработчики еще не достигли достаточно глубокого понимания про-
блем предметной области, ее приоритетов и критериев;
2. Круг инициаторов работ, а значит, потенциальных консультантов сфор-
мировался далеко не окончательно. Следовательно, есть опасность на-
чать делать не ту систему;
3. Мало информации о том, достаточно ли полон набор требований для
объективного принятия проектных решений. Приходится работать на
уровне гипотез (важное следствие предыдущих тезисов);
4. Еще не сформированы базовые элементы декомпозиции системы, ко-
торые должны стать точками последующего итеративного роста. Они
являются первыми, а значит, пробными для реализации компонентами;
5. Если команда разработчиков формируется для данного проекта, то рас-
становка кадров может быть далеко не оптимальной;
6. Часто разработчики в начале проекта не вполне владеют методами, ин-
струментами и т. п., как следствие, работа на первой итерации имеет
учебный аспект.
4.4. ТРЕБОВАНИЯ К ПРОГРАММНОМУ ИЗДЕЛИЮ И ЖИЗНЕННЫЙ ЦИКЛ 227

Можно выделить и другие особенности первой итерации, которые обусло-


влены тем, что она задает направление развития проекта на все время бу-
дущей жизни программы. Неудачен выбор направления — осуществимость
продуктивного итеративного наращивания возможностей программной си-
стемы сомнительна. Удачный выбор — это минимизация затрат на последу-
ющие переделки, реальная возможность использования принципа итератив-
ного наращивания, облегчение решения задачи отслеживания связей и др.
Для первой итерации с ее ближайшей проектной задачей роль этапов ана-
лиза и конструирования очень высока. Высока и цена ошибочных решений.
Осознание этого приводит к разработке специальных методов и подходов, ко-
торые целесообразно применять на первой итерации, а точнее, когда велика
степень неопределенности выбора. Эти методы не только не исключают, но и
предполагают переделку проектных решений, переписывание программного
кода и т. д., т. е. отчасти нарушают основные каноны итеративного проекти-
рования.
Отклонением от канонов на первой итерации следует признать и частич-
ный возврат к традиционному принципу проектирования, постулированному
для последовательно развиваемых программных проектов: не приступать к
программированию, пока все требования к системе не будут переработаны в
ее спецификации. Разница лишь в трактовке слов «все требования к систе-
ме». Для первой итерации итеративного проектирования эти слова означа-
ют предварительное накопление достаточного базового набора требований,
который позволяет обеспечить надежную основу дальнейшего итеративного
наращивания возможностей. Именно этот набор определяет первую ближай-
шую задачу, решаемую в начале развития проекта, с точки зрения архитек-
туры системы в целом.
Большинство требований на уровне анализа выражается в виде сценари-
ев, которые надо реализовывать на данной итерации. Это в полной мере от-
носится и к первой итерации. Но здесь исходный комплект сценариев играет
две дополнительные роли:
• он рассматривается как один из способов изучения прикладной обла-
сти. Разработчики согласуют реализуемые сценарии с инициаторами
работ и, тем самым, уточняют свое понимание задач проекта, назначе-
ние системы;
• являясь аналитическим выражением базового набора требований, ис-
ходный комплект должен представлять этот набор репрезентативно, т. е.
так, чтобы обеспечивать надежное развитие проекта.
228 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

Очень важна еще одна особенность первой итерации: к оценке ее результа-


тов нельзя подходить с позиций утилитарной полезности получаемых про-
граммных продуктов. Как следствие, смещаются критерии качества: на пер-
вый план выступают задачи демонстрации осуществимости проекта, продук-
тивности выбранного подхода и полноты базового набора требований с точ-
ки зрения итеративного наращивания. Иными словами, по указанным выше
причинам трудно ожидать, что первая итерация приведет к реально работо-
способному программному изделию, но правомерно требовать от нее дока-
зательства того, что данная команда, используя данный метод в конкретных
условиях, в дальнейшем приведет проект к успешным результатам. Это мне-
ние должно сложиться у заказчиков, руководства и, что не менее важно, у
работников в коллективе исполнителей.
Большую часть особенностей первой итерации выразить в модели жиз-
ненного цикла не представляется возможным. Они влияют на выбор мето-
дов ведения проектов на первой итерации. В свою очередь, эти методы мож-
но проецировать на модели жизненного цикла. В качестве примера одной из
таких моделей ниже приводится схема, описывающая организацию началь-
ных работ, которая принята в Центре объектно-ориентированных технологий
фирмы IBM. Данный метод получил название «Сначала в глубину», что соот-
ветствует выбранной стратегии.
Суть метода состоит в том, что разработка первой итерации проводится
мини-циклами реализации выбираемых сценариев. Используется два крите-
рия отбора сценариев для мини-циклов:
• реализацию можно осуществить быстро и
• получаемые результаты можно продемонстрировать наглядно и убеди-
тельно.
Полнота базового набора требований в методе «Сначала в глубину» дости-
гается за счет анализа последовательно выполняемых мини-циклов для вы-
деленных сценариев. Если в какой-то момент обнаруживается, что для пол-
ноты необходимо расширение исходного комплекта сценариев, то комплект
пополняется.
На рис. 4.16 представлена еще одна модификация гантеровской модели
жизненного цикла, отражающая развитие работ на первой итерации методом
«Сначала в глубину». Модель модифицирована в следующих отношениях:
1. По сравнению со стационарным периодом время, отводимое для ана-
лиза и конструирования, существенно увеличивается за счет этапа про-
4.4. ТРЕБОВАНИЯ К ПРОГРАММНОМУ ИЗДЕЛИЮ И ЖИЗНЕННЫЙ ЦИКЛ 229

Начальная Продолжение
фаза проекта
проекта ← Мини-циклы разработки сценариев →
← Автономная← Интеграция →
работа →

Пополнение базового
окружения проекта
ние →
← Программирование →
Оценка

Контрольные
точки
(события): 0 1 2 3 4 5 6 7
Выбор сценариев для Начата ←5’’ Сценарии ←8 Требования к следующей
реализации сделан 2’→ интеграция реализованы, итерации приняты
сценариев 5’→ базовый набор
Совместная работа
требований
с выбранными
определен
сценариями
Модели сценариев построены 3’→ Демонстрационные испытания
завершены 9 →

Рис. 4.16. Фазовое измерение модели жизненного цикла при объектно-


ориентированном развитии проекта методом «Сначала в глубину»
230 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

граммирования (конкретные временные соотношения зависят от осо-


бенностей выполняемого проекта и от условий его выполнения);

2. На общей части этапов анализа и конструирования (контрольные точки


2, 3) выделяется явно работа по определению и утверждению базово-
го набора требований и сценариев для реализации (группа сценариев
обозначена овалом с внутренними незакрашенными кружками). Таким
образом, появляется контрольная точка 20 ;

3. Этапы анализа и конструирования для выбранных сценариев выполня-


ются совместно (жирная стрелка между овалами). В результате стро-
ятся модели сценариев (контрольная точка 30 , модели обозначены за-
крашенными кружками), которые рассматриваются в качестве исход-
ных данных для спецификации реализуемых компонентов (контроль-
ная точка 5);

4. Для каждого из сценариев образуется мини-цикл его разработки. Раз-


работка мини-циклов включает в себя перекрывающиеся этапы авто-
номной работы и интеграции сценариев (контрольные точки 30 , 500 и 50 ,
7);

5. Фиксируется событие готовности результатов всех мини-циклов (кон-


трольная точка 500 ), которое означает завершение формирования базо-
вого набора требований;

6. Интеграция сценариев предполагает ревизию (возможно и переписы-


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

7. Вместо подготовки к распространению системы для прикладного ис-


пользования проводятся демонстрационные испытания, после завер-
шения которых (контрольная точка 9) осуществляется переход к следу-
ющей итерации;

8. Прерывания процесса выполнения первой итерации для обработки до-


полнительных требований не допускаются. По существу это означает,
что остается единственный вариант работы с такими требованиями: от-
кладывание их анализа и реализации до последующих итераций.
4.4. ТРЕБОВАНИЯ К ПРОГРАММНОМУ ИЗДЕЛИЮ И ЖИЗНЕННЫЙ ЦИКЛ 231

Организация работ методом «Сначала в глубину» не предполагает предва-


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

4.4.5. Фаза завершения

Завершение проекта редко описывают в моделях жизненного цикла струк-


турно. Обычно этот период только обозначается, а его работы лишь класси-
фицируются. Возможно, что разнообразие вариантов организации эксплуа-
тационной поддержки препятствует систематическому их изучению. Не сти-
мулирует изучение этих работ также неявное и не соответствующее действи-
тельности положение о том, что требования к системе, возникающие на фазе
завершения, относятся уже к другому проекту. В то же время промышленная
разработка программных систем всегда нуждается в организации как можно
более скорых откликов на пользовательские запросы: рекламации, пожела-
ния и требования развития.
232 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

В контексте изучения жизненного цикла с точки зрения обработки требо-


ваний задачу моделирования фазы завершения можно описать в стиле, кото-
рый был использован при учете трассировки требований. Цели этого моде-
лирования следующие.
a) Во-первых, это систематизация действий, которые необходимо выполнять
в качестве реакции на пользовательские запросы.
b) Во-вторых, это явное разграничение реакций, относящихся:
i) к текущей версии,
ii) к одной из следующих версий,
iii) к другому проекту.
Для развития итеративных проектов такое разграничение очень важно из-за
необходимости осуществлять одновременно и поддержку разных версий, и
наращивание возможностей системы10 .
Основой моделирования фазы завершения проекта (итерации) является
обратная связь с пользователями системы. Это, для коммерческого софта,
обычные сообщения о ходе эксплуатации: мнения, рекламации, пожелания,
нарекания, претензии и т. п. Для открытого софта необходимо также доба-
вить программные наработки и программные усовершенствования пользо-
вателей. Все подобные сообщения могут быть классифицированы, ранжиро-
ваны по степени важности для развиваемого (на данной фазе — обслужи-
ваемого) проекта. Но это обстоятельство в модели не учитывается: считает-
ся, что из пользовательских сообщений извлекаются требования к проекту в
целом или к его итерации. Сообщения, а значит, и требования могут посту-
пать в ходе эксплуатации в течение всего периода использования системы
или ее версии. Требования нуждаются в трассировке, о которой шла речь вы-
ше. По этой причине в качестве отправного момента моделирования фазы
завершения проекта (итерации) служит модель жизненного цикла, учитыва-
ющая трассировку.
В частности, в связи с необходимостью сочетания этих двух, часто концептуально проти-
воречивых, требований, в UNIX-подобных системах появилась концепция совместимости с
10

точностью до ошибок и уникальная система компоновки служебных программ разных вер-


сий для поддержки данного конкретного продукта. Этому способствует модульная система
организации программного обеспечения в UNIX и LINUX, контрастирующая, например, с
централизованной системой Windows, которая начинает саморазрушаться при одновремен-
ном использовании программ, требующих разных версий стандартных утилит и системных
библиотек.
4.4. ТРЕБОВАНИЯ К ПРОГРАММНОМУ ИЗДЕЛИЮ И ЖИЗНЕННЫЙ ЦИКЛ 233

В представленной на рис. 4.17 модели описываются операционные марш-


Начальная Итеративное зацикливание Завершение проекта (итерации)
фаза Использование
проекта
Период сопровождения
Решение о Окончание работ
Первичный реализации:
в другом проекте
Пополнение анализ в следующих
базового окружения
версиях
проекта
в текущей версии

Немедленная
Контрольные реализация
точки
(события): 0 1 2 3 4 5 6 7 8 9 10 Требование 11 12
Накопление, ←Реализация требований ←—— →
отклоняется
систематизация и →
Проверка реализации ← (b) Решение о требовании
группировка требований
Распростанение изменения начато→ принято
Решение о реализации
Извлечение требования
требований принято (c) → Сообщение поступило (a) →

Рис. 4.17.
руты, возникающие в связи с обработкой одного требования в ходе эксплуа-
тации программной системы. Для упрощения предполагается, что операци-
онные маршруты не зависят от накапливаемой информации. Это заведомо
огрубляющее предположение в том смысле, что принятие решений о требо-
вании фактически делается не только на основании первичных установок,
но и с использованием знаний о системе, пользователях, о текущем предста-
влении о приоритетах и предпочтениях. Можно считать, что подобные све-
дения используются в точках разветвления операционных маршрутов, но то,
как осуществляется такое использование, моделью не описывается.
Аналогично организации мини-цикла для трассировки требований, в мо-
дели периода эксплуатации началом обработки является поступление сооб-
щения о ходе эксплуатации системы, которое можно трактовать как содер-
жащее требования (контрольная точка (a)). Это событие может возникать в
любой момент периода сопровождения, т. е. обсуждаемая модель является
естественным продолжением модели с обработкой требования в мини-цикле
(см. § 4.4.3). Так как отобразить весь поток сообщений невозможно, рассма-
тривается операционный маршрут только одного сообщения, при этом по-
стулируется, что все сообщения обрабатываются подобно:
• поступление сообщения (контрольная точка (a));
234 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

• первичный анализ, в ходе которого из сообщения извлекаются требо-


вания. Этот период должен быть максимально кратким, ибо пользо-
вателю необходимо знать о реакции разработчиков на его сообщение.
Результатом первичного анализа является принятие решения о требо-
вании (контрольная точка (b), в которой происходит расщепление ли-
нии жизненного цикла). Возможны следующие не взаимоисключаю-
щие друг друга варианты такого решения:
– немедленная реакция — действия, направленные на быстрое устра-
нение замечания (либо принятие предложения, в случае открыто-
го софта), если это возможно, либо указание пользователю сро-
ков, когда и как будут учтены поступившие претензии или пред-
ложения, либо указание путей (возможно, временных) преодоле-
ния трудностей. Немедленная реакция выполняется всегда, в том
числе совместно с другими решениями;
– требование отклоняется — действия, указывающие пользовате-
лю причины отклонения требований и пути преодоления трудно-
стей;
– реализация требования или предложения в текущей версии — если
претензии обоснованы, а устранение замечаний, ошибок и т. п.
возможно в рамках обслуживаемой версии, то организуется ми-
ни-цикл обработки сообщения в итерации;
– реализация требования или предложения в одной из следующих
версий — если устранение замечаний в рамках обслуживаемой
версии невозможно или нецелесообразно, то сообщение переда-
ется для исполнения на одной из следующих итераций проекта;
– реализация требования или предложения в другом проекте — если
выясняется, что в данном проекте выполнить требование или учесть
предложение невозможно или нецелесообразно, то, быть может,
оно станет одним из аргументов в пользу организации нового про-
екта.
• мини-цикл обработки сообщения начинается с анализа, цели которого
обычны для итеративного развития проектов. В частности, определяет-
ся осуществимость реализации на данной итерации или целесообраз-
ность переноса ее на другую итерацию, образуется группа требований,
которые должны быть реализованы совместно. Выработка этих реше-
ний о стратегии реализации требований приурочивается к контрольной
4.5. ИТОГИ И ПЕРСПЕКТИВЫ 235

точке (c). Особенностью анализа в данном случае является то, что он


проводится, как возобновленный процесс, т. к. основные работы ите-
рации уже выполнены;

• реализация отобранных требований или предложений на данной ите-


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

• распространение изменений (контрольная точка 10) — деятельность,


направленная на то, чтобы сделанные исправления стали доступны для
всех пользователей обслуживаемой версии. При массовом использова-
нии программного изделия эта работа может потребовать значитель-
ных ресурсов.

Фаза завершения итерации включает этап окончания работ, содержание кото-


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

§ 4.5. ИТОГИ И ПЕРСПЕКТИВЫ

Обсуждение жизненного цикла представлено в настоящей главе как по-


следовательное развитие и уточнение понятий под влиянием потребностей
развивающихся методов и технологий программирования. Однако из этого
не должно складываться впечатление, что столь же прямолинейна историче-
ская линия развития представления о том, какие этапы и как проходятся в
течение жизни программы. Напротив, начиная с семидесятых годов XX сто-
летия, когда сформировалась потребность в изучении жизненных циклов, и
236 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

до наших дней варианты их моделей все множатся и множатся. Причина то-


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

• разработка программного обеспечения для встроенных систем,

• разработка программного обеспечения по уже существующему прото-


типу,

• разработка, включающая быстрое предварительное построение прото-


типа,

• построение сетевых комплексов,

• решение задач переиспользования программного обеспечения,

• решение задачи развития программного обеспечения сообществом спе-


циалистов без формальной организационной структуры,

• учет требований повышенной надежности разрабатываемых программ-


но-аппаратных систем,

• разработка адаптивных систем,

• разработка систем с настраиваемым интерфейсом,

• конструирование программных инструментов,

• использование технологических сред для разработки программных си-


стем (различные варианты моделей).

Этот список может быть продолжен. Так, обнаруживаются и отражаются в


моделях особенности жизненного цикла долго и быстро живущих программ,
длительных, средних и коротких проектов. Как показывает анализ моделей,
предлагаемых для разных ситуаций, они лишь уточняют и дополняют общие
положения, отслеживанию которых было уделено внимание в предыдущих
разделах.
4.5. ИТОГИ И ПЕРСПЕКТИВЫ 237

Среди всех мотивов моделирования жизненного цикла особое место за-


нимает систематизация работ, выполняемых при разработке программного
обеспечения. Систематизация — первый шаг на пути автоматизации любо-
го производства, и в частности, производства программ. Следующие шаги -
определение технологических маршрутов деятельности работников данного
производства, выявление узких мест, доступных для автоматизации, и раз-
работка инструментов для них. Далее процесс развивается вширь и вглубь:
охватываются автоматизацией другие части технологических маршрутов, со-
вершенствуются ранее построенные инструменты, формируются методы их
эффективного применения. Последнее означает формирование новых техно-
логий, и, как следствие, появляется потребность автоматизации новых видов
деятельности, обусловленных данными технологиями. Наконец, наступает
момент, когда совокупность потребностей в автоматизации, связанных, хотя
и не обязательно напрямую, с первоначальной систематизацией, формирует
качественно иную потребность в комплексной автоматизации. Это время по-
явления стандартов и стандартных решений, интеграции сложившихся тех-
нологий и доработка того, что не вписывается в интегральную схему.
В предыдущем абзаце представлен эскиз формирования произвольного
автоматизированного технологичного производства. Не является исключе-
нием и процесс технологизации производства программного обеспечения.
Есть, конечно, специфика, но пока еще не ясно, столь ли она значительна,
чтобы считать данное производство чем-то исключительным11 .
Первая стадия автоматизации программирования связана с поддержкой
этапа программирования. Здесь проявляется и специфика: систематизация
работ по производству программного обеспечения осуществлялась после осо-
знания того, что поддержка кодирования, хотя и способствует росту произ-
водительности труда, но не является достаточным для промышленного кон-
струирования программ. Появление на этой стадии систем программирова-
ния и всевозможных средств помощи в наборе текстов предшествовало пери-
оду, когда начинали внедряться разного рода отладочные средства. Именно
в это время (т. е. лишь к концу шестидесятых годов) в ответ на потребность
в разработке больших и сложных программ было осознано понятие жизнен-
ного цикла.
Сразу же обнаружилось узкое место программирования как производ-
Хотя уже отмечены некотрые принципиальные отличия программистских задач от стан-
дартных инженерных задач: в частности, для программирования отказывают отработанные
11

на инженерных задачах методики решения проблем, такие, как ТРИЗ.


238 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

ства: неразвитость методологий этапа конструирования, с одной стороны, а с


другой — невозможность сведения оценочных работ к тестированию. По су-
ществу это две стороны одной медали: нечеткость постановок задач на про-
граммирование влечет за собой большую часть трудностей этапа проверки.
В результате сформулированной потребности в строгих спецификациях про-
екта появилось осознание того, что этап конструирования может быть тех-
нологически регламентирован. И удачные организационные технологии ста-
ли появляться. Чаще это специализированные технологии, предназначенные
для разработки программ особого рода, но иногда и общие технологии, не-
которые из них впоследствии стали автоматизированными (в качестве кон-
кретного примера из этого ряда уместно указать на IDEF-технологию). Позд-
нее стали формироваться регламентирующие методики работы с требовани-
ями на этапе анализа. И хотя формализованных технологических процедур,
допускающих полные автоматические проверки, в аналитической части до-
биться так и не удалось (что свидетельствует об объективной трудности дан-
ной области), прогресс заметен: сегодня можно говорить о поддержке накоп-
ления первичных требований и их систематизации, об отслеживании связей
между требованиями и их реализациями в проекте и др.
Понятно, что описанный процесс не столь прям, как он представлен вы-
ше. В огромной мере на него влияли языкотворчество и тщетные надежды на
то, что появление очередного “самого хорошего” языка или “самой прогрес-
сивной” техенологии приведет к “решению всех проблем”. Для становления
технологий производственного программирования наиболее заметными ока-
зались методология структурного программирования, объектно-ориентиро-
ванное программирование и GNU технология движения Open Source. Пер-
вая из них позволила опять осознать ограниченность способностей человека,
на этот раз в связи с разработкой больших программ. Вторая — дала тол-
чок к разработке методов декомпозиции, приспособленных для преодоления
сложности. Третья — показала, как можно организовать технологию рабо-
ты без привычной для производства централизации (это, видимо, громадный
общепроизводственный и общекультурный вклад современной информати-
ки в развитие общества, пока еще недооцениваемый в других отраслях про-
изводства). Итеративная разработка привела к необходимости модернизации
основополагающих принципов проектирования программ и, в частности, к
новому понятию жизненного цикла.
Но, несмотря на это и другие влияния, стадия комплексной автомати-
зации технологий программирования стала возможной только при соответ-
ствующем уровне развития техники, который позволил эффективно приме-
4.5. ИТОГИ И ПЕРСПЕКТИВЫ 239

нять выразительные графические возможности при выполнении технологи-


ческих процедур конструирования программного обеспечения и поддержи-
вать надежные и хорошо структурированные депозитарии понятий. Немало-
важным обстоятельством, позволившим перейти к комплексной автоматиза-
ции, стало осознание того, что нельзя говорить реально о промышленном
программировании без поддержки технологических функций на всех этапах
жизни программ. Во 80-х годах XX века появился термин CASE-технология
(Computer Aided Software Engineering — компьютерная поддержка разработ-
ки программ), которым стали обозначать использование систем, обладающих
комплексными автоматизированными средствами поддержки разработки и
сопровождения программ.
Замечено, что, впрочем, вполне объяснимо, что наиболее удачным ока-
залось использование CASE-систем в тех специальных областях, в которых
уже были успехи и опыт технологичной практической работы, пусть даже
лишь на организационном уровне, а также в тех случаях, когда специаль-
ная область уже была обеспечена надежной теоретической базой. В первую
очередь здесь следует упомянуть о CASE-системах разработки баз данных
в развитых реляционных СУБД (к примеру, Oracle Designer 2000 в системе
Oracle). Успехи CASE-систем общего назначения скромнее, скорее всего по
причине отсутствия универсальных методов, пригодных для развития лю-
бых проектов. Поскольку представление о модели жизненного цикла всегда
является основой технологии, это еще раз подтверждает правомерность по-
строения разнообразных моделей.
Сегодня универсальные CASE-системы строятся из расчета не всеобщего
назначения, а в рамках применения развитых, но все-таки специальных ме-
тодологий. Несомненный прогресс в данной сфере достигнут для проекти-
рования, ориентированного на моделирование на этапах анализа и констру-
ирования. В рамках объектно-ориентированного подхода разработан унифи-
цированный язык моделирования UML (Unified Modeling Language), который
претендует на роль основы проектирования в методологии итеративного на-
ращивания возможностей программных систем (и является таковой для ны-
нешнего ООП). На базе этого языка построен ряд CASE-систем общего на-
значения с развитыми средствами. Наиболее используемой из них является
Rational Rose фирмы Rational Software, предложившей на рынок не только
инструментарий для использования UML, но и комплексную методику произ-
водства систем — Rational Unified Process (RUP). Данная методика, конечно
же, претендует на охват всех аспектов технологий современного программи-
рования, но Вы уже знаете, как необходимо относиться к таким претензи-
240 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

ям. Уместно отметить, что в качестве CASE-системы Rational Rose облада-


ет множеством средств, полезных для поддержки связи первых этапов про-
ектирования с этапом составления программ (кодирования), а также с эта-
пом оценки. В частности, проверяется, что моделирование на разных эта-
пах согласовано, что модельные соглашения, определения классов, других
элементов моделей и их взаимосвязи непротиворечивы. Уровень автомати-
ческого анализа высок настолько, что в принципе позволяет строить по моде-
лям так называемые реализации по умолчанию. Это заготовки программного
кода, включающие в себя описания классов и их методов в том виде, который
можно извлечь из моделей. Программист дополняет заготовки фрагментами,
детализирующими конкретную реализацию.
Построение реализации по умолчанию — не нововведение Rational Rose.
До этой системы оно активно применялось и в рамках систем визуально-
го программирования, и еще раньше в специализированных CASE-системах,
используемых, например, в развитых СУБД. Последнее примечательно: имен-
но для СУБД удалось связать реализацию по умолчанию с графическими мо-
делями информационных систем (ER-диаграммы). В Rational Rose и других
UML CASE-системах поддерживается построение реализаций по умолчанию
по моделям общего, а не специального назначения.
Реализация по умолчанию является лишь одним из приемов поддержки
связей между этапами жизненного цикла разработки программного обеспе-
чения с использованием Rational Rose. Именно идея комплексной поддержки
связанности рабочих продуктов разных этапов, а не отдельные приемы, кото-
рые появлялись и ранее, — главное для данной CASE-системы. Программное
воплощение этой идеи, пусть даже с существенными недоработками, следует
отнести к явным достоинствам данного инструментария.
Проанализируем теперь отрицательные следствия рекламных претензий
RUP на охват «всех рациональных технологий». Делается попытка механи-
ческого объединения средств, инструментов и методов довольно многих «ра-
циональных» подходов, но это приводит к эклектике, а для пользователя — к
нефиксированной технологии, что по сути своей означает одно — отсутствие
технологии. Применяя данную систему, пользователь обязан выстроить свои
регламенты: когда, как и в каком качестве будут применяться те или иные
средства, методы, инструменты. Если эти регламенты окажутся технологич-
ными, то можно рассчитывать на поддержку Rational Rose, но, к сожалению,
не в части проверки принимаемых для формируемой технологии соглаше-
ний. Затуманивается принципиальное ограничение, отделяющее области, где
целесообразно применение RUP, от тех, где оно противопоказано: RUP ори-
4.5. ИТОГИ И ПЕРСПЕКТИВЫ 241

ентировано прежде всего на задачи, где заказчику важнее форма, чем содер-
жание, и где успех проекта зависит в первую очередь от упаковки, и лишь во
вторую — от качества начинки.
Вопросы, которые затрагивались в настоящей главе, освещены в многочи-
сленных публикациях, посвященных технологии программирования. Боль-
шинство из них соответствуют скорее текущей конъюнктуре, чем сути про-
блемы. В качестве приятного исключения, как классическую работу, выдер-
жавшую испытание времением, можно указать на книгу Ф. Брукса “The Mythical
Man-Month. Essay on Software Engineering” (русский перевод первого изда-
ния, вышедшего в 1975г., см. в [14], юбилейного издания 1995г. — в [15]). Эта
монография по праву считается одной из лучших книг не только по данной
тематике, но и по программированию вообще. Сопоставление двух ее изда-
ний явно показывает, что проблемы, которые приходится решать при упра-
влении программными проектами, почти не изменились со времени перфо-
карт. Меняется только техническая поддержка.
Из ранних работ, не потерявших своей актуальности, прежде всего сле-
дует обратить внимание на монографию Гантера [23], содержащую, кроме
представленной выше модели, много полезной информации для организа-
ции работ над программными проектами. Систематизированные сведения о
понятии жизненного цикла и его применении в промышленном программи-
ровании можно найти в книге [52], которая к тому же дает представление о
состоянии дел в этой области в СССР к началу восьмидесятых годов. Весь-
ма обстоятельное исследование задач и методов проектирования и разработ-
ки программного обеспечения выполнено Боэмом. Его книга [11] постоянно
цитируется и в наши дни.
Современное представление о технологии проектирования программных
систем прочно связано с методологией объектно-ориентированного програм-
мирования. Всестороннее изложение данного подхода, его концепций, а так-
же общих методов разработки проектов в объектно-ориентированном стиле
можно найти в книге Буча [16]. UML и методы его использования в практи-
ческой разработке программных проектов хорошо изложены авторами это-
го языка в монографии [17]. Понятия, связанные с CASE-технологиями, до-
статочно четко излагаются в работах [64, 18]. В частности, в последней из
упомянутых публикаций достаточно подробно освещаются вопросы CASE-
технологий, связанных проектированием информационных систем.
Следующие ссылки помогут получить сведения об упомянутых выше кон-
кретных разработках. Книга [70] дает наиболее полное представление о СУБД
Oracle, в частности, об Oracle Designer 2000 и его месте в системе. IDEF-тех-
242 ГЛАВА 4. ЖИЗНЕННЫЙ ЦИКЛ

нология хорошо представлена в документе [85]. Информацию о RUP в целом


и Rational Rose в частности можно найти на сайте [88].
Задания для самопроверки

1. Модели традиционного представления о жизненном цикле: мотивация


разных моделей, содержание этапов. Общепринятая и классическая ите-
рационная модели.

2. Каскадная модель и ее мотивация. Понятия подтверждения, обзора, ве-


рификации и аттестации. Строгая каскадная модель.

3. Модель фазы-функции Гантера, ее мотивация и особенности. Понятие


технологической функции и интенсивности ее выполнения. Расщепле-
ние линии жизненного цикла.

4. Принципы итеративного проектирования в сопоставлении с последо-


вательным подходом. Этапы жизненного цикла при итеративном про-
ектировании.

5. Модификация модели фазы-функции и ее мотивация. Особенности на-


чальной и завершающей фаз. Переиспользование рабочих продуктов
проекта. Интенсивности технологических функций при объектно-ориентированном
проектировании.

6. Параллельное выполнение итераций. Виды технологического паралле-


лизма. Области совмещения работ. Спиралевидные модели жизненного
цикла.

7. Проблемы определения требований. Трассировка требований и поня-


тие трансформации требований. Схема трассировки требований. Ти-
пизация требований. Глоссарий проекта.

8. Учет трассировки требований в модели жизненного цикла. Результаты


анализа требований.

9. Особенности начальной итерации. Метод проектирования «Сначала в


глубину» и его модель.

10. Фаза завершения проекта (итерации) и моделирование обработки тре-


бований в период эксплуатации системы.
4.5. ИТОГИ И ПЕРСПЕКТИВЫ 243

11. Мотивация множественности моделей жизненного цикла. Модель жиз-


ненного цикла как основа построения технологии проектирования.

12. Специальные и универсальные модели и технологии. CASE-системы.


Понятие инструментальной поддержки технологии. Язык UML и мето-
дология RUP.
Часть II

Структуры программирования

244
245

Целью работы программиста всегда является создание программы, опи-


сывающей некоторый процесс. Программа есть структурное объединение сво-
их составляющих: выражений, операторов и др. Но отношение “быть соста-
вленным из” — лишь формально-лингвистическая основа структуры про-
граммы. В дополнение к ней нужно рассматривать, в частности, содержа-
тельную структуру, когда структурные единицы, выделяемые в программе,
отражают разбиение решаемой задачи на подзадачи. В этом случае говорят о
декомпозиции программы. Не менее важно для работы с программой струк-
турирование процесса ее выполнения, т. е. выделение в нем взаимодейству-
ющих статически заданных и динамически возникающих структурных еди-
ниц. При таком структурировании появляются процессы, вызовы процедур,
экземпляры объектов и т. д. Наконец, еще одним измерением, с которым при-
ходится иметь дело при составлении программ, является структурирование
данных, перерабатываемых в ходе выполнения программы.
Все эти виды структур взаимосвязаны, при программировании они пла-
нируются совместно, и далеко не всегда можно разделить работу программи-
ста, относящуюся к разным сторонам создаваемой системы. Если не брать в
расчет декомпозицию задачи, которая относится к уровню проектирования,
то с формальной точки зрения лингвистическое структурирование програм-
мы является первичным — программа строится как набор конструкций, ие-
рархически соподчиненных и соединенных для выполнения процесса. В по-
давляющем большинстве случаев программа задается до того, как процесс
будет запущен, и она определяет, какие входные данные и как будут перера-
батываться в выходные, какие промежуточные данные при этом будут поро-
ждаться.
Не так уж редки (и концептуально важны) схемы вычислений, при ко-
торых используются вычислительные процессы, порождающие программы
для дальнейшей обработки. Самый наглядный пример — компилятор, кото-
рый воспринимает текст на языке программирования, перерабатывает его в
последовательность команд конкретного компьютера, которая затем уже пе-
рерабатывает данные. В этом примере текст на языке программирования —
это структура данных для компилятора, в процессе исполнения программы
обрабатываемая до поступления других данных. Но отношение человека к
программе и к другим данным соврешенно различно. Поэтому говорится
о вычислителе, исполняющем программу на языке, отвлекаясь от того, что
для обработки основной части данных строится другая, рабочая, програм-
ма. Это естественная идеализация вычислительного процесса, позволяющая
раздельно обсуждать две структуры: программы и данных. Встречаются и
246

такие, кажущиеся экзотическими обычному программисту, случаи (напри-


мер, в Рефал, PROLOG, LISP), когда программа для обработки данных может
строиться в ходе переработки части основных данных, и в зависимости от
этих данных может появляться та или иная конкретизация программы даль-
нейшей обработки. Этому подходу уделено внимание в соответствующем ме-
сте (см. § 13.1, 13.2), а в данной части сосредоточим внимание на структурах,
которые за десятилетия практики работы программистов стали общеупотре-
бительными. При этом подчеркивается назначение каждой лингвистической
структурной единицы и ее связи с лругими задачами, возникающими в ходе
структурирования. Тем самым программистский опыт переводится на уро-
вень знаний и метода с уровня умений и композиций эмпирических рецеп-
тов.
Методически обоснованной отправной точкой реализации указанной уста-
новки традиционно считается рассмотрение конструкций структурного сти-
ля программирования. Нет никаких причин в данном курсе отказываться от
этой традиции, тем более что так называемые структурные конструкции ле-
жат в основании большинства других стилей, пусть даже в несколько ином
понимании. Альтернативные стили и подходящие для них средства, которым
отводится место в следующей части, удобно рассматривать на базе всесто-
роннего изучения наиболее распространенного в современной практике сти-
ля структурного программирования.
В данной части подробно разбирается построение программ в структур-
ном стиле. Для обеспечения перекрестных ссылок и в соответствии с одной
из главных наших задач: показать взаимосвязи и дать системную картину, в
необходимых случаях затрагиваются модификации и понятия, естественно
принадлежащие другим стилям.
Так как выражения являются основным строительным материалом про-
грамм данного стиля, изложение начинается с них. Затем рассматриваются
основные структуры управления во взаимосвязи с другими компонентами
программы, т. е. данными, потоками информации и призраками. Структура
информационного пространства подробно разбирается там, где накопленный
материал дает достаточные основания для разбора и дальнейшее продвиже-
ние без анализа этой структуры невозможно: в конце главы, посвященной
циклам, и в начале главы, посвященной подпрограммам.
В конце суммируется материал, посвященный структурам данных.
Глава 5

Выражения

В современых алгоритмических языках важнейшую роль играют выра-


жения: понятия, семантический смысл которых состоит в том, что они выра-
батывают значения. Выражений в этом смысле слова нет лишь в некоторых
языках, последовательно поддерживающих нетрадиционные стили (напри-
мер, в Рефале). Простейшими выражениями являются имена и литералы.
Например, все осмысленные составные части следующей записи на язы-
ке C
((x++)*(++y)/(*z=>a**z=>b))>=3.0 (5.1)
являются выражениями.
Рассмотрим подробнее структуру выражений. При этом, как всегда, мы
сопоставляем абстрактно-синтаксическую и конкретно-синтаксическую струк-
туру. Первая из них описывает текстовое строение выражения, а вторая —
вычислительные аспекты. В обоих представлениях в качестве основного «стро-
ительного блока» выражений выделяются операции с их операндами.
Для большей части примеров программ в данном разделе использован
язык С (точнее, соответствующее С подмножество языка С++). Переписы-
вание программ на любой другой традиционный язык не вызвало бы ника-
ких затруднений, но новых качеств изложению материала это бы не доба-
вило. Вместе с тем, семантика конструкций С и, скажем, Pascal’я различа-
ется, и это подчеркивается в пояснительном тексте. Для освоения материа-
ла важно всегда вычленять суть абстрактных вычислений примеров, отде-
ляя ее от прагматических наслоений. Именно этим обусловлен выбор С, ко-
торый, являясь широко используемым, весьма прагматичным и непоследо-
вательным языком, предоставляет много поводов для обсуждения решений,
принимаемых в нем и в других языках.

247
248 ГЛАВА 5. ВЫРАЖЕНИЯ

§ 5.1. ОПЕРАЦИИ

Новые выражения составляются из более простых выражений посред-


ством операций.
Определение 5.1.1. Операция — лексема языка, которая в абстрактно-син-
таксическом представлении является такой составляющей некоторого выра-
жения V , что остальные составляющие V сами являются выражениями, на-
зываемыми ее операндами.
Среди операций различаются префиксные — стоящие в конкретно-син-
таксическом выражении перед операндами; постфиксные — стоящие после
своих операндов; и инфиксные — стоящие между операндами.
Конец определения 5.1.1.
Пример 5.1.2. В выражении (5.1) вхождение ++ в x++ является постфиксом,
в ++y — префиксом, из двух идущих подряд звездочек ** первая звездочка
интерпретируется как инфикс, вторая — как префикс.
Конец примера 5.1.2.
Префиксные и постфиксные операции определяются в конкретно-синтакси-
ческой структуре выражения. Если же рассмотреть абстрактно-синтаксиче-
скую, то различия префиксных, постфиксных и инфиксных операций почти
исчезают.1
В современных языках программирования префиксные и постфиксные
операции одноместны. Но в трансляторах и системах символьных преобра-
зований часто используются промежуточные2 представления выражений, ко-
торые включают многоместные префиксы или многоместные постфиксы. В
таких представлениях все остальные типы операций исключаются. Если в
Оговорка «почти» связана с тем, что операции ++ и −− языков C и Java выполняются
по-разному в зависимости от того, применены они как префиксные или как постфиксные. В
1

первом случае увеличение операнда производится до вычисления выражения, во втором —


после него. В сочетании с совместностью вычисления операндов ‘нормальных’ операций,
таких, как двуместный +, это может привести к двусмысленности записи, т. е. к тому, что ее
значение не будет определено ни стандартом языка, ни даже наиболее распространенными
реализациями, и целиком оставлено на “добрую” волю оптимизаторов.
Поэтому приведенный нами пример является примером плохого программирования, и на
практике принято, что при записи выражений на C/C++ нужно иметь в операторе лишь одно
присваивание.
Промежуточными представлениями либо значениями обычно называются такие, которые
не видны ни на входе, ни на выходе системы.
2
5.1. ОПЕРАЦИИ 249

представлении выражений все операции являются префиксами, оно называ-


ется польской записью, или прямой польской записью, если все операции —
постфиксы, то обратной (инверсной) польской записью.3
Пример 5.1.3. Формула
(x + y)/(a ∗ b + 2) (5.2)
в польской записи выглядит как
/ + x y + ∗a b 2.
Можно строго доказать, что в случае операций с фиксированным числом ар-
гументов польская запись всегда позволяет обойтись без скобок. Конечно, это
уже не так, если, например, у нас есть одноместный и двуместный минус.
Конец примера 5.1.3.
Прямая и в особенности обратная польская запись есть конкретно-синтакси-
ческое представление дерева вычислений выражения. Тем самым она макси-
мально приближена к абстрактно-синтаксической структуре (если игнори-
ровать коммутативность и ассоциативность обычных алгебраических опе-
раций). Поэтому она применялась в некоторых языках, претендовавших на
прямое соответствие текстов программ их представлению в объектносм коде.
В частности, язык АЛМО использовал обратную польскую запись для пред-
ставления выражений.4
В практически забытом сейчас языке APL,5 где конкретно-синтаксиче-
ское представление программы увязано со структурой набираемого на пи-
Впервые такую запись применили польские логики львовско-варшавской школы для ло-
гических формул, где в то время (20-е гг. XX века) еще не было груза традиций внешней
3

формы.
АЛМО — русская попытка (1967 г.) создать то, чем затем стал язык C: язык высокого
уровня, максимально приближенный к архитектуре машины. АЛМО до середины 80-х гг.
4

использовался как язык-посредник некоторых семейств трансляторов: программы сначала


транслировались на АЛМО, а уже затем в машинные коды.
APL — язык, достаточно широко использовавшийся для написания небольших программ
программистами-одиночками в системах разделения времени вплоть до середины 80-х гг.
5

XX в. В APL было множество операций, в записи часто представлявшихся наложением не-


скольких машинописных символов друг на друга при помощи возврата каретки; операции
над элементами автоматически распространялись до операций над массивами; многие опе-
рации были совмещены с присваиваниями (оставшиеся в C рудименты этого, в частности,
упоминавшиеся выше ++ и−−) и т. п. В итоге APL породил одно из древнейших племен ха-
керов — совершенно непонятные однострочечники — писавших любую программу в виде
одной строки на APL, разобраться в которой было почти невозможно.
250 ГЛАВА 5. ВЫРАЖЕНИЯ

шущей машинке текста, принято другое, столь же последовательное, как и


в польской записи, решение, также прямо увязывавшее последовательность
операндов с вычислением выражения. Операции не имеют никаких приори-
тетов, они выполняются одна за другой в том порядке, в котором они на-
печатаны, и единственное средство изменить порядок вычислений — скоб-
ки. Например, выражение A*B+C*D понималось как математическая формула

Простейшие операции практически во всех современных языках програм-


(A ∗ B + C) ∗ D.

мирования имеют стандартное изображение. Это +, −, ∗, которые применя-


ются и для действительных, и для целых чисел, и интерпретируются едино-
образно.
Операцию деления стоит рассмотреть внимательнее. Интерпретация де-
ления для действительных и целых чисел в подавляющем большинстве язы-
ков программирования принципиально различается. Для действительных чи-
сел вычисляется приближение к частному двух чисел. Для целых чисел обыч-
но производится деление нацело, причем для положительных чисел все яс-
но: берется целая часть частного, а вот с какой стороны будет приближение
для отрицательных чисел. . . тут может быть самое безумное решение. Кое-
где (например, в языках Pascal и Алгол-68) явно разделяется целочисленное
деление и деление с действительным результатом. Обычно для действитель-
ного деления остается символ операции /, а для целочисленного вводится
новая операция, например, div. Поэтому результаты вычислений операторов

x := a/b;
(5.3)
x := a div b;
где x, a, b — целые переменные, могут быть различны.
Аналогичный эффект можно получить и в языке С, но здесь всплывает в
явном виде еще одно важное понятие современных языков.

x=float(a)/float(b);
(5.4)
x=a/b;

В первом присваивании явно указано, что аргументы переводятся в действи-


тельную форму, и, соответственно, выполняется деление действительных чи-
сел с действительным результатом. Затем этот результат переводится обратно
в форму целого числа, при этом обычно он усекается отбрасыванием дроб-
ной части.6
6
Как и всегда, единства здесь нет. Кое-где он округляется, кое-где дробная часть просто
5.1. ОПЕРАЦИИ 251

Операции, семантика которых состоит в переводе значения из одной фор-


мы в другую, называются приведениями. Операции приведения могут быть
как явными (в таком случае практически во всех современных языках они
имеют синтаксическую форму TYPE(<выражение>), берущую начало из Ал-
гола-68), либо неявными. Например, если у нас есть присваивание

где d —переменная типа double, x — типа float, n — типа int, то сначала


d=x*n;

целое n будет преобразовано в действительное одинарной точности, а затем


результат умножения — в действительное число двойной точности. Таким
образом, на абстрактно-синтаксических структурах операторов должны по-
являться еще и новые вершины — приведения значений к нужному типу.
Наряду с целочисленным делением вводится и операция взятия остатка,
но она также может быть самым непоследовательным образом определена
для различных комбинаций отрицательного делимого и отрицательного де-
лителя. В C операция взятия остатка обозначается %, а в языке Pascal —

Операция возведения в степень также практически везде, где она преду-


mod.

смотрена, обозначается ∗∗, но ее интерпретация сильно зависит и от языка


программирования, и от конкретной его реализации. Например, возведение
действительного числа в целую степень в некоторых реализациях делается
через умножение, а в других — показатель переводится в действительную
форму и применяются стандартные функции exp и log. Существовали7 даже
такие реализации, в которых возведение целого числа в целую степень дела-
лось при помощи перевода агументов в действительную форму, и, соответ-
ственно, вычисление (−1) ∗ ∗n могло привести к ошибке. Этот пример пока-
зателен: стремление к универсализации не всегда благо. Так что возведением
в степень там, где эта операция предусмотрена, во избежание неприятностей
лучше пользоваться лишь для целых чисел и для возведения положительных
действительных чисел в целую степень.8 В языках C и Pascal от операции
возведения в степень просто отказались.
Некоторые операции определяются лишь через конкретные машинные
отбрасывается. Определяться это может или в стандарте языка, или в описании конкретной
среды программирования. Особенно внимательными надо быть в тех случаях, когда дели-
тель либо делимое отрицательны.
7
А, может быть, существуют и сейчас.
Например, из стандарта языка FORTRAN-90 нельзя понять, чему равно выражение 0**0.
Разные трансляторы генерируют код, приводящий к разным результатам его вычисления.
8
252 ГЛАВА 5. ВЫРАЖЕНИЯ

представления операндов. Таковы, в частности, поразрядные логические опе-


рации в общераспространенных языках программирования. В языке C это
операции &, |, ~, ^. Для их понимания нужно помнить, что С рассчитан толь-
ко на такие машины, в которых целые числа (да и другие объекты) имеют
машинное представление в виде последовательности битов. Результат опе-
рации & — число, каждый двоичный разряд которого получается обычной бу-
левской конъюнкцией соответствующих разрядов операндов. Соответствен-
но, | означает поразрядную дизъюнкцию, ~ — поразрядное отрицание (ин-
вертирование всех битов), ^ — поразрядное исключающее или.
Операции над машинным представлением особенно эффективны в тех
случаях, когда число на самом деле используется как ящик, куда засунуто не-
сколько независимых значений, каждое из которых занимает несколько дво-
ичных разрядов.
Аналогичные операции в языке Pascal обозначаются and, or, not, xor.
Язык C отличается фантастической непоследовательностью в употребле-
нии символов операций. Один и тот же символ операций имеет абсолютно
разный смысл, когда он употребляется как бинарная операция и как унарная.
Например, * как бинарная операция означает умножение, а как унарная —
взятие значения по данному адресу. & как бинарная операция означает по-
разрядную конъюнкцию машинных представлений своих операндов, а как
унарная — получение адреса стоящего после нее имени. Даже столь безо-
бидная операция, как унарный +, может привести к изменению значения из-
за неявного преобразования в другой тип.
В языках C++, Ada и Алгол-68 все операции, имеющиеся для некоторо-
го типа, могут переноситься на любой другой тип данных, определенный в
программе. Для этого необходимо явно определить их смысл для аргумен-
тов нового типа. В Алголе-68 имеется даже внутренняя система определения
новых операций.

§ 5.2. ЛОГИЧЕСКИЕ ВЫРАЖЕНИЯ

Рассмотрение начнем с примера. Пусть требуется написать программу,


которая печатает «Да», если точка принадлежит области S, и «Нет» — в про-
тивном случае. Область S образована пересечением следующих «простых»
5.2. ЛОГИЧЕСКИЕ ВЫРАЖЕНИЯ 253

областей (см. рис. 5.1, где данная область выделена штриховкой):

O1 = {(X, Y ) | 0 6 X 6 1} полоса шириной 1 над осью X;


O2 = {(X, Y ) | X 6 Y } полуплоскость под прямой Y = X;
O3 = {(X, Y ) | X2 + Y 2 6 4} Круг радиуса 2
с центром в начале координат.

Точка принадлежит области, если она принадлежит пересечению областей


O1, O2 и O3.
Таким образом, задачу можно свести к следующим действиям:
1. проверить принадлежность точки области O1;

2. если свойство выполняется, то проверить его для O2;

3. если опять свойство выполняется, то проверить его для O3;

4. напечатать «Да», если опять получится истина;

5. во всех остальных случаях печатать «Нет».

Понятно, что порядок проверок может быть и другим — результат от него


не зависит.
Прежде всего надо договориться, как будет представляться точка в про-
грамме. Типа данных «Точка» в языке С нет, следовательно, его надо моде-
лировать имеющимися средствами. Представим точку парой вещественных
чисел X и Y . Эти числа — декартовы координаты точки на плоскости. Пред-
ставление областей в программе — это задание соответствующих им опре-
деляющих отношений над координатами, а проверка принадлежности точки
области — условие соответствующего условного оператора.

Программа 5.2.1

\* Проверка принадлежности области *\


#include <stdio.h>
int main()
{

printf( "Введите координаты в виде <число> <число>:");


float x, y;

scanf( "%f% %f", &x, &y );


if ( 0 <= x )
254 ГЛАВА 5. ВЫРАЖЕНИЯ

Y
Y=X

X2 + Y2 = 4

Y=1

Y=0
X

Рис. 5.1. Область плоскости


5.2. ЛОГИЧЕСКИЕ ВЫРАЖЕНИЯ 255

if ( x <= 1 )
if ( x <= y )
if ( x * x + y * y <= 4 )
printf( "Да\n");
else printf( "Нет\n");
else printf( "Нет\n");
else printf( "Нет\n");
else printf( "Нет\n");
return 0;
}

Стоит обратить внимание на то, что упорядоченное размещение в тексте


условных операторов очень помогает понять, как выполняется данная про-
грамма.
Вопрос: а нельзя ли сократить повторения одного и того же оператора —
печати «Нет»? Если не использовать логических значений, то существует
множество способов решения этой проблемы, но все они связаны с зада-
нием передач управления, которые обеспечивают однократное написание в
программе printf( "Нет\n");. Один из самых ненаглядных, а потому плохих,
вариантов демонстрируется следующим фрагментом:

Программа 5.2.2

if ( 0 <= x )
if ( x <= 1 )
if ( x <= y )
if ( x * x + y * y <= 4 ) printf( "Да\n");
else goto lbl;
else goto lbl;
else lbl: printf( "Нет\n");
else goto lbl;

Это сработает, но для понимания фрагмента человеком даже в таком про-


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

Программа 5.2.3
256 ГЛАВА 5. ВЫРАЖЕНИЯ

if ( 0 <= x )
if ( x <= 1 )
if ( x <= y )
if ( x * x + y * y <= 4 )
{
printf( "Да\n");
goto lb;
}
printf( "Нет\n");
lb:. . .

Он, по крайней мере, не вводит в заблуждение о какой-то исключительности


одного из вложенных условных операторов, да и повторов меньше.
Лучше в подобных случаях использовать сложные условия. Для этих це-
лей в языках программирования предусмотрены достаточно гибкие средства.
Это логические выражения, позволяющие строить условия, исходя из отно-
шений, выдающие логическое значение, будучи примененными к операндам
числовых типов9 . Обычно отношениями являются операции сравнения, ска-
жем, в языке C ==, <, >, <=, >=.
Теперь сравните две формы представления программ анализа условий:
Программа 5.2.4
if (Условие1)
if (Условие2) if ((Условие1)&&(Условие2)

{ Действие_1_2_3} { Действие_1_2_3}
if (Условие3) && (Условие3))

else { Действие_1_2_не_3} /* Но только если нет противоречий */


else { Действие_1_не_2} /*с другими действиями!*/
else { Действие_не_1 } else Действие_не_1

В языке С имеются операции: && — логическое И, || — логическое ИЛИ.


Операция “&&” выполняется над двумя операндами, вырабатывающими ло-
гические значения, и в результате вырабатывает true, если оба ее операнда
есть true, и false в противном случае.
Операция “||” выполняется над двумя операндами, вырабатывающими
логические значения, и в результате вырабатывает true, если хотя бы один
из ее операндов есть true, и false, если оба они есть false.
9
Сравните с предикатами из курса логики.
5.2. ЛОГИЧЕСКИЕ ВЫРАЖЕНИЯ 257

Что касается операции НЕ, то в стандартном языке С она не представлена.


В нем нет специальной операции логического отрицания, которая выполня-
лась бы над одним своим операндом, вырабатывающим логическое значение,
и в результате вырабатывала бы true при значении операнда false, и false в
противном случае.
Тем не менее, возможность добиться эффекта отрицания есть. Чтобы объ-
яснить это, придется обратиться к конкретному представлению логических
значений. В большинстве других языков (в частности, в расширении язы-
ка C С++) тип bool (логический тип) является одним из стандартных типов,
и можно использовать переменные и значения этого типа (true и false), не
зная, как они представляются на машинном уровне. В С такого типа нет, а
true и false представляются следующим образом: true — это любое целое,
отличное от нуля, а false — нуль.
Чтобы в этом убедиться, достаточно выполнить программу 5.2.5.

Программа 5.2.5

/* Тест логических значений /*


#include <stdio.h>
int main()

if ( 200 ) printf( "200 - это True.\n");


{

if ( -33 ) printf( "-33 - это True.\n");


if ( 0 ) printf( "0 - это False!\n");
return 0; /* нормальное завершение */
}

которая напечатает
200 - это True.
-33 - это True.
Это еще раз подчеркивает, что С создавался для конкретной архитектуры ма-
шины с командами перехода по нулевому или ненулевому значению.
Так что вместо чего-либо подобного “! (a>d)” (как, к примеру, в С++, где
“!” — операция отрицания) приходится писать “(a>d)== 0”. Все булевы опе-
рации согласованы с таким представлением.10
Стоит подчеркнуть явно, что тип bool в C++ совсем не то же самое, что, к примеру, в
языке Pascal. Множество значений этого типа состоит из двух классов эквивалентности с
10

точки зрения логических операций: всех ненулевых значений и нулевого значения. Но экви-
258 ГЛАВА 5. ВЫРАЖЕНИЯ

Программа 5.2.6

/* Попадание точки в область */


#include <stdio.h>
int main()
{

printf( "Введите координаты в виде <число> <число>:");


float x, y;

scanf( "%f% %f", &x, &y );


if( 0 <= x ) && ( x <= 1 ) && ( x <= y ) && ( x*x + y*y <= 4 )
printf( "Да\n");
else printf( "Нет\n");
return 0;
}

Выполнение условного оператора в программе 5.2.6 в идеале должно было


бы происходить следующим образом. Совместно вычисляются четыре опе-
ранда, разделенные знаками операции "&&", и, согласно определению этой
операции, если все четыре полученные значения есть "true", значением усло-
вия будет "true", в противном случае (т. е., когда хотя бы одно из отношений
ложно) значением условия будет "false". Тем самым выполняется требуемая
в условии задачи проверка.
Однако для языков C и С++ это совсем не так. И различия с приведен-
ной трактовкой обусловлены прежде всего тем, что для выяснения того, что
должно быть напечатано, нет нужды вычислять все указанные отношения,
когда какое-либо из уже вычисленных оказалось ложным. Аналогично, для
выяснения значения цепочки логических выражений, связанных операцией
ИЛИ (“||”), нет нужды вычислять все составляющие выражения, когда одно
из них оказалось истинным. По этим причинам в языке С++ принято, что
такие цепочки вычисляются слева направо до тех пор, пока результирующее
значение не будет выяснено (для цепочки логических выражений, связанных
операцией И, это первое ложное значение, а для цепочки выражений, связан-
ных операцией ИЛИ, это первое истинное значение).11 Указанный порядок
валентные значения все равно различны, и поэтому, в частности, операция сравнения двух
логических значений b1==b2 может выдать ложный результат, даже если оба значения вид-
ны программисту как true.
Иногда ошибочно утверждают, что в языке С при вычислении логических выражений ис-
пользуется сильная трехзначная логика Клини (см., напр., [38]), которая работает с неопре-
11
5.2. ЛОГИЧЕСКИЕ ВЫРАЖЕНИЯ 259

предписывается языком, что отличает данные операции и от операций языка


Pascal, и от всех остальных операций С.
В силу указанной трактовки условный оператор в программе 5.2.6 будет
выполняться в точности так, как фрагмент 5.2.3. Следовательно, с точки зре-
ния скорости выполнения обе записи эквивалентны. Для языка, в котором
операнды булевых операций вычисляются совместно, о скорости вычисле-
ний или даже о том, какие из операндов цепочки будут вычислены, сказать
ничего нельзя. К примеру, в системе программирования Turbo Pascal есть два
режима, управляемые прагматическими указаниями, которые разграничива-
ют два случая:

• вычислять все логические операнды;

• вычислять логические операнды до установления значения выраже-


ния.

Учитывая важность каждого режима, нужно знать, как эмулировать пол-


ное вычисление логических операций в С++. Для этого используются опера-
ции поразрядной логики (см. выше), которые для конкретных представлений
значений true и false работают правильно и заставляют вычислиться все опе-
ранды.
Следующий пример показывает, каким образом можно использовать ва-
рьирование вычисления логических операций для повышения наглядности
программ. Пусть требуется организовать процесс, в котором можно явно вы-
делить инициализацию, активизацию, выполнение и завершение как само-
стоятельные программные единицы: init, activate, run и terminate. Каждая из
них может выполняться строго вслед за предыдущей, если предыдущая про-
грамма выполнилась успешно. Если успешному выполнению init, activate,
run и terminate приписать значение true, а неуспешному — false, что означает
возврат каждой из этих функций с соответствующим значением, то процесс
в целом наглядно представляется следующим фрагментом:

деленностью и значение, скажем, конъюнкции ложно, если хотя бы один из ее операндов


ложен (другой может быть и не определен).
Но сильная трехзначная логика Клини базируется на исключительно сильном понятии со-
вместного вычисления, когда операнды настолько аккуратно исполняются, что даже ошибка
при исполнении одного из них не фиксируется, если какой-либо другой приведет к удаче. А
такая аккуратность выполнения совместных высчислений ни в одном современном языке не
обеспечивается. Анализ “логики”, используемой в языке С, см., напр., в книге [27].
260 ГЛАВА 5. ВЫРАЖЕНИЯ

if (init ( )
&& activate ( )
&& run ( )
&& terminate ( ))
{ ... }

§ 5.3. ПРИОРИТЕТ ОПЕРАЦИЙ

Программа 5.2.1, а точнее формулы, вычисляющие значение отношения

x ∗ x + y ∗ y <= 4 (5.5)

а также значение более сложного выражения

(0 <= x)&&(x <= 1)&&(x <= y)&&(x ∗ x + y ∗ y <= 4)

дают повод поговорить о приоритетах12 выполнения операций. На рис. 5.2


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

x ∗ (x + y) ∗ y <= 4

текстуально очень похоже на исходное, хотя оно имеет совсем иную абстракт-
но-синтаксическую структуру. В то же время, структура выражения

((x ∗ x) + (y ∗ y)) <= 4

совпадает с исходной.
Все дело в том, как определять текстовое (конкретно-синтаксическое) пред-
ставление выражений и его связь с абстрактно-синтаксическим представле-
нием. Можно следовать, например, такому определению:
Не путайте приоритеты операций с приоритетами процессов! Приоритеты операций —
понятие синтаксическое, необходимое для анализа текста программы, а приоритеты процес-
12

сов — попятие семантическое, работающее на этапе исполнения программной системы.


5.3. ПРИОРИТЕТ ОПЕРАЦИЙ 261

Условие

<=

Операнд Операнд
Константа
+

Значение = 4

Операнд Операнд

* *

Операнд Операнд Операнд Операнд


Переменная Переменная Переменная Переменная

Имя = x Имя = x Имя = Y Имя = Y


Рис. 5.2. Абстрактно-синтаксическая структура условия
262 ГЛАВА 5. ВЫРАЖЕНИЯ

<первичное выражение> [ <знак операции> <первичное выражение> ]*


<выражение> ::=

<первичное выражение> ::=


<имя переменной> |
<константа> |

<знак унарной префиксной операции><первичное выражение> |


“(” <выражение> “) ”|

<первичное выражение><знак унарной постфиксной операции>


<знак операции> ::= /* здесь приводятся не все знаки операций! */
“* ” | “/ ”|/* знаки операций приоритета 2*/
“+ ” | “- ”|/* знаки операций приоритета 1*/
“< ” | “<= ” | “= = ” | “!= ” | “>= ” | “>”/* знаки отношений приоритета 0*/
<знак унарной префиксной операции> ::=
& /* знаки остальных унарных операций пока не определяются */
<знак унарной постфиксной операции> ::=
++ /* знаки остальных унарных операций пока не определяются */

которое дополняется соглашениями о порядке вычислений:


1. Операция может выполниться тогда, когда вычислены все ее операнды;

2. Операнды операций вычисляются совместно;

3. Скобки в выражении задают принудительный порядок вычислений, со-


ответствующий скобочной структуре.

4. Знакам операций приписываются приоритеты; в языке C++ эти прио-


ритеты расставлены в соответствии со следующими правилами:

(a) знаки бинарных присваиваний (=, += и т. д.) получают низший


приоритет;
(b) знаки логических операций получают следующий приоритет за
знаками присваиваний;
(c) знаки поразрядных логических операций получают следующий
приоритет за знаками логических операций;
(d) знаки операций отношений получают следующий приоритет за
знаками поразрядных логических операций;
(e) знаки операций сдвига получают следующий приоритет за знака-
ми отношений;
5.3. ПРИОРИТЕТ ОПЕРАЦИЙ 263

(f) знаки операций типа сложения получают приоритет, следующий


за приоритетом знаков отношений;
(g) знаки операций типа умножения получают приоритет, следующий
за приоритетом знаков операций типа сложения;
(h) унарные постфиксные операции получают наивысший приоритет;
(i) унарные префиксные операции получают высший приоритет по-
сле приоритета знаков постфиксных операций;

5. Если в текстовой записи выражения есть фрагмент вида


<операнд><знак операции(1)>
<операнд> <знак операции(2)>

<знак операции(n)><операнд>
...

(произвольная последовательность аргументов и знаков операций, не


включающая скобок), то вычисление операций упорядочивается в со-
ответствии с их приоритетами, начиная с самого высокого, а при равен-
стве приоритетов операции выполняются в текстовом порядке: сначала
выполняется операция 1, затем — операция 2, и так далее.

Другой способ определения приоритетов связывает их с конкретно-син-


таксическими понятиями. Постулируется, что вычисление операций соот-
ветствует выводу выражения в грамматике (разумеется, приспособленной
для этих целей).
Сначала рассмотрим упрощенный пример:
<первичное выражение> ::=<имя переменной> |

<вторичное выражение> ::=<первичное выражение> |


<константа> |“(”<выражение> “)”

<вторичное выражение> [ “*”|“/”] <первичное выражение>


<выражение> ::=<вторичное выражение> |
<выражение> [ “+ ” | “-”] <вторичное выражение>
<имя переменной> ::=“a ” | “b”

Вывод выражения a ∗ (b + 1):


<константа> ::=“1” | “2”

<вторичное выражение> →
<выражение> →

<вторичное выражение> * <первичное выражение> →


<первичное выражение> * <первичное выражение> →
264 ГЛАВА 5. ВЫРАЖЕНИЯ

<первичное выражение> * (< выражение> )→


<имя переменной> * (< выражение> )→

a∗ (<выражение> + <вторичное выражение> )→ . . . →


a∗ (<выражение> )→

a ∗ (b+ <вторичное выражение>)→


a ∗ (b+ <первичное выражение> )→
a ∗ (b+ <константа>)→
a ∗ (b + 1)

Если вычисления следуют синтаксическому выводу выражения, то умноже-


ние не может быть выполнено, пока не выполнится сложение. Но выражение
a выводится длинно:
<выражение>→<вторичное выражение>→
<первичное выражение> →<имя переменной>→ a
В языке C++ выражения определяются следующим образом:
<первичное выражение> ::= <имя переменной> |

<постфиксное выражение> ::= <первичное выражение> |


<константа> |“(”<выражение> “)”

<постфиксное выражение> “[”<выражение> “]”


/* элемент массива */|
<постфиксное выражение> “(”[ <список выражений> ]? “)”
/* вызов функции */|
<постфиксное выражение> [ “++”|“- -”]
<список выражений> ::= <выражение присваивания> [ “,”<выражение при-
сваивания> ]*
<унарное выражение> ::= <постфиксное выражение> |
[“+ +” | “- -”] <унарное выражение> |
[“*” | “&” | “+” | “-” | “!” | “~”] <унарное выражение> |
“sizeof” <унарное выражение>
<выражение вида умножения> ::=<унарное выражение> |
<выражение вида умножения> [“*” | “/” | “%”] <унарное выражение>
<выражение вида сложения> ::=<выражение вида умножения> |
<выражение вида сложения> [“+” | “-”] <выражение вида умножения>
<выражение вида сдвиг> ::=<выражение вида сложения > |
<выражение вида сдвиг>[“< <” | “> >”]<выражение вида сложения>
<выражение вида отношения> ::=<выражение вида сдвиг> |
<выражение вида отношения> [“<” | “>” | “<=” | “>=”] <выражение вида сдвиг>
5.3. ПРИОРИТЕТ ОПЕРАЦИЙ 265

<выражение вида равенство> ::=<выражение вида отношения> |


<выражение вида равенство> [“= =” | “!=”] <выражение вида сложения>
<поразрядное AND-выражение> ::=<выражение вида равенство> |
<поразрядное AND-выражение> “&”<выражение вида равенство>
<поразрядное исключающее OR-выражение> ::=<поразрядное AND-выражение>

<поразрядное исключающее OR-выражение> “^”<поразрядное AND-выражение>


|

<поразрядное OR-выражение> ::=<поразрядное исключающее OR-выражение>


|

”<поразрядное исключающее OR-выражение>


<поразрядное OR-выражение> “|

<логическое AND-выражение> ::=<поразрядное OR-выражение>|


<логическое AND-выражение> “&&”<поразрядное OR-выражение>

< логическое OR-выражение> “|”<логическое AND-выражение>


<логическое OR-выражение> ::=<логическое AND-выражение> |

<условное выражение> ::= <логическое OR-выражение> |


<логическое OR-выражение> “?” <выражение> “:” <выражение вида присва-
ивания>
<выражение вида присваивания> ::= <условное выражение> |
<условное выражение> [ “=” | “*=” | “/=” | “%=” | “+=” | “-=” | “>>=” | “<<=” |
“&=” | “^=” | “|=”] <выражение вида присваивания>
<выражение> ::=<выражение вида присваивания> |
<выражение>“,”<выражение вида присваивания>
Заметим, что в языках С/С++ индексные выражения и параметры вызо-
вов функций также формально помещены в категорию постфиксных выра-
жений. В самом деле, то, что предшествует скобкам вокруг индексов массива
или аргументов функции, можно рассматривать как вычисление адреса мас-
сива либо функции. Этот формальный факт согласуется (видимо, случайно,
поскольку в остальных случаях столь глубоких прозрений в языке C не на-
блюдается) с тем, что с математической точки зрения и массивы, и функции
соответствуют функциям. Так что выражения вида a[i] и a(i) можно считать
приведением адреса массива (функции) к типу значения элемента массива
(результата функции). Разница лишь в том, что для массива алгоритм поиска
значения задан транслятором, а для функции его пишет сам программист.
В развитых машинно-ориентированных языках программирования (та-
ких, как BLISS и ЯРМО) есть средства описания алгоритмов доступа к памя-
ти, которые сближают процедуры и массивы. Подобные средства в прими-
тивизированном виде начали проникать в современные практические языки
266 ГЛАВА 5. ВЫРАЖЕНИЯ

(пока что на уровне конкретных систем программирования). Например, рас-


смотрим, отвлекаясь от мелких технических деталей, спецификаторы property
языка Object-Pascal и языка системы Visual C). После описаний типа

property a

возникают виртуальные данные — данные, функции извлечения и измене-


ния для которых определяет сам программист. Обращение к такому данному
интерпретируется как вызов его функции извлечения, а присваивание такому
данному — как вызов функции изменения.
Пример 5.3.1. Цепочка вывода x ∗ x + y ∗ y <= 4 из <выражения> выглядит
следующим образом:

<выражение вида присваивания> →


<выражение> →

<условное выражение> →

. . . → <выражение вида отношения> →


<логическое OR-выражение> →

<выражение вида отношения> <= <выражение вида отношения> →


. . . → <выражение вида отношения> <= 4 →
. . . → <выражение вида сложения> <= 4 →
<выражение вида сложения> + <выражение вида умножения> <= 4 →
<выражение вида умножения > + <выражение вида умножения> <= 4 →
<выражение вида умножения > + <выражение вида умножения> *<унарное
выражение> <= 4 →
. . . → <унарное выражение> * <унарное выражение> + <унарное
выражение> * <унарное выражение> <= 4 →
. . . → x ∗ x + y ∗ y <= 4
Если проследить, как появляются в этой цепочке знаки операций, то стано-
вится понятным, что, как и для упрощенных выражений, чисто формальны-
ми синтаксическими средствами достигается и вычисление, и построение
дерева абстрактно-синтаксической структуры, в точности соответствующее
тому, что требуется.
Конец примера 5.3.1.
Пример показывает, что понятие приоритета операции становится фор-
мально избыточным, если соответствующим образом определить граммати-
ку. Но в этом случае для выявления приоритетов операций нужно просма-
5.3. ПРИОРИТЕТ ОПЕРАЦИЙ 267

тривать все длинное формальное определение <выражения>, так что с со-


держательной точки зрения приоритеты остаются самым простым способом
объяснения порядка вычислений сложных выражений.
Приоритеты обязательно возникают, когда в языке имеется систематиче-
ский механизм определения новых операций. Примером такого языка являет-
ся Algol 68. Здесь средств контекстно-свободной грамматики для задания по-
рядка вычисления выражений недостаточно. При описании новой операции
заодно описывается и ее приоритет, и он является характеристикой опера-
ции во всей ее области действия. Оказывается, что смысл операции зачастую
можно определять по контексту, а вот приоритет ее должен быть атрибутом
обозначения операции, а не ее конкретной реализации для конкретных типов
операндов. Ведь для того, чтобы установить, к чему применяется операция,
нужно сначала синтаксически разобрать выражение, в которое она входит.
Фиксированные по определению языка приоритеты операций — локаль-
ное свойство обозначений, тогда как определяемый в программе приоритет
операции — это свойство вводимых в программе обозначений операции, ко-
торое не является контекстно-свободным. Поэтому возможно «растворение»
фиксированных приоритетов в синтаксисе, а определяемые приоритеты тре-
буют дополнительных методов анализа, которые можно организовывать толь-
ко на базе разбора текста программы, что приводит к усложнению транслято-
ра. Именно поэтому в языке С++ возможности определения операций огра-
ничены: из чисто прагматических соображений разработчики языка позво-
ляют связывать новую операцию лишь с априори заданными знаками опера-
ций, для которых фиксированы приоритеты. Понятно, что это решение отри-
цательно сказывается на выразительности программ.

Задания для самопроверки

1. Расклассифицируйте остальные вхождения операций в выражение (5.1).

2. Переведите выражение (5.2) в обратную польскую запись. Изучите ее


и объясните, почему в языке АЛМО использовалась обратная польская
запись и почему она до сих пор часто используется в промежуточных
представлениях транслируемых программ?

3. Проанализировав представления 5.2.4, покажите, когда не стоит поль-


зоваться сокращением цепочки вложенных уловных операторов через
один условный оператор с конъюнкцией условий.
268 ГЛАВА 5. ВЫРАЖЕНИЯ

4. Какая из строчек фрагмента (5.3) приводит к тому же результату, что


первая строчка (5.4)?

5. Объясните причины, почему в первом операторе (5.3) присваивание це-


лочисленной x значения вещественного выражения a/b корректно.
Глава 6

Разветвление вычислений

В традиционных языках программирования средства разветвления вычи-


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

§ 6.1. РАЗБОР СЛУЧАЕВ

Уже рассмотренные способы задания разветвления вычислений базиру-


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

269
270 ГЛАВА 6. РАЗВЕТВЛЕНИЕ ВЫЧИСЛЕНИЙ

Пусть требуется написать программу-калькулятор, которая по двум вво-


димым числам и знаку операции вычисляет значение выражения:

Разработка требуемой программы разбивается на две логически незави-


<число><операция><число>

симые подзадачи:
1. ввод данных в заданном формате;

2. подсчет и вывод результата.


Кроме того, стоит предусмотреть, чтобы после окончания счета была удоб-
ная возможность узнать, как закончились вычисления. Соответственно, про-
грамма должна содержать фрагменты, извещающие об этом окружение, и вы-
деляется третьяподзадача:
3. диагностика корректности результата счета.

Последнюю из упомянутых подзадач естественнее всего осуществлять пу-


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

нормальное завершение (код возврата = 0);


ошибка в формате ввода (код возврата = -1);
непредусмотренная операция (код возврата = -2);
деление на ноль (код возврата = -3).

Здесь значения кодов возврата выбраны произвольно, их конкретизация, во-


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

const int OK= 0;


const int SYNTAX_ERROR= -1;
const int BAD_OPERATION= -2;
const int ZERO_DIVISION= -3;
6.1. РАЗБОР СЛУЧАЕВ 271

Если когда-либо потребуется изменить указанные значения кодов, то сде-


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

/* переменные, представляющие аргументы */


char operation; /* переменная, представляющая операцию */
float x, y;

Для нужд второй из наших подзадач полезно определить переменную

которая будет хранить результат счета.


float result;

В данном конкретном случае можно было бы договориться о совмеще-


нии одной из переменных, представляющих аргументы, с переменной result.
Однако единственный аргумент в пользу такого совмещения — экономия па-
мяти — не выдерживает критики с точки зрения наглядности (а значит, по-
нятности) программы и потенциальной возможности ее расширения (нельзя
исключать из рассмотрения не столь уж экзотические случаи, когда при вы-
числении результата придется использовать аргументы операции несколько
раз).
С учетом приведенного комментария подзадача ввода данных в заданном
формате решается следующим образом:
printf( “Введите выражение для вычисления в виде \n“);

/* Приглашение к вводу */
printf( <число><операция><число>:\n“);

if( 3 != scanf( “%f%c%f“, &x, &operation, &y ) ) /*!!!*/

printf( "Синтаксическая ошибка!\n");


{

return SYNTAX_ERROR;

printf( “Введено выражение %f %c %f\n”, x, operation, y );


}

Многие начинающие программисты вместо строки, помеченной как /*!!!*/,


написали бы что-то подобное
272 ГЛАВА 6. РАЗВЕТВЛЕНИЕ ВЫЧИСЛЕНИЙ

с последующим анализом того, что получилось в результате выполнения вво-


scanf( "%f%c%f", &x, &operation, &y );

да. Но этот подход хуже предложенного по следующим причинам.

• Последующий содержательный анализ осмысленен, лишь если заведо-


мо знать, что введено два числа и один символ.

• Библиотечная функция scanf вырабатывает в качестве результата число


корректно введенных значений.

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

с тем, чтобы в дальнейшем не пытаться анализировать заведомо бессмыслен-


3 != scanf( “%f%c%f“, &x, &operation, &y )

ную информацию.
Подзадачу подсчета и вывода результата можно запрограммировать с ис-
пользованием набора вложенных условных операторов:

if ( operation = = ’+’ ) result = x + y;


else if ( operation = = ’-’ ) result = x - y;
else if ( operation = = ’*’ ) result = x * y;
else if ( operation = = ’/’ )
if ( y == 0 )

printf( ”Попытка деления на 0!\n”);


{

return ZERO_DIVISION;
};
else result = x / y;
else

printf (”Недопустимая операция!);


{

return BAD_OPERATION;
};

В принципе это довольно наглядно, пока случаев не слишком много. Кас-


кад вложенных условных операторов — это достаточно типичное их исполь-
зование, а потому, к примеру, в Алголе 68 для него придумана специальное
синтаксическое оформление (три варианта):

• если усл1 то действие 1 инес усл2 то действие 2 . . . иначе действие


F все
6.1. РАЗБОР СЛУЧАЕВ 273

• if усл1 then действие 1 elif усл2 then действие 2 . . . else действие F fi

• ( усл1 |действие 1 |: усл2 |действие 2 . . . |действие F )

6.1.2. Переключение

Частный случай каскада вложенных условных операторов, когда для раз-


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

• Он довольно часто может быть реализован эффективно.

Поэтому современные языки содержат оператор выбора со многими альтер-


нативами. Рассмотрим пример, как может выглядеть такой оператор выбора.

Программа 6.1.1

выбрать по значению оценка из


отчислиться;
зубы на полку;
2=>

4,5=> стипендия;
3=>

аспирантура
конец
5=>

Если списки значений в данной конструкции попарно не пересекаются, а вы-


ражение не может принимать значений, не указанных в списках, то в качестве
средства реализации такого выбора в некоторых языках есть возможность
образовать массив меток действий, индексируемый значениями из всех спис-
ков:1 .
const label[1:n] Метки=(Делай_1,. . . ,Делай_n);
Чтобы осмысленно использовать такую константу, в программе должен быть
фрагмент вида:
Приведенная ниже конструкция намеренно записана так, чтобы не принадлежать ни одно-
му конкретному языку программирования. Конструкции, практически изоморфные данной,
1

есть в языках FORTRAN и Algol 60.


274 ГЛАВА 6. РАЗВЕТВЛЕНИЕ ВЫЧИСЛЕНИЙ

Делай_1: <программа действий 1>;


go to Метка_завершения;
Делай_2: <программа действий 2>;
go to Метка_завершения;

Делай_n: <программа действий n>;


...

go to Метка_завершения;

Метка_завершения: . . .
...

В таком случае оператор выбора представляется транслятором (или про-


граммистом на машинном языке) в виде единственного оператора перехода:2
go to Метки[i];
Мы остановились на технических подробностях лишь потому, что такая реа-
лизация является единственной мотивировкой формы оператора выбора, пре-
дусмотренной в языке C. Реализация в C лишь слегка облагорожена по срав-
нению с переключателем тем, что варианты действий записываются строго
по порядку и нет возможности осуществить переход к варианту иначе, чем
через оператор выбора.
Стандарт языка C синтаксически объединяет в оператор выбора услов-
ный оператор (if-оператор) и так называемый switch-оператор, что соответ-
ствует их общему назначению: организации разветвляющихся вычислений:
<оператор выбора> ::= “if” “(” <условие> “)” <оператор>

В данном определении не отражено, что switch-оператор совместно с ва-


[ “else” <оператор> ]? |“switch” “(” <условие> “)” <оператор>

риантами действий является единой языковой конструкцией. По этой причи-


не авторы стандарта вынуждены приводить дополнительные разъяснения о
том, что <оператор> должен включать в себя операторы вида (один из вари-
антов конструкции <помеченный оператор>):
“case” <константное выражение> “:” <оператор>
либо

причем последний оператор может встречаться не более одного раза.


“default:” <оператор>

<Константное выражение> после “case” должно представлять значение,


которое может быть выработано <условием> (с учетом преобразований ти-
пов). Конструкция “default:” <оператор> выполняется, когда <условие> не
2
После которого как раз и естественно поставить метку завершения.
6.1. РАЗБОР СЛУЧАЕВ 275

вырабатывает значений всех <константных выражений> после “case” — ана-


лог варианта иначе в более логичных языках.
В С (как и в Алголе-60!) переход к выбираемому варианту не означает
того, что после завершения помеченного оператора управление будет пере-
дано в какое-то единственное для данного оператора выбора место в про-
грамме. После выполнения помеченного оператора управление передается
текстуально следующему оператору. Это правило влечет три следствия:

a) Если не выйти явно из выбранного варианта действий, то после его завер-


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

b) Следствием предыдущего в связи с тем, что в языке есть конструкция <пу-


стой оператор>, является возможность образовывать списки значений вы-
бора варианта действий. Это несколько подряд идущих фрагментов вида
“case” <константное выражение> “:”

c) Для прекращения выполнения оператора, входящего в “switch” “(” <усло-


вие> “)” <оператор>, следует использовать специальный оператор break;,
который передает управление оператору, текстуально следующему за пре-
кращаемым составным оператором (в данном случае за switch).3

Таким образом, оператор выбора в языке С может быть записан с помощью


switch-оператора, операторов с пометкой case и операторов break в при-
ближенном к предписанному в других языках виде. Правило хорошего тона
программирования: использовать switch-оператор только так, как описано
выше.
С учетом сказанного, реализация калькулятора демонстрируется програм-
мой 6.1.2, которая написана с соблюдением указанных соглашений.

Программа 6.1.2
Оператор break; является первым из рассматриваемых нами операторов структурного пе-
рехода, о которых упоминалось в главе о стилях программирования (см., напр., программу
3

3.3.1). Здесь его использование просто навязывается недоработками языка C/C++, а в дру-
гих случаях структурные переходы будут возникать как удобное средство прояснить логику
программы.
276 ГЛАВА 6. РАЗВЕТВЛЕНИЕ ВЫЧИСЛЕНИЙ

#include <stdio.h>
#include <math.h>

/* определим коды возврата программы */


#define OK 0
#define SYNTAX_ERROR -1
#define BAD_OPERATION -2
#define ZERO_DIVISION -3

int main()
{
float x, y, result;
char operation;

printf( ”Введите выражение для вычисления в виде \n” );


printf(”<число><операция><число>:\n”);

/* ввод выражения с проверкой, что все три поля введены успешно*/


if ( 3 != scanf( ”%f%c%f”, &x, &operation, &y ) )

printf( ”Синтаксическая ошибка!\n”);


{

/* выход из программы с кодом ошибки */


return SYNTAX_ERROR;

printf( ”Введено выражение %f %c %f\n”, x, operation, y );


}

switch( operation )
{
case ’+’:
result = x + y;
break;
case ’-’:
result = x - y;
break;
case ’*’:
result = x * y;
break;
case ’/’:
if ( y = = 0.0 )
6.1. РАЗБОР СЛУЧАЕВ 277

printf( ”Попытка деления на 0!\n”);


{

/* выход с кодом ошибки */


return ZERO_DIVISION;

}
result = x / y;
break;

printf( ”Недопустимый код операции: %c\n”, operation );


default:

/* выход из программы с кодом ошибки */


return BAD_OPERATION;

/* нормальное завершение */
printf( ”Результат = %f\n”, result );
return 0;
}
В других языках конструкция выбора представляется более логично. На-
пример, в языке Pascal выбор реализуется синтаксически единым операто-
ром вида
case <параметр выбора> of
<Список значений_1>: <Действие_1>;
<Список значений_2>: <Действие_2>;

<Список значений_n>: <Действие_n>;


...

Здесь каждый список значений — последовательность констант того же ти-


end;

па, что и параметр выбора, разделенных запятыми.


В стандарте языка Pascal
a) не предусматривается вариант иначе (следовательно, при необходимости
программист должен сам встраивать в программу нужные проверки);
b) требуется, чтобы все значения списков были константами;
c) явно оговаривается неопределенность результата выполнения оператора
выбора, если значение выражения не входит в объединение списков зна-
чений (как всегда в подобных случаях, неопределенность ликвидирует-
ся при конкретной реализации языка так, как это удобно разработчикам
транслятора).
278 ГЛАВА 6. РАЗВЕТВЛЕНИЕ ВЫЧИСЛЕНИЙ

В расширении языка Pascal, используемом системами программирования Turbo-


Pascal и Delphi, последним членом оператора case может быть вариант else.
Аналогична трактовка оператора выбора в Алголе-68, но там разрешает-
ся и вариант иначе, и динамическое вычисление значений, определяющих
выбор действий (что логически последовательнее, но несколько затрудняет
реализацию).
Различия операторов выбора обусловлены тем, что разработчики языко
по-разному решали задачу поддержки алгоритмов трансляции. Так, схема
трансляции case стандартного языка Pascal точно соответствует переключа-
тельной схеме, а Алгол-68 требует, чтобы оптимально реализуемые варианты
case распознавались бы транслятором, что предполагает большую изощрен-
ность его реализации.

6.1.3. Типы данных, связанные с разветвлением

Каждому типу операторов соответствуют свои типы данных. Один из ти-


пов данных, связанных с разветвлением, приобрел практически абсолютный
характер для языков программирования. Это — тип логических значений,
обеспечивающий бинарный выбор и вычисления по формулам булевой ло-
гики.
Разборам случаев соответствуют структуры с объединениями (называе-
мые также вариантные структуры и просто объединения). В языках C/C++
из-за отмеченных ранее несообразностей (см. стр. 62) такие структуры прак-
тически обязаны иметь частную форму, подобную следующей:
struct tagged
{
int type_tag;
union
{
int x;
float y;
char a;
}
}

Для операций с вариантной структурой следует организовать переключе-


ние случаев по значению type_tag и в каждом случае работать с соответству-
6.1. РАЗБОР СЛУЧАЕВ 279

ющим вариантом структуры. Примером служит следующий фрагмент про-


граммы.
Программа 6.1.3

typedef struct
{
int type_tag;
union
{
int x;
float y;
char a;
}
} tagged;
void smart_read(tagged *element);
{
...
}
int main();
tagged aa;
{
smart_read(&aa);
switch (aa->type_tag)
{
case 1: {process_integer(aa->x);break;};
case 2: {process_real(aa->y);break;};
case 3: {process_char(aa->a);break;};
}
...
}

Здесь функция умного чтения распознает, какого типа значение было введе-
но, и в соответствии с этим устанавливает тег в своем результате. По значе-
нию тега выбирается представление данных, соответствующее содержимому
переменной aa.
Именно подобное представление естественно и для результата, и для ар-
гумента оператора разбора случаев, если разобраться в логическом смысле
дизъюнкции. В самом деле, когда мы разбираем случаи A1 ∨ · · · ∨ An , мы
280 ГЛАВА 6. РАЗВЕТВЛЕНИЕ ВЫЧИСЛЕНИЙ

в каждом из случаев Ai знаем, какой из результатов Bj получится, и затем


собираем эти (возможно, совершенно разнородные) члены в единую дизъ-
юнкцию B1 ∨ · · · ∨ Bk . Наоборот, разбирая дизъюнкцию, мы каждый раз
знаем, какой именно случай мы разбираем, и в зависимости от этого выби-
раем соответствующее предположение Ai . Итак, доказав дизъюнкцию, мы
создаем значение, для которого в каждый конкретный момент нам известен
и индекс выполненного варианта, и соотвествующие этому индексу данные.
А разбирая дизъюнкцию, мы перебираем все варианты, и в каждом случае
рассматриваем только те данные, которые нужны.
Решение, предагаемое в языке Pascal, намного логичнее и красивее, чем
предложенное в C/C++/C#. Как уже говорилось, в этом языке вариантная струк-
тура обязательно предполагает тег, по которому выбирается один из вариан-
тов:
Программа 6.1.4

type tag=(star,planet, comet);


type data=record
case tg: tag of
star: (sd: star_data);
planet: (pd: planet_data);
comet: (cd:comet_data);
end;
...
begin
with data do
case tg of
star: Process_star(sd);
planet: Land(pd);
comet: Investigate(cd);
end;
...
end.

В Алголе 68 было принято еще более последовательное, однако опять-


таки не доведенное до конца решение. Явного селектора при объединении
типов не задавалось, и единственным способом выбрать вариант из объеди-
нения было разобрать случаи, соответствующие объединявшимся типам, при
помощи оператора case (что в принципе правильно). Но имен у компонент
6.2. ТАБЛИЧНОЕ ЗАДАНИЕ 281

объединения не было предусмотрено, и поэтому при объединении одинако-


вые типы сливались (что уже в те времена было грубой концептуальной не-
доработкой).

ТАБЛИЧНОЕ ЗАДАНИЕ РАЗВЕТВЛЕНИЙ


И ОПЕРАТОР ВЫБОРА ДЕЙКСТРЫ
§ 6.2.

Булевы выражения, условные операторы с elif, оператор выбора — все это


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

Сегодня этот метод анализа условий, кажется, уже прочно забыт. Тем не
менее, он весьма поучителен как с точки зрения обсуждения понятий языков
программирования, так и в практическом плане. Таблицы решений — это
способ раздельного задания (обдумывания, анализа и т. п.) составных усло-
вий и действий, связанных с вариантами. Достоинством их является то, что
значительно облегчается учет всех возможных вариантов разветвлений. При
этом забота о последовательности проверок и об алгоритмизации вычисли-
тельного процесса относится к реализации данного метода, и программист
не обязан об этом думать.
В общем случае таблица решений (см. табл. 6.1) состоит из двух частей:
условий и действий. Каждое из условий представлено перечнем значений
так, чтобы в колонках таблицы оказались представлены все сочетания зна-
чений всех условий. В перечень значений чаще всего входят T — истина
и F — ложь; допускается также указание, что значение некоторого условия
для выбора действия безразлично, это отмечается знаком −. Часть действия
представлена перечнем наименований действий, а в колонках под значения-
ми условий указывается метка (Х), если действие при данном наборе значе-
ний должно быть выполнено.
Что дает программисту использование таблиц решений? Во-первых, ра-
ботая с данными и действиями, организованными таким способом, можно
гарантировать, что будут рассмотрены все возможные сочетания условий.
При желании можно написать программу, которая проверяет это свойство
автоматически. Во-вторых, таблицы решений дают метод декомпозиции за-
282 ГЛАВА 6. РАЗВЕТВЛЕНИЕ ВЫЧИСЛЕНИЙ

Условие 1
Условие 2
T T T T F
T T F F

Условие N
...
T F T F F
Действие 1
Действие 2
X X X
X X X

Действие M
...
X X

Таблица 6.1. Таблица решений

дачи: разделение анализа условий и решений о выполнении действий. Везде,


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

Пол = муж
Нравится себе
T T T T T T

Считает, что нравит-


T T T T T T

ся другим
T T T T

Считает, что не нра-


вится другим
T T T T

Не знает T T T T
Добавить к ГM
Добавить к ГP
X X

Добавить к ГT
X X

Добавить к ГW
X
X X X X X X

Таблица 6.2. Таблица решений для обработки анкет

данных следующих групп (которые можно интерпретировать, например, как


профпригодность кандидатов на менеджера, рекламного агента, техничку и
рабочего):

ГМ: Мужчины, нравящиеся себе и уверенные, что нравятся другим, и


6.2. ТАБЛИЧНОЕ ЗАДАНИЕ 283

женщины, нравящиеся себе, но которые не знают о мнении других о себе;


ГР: Все, кто себе нравится и считают, что нравятся другим;
ГТ: Женщины, которые себе не нравятся и считают, что они не нравятся другим;
ГW: Мужчины.

В соответствии с выбранными критериями принадлежности индивидуу-


ма к той или иной группе мы отмечаем в таблице нужные комбинации усло-
вий. Обработка одного опросного листа сводится к прочтению ответов, в ходе
которого выбирается действие (см. табл. 6.2).
Превращение таблиц решений в программу с условными операторами —
автоматизируемая задача. Другие достаточно легко реализуемые задачи, свя-
занные с методом таблиц решений: расстановка истинности условий, груп-
пировка (склейка) столбцов с одинаковыми действиями, перестановка столб-
цов по принципу близости действий и т. д.
При составлении таблицы решений возникает псевдопроблема: а что де-
лать, если выполнены условия двух различных действий? Нужно понимать,
что ничего страшного в этом нет. Слишком часто и в жизни мы сталкива-
емся с выбором, когда нет критериев для предпочтения одного из вариантов.
Конечно, лучше всего (для отладки программ) было бы, если программа при
наличии нескольких выполненных вариантов выбирала бы действие для ис-
полнения случайно, но важно хотя бы программисту четко понимать, что в
данном месте ему выбор конкретного действия безразличен.

6.2.2. Охраняемые команды

Э. Дейкстра превратил таблицы решений в определение условного опе-


ратора
if
A1 → S1 ,
... , (6.1)
An → Sn
fi
Этот оператор практически является таблицей решений, в которой все усло-
вия уже выписаны явно. Ai называется охраной оператора Si : доступ к Si
284 ГЛАВА 6. РАЗВЕТВЛЕНИЕ ВЫЧИСЛЕНИЙ

возможен лишь при истинности условия охраны. Пример такого оператора

температура < 0 → замерзнуть;


if

температура > 0&температура < 100 → расплавиться;


температура > 100 → закипеть;
fi

Возникает вопрос о том, что происходит, когда в операторе некоторые


случаи не предусмотрены и поданы такие данные, что ни одна из охран не вы-
полнена. Дейкстра предложил считать, что в этом случае программа должна
выдавать ошибку. Это решение хорошо теоретически, но никуда не годит-
ся практически. Если уж мы попали в непредусмотренную ситуацию, нужно
по крайней мере проанализировать ее самим и выдать толковое сообшение,
а не раздражающую всех, кроме программиста, отлаживающего программу,
строчку типа «На с. 117 модуля Dubinda не выполнены условия охран» (сра-
зу видно, что Дейкстра, предлагая решение, думал лишь о программисте, а
не о пользователе). Так что система охраняемых операторов обязана быть
полна!
Именно табличное задание операторов выбора лучше всего соответствует
их логической сути: разбору случаев в доказательстве.4 Конструкция разбора
случаев имеет вид

Пусть доказано A1 ∨ · · · ∨ An .
Если выполнено A1 , то получается B1 .

Если выполнено An , то получается Bn .


...

Значит, B1 ∨ · · · ∨ Bn .

При разборе случаев они не обязаны исключать друг друга, и получающиеся


утверждения не обязаны все различаться.5
Заметим, что в традиционной логике длительное время делалась та же концептуальная
ошибка, что и в нынешнем программировании: при разборе случаев обращалось внимание
4

на то, что случаи должны быть взаимоисключающими. Это требование избыточно в логике,
так же как и в программировании. А вот тем, что набор рассмотренных случаев должен быть
полон, пренебречь нельзя.
Наша формулировка формально эквивалентна тем, какие обычно приводятся в учебниках
логики, например, в [34].
5
6.2. ТАБЛИЧНОЕ ЗАДАНИЕ 285

6.2.3. Условные выражения

В языках Algol-60, Алгол-68, C и Java имеется очень удобная конструкция:


условные выражения. Условное выражение языка C имеет вид

(P?A:B)

где A — выражение, значение которого вычисляется в случае истинности P,


B — в случае его ложности. Во всех других языках семантика условных вы-
ражений такая же.
Заметим, что в Алголе-68 практически все операторы выдают значение
и могут использоваться в качестве составных частей выражений. Например,
допустимо выражение

i + if P then j+ := 1; F (j) else abs(k := bad; z := k) fi.

В выражениях этого языка могут стоять даже разборы случаев типа case.
Дополнительной семантической функцией разделителя последовательно вы-
полняемых операторов “;” является забывание значения предшествующего
оператора.
Языки, в которых все операторы (а иногда и описания) вырабатывают зна-
чение, называются языками с концепцией значения. Такие языки хорошо со-
гласуются с некоторыми архитектурами компьютеров, в частности, со сте-
ковой архитектурой систем Barroughs и Эльбрус, где вырабатываемые зна-
чения помещаются в стек, вершиной которого служит последнее из получен-
ных значений, а также со старыми архитектурами, где был регистр (сумматор),
в котором всегда сохранялся результат предыдущей команды. Логически кон-
цепция значения соответствует взгляду на программу как на функцию, вычи-
сляющую результат по исходным данным. Концепция значения великолепно
сочетается со структурным программированием. Но ее отсутствие в языках
Pascal и C/C++ вполне естественно. Причина этого — в типах данных.
Современное программирование немыслимо без определения новых ти-
пов и сложных структур данных. Соответственно, если рассматривать все
выражения как вырабатывающие значения, нужно было бы строго опреде-
лить приведение данных: систему неявных преобразований данных предо-
ставленного типа в данные типа, требуемого по контексту.6 А соединение
В принципе это означает, что мы должны были бы определить для каждого вида контекста,
возникающего в программе, категорию (в смысле алгебры) всех имеющихся типов данных.
6
286 ГЛАВА 6. РАЗВЕТВЛЕНИЕ ВЫЧИСЛЕНИЙ

концепции значения с анархией преобразований, царящей в C/C++, или с бес-


системными обрывками понятия приведения, имеющимися в языке Pascal,
привело бы к неисчислимым неприятностям. К концепции значения мы еще
вернемся, когда будем обсуждать циклические вычисления (см. стр. 330).
Концепция приведения была корректно выделена лишь в Алголе 68. Она
последовательно проведена в нем для значений предопределенных типов и
простейших их вариаций (например, ссылок на такие значения), и только для
них. Перенос этой концепции на вновь определяемые типы данных прорабо-
тан не был.
Есть и еще одна причина, по которой концепция значения не вошла в ши-
рокий обиход. Она ориентирует язык на вычисления в собственном смысле
этого слова и разрушает иллюзию универсальности, когда язык делается яко-
бы для любых задач, а не для некоторого их класса.
Задания для самопроверки

1. (Для языка C) Объясните, почему не стоит пользоваться непустыми


действиями после case без break, за исключением, быть может, по-
следнего из разобранных случаев, и только тогда, когда нет default.
Глава 7

Циклические вычисления

Понятие цикла появилось еще при изобретении программирования Ав-


густой Адой Лавлейс. Повторяющиеся вычисления не должны требовать по-
вторения кусков программы. Если машина и сможет выполнить миллиард
команд в секунду, то программист столько за всю жизнь не напишет. Поэто-
му исполняемое много раз нужно записать один раз. Цикл является, пожа-
луй, характерным признаком структурного программирования. В этом стиле
программирования циклам уделяется наибольшее внимание по сравнению с
другими стилями.

§ 7.1. МОТИВАЦИЯ ЦИКЛИЧЕСКИХ ВЫЧИСЛЕНИЙ

Начнем с рассмотрения примера.


Программа 7.1.1

/*Простой калькулятор. Развитие программы 6.1.2*/

#include <stdio.h>
#include <math.h>

/* определим коды возврата программы */


#define OK 0
#define SYNTAX_ERROR -1
#define BAD_OPERATION -2
#define ZERO_DIVISION -3

int main()

287
288 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

{
float x, y, result;

char p; // для организации цикла


char operation;

printf(”Введите выражение для вычисления в виде \n”);


do {

/* ввод выражения с проверкой, что все три поля введены успешно */


printf( ”<число><операция><число>:\n”);

if( 3 != scanf( "%f%c%f", &x, &operation, &y ) )

printf( ”Синтаксическая ошибка!\n”);


{

return SYNTAX_ERROR; /* выход из программы с кодом ошибки */

printf( ”Исходное выражение: %f %c %f\n”, x, operation, y );


}

switch( operation )
{
case ’+’:
result = x + y;
break;
case ’-’:
result = x - y;
break;
case ’*’: result = x * y;
break;
case ’/’:
if( y = = 0.0 )

printf( ”Деление на ноль!\n”);


{

return ZERO_DIVISION;
}
result = x / y;
break;
default:
printf( ”BAD_OPERATION: %c\n”, operation );
return BAD_OPERATION;
}
7.1. МОТИВАЦИЯ ЦИКЛИЧЕСКИХ ВЫЧИСЛЕНИЙ 289

printf( ”result = %f\n”, result );


do

printf ("Следующее вычисление? (y/n) \n");


{

scanf ("%c", &p );


}
while ( ( p !=’y’) && ( p !=’n’) );

return 0; /* нормальное завершение */


if ( p = = ’n’ )

}
while ( true );
}

Проанализируем эту программу.

1. Обрабатывается последовательность троек вида:

которая берется из потока входных данных. Это — одна из наиболее


<число> <операция> <число>,

типичных ситуаций циклической обработки. Очередные данные тре-


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

2. Используется “бесконечный” цикл, который организован как искусствен-


но прерываемая в нужные моменты (для завершения работы) последо-
вательность одинаковых действий.

3. Программа прекращает работу в случае получения специального значе-


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

4. Появление цикла ставит новые задачи. Например, прекращение выпол-


нения программы в случае ошибки ввода не очень хорошо с пользо-
вательской точки зрения: желательно сделать попытку нейтрализации
ошибки (например, с помощью предложения пользователю повторить
ввод). До введения цикла задача нейтрализации ошибок не возникала.
Попытки изменить программу (например, в части ввода выражения с
290 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

проверкой) приводят к выводу о том, что непосредственное примене-


ние функции scanf не годится. Чтобы нейтрализовать ошибки, можно
сначала прочитать всю строку ввода, обрабатываемую за одну итера-
цию, а затем анализировать ее (подобно тому, как поступает типич-
ный транслятор), или же добиться аналогичного эффекта в ходе чтения
строки ввода.
5. Для организации цикла используется конструкция
do <оператор > while <выражение>

которая предполагает проверку необходимости завершения цикла по-


сле выполнения итерации — так называемый цикл с постусловием. В
таком цикле <выражение> рассматривается в качестве условия, истин-
ность которого предписывает продолжение выполнения цикла (пере-
ход к новой итерации), ложность — завершение цикла.
Для данного примера, в котором необходимость завершения или продолже-
ния выполнения цикла фактически определяется не на основе проверки вы-
ражения-условия, а в ходе итерационных действий, выбор вида цикла почти
безразличен. С тем же эффектом можно было использовать цикл с преду-
словием, в котором сначала выясняется необходимость выполнения первой
итерации:

где <выражение-условие> — <выражение>, вырабатывающее логическое зна-


while ( <выражение-условие> ) do <оператор>

чение.
Эти два вида цикла взаимозаменяемы. Цикл с предусловием эквивален-
тен условному оператору, у которого в качестве <Действия T> стоит цикл с
постусловием :
while <выражение-условие> do if (<выражение-условие>) do

условие>
<оператор> <оператор> while <выражение-

В данном примере с тем же эффектом, что и в предыдущем случае, можно


было использовать цикл for:
“for” “(” <инициализирующий оператор> <выражение-условие> “;”
<выражение пересчета> “)” <оператор>
где <инициализирующий оператор> — любой, в том числе и пустой, опе-
ратор, назначение которого — задание начальных значений переменных для
выполнения цикла,
7.1. МОТИВАЦИЯ ЦИКЛИЧЕСКИХ ВЫЧИСЛЕНИЙ 291

<выражение пересчета> — выражение, предназначенное для подготовки


новых значений для очередной итерации.
Цикл for эквивалентен следующему оператору:

{ <инициализирующий оператор>

<выражение пересчета>;
while ( <выражение-условие> ) { <оператор>

}}

Вид заголовка цикла for для данного примера —

поскольку ни инициализирующего оператора, ни выражения-условия, ни вы-


for (;;)

ражения пересчета не требуется.


Предписания стандарта языка говорят только о том, что результаты толь-
ко что приведенных вычислений будут совпадать. Но стандарт оставляет от-
крытым вопрос о том, каким путем достигается совпадение результатов. В
частности, фрагмент

согласно семантике, требует проверки истинности выражения True. Практи-


while ( True )

чески все производственные трансляторы ‘умеют догадываться’ о бессмы-


сленности такой проверки и удаляют ее из объектного кода, но гарантировать
этого нельзя. По этой причине для организации бесконечных циклов предпо-
чтительнее пользоваться конструкцией for (;;).
Циклы разных видов нужны для наглядности и выразительности про-
граммирования и для облегчения оптимизации объектного кода.
Пример 7.1.1. Рассмотрим цикл на языке C++

Этот заголовок указывает, что переменная i вне цикла не может быть исполь-
for ( int i = 0; i <= 10; i ++ ) <оператор>

зована — она создается для применения внутри оператора. Как следствие,


трансляторы имеют все основания поместить ее на одном из быстрых ре-
гистров, не заботясь о сохранении значения переменной после завершения
цикла. Из данной записи цикла видно, что переменная i носит служебный,
вспомогательный характер. Перенос описания куда бы то ни было вверх по
тексту лишь нарушает понимание программы и препятствует неявной авто-
матической оптимизации.
Конец примера 7.1.1.
292 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

§ 7.2. ПОТОКОВАЯ ОБРАБОТКА

Есть очень много типовых приемов программирования циклов, которые


могут быть объединены под общим названием потоковой обработки. Смысл
ее в том, что данные, перерабатываемые в цикле, можно представить в виде
некоторой последовательности — потока, а саму работу цикла вместе с его
подготовкой и завершением — как выполнение следующих шагов:
a) ИНИЦИАЛИЗАЦИЯ — действия, готовящие обработку потока;
b) Проверка, не исчерпан ли поток. Обозначается в таблицах как ПОТОК НЕ
ИСЧЕРПАН. Производит вычисление условия, указывающего на необхо-
димость выполнения оператора цикла или на его прекращение;
c) ПОРОЖДЕНИЕ ОЧЕРЕДНОГО ЭЛЕМЕНТА — создание, выделение, чте-
ние и тому подобные действия, в результате которых определяется эле-
мент для обработки;
d) ОБРАБОТКА ЭЛЕМЕНТА — действия, связанные с отдельным элемен-
том потока;
e) ЗАВЕРШЕНИЕ — действия, которые надлежит выполнить после оконча-
ния итераций.
Указанные шаги могут комбинироваться различным образом, и в результате
можно составить несколько схем потоковой обработки. Они перечислены в
таблицах, идущих ниже.

Схема Пример
ИНИЦИАЛИЗАЦИЯ printf(”Введите N:”);

пока ПОТОК НЕ ИСЧЕРПАН


scanf(”%i”,&N);S = I = 0;

цикл
while (i < N) {

ПОРОЖДЕНИЕ ОЧЕРЕДНОГО
ЭЛЕМЕНТА
I++;

ОБРАБОТКА ЭЛЕМЕНТА
конец цикла
S = S + I;

ЗАВЕРШЕНИЕ printf(”Сумма %i чисел = %i \n”, i, S


}

);

Таблица 7.1. Инициализация не включает порождение


7.2. ПОТОКОВАЯ ОБРАБОТКА 293

Схема Пример
ИНИЦИАЛИЗАЦИЯ,
ВКЛЮЧАЮЩАЯ ГЕНЕРАЦИЮ
printf(”Введите N:”);

пока ПОТОК НЕ ИСЧЕРПАН


scanf(”%i”,&N); S = 0;I = A = 1;

цикл
while (i <= N) {

ОБРАБОТКА ЭЛЕМЕНТА
ПОРОЖДЕНИЕ ОЧЕРЕДНОГО
S = S + A;

ЭЛЕМЕНТА
I++; A = I*I;

конец цикла
ЗАВЕРШЕНИЕ printf(“Сумма квадратов %i
}

чисел = %i \n“, i, S );
Разграничение порождения и об-
работки довольно условно. Напри-
S = 0; I = 1; A =0;

мер, стоит ли считать A=I*I; частью


while (i <= N)

обработки? Здесь продемонстри-


{ I++; S = S + A; A = I*I;}

рован вариант, где порождение и


обработка смешаны.

Таблица 7.2. Инициализация включает порождение

7.2.1. Порождение элементов потока

Способы порожения очередного элемента потока можно расклассифици-


ровать следующим образом:
a) независимая генерация, когда порождение никак не связано с алгоритмом
обработки. Характерный пример — использование вводимой последова-
тельности;
b) функциональная генерация, когда очередной элемент вычисляется как функ-
ция от его порядкового номера. Пример такой генерации — I++; A = I*I;
c) рекуррентная генерация, когда очередной элемент вычисляется как функ-
ция от предыдущего (в более сложных случаях — от нескольких преды-
дущих элементов). Пример — X = X / 10;
d) смешанная генерация, в которой сочетаются предыдущие виды генера-
ции.
Практические задачи часто требуют использования смешанной генерации
потока. Выделение других видов потоковой обработки преследует методи-
294 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Схема Пример
ИНИЦИАЛИЗАЦИЯ, ВКЛЮЧА- printf("Input X:"); scanf("%i",&X);
ЮЩАЯ ПОРОЖДЕНИЕ
цикл ОБРАБОТКА ЭЛЕМЕНТА
ПОРОЖДЕНИЕ
do printf ("%c", ’0’ + X % 10);

пока ПОТОК НЕ ИСЧЕРПАН


X = X / 10;

ЗАВЕРШЕНИЕ
while ( X != 0 )

Распечатывается обращенная последовательность цифр числа. Поток — X,


printf("\n");

X/10, (X/10)/10, . . . , 0. Обработка — печать остатка от деления X на 10.


Таблица 7.3. Инициализация включает порождение. Используется цикл с по-
стусловием

Схема Пример
ИНИЦИАЛИЗАЦИЯ
цикл ПОРОЖДЕНИЕ ЭЛЕМЕН-
printf(”Продолжить? (y|n)\n”);

ТА
do scanf ( "%c", c );

ОБРАБОТКА ЭЛЕМЕНТА // ОБРАБОТКА ОТСУТСТВУЕТ


пока ПОТОК НЕ ИСЧЕРПАН
ЗАВЕРШЕНИЕ
while (c != ’y’)||(c != ’n’)
if ( c = =’n’ ) return 0;

Таблица 7.4. Инициализация не включает порождение. Используется цикл с


постусловием

ческую цель: облегчить составление программ за счет разделения процесса


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

ai = exp(i ∗ ln(a))
7.2. ПОТОКОВАЯ ОБРАБОТКА 295

(функциональная генерация), либо с использованием рекуррентного соотно-


шения:
ai = ai−1 ∗ a.
Очевидно, что второй способ более эффективен. В данном случае он и более
нагляден из-за того, что нет нужды специально придумывать, как вычислять
степень. Более высокая эффективность рекуррентной генерации по сравне-
нию с функциональной связана с тем, что предшествующие элементы потока
так или иначе должны быть вычислены, и нерационально терять информа-
цию, полученную на предыдущих шагах цикла, если ее можно продуктивно
использовать.
Этот довод совершенно теряет силу, когда не требуется организовывать
поток. Так, если действие извлечения (или вычисления) очередного элемен-
та требует меньше ресурсов по сравнению с рекуррентным счетом, то потеря
прежней информации оправдана. К примеру, при больших i вычисление ai по
формуле exp(i ∗ ln(a)) может осуществиться быстрее, чем при циклическом
накоплении результата. Разумеется, это верно только тогда, когда в алгорит-
мы вычисления функций exp и ln не заложена потоковая генерация, эффек-
тивность которой сопоставима с рекуррентным накопительным умножением

Конец примера 7.2.1.


(ai−1 ∗ a).

Рекуррентная генерация очень полезна при решении задач суммирования


или другого накопительного вычисления, связанного с последовательностя-
ми.
Пример 7.2.2. Пусть требуется подсчитать сумму ряда:

a2 a4 a6 a2i
S =1− + − + · · · + (−1)i + ··· .
1 1∗2 1∗2∗3 1 ∗ 2 ∗ ··· ∗ i
Два варианта программы приведены в таблице 7.5. Главный недостаток пер-
вого алгоритма: функциональная генерация элементов суммируемого потока
приводит к повторному счету. На каждой итерации внешнего цикла во вну-
треннем цикле заново считаются составляющие членов ряда, предшествую-
щих искомому (генерация локального по отношению к задаче суммирования
потока троек чисел sn , u и f ). Вместе с тем, рекуррентное соотношение:

a2 −a2
ti = ti−1 ∗ (−1) = ti−1 ∗
i i
296 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Плохая программа Хорошая программа


/* Смысл переменных, которые не /* Суммирование */
прокомментированы см. в хоро-
шей программе */
#include <stdio.h>
#include <math.h>
... int main()

float a; // параметр
I = 0; t = 1; {

float a2; // квадрат параметра


S = t; a2 = a * a;

float t = 1; // член ряда


while ( abs ( t ) > e ) {

u = 1; // числитель float S = 1; // сумма


I ++;

f = 1; // знаменатель float e; // точность


sn= 1; // переменная для знака int I = 0; // номер члена ряда
k = 1; // служебная переменная printf( "Введите a и e:");

sn = - sn; // знак члена


while ( k <= I ) { scanf ( "%f %f", &a, &e );
while ( abs ( t ) > e )
a2 = a * a; {
u = u * a2 I ++;

/* Для повышения точности сче-


f = f / k; t = - t * a2 / I;

та вместо I!, которое при больших


S = S + t;

I быстро дает переполнение, вы-


числяется 1/I!; что позволяет про-
суммировать большее число чле-
нов ряда */

printf("\nСумма ряда = %f\n",S);


k ++; }

t = sn * u * f; S = S + t;} // конец внеш-


}

него цикла
return (0);

... }

Таблица 7.5. Программа для суммирования ряда


7.2. ПОТОКОВАЯ ОБРАБОТКА 297

показывает, что формирование внутреннего потока (цикла) избыточно.


Конец примера 7.2.2.
Программа из таблицы 7.5 демонстрирует использование и других типовых
приемов программирования. Перечислим некоторые из них, связанные с ци-
клической обработкой:
1. объединение циклов: вместо логически последовательного выполнения
нескольких циклов задается, когда это возможно, цикл с общими вы-
числениями;
2. чистка цикла: вынесение из цикла вычислений, которые на всех ите-
рациях оказываются одинаковыми;
3. повышение точности за счет преобразования формул для вычислений.1
4. знакопеременное суммирование. Существует две схемы: умножение на
−1 и использование знакового флага (F =-F;).

7.2.2. Фильтрация потока

Вообще говоря, использование рекуррентной генерации предпочтитель-


нее по сравнению с функциональной, однако далеко не всегда это возможно.
Пример 7.2.3. Пусть требуется напечатать N первых простых чисел (т. е.
делящихся нацело только на себя и на единицу). Функциональная генера-
ция для решения задачи неприменима, так как не существует просто опи-
сываемой функции, вычисляющей простое число по его номеру. Для реше-
ния данной задачи можно воспользоваться следующим приемом: во-первых,
заменить алгоритмически трудную генерацию последовательности простых
чисел генерацией охватывающего, включающего потока (мы будем генери-
ровать нечетные числа), и во-вторых, встроить в обработку фильтрацию: ис-
ключение составных чисел. В программе Prime_Numbers_1 фильтрация за-
дается как локальная потоковая обработка, в ходе которой проверяется, де-
лится ли анализируемое число на какое-либо из предшествующих чисел.
Нужно помнить, что математические эквивалентные преобразования и изоморфизмы
структур являются важнейшим компонентом культуры программиста. Переход к эквива-
1

лентной формулировке в математике соответствует переходу к другому представлению дан-


ных либо действий в программировании. Выигрыш, получаемый за счет того, что приме-
няется лучшая математическая формулировка, часто в порядки раз превышает выигрыш от
оптимизации самой программы.
298 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Программа 7.2.1
/* Prime_Numbers_1. Распечатка первых N простых чисел */
#include <stdio.h>
#include <math.h>

int main(void)

int N,i,j,k,Prime; /* назначение определяемых переменных


{

описывается при их использовании */


printf("Введите N:");

/* ОБЩАЯ ЧАСТЬ ВЫВОДА РЕЗУЛЬТАТА */


scanf("%i",&N);

printf("При N <= 0 простых чисел не существует");


if (N<=0)

printf("При N = 1 единственное простое число - 1");


else if(N= =1)

else

printf("Последовательность первых %i простых чисел есть: ",N);


{

printf("1,2"); /* Первые два простых числа выведены,


рассмотрены особые случаи и выведено
начало искомой последовательности */

/* ОБРАБОТКА ПОТОКА НЕЧЕТНЫХ ЧИСЕЛ: */


// ИНИЦИАЛИЗАЦИЯ потока нечетных чисел:

k=2; // k — счетчик напечатанных простых чисел


i=1;

while (k<N)

// ГЕНЕРАЦИЯ очередного нечетного числа:


{

// ФИЛЬТРАЦИЯ (проверка, является ли i простым)


i+=2;

/* оформлена как ОБРАБОТКА ЛОКАЛЬНОГО ПОТОКА */


// ИНИЦИАЛИЗАЦИЯ локального потока
j=3; /* 3 — это первое число, делимость
на которое надо проверять */
Prime=1; // переменная-индикатор делимости
7.2. ПОТОКОВАЯ ОБРАБОТКА 299

/* достаточно проверять делимость не для всех j,


while (j<=sqrt(i) && Prime) {

а лишь для тех, которые не превосходят


i — факт из теории чисел;

цикл проверки прекращается, когда


обнаружена делимость i на j */
// ОБРАБОТКА элемента локального потока

// ГЕНЕРАЦИЯ элемента локального потока


Prime=i%j;

/* так как i нечетно, проверяется делимость


j+=2;

только на нечетные числа, поэтому локальный


поток строится только из нечетных чисел */

// ФИЛЬТРУЮЩЕЕ УСЛОВИЕ
}

// ОБРАБОТКА потока простых чисел


if (Prime) {

//(отфильтрованные нечетные)
k++;
printf(",%i",i);

} // конец ОБРАБОТКИ ПОТОКА НЕЧЕТНЫХ ЧИСЕЛ


}

}
printf(".\n");
return 0;
}

В программе Prime_Numbers_1 имеются избыточные вычисления. В частно-


сти, в цикле

while (j<=sqrt(i) && Prime)


{ Prime=i%j; j+=2; }

много излишних√проверок — достаточно поверять делимость не на все j, не


превосходящие i, а лишь на простые, т. е. на те, которые ранее уже были
найдены и напечатаны. Это — одно из многих обстоятельств, которое заста-
вляет нас писать программы, запоминающие не только те данные, которые
непосредственно нужны для следующего шага, но и историю вычислений.
300 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Конец примера 7.2.3.


Использование фильтров, или фильтрованная обработка — это широко ис-
пользуемый в программировании типовой прием. Он применяется, когда не-
возможно или неудобно говорить о точном вычислении какой-либо обраба-
тываемой структуры данных (в приведенной программе — поток), но можно
построить объемлющую структуру и предложить условие отбраковки (фильтр),
которое выделяет требуемую для переработки информацию.
Общие схемы фильтрованной потоковой обработки можно получить из
схем простой обработки потока путем незначительной модификации: доба-
вления проверки некоторого условия. С формальной точки зрения, понятие
фильтрованной обработки потока избыточно: это частный случай простой
потоковой обработки, если проверка условия считается частью обработки
элемента. Цель введения фильтрованной обработки методическая — она по-
могает разбивать решаемую задачу на относительно более простые подзада-
чи. Это хорошо видно на примере программы Prime_Numbers_1.

Схема Пример
ИНИЦИАЛИЗАЦИЯ printf("Введите

пока ПОТОК НЕ ИСЧЕРПАН


N:");scanf("%i",&N);S = I = 0;

цикл
while (I < N) {

ПОРОЖДЕНИЕ ОЧЕРЕДНОГО
ЭЛЕМЕНТА
I++;

если ФИЛЬТРУЮЩЕЕ УСЛО- if ( I/10 % 2 ) // вторая


ВИЕ
то ОБРАБОТКА ЭЛЕМЕНТА S = S + I; // цифра
конец цикла } // нечетная
ЗАВЕРШЕНИЕ printf("Сумма = %i \n", S );

Таблица 7.6. Фильтрация. Инициализация не включает порождение

7.2.3. Потоки и данные

В структурном программировании операторы тесно связаны с обрабаты-


ваемыми данными, в частности, для каждого оператора имеются те типы дан-
ных, которые естественно использовать именно в связи с данным операто-
ром. Иногда эти типы данных явно включаются в языки программирования
7.2. ПОТОКОВАЯ ОБРАБОТКА 301

Схема Пример
ИНИЦИАЛИЗАЦИЯ, ВКЛЮЧА-
ЮЩАЯ ГЕНЕРАЦИЮ
printf("Введите N:");
scanf("%i",&N);S = 0; I = A =

пока ПОТОК НЕ ИСЧЕРПАН


1;

цикл
while (I <= N) {

если ФИЛЬТРУЮЩЕЕ УСЛО-


ВИЕ
if ( I/10 % 2 )

то ОБРАБОТКА ЭЛЕМЕНТА
ПОРОЖДЕНИЕ ОЧЕРЕДНОГО
S = S + A;

ЭЛЕМЕНТА
I++; A = I*I;

конец цикла
ЗАВЕРШЕНИЕ
}
printf("Сумма = %i \n", S );

Таблица 7.7. Фильтрация. Инициализация включает порождение

и, соответственно, в программы (например, альтернативные структуры для


операторов выбора); иногда они не включаются в них вообще, оставаясь при-
зраками в чистом виде (например, счетчики шагов для циклов с пред- или
постусловием); иногда такие типы данных включаются частично и непосле-
довательно. Именно последний случай имеет место для потоков, и, в связи
с этим, мы в данном параграфе рассматриваем некоторые из типов данных,
которые могут представлять потоки в конкретных программах.
Иногда строгая потоковость препятствует эффективности (см., например,
программу Prime_Numbers_1, при обсуждении которой упоминалось о по-
вторном счете). Поток — это, прежде всего, локальная организация обра-
ботки: программа “забывает” о том, что делалось с предыдущими элемен-
тами потока. Естественно, что языки программирования предусматривают
возможность запоминать информацию о том, что уже происходило. Рассмо-
трим пример (еще одна модернизация программы 7.2.1 Prime_Numbers_1),
показывающий, как можно ликвидировать повторный счет.
Программа Prime_Numbers_1 производит лишний счет, когда пытается
проверить делимость числа на все нечетные числа из диапазона 2 6 i 6
k . Легко видеть, что достаточно проверять делимость на уже вычислен-
h√ i

ные простые числа. Чтобы не потерять их, нужна дополнительная память.


Таким образом, в рамках задачи о простых числах выделяется подзадача со-
хранения получаемой информации и ее использования. Для этой подзадачи,
302 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Схема Пример
ИНИЦИАЛИЗАЦИЯ, ВКЛЮЧА-
ЮЩАЯ ГЕНЕРАЦИЮ
printf("Введите N:");
scanf("%i",&N);S = 0; I = A =

цикл если ФИЛЬТРУЮЩЕЕ


1;

УСЛОВИЕ
do if ( I/10 % 2 )

то ОБРАБОТКА ЭЛЕМЕНТА
ПОРОЖДЕНИЕ ОЧЕРЕДНОГО
S = S + A;

ЭЛЕМЕНТА
I++; A = I*I;

пока ПОТОК НЕ ИСЧЕРПАН


ЗАВЕРШЕНИЕ
while (I <= N);
printf("Сумма = %i \n", S );

Таблица 7.8. Фильтрация. Инициализация включает порождение. Использу-


ется цикл с постусловием

как и для всех2 задач, можно ставить следующие вопросы:


a) Какова цель решения? Для чего будет нужно полученное решение? При-
менительно к данной подзадаче: для чего нужно запоминать получаемые
простые числа?

b) Как будет использовано решение? Это включает анализ возможных вари-


антов применения конструируемой программы. Применительно к данной
подзадаче: как найденные и запомненные простые числа будут участво-
вать в процессе решения объемлющей задачи?

c) Как решается поставленная задача? Этот вопрос ставится последним,


только после уяснения ответов на два предыдущих. Для подзадачи о за-
поминании простых чисел вопрос конкретизируется следующим образом:
каковы способы хранения получаемых данных и их извлечения?

Цель решения в учебных задачах, как правило, формулируется достаточно


точно3 (в задаче о простых числах — сокращение цикла проверки делимо-
сти). Что касается второго вопроса, то его решение может повлечь за со-
бой такие следствия, которые влияют на критерии оценки решения задачи.
Не только для программистских! Эти вопросы являются частью системного и логического
подхода к проблеме.
2

В реальной практике, как правило, она формулируется неточно и неполно, а в худшем


случае прямо ошибочно.
3
7.2. ПОТОКОВАЯ ОБРАБОТКА 303

Схема Пример
ИНИЦИАЛИЗАЦИЯ printf("Введите N:");
scanf("%i",&N);S1 = 0; S2 = 0;

пока ПОТОК НЕ ИСЧЕРПАН


I = 0;

цикл
while (I < N) {

ПОРОЖДЕНИЕ ОЧЕРЕДНОГО
ЭЛЕМЕНТА
I++;

если РАЗДЕЛЯЮЩЕЕ УСЛОВИЕ


то ОБРАБОТКА 1 ЭЛЕМЕНТА
if ( I % 2 )

иначе ОБРАБОТКА 2 ЭЛЕМЕН-


S1 += I;

ТА
else S2 += I;

конец цикла
ЗАВЕРШЕНИЕ printf("Сумма 1 = %I, Сумма 2 = %I
}

\n", S1, S2 );

Таблица 7.9. Разделение обработки на два потока

Применительно к рассматриваемому случаю распечатки первых N простых


чисел, по-видимому, ничего принципиально нового за счет запоминания не
получить. Но если чуть изменить постановку исходной задачи и сформули-
ровать ее как распечатку N -ого простого числа, то сам способ запоминании
промежуточных результатов может повлиять на стратегию решения вообще.
Так, если говорить о долговременном запоминании, то конструируемая про-
грамма может рассматриваться с совершенно новых позиций:
• Расширение масштаба решаемой задачи. Для выявления простоты в
этом случае достаточно проверять делимость не на все числа, меньшие
проверяемого числа (пусть даже нечетные), а только на простые числа.
Ранее идея могла возникнуть лишь умозрительно, поскольку распеча-
тывание всех простых чисел с номерами, меньшими либо равными N ,
количественно ограничивает задачу. Ее просто не имеет смысла решать
при таких N , при которых эффект сокращения проверок не будет ска-
зываться. Развитие идеи сокращения проверок за счет хранения данных
приводит к мысли о том, что можно обойтись вообще без деления, за-
менив его последовательным вычеркиванием составных чисел, как это
делается в решете Эратосфена;
• Замена вычисления извлечением значения. При многократном поиске
304 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Схема Пример
ИНИЦИАЛИЗАЦИЯ
пока ПОТОК НЕ ИСЧЕРПАН
int ch; int Sd=0, Sl=0, S=0;

цикл
while ((ch=getchar())!=’\n’) {

//генерация вместе с проверкой


ПОРОЖДЕНИЕ ОЧЕРЕДНОГО
ЭЛЕМЕНТА
если РАЗДЕЛЯЮЩЕЕ УСЛОВИЕ if (’0’<=ch && ch<=’9’)

то ОБРАБОТКА 1 ЭЛЕМЕНТА
1

иначе если РАЗДЕЛЯЮЩЕЕ


Sd ++;

УСЛОВИЕ 2
else if (’a’<=ch && ch<=’z’)

то ОБРАБОТКА 2 ЭЛЕМЕНТА
иначе если РАЗДЕЛЯЮЩЕЕ
Sl ++;

УСЛОВИЕ 3

иначе АЛЬТЕРНАТИВНАЯ ОБ-


... ...

РАБОТКА
else S ++;

конец цикла
ЗАВЕРШЕНИЕ
}
printf ("Sd=%i, Sl=%i, S=%i,\n
Total=%i \n",Sd,Sl,S,Sd+Sl+S);

Таблица 7.10. Разделение обработки на несколько потоков. Используются


условия
7.2. ПОТОКОВАЯ ОБРАБОТКА 305

Схема Пример
ИНИЦИАЛИЗАЦИЯ
пока ПОТОК НЕ ИСЧЕРПАН
int ch; int Sa=0, Sb=0, S=0;

цикл
while ((ch=getchar())!=’\n’)

ПОРОЖДЕНИЕ ОЧЕРЕДНОГО { //генерация вместе с проверкой


ЭЛЕМЕНТА
Выбор РАЗДЕЛЯЮЩЕЕ выраже-
ние из
switch ( ch ) {

СПИСОК ЗНАЧЕНИЙ 1 :
ОБРАБОТКА 1 ЭЛЕМЕНТА
case ’a’:

СПИСОК ЗНАЧЕНИЙ 2 :
Sa ++; break;

ОБРАБОТКА 2 ЭЛЕМЕНТА
case ’b’:

СПИСОК ЗНАЧЕНИЙ 3 :
Sb ++; break;

иначе АЛЬТЕРНАТИВНАЯ ОБ-


... ...

РАБОТКА
default: S ++; }

конец цикла
ЗАВЕРШЕНИЕ
}
printf ("Sa=%i, Sb=%i, S=%i,\n
Total=%i \n",Sa,Sb,S,Sa+Sb+S);

Таблица 7.11. Разделение обработки на несколько потоков. Используется


оператор выбора

N -ых простых чисел для использования (распечатки) запоминание по-


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

• Прерывание-возобновление вычислительного процесса. Нет необходи-


мости запускать повторный поиск простого числа с самого начала, ко-
гда найдены некоторые предшествующие ему числа. Можно просто за-
пустить продолжение ранее прерванного процесса.

Из этого видно, что стратегия решения задач существенно зависит от того,


какие средства программист предполагает использовать для запоминания.
Применительно к задаче о простых числах первое, что стоит проанали-
зировать, — это возможность использования массива как структуры данных
языка в качестве хранилища ранее найденных простых чисел.
306 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Схема Пример
ИНИЦИАЛИЗАЦИЯ
пока ПОТОК НЕ ИСЧЕРПАН
char ch; int I=0;

цикл
while ((ch=getchar())!=’\n’)

ПОРОЖДЕНИЕ ОЧЕРЕДНОГО { //генерация вместе с проверкой


ЭЛЕМЕНТА
ПРЕДОБРАБОТКА ЭЛЕМЕНТА
Если УСЛОВИЕ ВЫХОДА
I ++;

То ОБРАБОТКА 1 ЭЛЕМЕНТА
if ( ch = = ’a’ ) {

ВЫХОД ИЗ ЦИКЛА
printf ("№первого a=%i",I);

ПОСТОБРАБОТКА ЭЛЕМЕНТА // постобработка отсутствует


break;

конец цикла
ЗАВЕРШЕНИЕ
}

printf ("букв a нет!");


if ( ch = = ’\n’)

Таблица 7.12. Используется принудительный выход из цикла (пример)

Программа 7.2.2

/* Prime_Numbers_2. Распечатка N -ого простого числа,


использование массива */
#include <stdio.h>
#include <math.h>
int N,i,j,k;
int Prime;
int a[1000];
int main(void) {
printf("Write N:");
scanf("%i",&N);
a[1]=1;
a[2]=2;
k=2;

printf("При N <= 0 простых чисел не существует \n");


if (N<=0)

else {
i=1;
while (k<N) {
i+=2;
7.2. ПОТОКОВАЯ ОБРАБОТКА 307

Схема Пример
Если в языке можно использовать :МеткаУровня: пока
операторы завершения, то про-
грамма улучшается: цикл если ( ch = = ’a’ )
((ch=getchar())!=’\n’)

то printf ("№первого a=%i",I);


— нет нужды в каскадном за- завершить МеткаУровня
вершении; все
— исчезают избыточные про- иначе printf ("букв a нет!");
верки; // иначе относится к циклу
— повышается наглядность все
программы. /*Конструкция цикла и условий
взята из языка ЯРМО */

Таблица 7.13. Цикл с выходом из уровня

for( j=2, Prime = true; Prime && a[j]<=sqrt(i); j++)


Prime = i%a[j];
if (Prime) a[++k] = i;

printf("%i-ое простое число = %i\n", N, i);


}

}
return 0;
}

Эта программа не сложнее Prime_Numbers_1 (внешне она выглядит даже


проще, т. к. текст не перегружен комментариями, использованными ранее
для разъяснения потоковой обработки, нет нужды в общей части вывода ре-
зультата, вместо которой инициализируются два первых значения массива и
др.) Не следует опасаться неэффективности в связи с тем, что в условии за-
вершения цикла for выражение обращается к вызову функции (sqrt(i)). Все,
что может быть вычислено вне итераций цикла, будет вынесено транслято-
ром до цикла. По аналогичным причинам, значение переменной с индексом
a[j] не будет извлекаться из массива дважды. Несколько сложнее дело обстоит
с двойным вычислением истинности Prime: во время проверки условия окон-
чания цикла и после цикла. Не всякий транслятор сможет выстроить вычи-
сления так, чтобы исключался повторный счет. Однако, даже если в данной
системе программирования такая оптимизация не предусматривается, это не
страшно: лишняя проверка условия выполняется вне внутреннего цикла, а
308 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Схема Пример
ИНИЦИАЛИЗАЦИЯ
пока ПОТОК НЕ ИСЧЕРПАН
char ch;

цикл
while ((ch=getchar())!=’\n’)

ГЕНЕРАЦИЯ ОЧЕРЕДНОГО { //генерация вместе с проверкой


ЭЛЕМЕНТА
ПРЕДОБРАБОТКА ЭЛЕМЕНТА // предобработка отсутствует
если УСЛОВИЕ ВЫХОДА
то ОБРАБОТКА 1 ЭЛЕМЕНТА // пропуск буквы a
if ( ch = = ’a’ )

ВЫХОД ИЗ ИТЕРАЦИИ
ПОСТОБРАБОТКА ЭЛЕМЕНТА
continue;

конец цикла
printf ("%c", ch);

ЗАВЕРШЕНИЕ
}
printf (’\n’);

Таблица 7.14. Используется принудительный выход из итерации (пример)

потому на эффективности вычислений она практически не скажется, а устра-


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

• Чем является данная структура, какова математическая и прагматиче-


ская сущность данной структуры?

• Как она представляется в программе?

С точки зрения математики и прагматики можно дать следующее определе-


ние.
Решение этой проблемы, убивающее двух зайцев сразу, может быть дано с помощью
структурного перехода:
4

//Присваивание, совмещенное с проверкой


for (j=2, Prime=true;a[j]<=sqrt(i);j++)
if (Prime=i%a[j])
{
a[++k]=i;
break; };
7.2. ПОТОКОВАЯ ОБРАБОТКА 309

Определение 7.2.4. Массив элементов типа T с индексами из I —функция,


перерабатывающая индексы в элементы, значения которой запоминаются при
их первом вычислении и поэтому практически не требуют ресурсов для по-
вторного вычисления.
Конец определения 7.2.4.
Итак, массив — предвычисленная функция. В таком случае присваивание
элементу массива — несколько неточное обозначение. Это на самом деле
действие над всем массивом.5 Но, в соответствии с духом структурного про-
граммирования, в котором действия локальны, можно не акцентировать вни-
мание на данном обстоятельстве, хорошо разобранном, в частности, в книге
Гриса [27].
Вторая сторона структуры данных — как ее представляют. Для нас прак-
тически неважно, что массив обычно занимает в памяти связный сегмент;
важно, что он размещается в памяти и что операция вычисления элемента
массива по его индексу очень быстрая. Линейное расположение элементов
массива — всего лишь один из способов обеспечить эти требования. Ограни-
чения, накладываемые традиционной реализацией, заложенной в стандарты
большинства языков, привели к тому, что в большинстве языков массив вос-
принимается как функция из статически заданного линейно упорядоченного
множества индексов. Поэтому естественная конструкция типа

int n;
Read(n);
int[1:n] a;

недопустима в языках Pascal и C.


Подобные возможности предоставляют Algol-60 и Алгол-68. Массивы, раз-
мер которых вычисляется и фиксируктся в момент исполнения их описания,
получили название динамических массивов, в отличие от статических мас-
сивов яызков Pascal и C. Но в ряде случаев в момент исполнения описания
массива мы еще не знаем, какого размера память будет требоваться, и тогда
хотелось бы иметь возможность наращивать массив по мере необходимости,
а также отказываться от памяти. В Алголе-68 подобные массивы называются
гибкими или подвижными (flexible).
Начало Microsoft-specific фрагмента
Поскольку такая конструкция необходима, особенно при обмене данными
5
Пожалуй, впервые на это обратил внимание Хоор в книге [29]
310 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

между удаленными программами, она была введена через “черный ход”: че-
рез тип данных Variant, который является потенциально бесконечным объ-
единением типов. В частности, значениями типа Variant могут быть гибкие
массивы, имеющие тип SafeArray. Для гибких массивов имеется специальная
операция создания такого массива. Работу с гибкими массивами иллюстри-
рует следующий фрагмент програмы.
Программа 7.2.3

unsigned int n;
float x,y;
long i;
scanf(&n,&x);
SAFEARRAY *pSa = NULL;
SAFEARRAYBOUND sabound[1];
sabound[0].lLbound = 1;
sabound[0].cElements = n;
pSa = SafeArrayCreate(VT_R4, 1, sabound);
i=5;
SafeArrayPutElement(psa, &i,&x);
SafeArrayGetElement(psa, &i, &y);
if(x!=y){return -1;}
scanf(&sabound[0].cElements);
SafeArrayRedim(psa,sabound);

Здесь мы вначале формируем структуру из граничных пар массива (он не обя-


зательно начинается с 0!) Затем создаем массив действительных чисел (тип
элементов задается константой VT_R4) нужной нам длины. Затем мы запи-
сываем в него элемент, читаем этот элемент, проверяем корректность опе-
раций,6 и, наконец, изменяем границы массива. Cпособ работы с гибкими
массивами через тип данных SafeArray стал практически общепринятым в
сегодняшней практике.
Конец Microsoft-specific фрагмента
При первом же просмотре текста программы Prime_Numbers_2, возни-
кает законный вопрос: откуда взялось число 1000 для количества элементов
массива? Обсуждение этого решения дается в таблице 7.15.
Не беспокойтесь о возможном нарушении защиты памяти! Каждая операция с SafeArray
проверяет значения индексов и не допускает их выхода за границы.
6
7.2. ПОТОКОВАЯ ОБРАБОТКА 311

Варианты ответов Возражения


a) Я не буду ис- 1. Кто-то другой (или сам автор через опреде-
пользовать числа, ленное время) окажется в роли неосведомленного
превосходящие пользователя;

2. Использование программы, как части некото-


1000

рой системы, в которой нужна реакция на все слу-


чаи вычислений;
3. Модификация и развитие программы.
b) 1000 хватит на 1. Программа ненадежна, т. к. иногда она ведет се-
все случаи жизни бя непредсказуемо;
2. Все случаи жизни предусмотреть невозможно.
c) Если надо, легко 1. Это не так, когда размеры простых чисел ока-
заменить 1000 на зываются превосходящими емкость значения ти-
большее число па int;
2. Это не так, когда требуется экономить память.
d) Почему бы не Ну вот еще, буду я пользоваться чем-то, приду-
воспользоваться манным Microsoft! Я работаю в стандарте.

Общее возражение: подмена вычисления извле-


SafeArray?

чением значения и механизм прерывания-возоб-


e) ...

новления вычислительного процесса ограниче-


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

Таблица 7.15. Обсуждение числа элементов массива


312 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Надежность программы можно (и нужно!) повысить путем встраивания


дополнительной проверки того, что N не превосходит размера массива a.
Но это не избавит от необходимости учета того, что размер ячейки может
оказаться недостаточным для хранения очередного простого числа. А если
воспользоваться типом double int для массива a, то это удвоит потребность
в памяти. Таким образом, размеры массива должны быть согласованы с раз-
мерами хранимых в нем значений.
Сократить потребности в памяти при решении запоминать ранее найден-
ные простые числа можно, если хранить не сами числа, а разности соседних
простых чисел. Разности требуют существенно меньше места, чем сами чи-
сла. Это видно уже из сопоставления указанных величин для начала после-
довательности простых чисел:
1 - 7 2 19 2 37 6 53 6 71 4 89 6 107 4
2 1 11 4 23 4 41 4 59 4 73 2 97 8 109 2
3 1 13 2 29 6 43 2 61 2 79 6 101 4 113 4

Разумеется, таких наблюдений недостаточно, требуется математическая


5 2 17 4 31 2 47 4 67 6 83 4 103 2 127 14

проработка вопроса, чтобы определить, насколько оправдан переход к разно-


стям, когда и как скажется эффект разницы роста скоростей двух последова-
тельностей и т. д. При этом надо помнить и о том, что хранение разностей
потребует дополнительно разработать алгоритм перехода от разностей к са-
мим простым числам (чуть ниже будет показано, что можно согласовать ра-
боту этого алгоритма с организацией проверки простоты очередного числа).
Еще один важный вопрос, в связи с программой Prime_Numbers_2, о том,
почему выбрана проверка простоты числа i в данном порядке. Исходные усло-
вия для организации проверки минимальны: наличие множества найденных
простых чисел, не превосходящих i. Никаких предположений об упорядо-
ченности этого множества нет. Да и не нужны они, если речь идет о матема-
тической задаче. Способ порождения этого множества таков, что его элемен-
ты появляются в соответствии с последовательностью простых чисел. Если
бы явно требовался другой порядок, его нужно было бы построить. Если же
не требуется никакой порядок, то, в частности, можно остановиться на по-
рядке порождения, как целесообразно для решаемой задачи, поскольку ве-
роятность делимости числа на малые числа выше, чем на большие. Это на-
блюдение использовано в программе Prime_Numbers_2 при выборе порядка
итераций проверочного цикла:

for( j=2, Prime = true; Prime && a[j]<=sqrt(i); j++)


7.2. ПОТОКОВАЯ ОБРАБОТКА 313

Prime = i%a[j];

Данный порядок оказывается согласованным с решением о хранении разно-


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

Программа 7.2.4

Распечатка N -ого простого числа, переиспользование вычислений.


/* Prime_Numbers_3. */
#include <stdio.h>
#include <math.h>
int N,i,j,k;
int Prime;
int a[1000];
int main(void) {
a[1]=1;
a[2]=2;
a[3]=3; //!
k=3; //!
do {
printf("Write N:");
scanf ("%i",&N);

printf("При N <= 0 простых чисел не существует \n");


if (N<=0)

else {
if (k>=N) i = a[N]; //!
else { //!
i=a[k]; //!
314 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

while (k<N) {
i+=2;
for( j=2, Prime = 1; Prime && a[j]<=sqrt(i); j++)
Prime=i%a[j];
if (Prime) a[++k]=i;
}

printf("%i-ое простое число = %i\n", N, i);


} //!

}
scanf("%c",&N);

while (N !=’n’); //признак конца счета


}

return 0;
}

В Prime_Numbers_3 курсивом отмечены строки, добавленные для организа-


ции цикла, а знаками //! выделены изменения и добавления, сделанные для
организации переиспользования.
Может показаться неочевидной модификация инициализации массива, т. е.
присваивание значения его третьему элементу, и, соответственно, оператор
k = 3; Это сделано из следующих соображений. Просмотр потока лишь не-
четных чисел делает избыточной проверку делимости на два. В то же время
надо позаботиться о том, чтобы начало просмотра новых нечетных чисел бы-
ло корректным как на первой итерации цикла, так и в дальнейшем. Иными
словами, требуется, чтобы оператор i = a[k]; приводил к нечетному i в обоих
случаях. Присваивать k единицу нельзя — на нее делятся все числа, а сле-
довательно, целесообразно выбрать минимальное простое нечетное число,
большее единицы, т. е. три.
Если требуется пользоваться результатами вычислений при разных запус-
ках программы (или в других программах) и при этом заменять вычисления
извлечениями значений, то естественным будет сохранение результатов ра-
боты программы в файле (и считывание предыдущих результатов из файла).
Изменения, требующиеся в этом случае, показаны в следующей программе.
Программа 7.2.5

/* Распечатка N -ого простого числа, файловая версия. */


/*Prime_Numbers_4. */
#include <stdio.h>
7.2. ПОТОКОВАЯ ОБРАБОТКА 315

#include <math.h>
int N,i,j,k;
int Prime;
int a[1000];
FILE *f;
int main(void) {

// Фрагмент, ответственный за
f = fopen("prime.txt","r"); //

// инициализацию массива a
fscanf(f,"%i:",&k);
for (i=1;i<=k;i++)
fscanf(f,"%i",&a[i]); //
fclose(f); //
do {
printf("Write N:");
scanf ("%i",&N);
if (N<=0)
printf("If N <= 0 then there is no prime numbers\n");
else {
if (k>=N) i = a[N];
else {
i=a[k];
while (k<N) {
i+=2;
for (j=2,Prime=true; Prime && a[j]<=sqrt(i); j++)
Prime = i%a[j];
if (Prime) {
a[++k]=i;
printf("%i ", i);
}
}
}
printf("\nIf N = %i - %i\n", N, i);
}
scanf("%c",&N);
}
while (N != ’n’);

// Фрагмент, ответственный за
{ f = fopen("prime.txt","w"); //

// долговременное хранение
fprintf(f,"%i:",k);
for (i=1;i<=k;i++)
316 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

fprintf(f,"%i",a[i]); // полученных результатов


} //
fclose(f); //
return 0;
}

Как и раньше, в этой программе курсивом выделены добавления и измене-


ния текста предыдущей программы. Стоит отметить, что фрагмент, ответ-
ственный за долговременное хранение полученных результатов, может быть
улучшен, так как нет надобности перезаписи файла, когда он фактически не
меняется.
В программе Prime_Numbers_4 получено отображение массива выявлен-
ных простых чисел на файл. В данном случае файл рассматривается не как
новая структура данных, а просто как способ быстрой инициализации без
необходимости каждый раз пересчитывать старые результаты (ведь никто не
пересчитывает первые несколько десятков знаков числа π).
Вообще говоря, файл также может являться и полноценной структурой
данных, активной единицей. Например, вместо считывания массива можно
заменить все операции обращения к нему операциями поиска в файле. Полу-
чится нечто вроде пополняемого списка простых чисел: либо находим и из-
влекаем нужное, либо добавляем новые простые числа. Это показывает, что
файл является одной из структур данных, естественно соответствующих
потоку. В данном случае файл, как и массив, является машинным предста-
влением предвычисленной функции, но результаты вычислений хранятся в
долговременной памяти.
Такой способ долговременного запоминания подходит для очень боль-
ших чисел, когда размеры используемого “списка” превосходят возможности
оперативной памяти или разрядность используемых типов данных. Кстати,
при таких условиях может оказаться, что имеет смысл вернуться к самой пер-
вой версии программы без запоминания, противопоставив большой объем
(избыточных) вычислений огромному количеству затрачиваемой памяти.
Итак, массивы и файлы являются структурами данных, естественно соот-
ветствующими потокам. А структура, которая теоретически адекватна пото-
ку — потенциально бесконечная последовательность данных — остается для
программ важным призраком, стоящим за частичными представлениями.
Пример 7.2.5. Задача об N максимальных числах.
Цель настоящего примера — продемонстрировать начинающему программи-
7.2. ПОТОКОВАЯ ОБРАБОТКА 317

сту многообразие вариантов решения даже довольно простой задачи. Ситу-


ация, когда разработчик программы вынужден делать выбор того или иного
пути задаваемых вычислений, внешне кажущихся равнозначными, являет-
ся весьма типичной. В то же время далеко не всегда можно сформулировать
ясные критерии такого выбора.
Когда в своей работе программисту приходится отдавать предпочтение
одним решениям перед другими, он, прежде всего, определяет условия функ-
ционирования данного фрагмента программы. Иными словами, он должен
выяснить, как сказывается эффективность фрагмента на пользовательских
характеристиках программы в целом. Из ответа на этот вопрос можно по-
нять:
a) следует ли экономить память;
b) оптимизировать ли время исполнения фрагмента;
c) нужно ли заботиться о компактности программы;
d) каковы требования к интерфейсу данного фрагмента и окружающей его
программной среды;
e) насколько критична наглядность программы в данном месте?
От правильности такой оценки зависит качество создаваемого программного
изделия7 .
Хотелось бы, чтобы из приводимого ниже материала стало очевидным,
что рассуждения по поводу решаемой задачи могут оказаться не менее полез-
ными для выработки варианта, чем, к примеру, прямой подсчет скорости ра-
боты и занимаемой памяти. Они могут указать на путь, мимо которого легко
пройти при прямолинейном программировании. Даже если такой путь будет
отвергнут, польза от его рассмотрения в том, что он будет отвергнут моти-
вированно. Возможно, что после изменения ситуации8 отвергнутый подход
окажется предпочтительнее.
И последнее предварительное замечание. Разработка программы — это
не только разработка ее алгоритма, но и, прежде всего, выбор наиболее под-
ходящих структур данных. У начинающего программиста в арсенале пока
Следует к тому же отметить, что опытный программист никогда не позволит себе не-
оправданных расходов ресурсов. Сравните с поведением, скажем, программного обеспече-
7

ния фирмы Microsoft, которое занимает все доступные ресурсы.


8
Часто задача динамически меняется в ходе ее решения.
318 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

мало вариантов, между которыми приходится выбирать типы переменных,


размещение их в памяти и др. Но даже в этом случае следует подходить к вы-
бору осознанно, чтобы приучить себя к соответствующей дисциплине. Зача-
стую именно выбор структуры данных программы диктует применение того
или иного алгоритма. В то же время процесс выбора далеко не так прямо-
линеен, как это может показаться на первый взгляд. Как правило, приходит-
ся делать множество итераций, прежде чем система из алгоритма и структур
данных станет реализуемой программой. Общего рецепта здесь нет, и можно
надеяться только на выработку ориентиров, совокупность которых в конеч-
ном итоге и определяет программистскую квалификацию разработчика.
Что же касается выбора языка и системы программирования, которые ис-
пользуются для решения задачи, то по важности это вопрос второго порядка.
Если на этот выбор программист повлиять не в состоянии, то ему придется
приспосабливаться к внешним условиям и независимо от предпочтений ра-
ботать с тем, что есть. Иными словами, квалифицированный программист
должен легко адаптироваться к любой среде программирования. Именно
по этой причине в данном разделе мы иллюстрируем разработку програм-
мы, используя не С/С++, как ранее, а Pascal (стандарт языка). Мы намеренно
опускаем объяснение некоторых конструкций (изучение их — предмет эле-
ментарного самообразования либо особого курса для тех, кто не умеет учить-
ся сам), считая, что их смысл может быть извлечен из контекста при чтении
программы. Впредь мы будем чередовать эти и другие языки, почти произ-
вольно выбирая один или другой, с тем, чтобы у читателя складывались не
стереотипы, а предпочтения.
Рассмотрим простую учебную задачу, которая затем будет обобщена с це-
лью иллюстрации возможных подходов к решению и к анализу вариантов. По
ходу изложения критикуются решения, принимаемые без анализа ситуации,
отмечаются типичные ошибки. Учебная цель примера — дать в простейшей,
модельной, ситуации наглядное представление о том:

a) как можно настраивать программу на условия применения?

b) какие вопросы целесообразно задавать себе при составлении программ?

c) какие мотивации принимаемых и отвергаемых решений обыкновенно со-


провождают развитие алгоритма?

Задача поиска максимального элемента потока чисел, символов или зна-


чений какого-либо другого типа, обладающего операцией отношения срав-
7.2. ПОТОКОВАЯ ОБРАБОТКА 319

нения значений, — классический пример, с которого часто начинают зна-


комиться с задачами на составление алгоритмов. В варианте для чисел она
формулируется следующим образом:
Пусть имеется вводимая последовательность чисел (для определенности, це-
лых, принадлежащих некоторому диапазону, например, от 1 до 1000), посту-
пающих на вход программы одно за другим, — поток. Требуется написать
программу, которая из входного потока выбирает и печатает самое большое
число.
Как чаще всего бывает на практике, задача поставлена не совсем точ-
но: не указывается, как формируется поток. Это означает, что постановщик
ее оставляет вопрос о формировании потока на рассмотрение разработчика
программы. Следовательно, мы в праве предложить свой метод. Для просто-
ты будем считать, что поток строится с помощью клавиатурного ввода чисел,
а признаком его конца является ввод числа, которое в указанный диапазон от
1 до 1000 не входит.
В качестве других вариантов формирования потока можно указать, на-
пример, на случайную генерацию чисел с заранее фиксированным (с помо-
щью клавиатурного ввода) количеством элементов потока или с нефиксиро-
ванным количеством элементов. Во втором случае следует предусмотреть,
каким способом генерация потока завершается. Допустимы различные вари-
анты завершения: порождение при генерации потока такого числа, которое в
указанный диапазон от 1 до 1000 не входит, прекращение генерации потока
при нажатии клавиши и др. При выборе случайной генерации целесообразно
позаботиться о том, чтобы пользователь программы был в состоянии просма-
тривать числа потока.
Мы выбрали самый простой метод формирования потока, чтобы не пе-
регружать последующее обсуждение деталями. Тем не менее, настоятельно
советуем читателю запрограммировать все упомянутые выше методы и срав-
нить их с разных точек зрения (простота программы, удобство использования
и др.).
Хотя решение поставленной задачи вы вполне можете выполнить само-
стоятельно (что настоятельно рекомендуется сделать в качестве упражне-
ния), для последующего анализа мы приведем его в виде программы M11:
Программа 7.2.6

{ переменная для текущего максимума }


program M11 (Input, Output);

{ переменная для текущего числа потока }


var A,
X : Integer;
320 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

begin
if not eof
then begin
{1.1} read ( A ); X := A;
while (X > 0) and (X < 1000) and not eof do
begin
read ( X );

{ текущий максимум надо сменить }


{1.2} if A < X
then A := X
end;
write ( A )
end;
end.

Приведенное решение не требует обсуждения. Стоит разве что упомянуть о


решении, в котором предварительно входной поток переписывается в мас-
сив. Оно никуда не годится (это типичная ошибка начинающего, неправиль-
но понимающего принцип деления задачи на подзадачи: сначала организо-
вать ввод — подзадача 1, а затем искать максимум — подзадача 2). Крити-
ка такого решения даже не в том, что массив в данном случае — это всегда
ограничение длины потока и дополнительный расход памяти; главное, что
оно предполагает двойной просмотр потока, тогда как решение подзадач це-
лесообразно совместить.
Интересна не сама по себе рассмотренная задача, а ее развитие и обобще-
ния, причем такие, решение которых можно получить из приведенного путем
последовательного уточнения, приспосабливания уже имеющейся “наработ-
ки” для новой задачи.
Предлагается обсудить следующее обобщение задачи:
Написать программу, которая из входного потока выбирает и печатает в по-
рядке возрастания N самых больших чисел, где N — константа. Если раз-
личных чисел потока меньше N , то печатать все найденные числа.
Ее решение — тоже цикл, оно также должно включать запоминание мак-
симумов, но не одного, а сразу нескольких. Таким образом, возникает массив
(будем именовать его A). Смена же максимума на новое значение реализует-
ся посредством замены элемента в A. Эта идея может быть реализована по-
разному.
Бросается в глаза то, исходное задание A (аналог строки 1.1. в программе
7.2. ПОТОКОВАЯ ОБРАБОТКА 321

М11) можно описать как перепись (разумеется, без повторений) начала вход-
ного потока, а поиск убираемого элемента (см. строку 1.2) — с помощью
просмотра A и поиска в нем минимума. При переписи приходится ввести
индекс заполненности A (в дальнейшем он обозначается L), который растет,
когда место в A еще не исчерпано (L < N ). Если N невелико, вопрос о це-
не разделения переписи без повторений и просмотра не возникает, и вполне
приемлемо решение, когда работа с A организуется прямо, циклом с провер-
ками каждого элемента массива:
Программа 7.2.7

{ значение N = 5 выбрано для определенности }


program MN2(Input, Output);

A : array [1..N] of Integer; { переменная для текущих максимумов }


const N = 5;

{ переменная текущего числа из потока }


var

{ индекс заполненности массива A }


X,

i : Integer; { индекс для просмотра массива A }


L,

begin
if not eof
then
begin
L := 0;
while (L < N) and (X > 0) and (X < 1000) and not eof do
begin
read ( X ); i := 1;
while (i <= L) and (X <> A[i]) do i := i + 1;

then { Х в A не найден }
if i > L

begin L := L + 1; A[L] := X

{ массив A заполнен }
end;
end;
while (X > 0) and (X < 1000) and not eof do
begin
read ( X ); i := 1;

{ удаляемый элемент имеется? }


while (i <= L) and (X <= A[i]) do i := i + 1;
if i <= L
then A[i] := X
end;
for i := 1 to L do write ( A[i] )
322 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

end;
end.

Однако с ростом N эффективность программы будет падать из-за того, что


приходится просматривать все большее и большее количество чисел. Возни-
кает желание улучшить просмотр, а заодно и перепись. Это можно сделать
при специальной организации A: использовать упорядочивание при заполне-
нии.
Идея упорядочивания весьма продуктивна, а потому ее обсуждение будет
проведено специально. Здесь же хотелось бы оценить первоначальный план
с другой стороны: оправдана ли предварительная перепись? При вниматель-
ном рассмотрении становится ясно, что заполнение A можно программно со-
вместить с просмотром и поиском убираемого элемента. Для этого надо ис-
пользовать индекс заполненности L как границу просмотра. Программа MN3,
реализующая эту идею, приводится ниже.
Программа 7.2.8
program MN3(Input, Output);
const N = 5;
var A : array [1..N] of Integer;
X,

i, j : Integer; { индексы для просмотра массива A }


L,

begin
if not eof
then
begin
L := 0;
while (X > 0) and (X < 1000) and not eof do
begin
read ( X ); i := 1;
while (i <= L) and (X <= A[i]) do i := i + 1;
if i <= L

{ исключение повторений }
then

{ A должен быть модифицирован, найден }


if X <> A[i]

j := i + 1; { кандидат на удаление }
then begin

while (j <= L) and (X < A[j]) do


7.2. ПОТОКОВАЯ ОБРАБОТКА 323

{ проверка остатка массива A. Здесь используется то, что }


{при ложности первого условия второе не проверяется! }

if L < N then L := j; { пополнение }


j := j+1;

A[j] := X; { модификация }

{ кандидат на удаление в A не найден}


end

{ нужно ли пополнение A? }
else
if L < N
then begin L := i; A[i] := X end;
end;
while (X > 0) and (X < 1000) do
begin
read ( X ); i := 1;

{ удаляемый элемент имеется? }


while (i <= L) and (X < A[i]) do i := i + 1;
if i <= L
then A[i] := X
end;
for i := 1 to L do write ( A[i] )
end;
end.

При росте N программа MN2 все еще будет неэффективной из-за увеличения
накладных расходов на полный просмотр массива A. Как уже отмечалось,
это можно поправить за счет упорядочивания массива. Предварительно от-
метим, что формулировка решаемой задачи не совсем точна: она оставляет
свободу разработчику в выборе того, в какой последовательности надо вы-
водить полученные результаты. Если это безразлично, то решение вопроса,
использовать упорядочивание или нет, разработчик программы вправе моти-
вировать только логикой построения алгоритма и оценкой эффективности. В
противном случае, когда требуется вывести результат в том или ином поряд-
ке (для определенности — по возрастанию), необходимость упорядочивания
диктуется условием задачи. И тогда возможное решение, связанное с допол-
нительным упорядочиванием массива A после завершения работы с потоком,
становится совершенно неприемлемым: оно приведет к заведомой неэффек-
тивности программы.
Упорядочивание массива A при заполнении, кроме уже рассмотренного
пополнения числа элементов A, требует реализовать вставку X внутрь мас-
сива A в двух вариантах:
324 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

a) когда A не заполнен, и

b) когда A заполнен.

Вставка в конец незаполненного A есть частный случай первого варианта.


При принятой для рассматриваемой задачи стратегии хранения накапливае-
мых максимальных элементов потока с помощью массива, а не какой-либо
иной структуры данных, оба варианта требуют сдвига части массива (пере-
писи) для освобождения места под X. В будущем мы изучим структуры дан-
ных, использование которых позволит избежать такой переписи, и именно от
них можно ожидать существенного повышения эффективности программы.
Однако это уже будут другая стратегия и другие методы программирования,
обсуждение которых стоит отдельного разговора. В рассматриваемой ниже
программе MN3 используется простейшее из упорядочиваний, связанное с
непосредственным заполнением массива из потока.
Для определенности применяется упорядочивание по возрастанию. Чи-
тателю предлагается самостоятельно разработать программу, в которой мас-
сив A упорядочивается по убыванию, и сравнить эффективность двух реше-
ний задачи (есть основания полагать, что качество второго решения окажется
несколько выше).
Если i — индекс такого элемента массива A, что A[i] < X и при k > i все
A[k] > X, то

• в варианте (a) местоположение для размещения X в A определяется ин-


дексом i+1, требуемый сдвиг есть перепись A[i+1], . . . , A[L] в A[i+2], . . . ,
A[L+1], а L увеличивается на единицу;

• в варианте (b) местоположение для размещения X в A определяется ин-


дексом i, требуемый сдвиг есть перепись A[2], . . . , A[i] в A[1], . . . , A[i-1].

Комментария требует использование признака incl для проверки завершения


цикла поиска возможного местоположения X в A. Этот признак остается лож-
ным во всех случаях, когда ситуация, связанная с необходимостью модифи-
кации A остается неопределенной, т. е. когда требуется продолжить поиск
текущего X.
Еще одно замечание, касающееся приводимой ниже программы. Всегда
следует стремиться к тому, чтобы более ранние проверки как можно опреде-
леннее выявляли различные пути вычислений. Читателю предлагается опре-
делить, что порядок выполняемых проверок существенен.
7.2. ПОТОКОВАЯ ОБРАБОТКА 325

Программа 7.2.9
program MN3(Input, Output);
const N = 15;
var A : array [1..N] of Integer;
X, L, i, j : Integer;
incl : Boolean;
begin
if not eof
then
begin
read (A[1]); L := 1;
while not eof do
begin
read ( X ); i := L; incl := False;

{ цикл поиска места вставки X в A }


if (X > 0) and (X < 1000) then

if A[i] < X {нашли в A первое такое место}


repeat

then begin { будут сдвиг и завершение цикла }


incl := True;

{ А не заполнен, нужен сдвиг вправо: }


if L < N then

begin for j := L downto i+1 do A[j+1] := A[j];


A[i] := X; L := L+1
end

{ А заполнен, нужен сдвиг влево: }


else

begin for j := 1 to i-1 do A[j] := A[j+1];


A[i] := X
end
end
else if A[i] = X

{нашли элемент, равный X, цикл завершается}


then incl := True

else A[i] > X


begin i := i + 1;
incl := i = 0
end
326 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

{только при i <> 0 и}


{A[i] > X цикл продолжается }
until incl { условие цикла поиска места вставки X }
end;
for i := 1 to L do write ( A[i] )
end
end.

Конец примера 7.2.5.

7.2.4. Потоки и цикл for

Цикл for синтаксически объединяет инициализацию, проверку окончания


цикла и генерацию нового элемента. Дадим его конкретно-синтаксическую
структуру для языка C:
“for” “(” <инициализирующий оператор> <выражение-условие> “;” <вы-
ражение пересчета> “)”<оператор>
Такая структура оператора цикла исторически сложилась для достижения
сразу трех целей:
1. наглядности;

2. оптимизации: упрощения распознавания случаев, которые допускают


ускоренное выполнение программы;

3. обогащения возможностей программирования: например, за счет воз-


можности опускать части оператора for цикл while становится избы-
точным в C/C++.

Оптимизационное назначение цикла for — это традиция, восходящая к Algol


60 и даже к языку FORTRAN. В цикле for большинства языков программи-
рования, в частности в ряде случаев его употребления в С, можно явно вы-
делять так называемые параметры цикла — переменные, принимающие те
значения, при которых должны быть пройдены итерации. Переменные цикла
должны рассматриваться и использоваться как связанные переменные данно-
го оператора, то есть (так же, как в логике) как такие переменные, которым
не могут быть приданы конкретные значения извне, которые обязаны пробе-
жать все множество, определенное в цикле, в том порядке (а это — отличие
от логики), как предписывает цикл.
7.2. ПОТОКОВАЯ ОБРАБОТКА 327

Например, во фрагменте

оператор, обозначенный многоточием, выполняется при i, равном 0, 1, . . . , 99.


for ( i = 0; i < 100; i++ ). . .

i — это параметр данного цикла. В других языках параметр цикла —понятие,


выделенное синтаксически. Например, в языке Pascal

переменная i внутри S имеет статус параметра цикла. В частности, явное при-


for i:=0 to 99 do S

сваивание ей нового значения сигнализируется транслятором как серьезное


предупреждение. В языке Алгол-68 цикл имеет еще более изощренную фор-
му:

Соответственно, здесь конец цикла отмечен скобкой, парной к его началу,


for i from 0 to 99 while P<i> do S<i> od

параметр цикла выделен явно и, более того, чтобы окончательно исключить


недоразумения, объявляется константой, которая описана данным операто-
ром и имеет областью действия лишь условие P и оператор S. Так что все
вопросы, связанные с присваиванием параметру цикла, отпадают сами со-
бой. Естественно, что при следующей итерации значение параметра цикла
будет новым, а после выхода из цикла любым способом параметр просто пе-
рестает существовать, и вопрос о его последнем значении также сам собой
отпадает. Решение, принятое в языке Ada, синтаксически ближе к решению
Алгола-68, а семантически — к принятому в языке Pascal.
Если переменная i будет размещена на быстром регистре, то цикл будет
выполняться быстрее по сравнению со случаем размещения этой перемен-
ной в основной памяти. Возможно, что цикл допускает другой порядок пе-
ребора итераций с эквивалентными результатами счета, а для вычислитель-
ной машины этот другой порядок окажется более эффективным. Выясняются
подобные свойства при анализе оператора цикла, учет их весьма желателен
для качества объектного кода, и поэтому преобразование цикла (в качестве
неявной процедуры) вставляется в оптимизацию, производимую промыш-
ленными трансляторами.9
Чтобы позволить транслятору осуществлять оптимизирующие преобра-
Эта услуга порою превращается в медвежью, поскольку автор программы может даже
не подозревать, что итерации цикла идут, по решению “сверхкомпетентного” транслятора, в
9

другом порядке. Тем более бывает шокирован программист, когда после вставки в цикл вро-
де бы ничего не меняющего оператора программа перестает работать в том месте, где она
уже была полностью отлажена, а затем, после череды панических хаотических изменений,
внезапно вновь начинает работать как надо. А последствия паники и хаоса будут расхлебы-
ваться еще долго. . .
328 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

зования, в описания некоторых языков (Алгол, Pascal и др.) включают до-


вольно странное, на первый взгляд, указание: значение параметра цикла for
не определено после естественного завершения, т. е. завершения из-за лож-
ности условия продолжения (в примере i < 100). Смысл указания становит-
ся понятным в контексте приведенных выше рассуждений: оно разрешает
разработчикам трансляторов программировать циклы так, как это удобно в
конкретной вычислительной среде.
Пример 7.2.6. Оптимизация, цикл for и массивы. Рассмотрим фрагмент про-
граммы:

for ( int i = 1; i < 100; i++ )


a[i] = b[i] = i;

Путем анализа текста можно узнать, что этот цикл можно реализовать в ко-
дах процессора Intel-80?86 следующим образом (в частности, организовав
обратный порядок итераций):
// инициализация i(register ecx) i = 99
// запись i в b[i]
mov ecx,64h

// запись i в a[i]
c1: mov [b - ecx*4],ecx

// уменьшение ecx, goto, если не 0


mov [a - ecx*4],ecx
loop c1
Как вы думаете, сможет ли какой-либо транслятор построить подобный
код для данного фрагмента программы?
Конец примера 7.2.6.
Само определение цикла for указывает на то, что эта конструкция отража-
ет схему потоковой обработки с инициализацией, включающей генерацию
элемента потока (см. табл. 7.16).
Однако из-за того, что все составляющие данного цикла (как, впрочем,
и большинства других языковых конструкций) могут использоваться в смы-
слах, далеко уходящих от первоначального, с помощью цикла for возможны
реализации иных схем (см., к примеру, использование for (;;), которое обсу-
ждалось выше). Тем не менее, использование конструкций, специально при-
способленных для изображения схем вычислений, чрезвычайно полезно с
точки зрения качества программ и соблюдения правил хорошего стиля. Соб-
ственно говоря, именно на этом пути все языковые конструкции появились
или, по крайней мере, приняли свои современные формы.
7.3. ЛОГИЧЕСКАЯ СТРУКТУРА ЦИКЛА 329

Схема Пример
ИНИЦИАЛИЗАЦИЯ, ВКЛЮЧА- for (<инициализирующий опера-
ЮЩАЯ ГЕНЕРАЦИЮ тор>;
пока ПОТОК НЕ ИСЧЕРПАН
цикл
<выражение-условие>;

ОБРАБОТКА ЭЛЕМЕНТА Задается как часть <операто-


ра>, указанного ниже
ГЕНЕРАЦИЯ ОЧЕРЕДНОГО <выражение пересчета>)
ЭЛЕМЕНТА
конец цикла
ЗАВЕРШЕНИЕ Явно не записывается
<оператор>

Таблица 7.16. Цикл for

§ 7.3. ЛОГИЧЕСКАЯ СТРУКТУРА ЦИКЛА

Чем выше мы поднимаемся по лестнице абстракций алгоритмических


языков, тем более мощные средства анализа приходится применять для вы-
явления сути предлагаемых конструкций и для их анализа. Во вводном курсе
программирования грех и игнорировать возникающие сложности (общепри-
нятость греха не является извинением для грешника), и углубляться в них.
Здесь мы даем поверхностный анализ, а более серьезный анализ является
предметом одной из глав фундаментального курса (скажем, «Семантики ал-
горитмических языков»).
7.3.1. Инвариант и параметры цикла

Любой вид потоковой обработки — это схематическое описание процес-


са, управляемого последовательностью Si , такой, что получение элемента
Si+1 из элемента Si задано генерацией потока.10 С формальной точки зре-
ния, итерации любого потокового цикла можно рассматривать как обработку
некоторого “общего” потока, состоящего из сложных (структурированных)
элементов:11 каждый такой элемент — вся совокупность данных, изменяю-
щихся в итерации, но остающихся постоянными во время каждого ее шага.
Так что цикл Алгола-68 остановился в развитии на полпути к важной кон-
цепции: параметром цикла должна была быть структура всех закономер-
Указанные в § 7.2.1 варианты независимой и функциональной генерации потока на уровне
рассмотрения логической структуры цикла можно считать вырожденными случаями.
10

11
Здесь мы столкнулись с необходимостью введения сложных структур данных!
330 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

но изменяющихся элементов.12 Для параметров цикла язык С предоставляет


значительно более последовательное и красивое решение. Параметры цикла
могут быть описаны при инициализации цикла, остается лишь вопрос, как
вынести наружу финальные значения тех из них, которые нужны после ис-
полнения цикла.
Конечно же, концепция выражений, вырабатывающих значение, также
принятая в Алголе-68, могла бы решить данную проблему, если в качестве
значения цикла выдавалась бы структура всех его параметров, либо подструк-
тура тех параметров, которые описаны как внешние, а дальше уж програм-
мист пусть разбирается, что с нею делать. Но именно в операторе цикла Ал-
гол-68 допустил очередную непоследовательность: этот оператор значения
не выдает. В итоге видим, что ни в одном общераспространенном языке про-
граммирования проблема параметров цикла даже не была правильно поста-
влена.
В качестве редкого исключения можно указать язык ЯРМО, созданный в
Новосибирском филиале ИТМ и ВТ для поддержки разработки системного
программного обеспечения на ЭВМ БЭСМ 6 и обеспечившего возможность
конструирования программ для вычислительных комплексов Эльбрус еще до
того, как эти машины начали выпускаться. В языке ЯРМО концепция значе-
ний реализована в полной мере: все операторы вырабатывают значение, при-
чем в большинстве случаев значение сложной конструкции наследуется от
вложенного оператора, исполнявшейся непосредственно перед ее заверше-
нием. Даже оператор присваивания был заменен на оператор засылки, явно
запоминающий значение предыдущего оператора:
<источник значения>→<получатель значения>
Символ “;” означает забывание последнего значения.
Концепция структуры параметров цикла позволяет поднять еще один важ-
ный вопрос. Цикл типа потока бессмысленен, если из него нет выхода. По-
этому каждый шаг цикла должен приближать нас к его завершению. Но чи-
сло шагов цикла не обязано задаваться явно. В цикле, управляемом условием,
чаще всего число оставшихся шагов в программе не фигурирует явно (иначе
задавался бы цикл for). В данном случае мы очередной раз сталкиваемся с
проблемой значений-призраков. По определению для вычисления програм-
мы призраки как минимум не нужны (порою они могут быть даже вредны),
но нужны для ее обоснования (см. стр. 127). Неявным значением-призраком
Недоведенность до конца принципиально новых возможностей, ощущавшихся создате-
лями Алгола-68, явилась основной бедой данного языка.
12
7.3. ЛОГИЧЕСКАЯ СТРУКТУРА ЦИКЛА 331

для любого потокового цикла является ординал, задающий верхнюю оценку


числа оставшихся шагов. Поскольку любая убывающая последовательность
ординалов за конечное число шагов доходит до нуля, то наличие такого при-
зрака гарантирует конечность цикла.
Проблема параметров цикла тесно связана с другой проблемой. Какова
логическая сущность цикла, задающего потоковую обработку?
Потоковый цикл соответствует математическому доказательству по ин-
дукции. Базис индукции — его инициализация, шаг индукции — итерация
цикла, а завершение соответствует переходу от индуктивного утверждения к
тому, что требовалось доказать.13
Теперь рассмотрим форму рассуждения по индукции. Шаг индукции име-
ет вид (мы выписали его с явным указанием всех параметров цикла):
 
A(i, x1 , . . . , xn ) ⇒
∀i (7.1)
A(i + 1, f1 (i, x1 , . . . , xn ), . . . , fn (i, x1 , . . . , xn ))
Здесь видно, что при индукции изменяются значения параметров, но сохра-
няется отношение между ними. В математике это отношение называется пред-
положением (или утвеждением) индукции, в программировании — инвари-
антом цикла.
Пример 7.3.1. Для программы Prime_Numbers_1 инвариантом основного
цикла является простая логическая формула
Prime ⇔ P rime(i),
где P rime — предикат ‘Быть простым числом’. Для следующей программы
Prime_Numbers_2 инвариант основного цикла формулируется значительно
сложнее. Его удобнее записать в виде предложения русского математическо-
го языка:
P r(j) 6 i < P r(j + 1), где P r — функция, перечисляющая
простые числа в порядке возрастания, и Prime имеет значение
P r(j) = i, и для всех k < j a[k − 1] = P r(k).
Даже проблема призраков впервые была осознана в математической логике именно для
рассуждений по индукции — парадокс изобретателя: очень часто для успешного доказа-
13

тельства по индукции требуется усилить доказываемое утверждение, а в дальнейшем было


строго обосновано, что имеются утверждения вида
∀i f (i) = 0,
где f — вычислимая функция, для доказательства которых нужно привлекать сущности
сколь угодно высоких порядков (см., напр., [63]).
332 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Для последующих вариаций программы инвариант цикла еще больше услож-


няется.
Заметим, что в инвариантах часто используются теоретические предика-
ты и теоретические функции, в некотором смысле дублирующие вычисляе-
мые в программе значения (например, a[k − 1] и P r(k) выше). Это необхо-
димо, поскольку, задавая призраки, мы отвлекаемся от вопроса об их вычи-
слимости и концентрируемся на их свойствах и их отношениях к понятиям
программы. Эти понятия, в частности, могут в отдельных случаях и совпа-
дать с призраками (ведь функция P r(k) определена для всех натуральных k,
а не только для множества индексов массива).
Конец примера 7.3.1.
Предикат, принимающий значения истина, пока выполняется тело цикла, и
ложь, когда выполнение тела цикла прекращается, называется полным инва-
риантом цикла. Для выделенного ниже, в программе 7.3.2, цикла имеется,
например, следующий полный инвариант:

Полный инвариант существенно зависит от выбора присутствующих в нем


(imi <= ima) && (x != a[( imi + ima )/2 ])

значений и призраков и, даже если их выбор фиксирован, может быть не


единственным.

7.3.2. Цикл Дейкстры и цикл-‘паук’

Данный параграф перебрасывает мостик к следующей теме нашего изло-


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

do
P1 → S1 ,
...
Pn → Sn ,
od

Итерация, как и в условном операторе с охраняемыми командами, начинается


с совместного вычисления охран. Затем выбирается то из действий, которое
соответствует истинной охране (если истинных охран несколько, то, как и
в условном операторе, любое, но одно). Выполняется это действие, и цикл
возобновляется.
7.3. ЛОГИЧЕСКАЯ СТРУКТУРА ЦИКЛА 333

Таким образом, условие выхода из цикла неявно: это ложность всех охран.
Заметим, что Дейкстра противоречит сам себе в определении цикла и услов-
ного оператора. В условном операторе он резко выступал против умолчаний
и на этом основании отвергал часть else. В операторе цикла он же суще-
ственнейшим образом использовал умолчание как критерий завершения ци-
кла. Если уж явно выписывать условия, то все существенные. Альтернатива
возможна лишь для вылавливания непредусмотренных случаев, возможно,
вызванных неправильным использованием программы. Поэтому предлага-
ется следующий вариант цикла (цикл-‘паук’), в котором условия завершения
также перечислены явно и есть альтернативное завершение else, например,
для сообщения об ошибке.

do
P1 → S1 ,
...
Pn → Sn ,
out
Q1 → T1 ,
...
Qm → Tm ,
else
Error_Analysis
od

В отличие от традиционного цикла, логические корни цикла Дейкстры и ци-


кла-паука находятся в таких логиках, которые противоречат классической,
например, в нильпотентных логиках. Эти формы циклов естественно сопря-
гаются с программированием от состояний, а при структурном программи-
ровании они нужны лишь для действий, которые трудно сводятся к потокам.
7.3.3. Совместный цикл

Какую дисциплину выполнения цикла не отражает ни цикл for, ни другие


виды циклов в языке С и в других языках традиционного типа? Нет цикла,
итерации которого выполнялись бы независимо от того, в каком порядке
появляются значения параметра цикла. Иными словами, хотелось бы иметь
в языке конструкцию
для всех <параметр цикла> из <множество значений параметра> <оператор>
Смысл цикла, о котором идет речь, в том, что <оператор> выполняется
при всех значениях из указываемого множества в произвольном порядке, т. е.
334 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

совместно. Такая конструкция была введена, в частности, в язык SETL,14 со-


здававшийся как один из наиболее хорошо продуманных проектов языка про-
тотипирования весьма высокого уровня. Она оказалась полезна, по крайней
мере, в двух отношениях:
• отпала необходимость фиксировать порядок перебора значений пара-
метра цикла, когда это фактически не требуется, следовательно, соста-
вляемые программы избавляются от лишнего, а значит, становятся по-
нятнее;
• стала естественной совместность вычисления совокупности операто-
ров, каждый из которых представляет одну итерацию цикла, и как след-
ствие, облегчилось распараллеливание.
Оператор совместного цикла
‘ {’ < параметр > ‘ ∈ ’ < множество значений > ‘|’ < оператор > ‘} ’
(или (∀ x∈ X: S), как это, с точностью до деталей, записывалось в языке
СЕТЛ), позволяет исполнять S для различных x в порядке, наиболее подхо-
дящем к конкретной вычислительной ситуации, например, дающем наиболь-
шую эффективность вычислений (в частности, это существенно при возмож-
ности использовать несколько процессоров для распараллеливания обработ-
ки).
Поскольку конструкция для всех в языках традиционного типа отсут-
ствует, для организации параллельных вычислений приходится производить
специальный анализ с целью выявить случаи, когда потоковая обработка до-
пускает распараллеливание. Здесь мы сталкиваемся со всеми недостатками
полуавтоматического процесса анализа объекта, логическая структура кото-
рого концептуально противоречит цели, для которой его хотят использовать.
Несколько слов о множестве значений параметра в цикле для всех. Спо-
соб задания этого множества не является принципиальным. К примеру, в язы-
ке Pascal для его задания естественно было бы воспользоваться средствами
оперирования с типами (подобно тому, как определяются возможные значе-
ния индексов массивов). Можно, в частности, использовать теоретико-мно-
жественные операции (как это делается в языке СЕТЛ), говорить о вызовах
функций, вырабатывающих значения множественного типа и т. д. А вот ре-
куррентное определение множества значений параметра чаще всего будет
Мы будем пользоваться для данного языка русским именем СЕТЛ, поскольку он был со-
вместной советско-американской разработкой.
14
7.3. ЛОГИЧЕСКАЯ СТРУКТУРА ЦИКЛА 335

ошибкой, т. к. оно навязывает задание потока, для которого итерации выпол-


няются последовательно, а не совместно. Иными словами, в таких случаях
более естественным является цикл for традиционных языков. Впрочем, и в
этих случаях задание цикла для всех можно было бы трактовать как явное
указание на то, что вся зависимость итераций друг от друга фактически сво-
дится лишь к перевычислению параметра цикла. Но и тогда лучше было бы
воспользоваться описанным в § 7.3.1 понятием списка параметров цикла.
Таким образом, мы видим здесь еще одно концептуальное противоречие:
в одном и том же фрагменте программы нужно использовать либо только
потоки, либо только совместные циклы. Оно пока что остается потенциаль-
ным, поскольку совместные циклы в обиход еще не вошли (хотя, видимо, на
следующем витке развития к ним вернутся как к мощному средству прото-
типирования и распараллеливания).
Если говорить о логической аналогии, то совместные циклы соответству-
ют скорее ограниченным кванторам всеобщности, чем какому-то другому ло-
гическому понятию. Таким образом, принятое в СЕТЛе обозначение соответ-
ствует сути совместных циклов.

7.3.4. Входные и выходные потоки. Сопрограммы

До сих пор потоки рассматривались исключительно с одной позиции: с


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

// Псевдослучайные числа
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

void main()
{
int i;

// Напечатать 10 чисел
srand( (unsigned)time( NULL ) );
for( i = 0; i < 10; i++ )
336 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

printf( "%6d\n", rand() );


}

Эта программа подпадает под схему 1 с инициализацией, не включающей ге-


нерацию. Но что можно сказать о функциях srand и rand, если рассматривать
их независимо от использующей программы? Первая из них инициализиру-
ет поток, а вторая генерирует его элементы независимо от того, как будут
они применяться. Иными словами, здесь поток — цель некоторой (оставшей-
ся вне рассмотрения) обработки, или, по-другому, выходной поток, который
является входным потоком для программы 7.3.1.
С другой стороны, каждая итерация цикла программы 7.3.1 поставляет
число, которое требуется напечатать, функции printf. Вполне правдоподобно
представить реализацию этой функции как часть программы, которая рас-
печатывает последовательность выводимых элементов, передаваемых через
параметры printf (эта программа производит еще другую дополнительную
работу, обеспечивающую корректность вывода: оперирование с позициями
курсора на экране, форматирование и др.). Иными словами, имеет место вы-
ходной поток элементов, генерируемый программой 7.3.1. Таким образом,
программа 7.3.1 потребляет один поток и в ходе его обработки порождает
другой поток.
Вполне допустимы и практически применяются схемы организации ра-
боты с потоками, когда выходной поток формируется несколькими програм-
мами (фрагментами программ) или когда поток потребляется несколькими
программами (фрагментами программ). В общем случае возможно несколь-
ко (m) производителей и несколько (n) потребителей потоковых данных. Их
взаимодействие корректно, если
Не может случиться так, что потребители запрашивают
больше данных, чем произведено производителями.
(7.2)

Если взаимодействия реализуются при последовательной дисциплине вычи-


слений, то других ограничений не предполагается. Возможности согласова-
ния порождения и потребления потока в этом случае сводятся к одной из
следующих схем:

1. Разделение: генерация хранимого потока производителями с последу-


ющим извлечением данных потребителями. По существу здесь нет вза-
имодействия. Работа производителей и потребителей не влияет друг на
друга.
7.3. ЛОГИЧЕСКАЯ СТРУКТУРА ЦИКЛА 337

2. Запрашивание: по мере надобности очередного элемента потока для


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

3. Предъявление: по мере порождения очередного элемента потока произ-


водителем вызывается необходимый для обработки потребитель, кото-
рому предъявляется этот элемент. Таким образом, управление осуще-
ствляется программой производителя, которая указывает, когда и како-
го из потребителей нужно активизировать.

4. Общее управление: строится специальная управляющая программа, ко-


торая поочередно вызывает производителей и потребителей, тем са-
мым организуя выходной/входной поток.

Последние три схемы описывают механизмы сопрограммного взаимодей-


ствия группы подпрограмм, называемых сопрограммами (процедур, модулей
и др.,) в случае дисциплины последовательных синхронных вычислений.
Суть механизма сопрограммного взаимодействия в общем случае, т. е.
без привязки к потоковой обработке, состоит в следующем. В ходе выполне-
ния какой-либо подпрограммы группы происходит приостановка ее работы
с тем, чтобы вызвать для выполнения другую подпрограмму группы (способ
приостановки не фиксируется). В какой-то момент приостановленная под-
программа возобновляет свою работу в том состоянии, в котором она была
приостановлена (важный частный, но не единственно возможный случай —
запоминание точки приостановки с тем, чтобы возобновление продолжилось
с этой точки программы).
Сопрограммное взаимодействие естественно обобщается на процессы,
которые возникают при выполнении программ. Программа — это статиче-
ское понятие (текст, код), процесс — понятие динамическое, определяемое
как выполнение некоторой программы. Одна программа может породить мно-
го процессов (пример — рекурсия), но любой процесс связан с вполне опре-
деленной программой процесса. Так что словосочетание ‘приостановка ра-
боты программы’ понимается как приостановка выполнения процесса, ею
порожденного (одного из них), а ‘возобновление работы программы’ есть не
что иное, как возобновление выполнения процесса, ею порожденного и ранее
приостановленного (каким-либо способом).
338 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Понятие сопрограммных взаимодействий можно продуктивно использо-


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

Программа 1 Программа 1
Приостановленны

Активный Потенциально
й процесс

процесс активный
П11 П12 П11 П12 процесс
Приостановленные

Порождение Порождение
(генерация) Обработанные элементы (генерация)
процессы

Общий ресурс
трех процессов Поток
Необработанные элементы
П21 П21
Потребление Потенциально Потребление
(обработка) активный процесс (обработка)

Программа 2 Программа 2

а) буферизация отсутствует б) потоковая бу феризация

Рис. 7.1. Взаимодействие процессов с разделяемыми ресурсами

видов такого ресурса, который характеризуется определенными правилами


доступа. Это динамически пополняемая последовательность со слабой зави-
симостью пополнения и извлечения элементов: для корректности обработки
необходимо выполнение условия (7.2).
7.3. ЛОГИЧЕСКАЯ СТРУКТУРА ЦИКЛА 339

Потоки — это один из наиболее важных видов ресурсов, совместно ис-


пользуемых сопрограммами. С их помощью реализуется часто применяемый
на практике механизм буферизации. Идея механизма буферизации в том, что
разделяемый несколькими процессами ресурс «прокачивается» через буфер
(очередь, поток, иную структуру данных), пополняемый процессами-произ-
водителями, и опустошаемый процессами-потребителями (см. рис. 7.1, часть
б). В этом случае производители могут генерировать элементы, не дожидаясь
запросов потребителей.
Нужно отметить, что буферизация полезна, прежде всего, как логическое
средство и только во вторую очередь как средство распараллеливания работ.
Причем последнее вовсе не обязательно!
Буфер может иметь фиксированные размеры, и тогда его заполненность
приводит к невозможности (к блокировке) работы производителей до тех
пор, пока потребители не очистят его. В вырожденном случае, когда длина
буфера не может превышать единицы, буферизация фактически отсутству-
ет, и применимы схемы согласования 2–4. Для асинхронного случая к ним
добавляются схемы использования средств управления параллельными вы-
числениями: операторы синхронизации, ожидания и т. п. В случае неогра-
ниченного буфера становится возможной и схема 1, но, как уже отмечалось,
фактически это отсутствие взаимодействия.
Имеются виды буферизации, которая базируется не на потоковом извле-
чении данных. Заполнение буфера — генерация потока, но извлечение дан-
ных может быть организовано не только как очередь, но и как стек, и как
множество (когда порядок извлечения непринципиален). Выбор типа буфера
обусловливается особенностями решаемой задачи.
Рекомендуем посмотреть пример в § 8.6.3, в котором активно использу-
ется буферизация в виде линии задержки.
Все рассмотренные выше виды буферизации представляют собой пассив-
ную буферизацию, т. е. такую, которая не участвует в выполнении существен-
ных функций производителей и потребителей данных, а используется лишь
для хранения. Наряду с ней употребительна и активная буферизация, когда
с хранением совмещается ряд других функций. К примеру, если для обработ-
ки требуется только часть данных некоторого потока, то можно организовать
фильтр для отбора нужных данных перед помещением их в буфер либо перед
извлечением их. Этот прием можно усмотреть в решении задачи о простых
числах, если считать хранилище уже найденных чисел буфером. Легко заме-
тить, что фильтрация перед сохранением данных в таком случае зависит от
свойств уже накопленного материала (числа, кратные уже запомненным про-
340 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

стым числам, не сохраняются). Это довольно типичная ситуация, равно как и


та, в которой условие выходного фильтра формулируется с использованием
свойств хранимых данных. Собственно говоря, подобные зависимости мож-
но рассматривать как признак того, что следует организовывать активную
буферизацию.
Не следует думать, что в буфере всегда хранятся именно те значения, ко-
торые генерируются поставщиками данных и извлекаются для потребления.
Так, часто активная буферизация применяется, когда требуется, к примеру,
хранить уплотненные данные или размещать в буфере значения, по которым
требуемые данные определяются путем вычислений. Таков, в частности, тот
пример задачи о простых числах, когда оказалось выгоднее сохранять их раз-
ности, а не сами значения.
Из всего предыдущего рассмотрения видно, что буферизация сначала долж-
на рассматриваться как общий прием декомпозиции программ, а когда реше-
ние о буферизации принято, можно говорить о языковом оформлении и далее
о реализации.
До сих пор учет параллельного выполнения процессов в качестве задачи
рассматривался лишь в принципе — схемы 1–4 задают строго определенный
порядок, условие (7.2) вообще не связано с последовательностью или парал-
лельностью вычислений. При асинхронной параллельной работе процессов
производителей и потребителей для обеспечения корректности разделения
общего ресурса нужно решать задачу блокировки чтения/записи данных. Ис-
ходные условия для нее сводятся к двум положениям:

1. Разрешаются ситуации, когда:


любое число потребителей одновременно читают ранее
записанные данные общего ресурса;
(7.3)

2. Недопустимы ситуации, когда:


два или более производителей пытаются записать данные
в общий ресурс одновременно;
(7.4)

один или более потребителей пытаются прочитать данные


общего ресурса, когда некоторый (любой) производитель
записывает данные.
(7.5)
7.3. ЛОГИЧЕСКАЯ СТРУКТУРА ЦИКЛА 341

Это классическая задача синхронизации взаимодействия писателей (произ-


водителей) и читателей (потребителей), работающих с общим ресурсом. Если
взаимодействие буферизируется с помощью потока, то условие (7.5) ослабля-
ется. Вместо него требуется выполнение условия (7.2).
До сих пор обработка потоков связывалась с внешними управляющими
действиями. Возможен и другой взгляд на поток, при котором считается, что
программные единицы управляются входными потоками данных, именуе-
мых обычно сообщениями. Элементы потока сообщений могут восприни-
маться программной единицей и, тем самым, либо возбуждать соответству-
ющую реакцию, либо игнорироваться, в случае, когда данное сообщение не
предназначено для этой программной единицы.
Здесь, как и ранее, более точно надо говорить о процессах, а не о про-
граммных единицах, так что слова «управление программной единицы вход-
ным потоком событий» следует трактовать исключительно как управление
процессом, который порожден данной программной единицей (одним из них).
Стоит отметить, что современная трактовка такого управления чаще всего
связывается с объектами, получающими сообщения и возбуждающими ре-
акцию на них посредством вызова соответствующих методов.
Самое примечательное в потоках-сообщениях то, что их использование
позволяет в максимальной степени стандартизовать управление, а именно:
составляется единый цикл генерации сообщений и их рассылки процессам,
и те из процессов-получателей, которые предусматривают реакцию на дан-
ное сообщение, осуществляют нужную обработку в пределах собственной
компетенции.
Современные системы программирования включают в себя средства ор-
ганизации потоков-сообщений для управления процессами и обеспечивают
возможности реализации стандартного потокового управления. В подавля-
ющем большинстве случаев это связывается с объектной ориентированно-
стью. Примеры: Visual Basic, Delphi, Visual C++ и др.
В отличие от программ, которые рассматривались до сих пор и которые
можно назвать программами “одного измерения” (некая последовательность
описаний и действий, понятная сама по себе, если знаешь язык), стандарти-
зация потокового управления позволяет перейти к качественно иному стилю
программирования. В рамках этого стиля, получившего название событий-
но-ориентированного (см. § 3.5), программист готовит (под-) программы, ре-
агирующие на события, происходящие в системе. При этом стандартизован
способ передачи такой программе сведений о том, что настало ее время (на-
ступило событие).
342 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Важное уточнение! Реагируют такие программы не сами по себе, а че-


рез объекты, которые их содержат в качестве методов. Такое опосредованное
действие удобно во всех отношениях:
• Объект содержит все, что нужно для задания реакции на события. В
частности, он знает, на какие события предусмотрена его реакция.
• Можно создавать копии объектов (методы не дублируются).
• Контекст методов — это атрибуты объекта, другие его методы и, ра-
зумеется, описания, в которые погружено описание объектов данного
класса.
Активизация реакции объекта на событие задается следующей схемой:
если у объекта есть метод для данного события,
то этот метод вызывается для исполнения
иначе этот объект ничего не делает;
Это одна из мотиваций объектного подхода 15 ). При рассмотрении программ,
построенных для отработки реакций на события, самое важное то, что все
такие программы могут быть стандартизованы. Обсуждение такой схемы см.
в § 3.5.
В случае событийно-ориентированного программирования схема програм-
мы, называемой обычно проектом, строится следующим образом:
1. инициировать первичный набор объектов //(в дальнейшем он может
меняться, менять свои свойства и др.);
2. цикл // бесконечный цикл опроса-ожидания событий
если есть событие
то
цикл для всех объектов из набора
// опрос текущего набора объектов
если у объекта есть метод для данного события
то вызывать этот метод для исполнения
конец цикла
конец цикла
Не самая главная, не самая важная, но существенная и вдобавок такая, с которой програм-
мист немедленно сталкивается при программировании на современных инструментальных
15

системах, например, на Visual C++ или Delphi.


7.3. ЛОГИЧЕСКАЯ СТРУКТУРА ЦИКЛА 343

7.3.5. Понятие обстановки вычислений.


Действия, меняющие обстановку

Любой вид потоковой обработки — это схематическое описание процес-


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

/* Binary Search*/
#include <stdio.h>

// Размер массива поиска


#include <math.h>

// Массив поиска
#define SZA 10
int a[SZA];

void init () // Функция задания начальных значений a


{
...
}

int main()

// Переменная для организации потока ввода


{
char c;
init ();

// Переменная для искомого значения


do {
int x;
344 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

// Нижний индекс поиска


// Верхний индекс поиска
int imi = 0;

// Служебная переменная:
int ima = SZA;

//полуразность imi и ima


int h;

int response; // Ответ

printf ("Input: ");

// Заранее предполагаем неудачу


scanf ("%i", &x);
response=-1;
while (imi <= ima)
{
h = (imi+ima)/2;
if (x == a[h]) {response=h;break;}
if (x > a[h]) imi = h+1;
else ima = h-1;
}
printf ("Output: %i\n", response);
scanf ("%c", &c);
}
while ( c != ’n’);
return 0;
}

Заметим, что переменная response совмещает в себе две функции: индекса


нужного элемента, если он найден, и сообщения о неудаче, если такого эле-
мента нет. Такое совмещение часто целесообразно: особые значения резуль-
тата служат естественным сигналом об исключительной ситуации. break;
выводит нас за пределы двух операторов сразу, поскольку исполняется вну-
три условного оператора, а заканчивает минимальный объемлющий цикл.
Далее, поскольку мы ищем хотя бы одно решение, у нас в программе прак-
тически вычисляется квантор существования, т. е. дизъюнкция целого мно-
жества элементов. Поэтому в качестве начального значения ответа берется
−1, логически соответстующая единице дизъюнкции: лжи, отсутствию ре-
шения.16
Это общее правило: если вычисляется выражение вида i∈I ai , то в качестве начально-
N
го значения берется единичный элемент операции ⊗. Поэтому, например, при вычислении
16

суммы начальным значением обычно принимается 0.


7.3. ЛОГИЧЕСКАЯ СТРУКТУРА ЦИКЛА 345

Выделенный наклонным шрифтом цикл не связан с каким-либо потоком,


но, тем не менее, он выполняется определенное число раз тогда, когда зна-
чения переменных a, x, imi и ima удовлетворяют условиям, заданным в заго-
ловке цикла и во внутренних условных операторах.
На рассмотренном примере мы выделяем важное неформальное понятие.

Фрагмент программы выполняется в некоторой обстановке,


состоящей из тех данных, которые:

a) определяют поведение выполнения фрагмента, и


(7.6)

b) изменяются при выполнении фрагмента.


Эти два признака характеризуют понятие обстановки. В частности, локаль-
ные данные, возникающие и исчезающие вместе с фрагментом, не являются
элементами его обстановки. Не следует включать в обстановку и то, что не
является формально необходимым, даже если содержательный смысл фраг-
мента при его удалении страдает.17 Применительно к выделенному фрагмен-
ту программы 7.3.2 это переменная h. С одной стороны, она не обладает са-
мостоятельным формальным смыслом, а с другой — ее использование как
среднего арифметического imi и ima облегчает понимание смысла выделен-
ного фрагмента. Сами переменные imi и ima можно рассматривать или с ло-
кальной точки зрения, внутри одной итерации, и тогда тело цикла трактуется
как поиск x среди элементов массива a[imi],. . . , a[ima]; или глобально, инте-
ресуясь их предельными значениями, и тогда весь цикл трактуется как поиск
x среди элементов массива a[0],. . . , a[SZA].
В обстановку могут входить и призраки, например, оценка числа остав-
шихся шагов цикла с предусловием.
Обычно в языках программирования нет средств явного указания обста-
новки фрагментов программ, поскольку данное понятие остается неуточнен-
ным до конца. В какой-то мере суррогатом обстановки являются правила ло-
кализации имен, задающие контексты фрагментов программы. Дадим неко-
торые формальные определения, связанные с контекстами.
Определение 7.3.2. Локальным контекстом фрагмента программы назы-
вается совокупность всех локальных описаний констант, переменных и дру-
гих имен, записанных в данном фрагменте.
Еще раз напомним, что часто содержательный смысл фиксируется при помощи призра-
ков.
17
346 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Глобальный контекст фрагмента программы — объединение локальных


контекстов всех фрагментов программы, которые окружают данный фраг-
мент в тексте, либо другим образом указаны как источники описаний, ис-
пользуемых в данном фрагменте программы (например, в фрагментах, ко-
трые включены как библиотечные файлы директивой #include языка C/C++
или находятся в модулях, подключаемых директивой uses языка Object Pascal).
Если при объединении контекстов возникают коллизии или противоречия
(несколько одинаковых имен или одно и то же имя в разных смыслах), они
разрешаются согласно правилам локализации имен, задаваемым в описании
языка или системы программирования.
Конец определения 7.3.2.
Для обсуждаемого фрагмента 7.3.2 принципиально, что int a [SZA] — часть
глобального описания, поскольку a является элементом обстановки и вы-
деленного фрагмента, и функции main. Не очень принципиально, что дан-
ная переменная разделяется, т. е. совместно используется двумя функциями.
Описание int h; вполне можно было бы локализовать внутри выделенного ци-
кла и совместить это описание с присваиванием значения:

Такая модификация разумна, поскольку она следует правилам:


int h = (imi+ima)/2;

a) описывать переменные, используя как можно более узкие области види-


мости (переменная должна быть видна в точности там, где она употребля-
ется),

b) стремиться к использованию инициализации переменных при их описа-


нии.

Определение 7.3.3. Cостояние контекста в языке традиционного типа —


пара, состоящая из соответствия Z, сопоставляющего каждой переменной
контекста ее значение в конкретный момент выполнения программы и вер-
шины V абстрактно-синтаксического дерева представления программы, где
находится выполняемое действие.
Состояние обстановки — аналогичная пара, где Z задает значения пере-
менных обстановки.
Конец определения 7.3.3.
Характер изменения состояния обстановки в значительной степени предо-
пределяет организацию вычислений вообще и циклических вычислений в
7.4. АС-ПРЕДСТАВЛЕНИЕ ЦИКЛОВ 347

частности. Обстановка циклического фрагмента определяет, например, бу-


дет ли выполняться цикл (условие срабатывания). Для цикла do . . . while true
это условие тождественно истинно, для циклов других видов оно совпадает
с условием цикла.

§ 7.4. АБСТРАКТНО-СИНТАКСИЧЕСКОЕ ПРЕДСТАВЛЕНИЕ ЦИКЛОВ

7.4.1. Представление циклов

Абстрактно-синтаксическое представление оператора циклов достаточ-


но просто. Для циклов с пред- и постусловиями с точностью до наименова-
ний головных вершин структуры оказываются одинаковыми (см. рис. 7.2).
Разница проявляется только в интерпретации.

Оператор Оператор
цикла с pred- цикла с post-
условием условием

Условие Действия Условие Действия


Выражение Операторы Выражение Операторы

Рис. 7.2. Абстрактно-синтаксическое представление оператора циклов с


пред- и постусловиями

Алгоритм работы абстрактного вычислителя для <Цикла с предуслови-


ем> сводится к следующему:

1. Вычислить ветвь <Условие>.

2. Если результат вычисления 1 — истина, то


Вычислить ветвь <Действия>.
Перейти к 1.

3. Завершить вычисление <оператора Цикла с предусловием>.


348 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

Алгоритм работы абстрактного вычислителя для <Цикла с постусловием>


сводится к следующему:

1. Вычислить ветвь <Действия>.

2. Вычислить ветвь <Условие>.

3. Если результат вычисления 2 — истина, то


Перейти к 1.

4. Завершить вычисление <оператора Цикла с постусловием>.

Немного сложнее абстрактно-синтаксическое представление и алгоритм


его вычисления для оператора цикла for. Его предлагается разработать само-
стоятельно.

7.4.2. Структурные переходы

Представленных алгоритмов недостаточно для абстрактного вычисления


операторов цикла: не учитывается семантика вычислений операторов break;
и continue;. Их абстрактно-синтаксическое представление (см. рис. 7.3) еще

Оператор Оператор
break continue

Рис. 7.3. Абстрактно-синтаксическое представление оператора завершения


выполнения цикла, переключения и итерации

проще, чем в предыдущем случае.


Алгоритмы работы абстрактного вычислителя могут вызвать некоторые
затруднения в связи с нелокальностью их действия: завершается не та кон-
струкция, которая в данный момент вычисляется, а некоторая прародитель-
ская. Завершаемая конструкция, возможно, отстоит на много уровней от ме-
7.4. АС-ПРЕДСТАВЛЕНИЕ ЦИКЛОВ 349

ста, где вызван оператор завершения (потенциально расстояние между ними


не ограничено)18 . Это новая ситуация абстрактных вычислений.
Обратим внимание, что инструкция «Вычислить», используемая в каждом
из приведенных ранее алгоритмов работы абстрактного вычислителя, явля-
ется его рекурсивным вызовом. Эта рекурсия отражает вложенность абстракт-
но-синтаксических конструкций. И цикл, и итерация цикла, и переключение,
которые нужно завершать, всегда уже начали выполняться. Когда выполняет-
ся оператор завершения, они только приостановлены, следовательно, нужно
найти соответствующий вызов и прервать его вместе со всей цепочкой после-
дующих вызовов. Таким образом, следует откорректировать алгоритмы рабо-
ты абстрактного вычислителя для конструкций, в которых заданы переходы
к соседним ветвям.
Эта задача может решаться по-разному. Чаще всего определяют специаль-
ные флаги: F_break и F_continue. Они выставляются при выполнении опера-
торов break; и continue; с тем, чтобы прекратить нормальный ход вычисле-
ний.19
В частности, алгоритм для break; можно задать так:
1. Выставить F_break.
2. Завершить вычисление <оператора Завершения> break;.
Алгоритмы работы абстрактного вычислителя для конструкций, которые мо-
гут прекратить выполняться в результате операторов завершения break; и
continue;, необходимо откорректировать, добавляя в них проверку флагов.
Так, для <Последовательно выполняемых операторов> описание алгоритма
из § 2.3.3 исправляется следующим образом:
1. Если ветвей у вершины нет, то Завершить вычисление <Последова-
тельно выполняемых операторов>.
2. Установить первую ветвь вершины в качестве очередной.
3. Если F_break выставлен, то
Завершить вычисление <Последовательно выполняемых операторов>.
В теоретическом программировании доказывается, что структурных переходов на фик-
сированное число уровней вверх не хватает для структурирования произвольных схем про-
18

грамм.
Именно такое решение предлагается и при формальном устранении переходов в доказа-
тельстве теоремы Бема-Джакопини.
19
350 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

4. Если F_continue выставлен, то


Завершить вычисление <Последовательно выполняемых операторов>.

5. Вычислить очередную ветвь вершины.

6. Если следующей ветви для очередной ветви у вершины нет, то


Завершить вычисление <Последовательно выполняемых операторов>
иначе Установить следующую ветвь вершины в качестве очередной

7. Перейти к 3.

Как легко видеть, здесь более подробно раскрыто, что означает приведенное
ранее «Вычислить последовательно все ветви вершины». Сброс флагов —
задача вычисления не <Последовательно выполняемых операторов>, а то-
го цикла или переключения, которое должно завершится. Значит, алгоритмы
работы абстрактного вычислителя для циклов с пред- и постусловием тоже
корректируются. Ниже приводится модифицированный алгоритм для <Ци-
кла с предусловием>:

1. Вычислить ветвь <Условие>.

2. Если результат вычисления 1 — истина, то


Вычислить ветвь <Действия>.

3. Если F_break и F_continue сброшены, то


Перейти к 1.

4. Сбросить F_continue. // Нет нужды его специально проверять!

5. Если F_break выставлен, то


Сбросить F_break. Завершить вычисление <оператора Цикла с преду-
словием>.

6. Перейти к 1.

Аналогично корректируются остальные алгоритмы. Предлагается проделать


это самостоятельно (см. задачу 10 на стр. 361).
Приведенные алгоритмы работы абстрактного вычислителя характерны в
том отношении, что показывают возможность использования небольшой об-
щей памяти для экстренного завершения всех вызовов типа “Вычислить”: это
7.4. АС-ПРЕДСТАВЛЕНИЕ ЦИКЛОВ 351

переменные F_break и F_continue. В реальных трансляторах могут оказаться


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

:Метка уровня:
начало // конструкции 1;
операторы
начало // конструкции 2;
операторы
начало // конструкции 3;
операторы
завершить Метка уровня; // т. е. прекратить
// выполнение конструкции 1 и
// всех вложенных конструкций,
// с передачей управления
// к строке !!!
операторы
конец // конструкции 3;
операторы
конец // конструкции 2;
операторы
конец // конструкции 1;
/*!!!*/
...

то это есть просто спрятанный go to (ниже он и его метка выделены курси-


вом):
Программа 7.4.2
352 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

:Метка уровня:
начало // конструкции 1;
операторы
начало // конструкции 2;
операторы
начало // конструкции 3;
операторы
go to РМ // метка, добавляемая транслятором
операторы
конец // конструкции 3;
операторы
конец // конструкции 2;
операторы
конец // конструкции 1;
РМ: /*!!!*/
...
Любой конкретный вычислитель имеет среди своих команд оператор без-
условного перехода по адресу, а потому для реализации завершения доста-
точно следующего. Просматривая транслируемый текст, надо:
1. расставить метки, куда надо переходить при выполнении завершения
(а также в иных случаях, например, при организации циклов),
2. составить таблицу таких меток с полями: имя метки и адрес в про-
грамме,
3. вставить вместо завершителей команды безусловных переходов.
(несколько усложняют эту схему возможные описания, вложенные в кон-
струкции 1–3).
Эту идею не так просто применить к абстрактному вычислителю. Дело
в том, что здесь существенно используется предварительное преобразова-
ние текста, осуществляемое транслятором, которое никакого отношения к
выполнению программы не имеет. Определяя абстрактный вычислитель, мы
задавали его как интерпретатор, который не имеет предварительной глобаль-
ной информации о синтаксическом дереве исполняемой программы.
Тем не менее, имеется более эффективная, чем преобразование Бема-Джа-
копини, и, главное, более надежная и гибкая схема вычисления завершите-
лей, реализуемая интерпретатором. Но для этого необходима память вычи-
слителя, включающая структурированый стек контекстов (см. рис. 7.4).
7.4. АС-ПРЕДСТАВЛЕНИЕ ЦИКЛОВ 353


Стек растет вниз. Нынешний кон-
C текст назван Контекст 0. Самая
нижняя единица в стеке — текущий
результат. Стек структурирован
Контекст 2 как стек стеков. Каждый подстек
включает данные для блока про-
граммы. Таким образом, каждый
B
из подстеков задает контекст для
вычислений в соответствующем
Контекст 1 блоке. В начале каждого подстека
находится его маркер, который
A содержит всю необходимую слу-
жебную информацию о подстеке, в
Контекст 0 частности, ссылку на предыдущий
маркер.
Рис. 7.4. Стек и контексты

Для абстрактного вычислителя со стеком в начале обработки каждой кон-


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

1. Найти на стеке все экземпляры контекста, которые соответствуют вло-


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

2. Завершить вложенные конструкции, т. е. последовательно, в порядке


нахождения ликвидировать найденные контексты (в частности, пер-
вым ликвидируемым окажется контекст, непосредственно содержащий
завершитель, который перед своим уничтожением передает управле-
ние следующему найденному и т. д.).
354 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

В результате активным окажется как раз тот контекст, который должен про-
должать свои действия после выполнения завершения, причем именно с те-
ми ветвями дерева абстрактного синтаксиса, которые должны обрабатывать-
ся после только что выполненной ветви. Вычислитель даже может не знать,
каким образом окончилось вычисление этой ветви: принудительно или есте-
ственно. Таким образом, за счет вынесения на стек информации о состоя-
нии контекстов вычислителя, которые были приостановлены до тех пор, пока
не будут обработаны вложенные в них конструкции, появилась возможность
очистить вычисления от инородных проверок.
При реализации языков высокого уровня на современных системах про-
граммирования практически всегда создается структура контекстов, и чаще
всего именно она используется для реализации структурных переходов. Струк-
тура контекстов воплощена даже на аппаратном уровне, в частности, в систе-
мах команд машин серий Barroughs и Эльбрус. Такие машины называются
машинами со стековой архитектурой. Часто стековая архитектура и тегиро-
вание (см. рис. 1.7) совмещаются.
В языках, разрабатывавшихся для машин со стековой архитектурой, прак-
тически всегда имеется самая общая конструкция завершения: завершитель
с меткой уровня. В частности, в языке Эль-76 (см. [25]: язык высокого уров-
ня, который одновременно мог рассматриваться как машинно-ориентирован-
ный язык для машин серии Эльбрус) метки могут ставиться только к завер-
шенным структурным конструкциям (блокам) программы и переход на метку
означает завершение соответствующего блока.
Пожалуй, только в языках ЯРМО и LISP концепция структурных перехо-
дов корректно соединена с концепцией вырабатываемых значений. Практи-
чески каждый оператор в ЯРМО может иметь часть иначе, в которой про-
изводится вычисление значения выражения после его неявного завершения.
При явном завершении (например, при применении структурного перехо-
да), программист должен позаботиться о том, чтобы последнее исполняемое
предложение давало значение нужного типа, семантически подходящее для
того, чтобы стать значением завершаемого блока. Насчет языка LISP смотри-
те главу о функциональном программировании.
При структурных переходах возникает еще одна трудная проблема. В дан-
ном контексте могут быть созданы динамические значения, которые физиче-
ски располагаются в другой части памяти, а контекст содержит лишь ссылку
на них. В частности, именно так работает выражение new в C/C++, см., на-
7.4. АС-ПРЕДСТАВЛЕНИЕ ЦИКЛОВ 355

пример, фрагмент программы 7.7:

float (*cp)[25][10];
(7.7)
cp = new float[10][25][10];

и функция New(Pointer) в Pascal. Динамические значения почти наверняка


будут создаваться, если вы пользуетесь аппаратом ООП.
Конечно же, в технологиях и методиках програмирования дается масса
советов и требований, как лучше работать с динамическими значениями. В
идеальном случае, когда все требования всегда соблюдаются, получающая-
ся картина контекстов приобретает вид, изображенный на рис. 7.5. Даже в


C

Рис. 7.5. Идеальная ситуация с контекстами

случае идеального соблюдения всех требований видно, что операция завер-


шения становится нетривиальной. Выходя из контекста, нужно уничтожить
все созданные в нем динамические значения, которые, в свою очередь, могли
создать другие значения.
Но в реальности либо в Вашей программе, либо в используемых ею па-
кетах и модулях почти всегда есть нарушения требований технологии и хо-
рошего тона в программировании. В этом случае ситуация приобретает еще
356 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

более унылый вид (рис. 7.6) и задача освобождения памяти становится тяже-

Рис. 7.6. Реальная ситуация с контекстами

лой. Появляются две типичные ошибки:

• Освобождается и уничтожается значение, на которое уже ссылаются из


объемлющего, сохраняющегося контекста. Тогда, как говорят, некото-
рые ссылки “повисают”.

• Забывают удалить ненужное значение, и возникает ситуация, извест-


ная программистам под названием утечка памяти (memory leak), един-
ственным лекарством от которой чаще всего является перезагрузка си-
стемы. Эта ситуация показана на рис. 7.6 на примере изолированного
объекта, к которому нет доступа.

Задача освобождения памяти в реальной системе называется сборкой мусора


(garbage collection). Для нее разработано множество алгоритмов, но все они
либо ограничены по области применимости, либо не могут предотвратить
7.4. АС-ПРЕДСТАВЛЕНИЕ ЦИКЛОВ 357

утечки памяти и (чаще всего) появления висячих ссылок.20 Поэтому удале-


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

Рис. 7.7. Дерево контекстов

ных контекстов есть несколько подчиненных им активных либо приостано-


вленных контекстов.
Как уже говорилось, это — типичная ситуация при практическом решении теоретически
неразрешимых задач. Задача сборки мусора является таковой, если не дублировать память.
20

А дублирование памяти практически редко используется.


358 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

7.4.3. Исключения

Еще одним важным случаем, когда приходится покидать оператор, не ис-


полнив его до конца, является возникновение исключительной ситуации. Для
обработки исключительных ситуаций, начиная с языка Ada, вводятся специ-
альные средства. Рассмотрим их на примере C++.

try

catch (<Имя исключения>)


<Оператор try> (7.8)
<Оператор catch>

Конструкция catch может повторяться несколько раз. Внутри <Оператора


try> может использоваться конструкция
throw <Имя исключения>;
Эта конструкция возбуждает исключение с соответствующим именем.
Кроме того,21 все ненормальности, возникающие в ходе исполнения опе-
раторов внутри <Оператора try>, также вызывают исключения со стандарт-
ными именами. В частности, такие (обычно вылавливаемые лишь по непри-
ятностям, возникающим впоследствии) ошибки, как переполнение диапазо-
на целых чисел или выход индекса за границы массива, также возбуждают
исключения. При возникновении исключения:
1) уничтожаются все возникшие внутри <Оператора try> локальные объек-
ты,

2) ищется соответствующий имени исключения обработчик исключения, т. е.


<catch>;

3) если обработчик найден, то


исполняется найденный обработчик;

4) если обработчик не найден, то


управление передается оператору try-catch, находящемуся в объемлю-
щем контексте, который обрабатывает исключение по тем же правилам;
В стандарте языка С++ это оговорено явно, но в Visual C это работает, лишь если включена
соответствующая директива периода трансляции.
21
7.4. АС-ПРЕДСТАВЛЕНИЕ ЦИКЛОВ 359

5) если обработчик исключений не найден и объемлющего оператора try-


catch нет в контексте программы, то
вызывается функция завершения работы программы;
6) завершается оператор (7.8).
Возникает вопрос: что считается объемлющим контекстом? Тот контекст, в
который вложен оператор try-catch в тексте самой программы или контекст
подпрограммы, вызвавшей другую подпрограмму, в тексте которой находит-
ся данный оператор? И здесь мы естественно переходим к следующей главе
курса, в которой подробно разбираются процедуры и аспекты работы про-
грамм, связанные с их вызовами.
Но перед завершением главы нужно упомянуть о еще одной возможно-
сти. В языках Java и Object Pascal оператор try может завершаться опера-
тором finally, который выполняется в любом случае, и при нормальном за-
вершении, и при структурном выходе, и при ошибке. В языке C++ можно
добиться почти такого же эффекта, использовав catch(. . . ), но нормальный
выход в этом случае остается неотработанным.
Идея поддержки работы с исключительными ситуациями плодотворна и
в методическом плане. В самом деле, удобнее программировать действия,
не заботясь о том, как должна реагировать программа на ситуации, когда
нарушается естественная логическая последовательность действий. Описа-
ние необходимых в этих ситуациях действий в таких случаях выносится из
основной «канвы повествования» программы в специальные разделы, четко
разграничивающие то, что необходимо выполнить в каждой из ситуаций. К
примеру, вы пишите серьезную вычислительную программу, которая может
работать с некорректными данными, вызывающими деление на ноль. Учет
этого в алгоритме потребовал бы реализовать соответствующие проверки,
нарушающие логику изложения, а вот отдельное описание того, что нужно
делать, когда такое происходит, помогает, в частности, при организации стан-
дартного реагирования на данную ситуацию.
Как уже отмечалось (см. § 3.5), оперирование с исключительными си-
туациями можно рассматривать как один из вариантов программирования в
событийно-ориентированном стиле. Здесь события — возникновение ситуа-
ций, для которых планируется реакция. Если таких событий много, а основ-
ной процесс, для которого следует запрограммировать реакцию на исключе-
ния, логически не очень сложен, то использование средств поддержки опе-
рирования с исключительными ситуациями является реализацией стиля про-
грамирования от событий. Как и в общем случае, здесь возникает вопрос о
360 ГЛАВА 7. ЦИКЛИЧЕСКИЕ ВЫЧИСЛЕНИЯ

том, с какими событиями-исключениями можно иметь дело, какими спосо-


бами возбуждается реакция на исключения.
Есть еще одна методическая трудность работы с исключениями: что де-
лать после отработки реакции? Как правило (и это зафиксировано в при-
веденном выше описании действий абстрактного вычислителя) реакция за-
канчивается завершением блока, который определяет область действия си-
туации (конструкция try). Если не считать тривиального (и совершенно не-
гибкого) прекращения вычислений программы после отработки реакции, то
принципиально возможны еще два решения: возврат в точку выполнения, в
которой произошел вызов реакции (это трактуется как исправление ошибки
при выполнении реакции), и завершение блока, в котором определено пред-
писание о реагировании на ситуацию (это трактуется как невозможность ис-
правления ошибки и, как следствие, невозможность выполнения указанно-
го блока). Второе решение легко моделируется с помощью описанного выше
механизма. Что касается трактовки исключения как исправления ошибки, то,
хотя иногда она и удобна (например, в задаче синтаксического анализа), она
пытается поднять исключение до уровня того, что в системе сентенциаль-
ного программирования Prolog называется backtracking. Такой вид возвратов
исключительно плохо совместим со структурным программированием, да и
с другими стилями, кроме сентенциального, и поэтому такой трактовки ис-
ключения стараются избегать. Заметим, что указанный нами частный случай
синтаксического анализа как раз и относится к числу задач, где естественней
всего сентенциальное программирование.

Задания для самопроверки

1. Какие типовые приемы из перечисленных выше использованы в про-


граммах из таблицы 7.5?

2. Составить программу, аналогичную 7.2.2, в которой не требуется ни


переменная Prime, ни двойная проверка ее значения.

3. Изменить програму 7.2.5 таким образом, чтобы файл не перезаписы-


вался, если в этом нет нужды.

4. Изменить програму 7.2.5 таким образом, чтобы файл использовался как


словарь ранее сосчитанных простых чисел, пополняемый в тех случа-
ях, когда в нем не удается найти нужного элемента.
7.4. АС-ПРЕДСТАВЛЕНИЕ ЦИКЛОВ 361

5. Какое технологическое решение Вы могли бы предложить для явного


выделения параметров цикла в языке, которым Вы пользуетесь?

6. Каковы логические причины концептуального противоречия между со-


вместными циклами и потоками?

7. Разработать абстрактно-синтаксическое представление и алгоритм его


вычисления для оператора цикла for.

8. Разработать абстрактно-синтаксическое представление и алгоритм его


вычисления для оператора try-catch.

9. Объясните, почему в фрагменте программы 7.7 переменная cp имеет


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

10. Разработать алгоритм вычисления цикла for для случая структурных


завершений.
Глава 8

Подпрограммы

Формирование понятия подпрограммы является одним из наиболее зна-


чительных достижений в области информатики. Необходимость использо-
вания подпрограмм осознавалась еще на самом раннем этапе применения
вычислительной техники. В течение всего периода существования програм-
мирования это понятие постоянно развивалось.
Первоначально подпрограммы понимались исключительно как средство
сокращения записи алгоритмов путем ‘вынесения за скобки’ их общих ча-
стей. Очень быстро оформилось понятие подпрограммы как библиотечного
средства, — такой программной единицы, которая может (и должна) быть
использована в различных программах, в том числе и в программах различ-
ных разработчиков. Как только было осознано, что конструирование сколько-
нибудь серьезной программы немыслимо без разбиения ее на относительно
независимые части, т. е. без так называемой декомпозиции программ на мо-
дули, роль подпрограмм существенно возросла: их начали рассматривать в
качестве основы модуляризации. Под модуляризацией понимается разбиение
программы на относительно независимые части, допускающие независимые
разработку и использование, взаимосвязи между которыми указываются яв-
но.
В патриархе семейства языков программирования FORTRAN подпрограм-
мы использовались прежде всего как средство модуляризации, и в этом от-
ношении FORTRAN по меньшей мере на 25 лет опередил другие языки про-
граммирования. Правда, изначальные технологические цели модуляризации
языка FORTRAN были совсем не теми, для которых она актуальна сейчас:
независимое, абстрактное рассмотрение подпрограмм. В данном языке мо-
дульность — это, в первую очередь, одно из средств раздельной компиля-

362
363

ции и управляемой раздельной загрузки в память для последующего выпол-


нения программных единиц, хранимых на внешних носителях. Что касает-
ся независимости разработки подпрограмм, то, как оказалось впоследствии,
модульность на уровне компиляции вполне может применяться и для этих
целей. И этим стали активно пользоваться.
Мы видим, что есть два вида модуляризации:
• технологическая модуляризация, предназначенная для поддержки не-
зависимой разработки подпрограмм на уровне декомпозиции програм-
мы, и

• модуляризация для сегментации, поддерживающая независимую транс-


ляцию и исполнение программных единиц, называемых обычно загру-
зочными модулями, или сегментами.

Задача сегментации программы актуальна, когда требуется работать в усло-


виях дефицита памяти; она не противоречит технологической модуляриза-
ции, а потому, следуя FORTRANовской традиции, два вида модуляризации
очень часто смешиваются. Но это разные задачи, которые нужно решать в
такой последовательности: сначала определяется логическая декомпозиция,
а после того, как она построена, строится декомпозиция на сегменты.1
Две стороны подпрограмм — использование для выделения общих ча-
стей алгоритмов и для декомпозиции сложных программ — по сей день явля-
ются главными. Средства языков программирования, предназначенные для
поддержки обоих аспектов подпрограмм, постоянно развиваются и данный
раздел курса программирования нельзя считать полностью устоявшимся. Ви-
Если происходит их смешивание, а такое можно встретить сплошь и рядом, то в первую
очередь страдает сегментация. К примеру, на логическом уровне вполне естественно груп-
1

пировать в одном модуле все средства работы с какой-либо структурой данных, включая
инициализацию, обработку и уничтожение. Более того, именно такая организация модулей
предписывается современными технологиями ООП как практически безальтернативная. Но
на уровне сегментной декомпозиции ситуация меняется: инициализация и уничтожение —
разовые действия, отнесение которых к одному сегменту с обработкой приводит к ничем
не оправданному разбуханию загрузочного модуля. Это давно знали программисты-прак-
тики и предлагали разделять виды модуляризации. В этом плане показательным является
язык ЯРМО, где возможности приписывания фрагментов кода к разным загрузочным сег-
ментам независимы от логической декомпозиции. Несмотря на очевидные преимущества
подобного разделения, еще и сегодня общераспространенные системы программирования
рассматривают сегментную декомпозицию в подчинении технологической, объявляя, что
программные единицы, которые можно подключать к сегментам, есть логические модули.
364 ГЛАВА 8. ПОДПРОГРАММЫ

димо, в ближайшие десятилетия здесь еще будут принципиальные нововве-


дения и изменения.
Если раньше понятие подпрограммы связывалось исключительно с от-
дельными алгоритмами, которые ими реализуются, то в современных языках
понятие подпрограммы в большей мере соотносится с совместным опреде-
лением взаимосвязанных алгоритмов, работающих над общим (локальным
для всего набора алгоритмов) пространством данных. Поэтому, если преж-
нее понятие библиотеки — это просто набор реализаций тематически связан-
ных алгоритмов, то нынешние библиотеки объединяют подпрограммы во-
круг данных, над которыми они оперируют, и тем самым помогают модуля-
ризации. Это развитие отражает потребности технологии конструирования
программ.
Что же касается сегментной декомпозиции, то ее задачи в условиях по-
стоянного снижения стоимости памяти сегодня ушли на второй план. Как
следствие, сейчас не появляются новые или уствершенствованные средства
ее поддержки. Однако было бы опрометчиво полагать, что проблемы сегмен-
тирования уже никогда не станут вновь актуальными.

§ 8.1. ВИДЫ ПОДПРОГРАММ

Проще всего рассматривать подпрограмму как на операционную едини-


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

b) процедуры, выполнение которые используются лишь для преобразования


данных, доступных в контексте вызова, и которых не порождает значений
(вызов процедуры — это оператор).

Эти два понятия естественно объединяются путем формального расширения


множества всех мыслимых значений всех типов специальным значением ни-
что (void). Считается, что процедура есть функция, которая вырабатывает
такое значение. Данный прием принят во многих развитых языках програм-
мирования, начиная с Алгола-68, в частности, в С, C++ и Java. Поэтому в
перечисленных языках понятие подпрограммы не вводится. Есть лишь по-
нятие функции.
8.1. ВИДЫ ПОДПРОГРАММ 365

Более того, даже для такого строгого языка как Pascal, стандарт кото-
рого с целью защиты от возможных ошибок запрещает использовать вызов
функции как процедуры (если Average — функция, оператор Average; был
бы некорректен), конкретные системы программирования (например, Turbo
Pascal) допускают такую “вольность”.
В настоящем изложении для подпрограмм обоих видов используется тер-
мин процедура. О функциях говорится либо тогда, когда хочется явно указать,
что процедура вырабатывает требующееся нам значение и используется вну-
три выражения, либо когда нужно сослаться на конкретный язык (например,
С/С++).
Некоторые подпрограммы оказываются одним из первых средств, с ко-
торыми приходится сталкиваться при знакомстве с языками программирова-
ния. Это такие подпрограммы, которые предоставляются языком или систе-
мой программирования:

• стандартные подпрограммы, представленные и зафиксированные в


описании языка;

• встроенные подпрограммы, алгоритмы которых реализуются в рамках


системы программирования (в некоторых языках стандартные и встро-
енные подпрограммы не различаются);

• библиотечные подпрограммы, которые предоставляются в системе про-


граммирования дополнительно и не регламентируются языком.

Примерами стандартных подпрограмм языка Pascal являются функции


Succ и Pred. К встроенным подпрограммам языка Pascal относятся процеду-
ры и функции работы с файлами Read, Write, Eof и др. В языках С/С++ встро-
енные и стандартные подпрограммы формально не различаются. Например,
exp воспринимается программистом как стандартная функция, но ее опре-
деление содержится (в оговоренном стандартом языка) библиотечном файле

Библиотечные подпрограммы предназначены для поддержки определен-


math.h

ных видов работ и обычно группируются в “тематические” библиотеки. При-


мером такой библиотеки может служить пакет afxwin.h, который содержит
действия с простейшим окном в системе Windows или пакет Canvas для про-
граммирования простейших графических изображений в окне программы,
создаваемой Delphi.
366 ГЛАВА 8. ПОДПРОГРАММЫ

Техника использования стандартных, встроенных и библиотечных под-


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

мы обеспечиваем доступ к описаниям из данной библиотеки, использовав


namespace <Имя>{. . . };

оператор

Эти два средства работы с библиотеками C++ поддерживают модуляриза-


using <Имя>;

цию, хотя и очень ограниченно. В системе Delphi модуляризация продумана


и реализована намного лучше: в частности, при описании модуля в качестве
библиотеки для внешнего использования программист имеет возможность
указать не только предоставляемые и скрываемые части, но и то, какие дей-
ствия следует выполнить при активизации библиотеки и при завершении ра-
боты с ней.
Понятие подпрограммы-модуля (или просто модуля) в простейшем слу-
чае может пониматься как совокупность совместно используемых описаний.
Только этот аспект модулей заложен в понятие бибилиотечных файлов C. В
более развитых системах модульности центральной задачей является выде-
ление таких программных единиц, которые допускают независимые разра-
ботку, модификацию и использование. Таким образом, в модуле появляется
открытая часть, обычно называемая интерфейсом.
Определение 8.1.1. Интерфейс программного фрагмента — совокупность
описаний из его локального контекста, которые предоставляются другим фраг-
ментам программы, использующим данный, и, соответственно, входят в их
глобальный контекст. Реализация программного фрагмента — совокупность
описаний из его локального контекста, которые остаются недоступными для
фрагментов, использующих данный.
8.1. ВИДЫ ПОДПРОГРАММ 367

Конец определения 8.1.1.


В модулях различаются части локального контекста, которые образуют ин-
терфейс модуля и его реализацию. В модулях языка Object Pascal явно выде-
ляются части interface и implementation. В первой лежат лишь описания, а
во второй — конкретные реализации.
Библиотеки языка С развитых средств модульности не содержат. Но в
объектно-ориентированных языках С++, Java и Object Pascal (часто называ-
емый Delphi) есть понятие класса, которое в одном из своих аспектов можно
рассматривать как комплект данных и процедур (методов), связанных с эти-
ми данными. Классы обеспечивают гораздо более высокий уровень модуля-
ризации, достаточный для применения современных технологий программи-
рования.
В описании класса языка C++, Object Pascal или Java то, что не полагается
видеть другим, объявляется как private, а то, что другим полагается видеть —
public.

Пример 8.1.2. Класс на языке С++, представляющий частный вид техниче-


ской системы — систему управления температурой жидкости.
Программа 8.1.1
class CHeatSystem : public CTechSystem;
{
// Interface:
public:
CHeatsystem(){. . . }

// System Data
struct Temperature;
int GetTemperature(Temperature* x)
{
return x->outcoming_liquid;
}
// Implementation
protected:
typedef struct
{
int working_area,
368 ГЛАВА 8. ПОДПРОГРАММЫ

input_stream, outcoming_liquid;
} Temperature;
...
};
Здесь Temperature не дана пользователю данного класса как полностью из-
вестная ему структура, и он не знает внутреннего строения Temperature, ко-
торое может быть в любую секунду заменено без вреда для использующих
программ. Программист, использующий модуль, может даже не подозревать,
что на самом деле тип Temperature хранит не только температуру на выхо-
де системы, которая ему нужна, но и другие логически связанные с ней ха-
рактеристики, используемые внутренними процедурами моделирования или
управления.
Конец примера 8.1.2.
Несколько слов об употреблении терминов. Понятие ‘подпрограмма-модуль’
носит абстрактный характер: оно не связано ни с конкретным языком, ни
с какой-либо системой программирования или же операционной системой.
Если говорят о некотором языке, то для его средств модуляризации, есте-
ственно, используют термины этого языка. Например, когда требуется ука-
зать на подпрограмму-модуль конкретного языка, делается явное уточнение:
библиотека, библиотечный файл С и т. д. Однако термин ‘библиотека’ также
обозначает различное содержание. К примеру, как мы уже имели возмож-
ность убедиться (см. п. 1.1.2), в любой реализации языка С/С++ представле-
но по крайней мере три вида библиотек (если смотреть со стороны системы
программирования).
Если взглянуть со стороны операционной системы, то окажется, что при-
нято различать библиотеки по их отношению к процессу загрузки программ.
Появляется два вида библиотек: статически загружаемые (SLL — static linked
library) и динамически загружаемые (DLL — dynamic linked library). Не будет
большой ошибкой считать, что SLL транслируются вместе с основной про-
граммой и являются частью ее объектного кода (неточность здесь в том, что,
конечно же, эти библиотеки хранятся в почти до конца оттранслированном
виде, готовом к подключению на самой последней стадии работы транслято-
ра и загрузчика). Что касается DLL, то эти библиотеки отстоят от трансляции
языка программирования еще дальше: когда потребность в соответствующих
средствах выясняется в динамике выполнения программы, операционная си-
стема обращается к хранилищу DLL, извлекает из него нужную подпрограм-
му-модуль, приводит ее к виду, в котором программы библиотеки могут быть
8.1. ВИДЫ ПОДПРОГРАММ 369

выполнены (выделяет память, настраивает адреса и т. д.), и тем самым дела-


ет DLL частью выполняемой программы. Еще один шаг в том же направле-
нии делают системы клиент-сервер (в частности, системы CORBA, BENTO,
COM и их развития). Когда подпрограмме-клиенту требуются функции под-
программы-сервера, она устанавливает связь с сервером, который может на-
ходиться в другой вычислительной системе, и передает ему запрос и данные.
Сервер возвращает программе результаты, а также может модифицировать
предоставленные данные.
Иллюстрацией модульности может служить совокупность средств, опре-
деляющих регламент работы с некоторой структурой данных, например, дис-
циплину очереди, которая характеризуется следующими правилами:
1. элементы помещаются в очередь для хранения, они появляются по мере
необходимости, определяемой использующей программой;
2. для обработки элемента очереди он извлекается из нее и после этого
становится недоступным;
3. из очереди элементы извлекаются строго в порядке их поступления в
очередь; очередь может быть в трех состояниях: пустая, когда извле-
чение элемента невозможно, заполненная, когда нельзя поместить эле-
мент в очередь, и нормальная, когда возможно как пополнение очереди,
так и извлечение из нее элемента.
Для работы с очередями целесообразно подготовить комплект процедур и
функций с тем, чтобы их можно было использовать, не заботясь о деталях
реализации доступа к элементам и помещения их в очередь. Состав такого
комплекта может быть следующим:
1. функция, вырабатывающая значение состояния очереди: пустая, запол-
ненная и нормальная, в зависимости от наличия сохраняемых элемен-
тов;
2. функция, вырабатывающая значение элемента очереди и извлекающая
элемент из очереди;
3. процедура, помещающая элемент в очередь для хранения.
Для использования очереди достаточно перечисленных средств оперирова-
ния; необходимо иметь сведения о том, что происходит при их употребле-
нии, и нет никакой потребности знать, как конкретно представлена очередь
в реализации.
370 ГЛАВА 8. ПОДПРОГРАММЫ

Если теперь обратиться к реализации безотносительно к тому, как перечи-


сленные программные единицы используются, то легко заметить, что сред-
ства комплекта взаимосвязаны. Их должны объединять представляющие оче-
редь структуры данных, например, массив-хранилище элементов и два ука-
зателя: один на начало очереди, т. е. на первый из имеющихся в ней элемент,
другой на конец, т. е. на последний элемент. В свое время мы разовьем этот
пример до программной реализации и покажем, как можно использовать об-
суждаемый модуль.
Понятие подпрограммы-модуля требует развития понятия контекста. Кро-
ме локального и глобального контекста отдельного фрагмента появляются
дополнительно:
Определение 8.1.3. Общий контекст модуля — совокупность данных и дру-
гих программных единиц, описанных в модуле для совместного использова-
ния процедурами модуля, включая и сами процедуры модуля. Предоставля-
емый контекст модуля — часть средств общего контекста модуля, разрешен-
ная для использования программами, применяющими данный модуль.
Конец определения 8.1.3.
По отношению к подпрограмме-модулю общий контекст является локаль-
ным (в точном соответствии с приведенным ранее определением), а по отно-
шению к средствам из предоставляемого контекста он является частью2 гло-
бального контекста. В языках программирования, которые проявляют осо-
бую заботу о защите данных от нерегламентированного использования, не
предусматривается употребление в модуле иных средств, кроме как из об-
щего контекста.
Продолжая обсуждение примера с очередями, следует указать, что:

• перечисленные три процедуры составляют предоставляемый контекст


соответствующего модуля;

• эти программные единицы плюс конкретная реализация представления


очередей, например, через массив-хранилище элементов и указатели на
начало и на конец очереди — общий контекст модуля;

• необходимые для реализации процедур внутренние данные оказывают-


ся локальным контекстом последних.
2
Возможно, невидимой для программиста, использующего модуль.
8.2. ИМЕНОВАНИЕ ПРОЦЕДУР 371

Отмеченное ранее разграничение того, что делает модуль, и как он это дела-
ет, указывает на повышение уровня абстрактности при совместном описании
процедур в рамках единого модуля.
В С/С++ средства модуляризации программ связываются с понятием за-
головочного (header) файла, в котором явно описывается, что предоставляет-
ся модулем (текстовым файлом), озаглавленным этим заголовочным файлом.
Все остальное является локальным для данного модуля3 .
В стандартном языке Pascal средства модуляризации программ, позволя-
ющие объединять описания процедур и функций, отсутствуют. Можно ска-
зать, что язык предоставляет возможность модуляризации только на уровне
операционных единиц, т. е. он допускает лишь отдельные описания процедур
и функций. Это препятствует повышению уровня абстрактности программ,
делает невозможным осуществление контроля выполнения регламента ис-
пользования данных, имеет ряд других отрицательных следствий. Для улуч-
шения языка в данном отношении системы программирования расширяют
стандартный Pascal, вводя в него средства модуляризации. Что касается ис-
пользования стандартного языка Pascal, то он практически вышел из употре-
бления. Если же приходится его использовать, то модульность имитируется
при помощи организационных мер: выбор имен, специальное расположение
текстов программ при распечатке, комментирование. Впрочем, такого рода
моделирование — обычное дело программиста, когда нет желаемых средств
в используемом языке либо системе.
Для профессиональной разработки алгоритмов и реализации их на языке
необходимо научиться не только описывать свои подпрограммы, но и приме-
нять и перестраивать при необходимости чужие, овладеть практически все-
ми возможностями, предоставляемыми инструментальным языком в части
оперирования с процедурами.

§ 8.2. ИМЕНОВАНИЕ ПРОЦЕДУР

Одним из основных средств языка, без которого невозможно использо-


вание процедур, является именование, т. е. обозначение процедуры именем,
замещающим ее алгоритм при использовании.
Когда языки программирования были преимущественно машинными язы-
Вообще говоря, это противоречит определению в стандарте языка препроцессора, как
программы, встраивающей вместо #include текст включаемого файла; все становится на
3

свои места, когда в С++ строго определяется понятие пространства имен namespace.
372 ГЛАВА 8. ПОДПРОГРАММЫ

ками, роль имен играли адреса входа в подпрограммы (в случае ассемблер-


ных языков имеет смысл говорить о символическом адресе, т. е. об адресе, по-
лучающем символическое обозначение). По мере устранения из языков ма-
шинных зависимостей имя подпрограммы получает самостоятельность, оно
становится обозначением алгоритма. Упоминание имени, как правило, вызы-
вает вычислительный процесс, задаваемый именем алгоритмом. Имя проце-
дуры позволяет при описании процедуры не заботиться о том, как она будет
использоваться, а при использовании — не знать, как реализован обозначае-
мый именем алгоритм. Таким образом, именование процедур — это одно из
средств, позволяющее при описании алгоритма сосредоточить внимание на
том, как реализовать то, что требуется в данной конкретной задаче и исполь-
зовать уже имеющиеся методы, не вдаваясь в их внутреннюю структуру.
В конструировании программ разделение аспектов, соответствующих во-
просам что? и как?, подобное только что проведенному, отражает весьма
важный принципа методологии работы со сложными системами, в том чи-
сле и программирования — принцип абстрагирования, который заключается
в разделении уровней.
• На уровне разработки программной единицы программист стремится
не знать о ее использовании, т. е. абстрагироваться от уровня исполь-
зования.
• На уровне использования программной единицы желательно не знать,
как она устроена (реализована), т. е. рассматривать ее как неделимое
целое, абстрактно.
Если какое-либо средство языка ориентировано на то, чтобы облегчить про-
граммисту разделение уровней, то оно поддерживает следование принципу
абстрагирования при конструировании программ. В этом отношении имено-
вание — одно из многих средств поддержки принципа абстрагирования. С
другими такими средствами мы встретимся в дальнейшем.
Применительно к языку С/С++ именование задается заголовком функции,
в котором обязательно присутствует имя, идентифицирующее функцию:
void MyProc (Matrix M, const int SizeM)
int MyFunc ()
void PrintMatr (Matrix M, const int SizeM)
void main()
Здесь MyProc, MyFunc, PrintMatr и main — имена функций. Если хочется
определить процедуру, то в заголовке указывается спецификатор void, ука-
8.2. ИМЕНОВАНИЕ ПРОЦЕДУР 373

зывающий на то, что данная операционная подпрограмма не вырабатывает


значение.
Обозначаемый алгоритм текстуально описывается непосредственно сле-
дом за заголовком в виде заключаемой в операторные скобки { и } последо-
вательности операторов. Эта часть процедуры называется ее телом:

int MyFunc ()

// Тело процедуры
{ //
...
} //

Содержательно, тело процедуры — это описание алгоритма, выполнение


которого активизируется при вызове процедуры.
При абстрактно-синтаксическом представлении описания процедур не-
обходимо решить вопрос согласования такого представления с конструкци-
ей вызова процедуры. Необходимо, чтобы абстрактный вычислитель, обра-
батывая описание процедуры, обеспечил связь вызова с описанием, чтобы
при обработки вызова процедуры осуществлялся переход к телу, которое ис-
пользуется как оператор замещающий оператор вызова. В рамках этой задачи
именованию отводится следующая роль: нужно построить связь между име-
нем процедуры и ее телом. Если говорить о процедурах, не вырабатываю-
щих значения и не имеющих параметров, то содержательно задача сводится
к образованию абстрактно-синтаксической конструкции, представленной на
рис. 8.1. Этой конструкцией устанавливается атрибут <тело>, который мож-
но трактовать как адрес абстрактно-синтаксического представления дерева
процедуры. <Тело> может быть вычислено в позиции оператора, это обеспе-
чивает возможность выполнения конструкции вызова как выполнения тела.
При вызове процедуры абстрактный вычислитель, действующий в соот-
ветствии с любым из двух алгоритмов вычисления структуры конструкции
вызова (см. рис. 2.10. из § 2.3.4), прежде всего должен найти имя вызывае-
мой процедуры. Если имя не найдено, то это квалифицируется как ошибка
конструкции вызова.
Зададимся вопросом о том, где могут (или должны) храниться имена, что-
бы обеспечить возможность их эффективного нахождения? Согласно семан-
тике описания процедуры, как и любого другого описания, все имена рассма-
триваются в качестве атрибутов конструкций, в которых они появляются. В
соответствии с этим можно организовать и хранение описываемых имен как
374 ГЛАВА 8. ПОДПРОГРАММЫ

Имя
/* процедуры */

Атрибут
тело
Оператор
Тело
процедуры

Рис. 8.1. Простейший случай результата обработки абстрактным вычислите-


лем описания процедуры

атрибутов соответствующих вершин дерева. Однако это не очень удобно по


следующим причинам:

• хранение и поиск имен отделены от работы с деревом абстрактного


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

• обеспечение работы со стеком контекстов (см. § 7.4.2) естественно ор-


ганизовывать при обработке описаний, когда есть возможность сгруп-
пировать секции контекстов без дополнительных просмотров дерева;

• для внешних по отношению к программе сущностей указать конструк-


ции, в которых появляются их описания, затруднительно (принятое имен-
но с этой целью в Алголе 68 соглашение о стандартном вступлении пло-
хо согласуется, например, с потребностью модульной структуры би-
блиотек).
8.2. ИМЕНОВАНИЕ ПРОЦЕДУР 375

Таким образом, требуется хранилище имен, которое необходимо и достаточ-


но для решения задач идентификации и работы со стеком контекстов. В ка-
честве структуры такого хранилища, обычно называемого таблицей имен,
целесообразно использовать дерево контекстов, каждая вершина которого
содержит секцию стека контекстов, соответствующую комплекту объектов,
описываемых в некоторой конструкции программы. Путь от корня к какой-
либо из вершин этой таблицы отражает появляющуюся в динамике вычисле-
ний структуру стека контекстов.
В современных языках программирования все чаще появляются проце-
дуры с одним и тем же именем, но обозначающие разные алгоритмы. В этой
связи встает вопрос о том, как трактовать именование таких процедур.
Прежде всего, данная ситуация знакома по другим языковым конструк-
циям, когда одно и то же обозначение имеет различный смысл, раскрывае-
мый в зависимости от ситуации. Так, в Pascal очень много смыслов имеет
служебное слово end. Каждый из них распознается при синтаксическом ана-
лизе текста и далее конкретного синтаксиса не распространяется. Различные
смыслы обозначаются знаками операций. Например, в C/C++, Pascal и дру-
гих языках знак “+” связан с различными алгоритмами для разных типов ар-
гументов, и типы аргументов указывают, какой из алгоритмов выбрать. Еще
больше вариантов смыслов в C/C++ имеет знак “*”, который обозначает и
разные алгоритмы умножения, и то, что применяется указатель. Для опреде-
ления того, что имеется в виду при употреблении знака “*”, используется как
его синтаксическая позиция, так и типы аргументов. Заметим, что разное ис-
пользование символа (например, “+” как знак операции “+” или часть знака
“++”) — не относится к рассматриваемому вопросу, поскольку само понятие
смысла определяется, начиная с уровня лексем.
Эти иллюстрации показывают, что в языке программирования одно и то
же обозначение может относиться к разным сущностям, но за счет дополни-
тельной информации всегда однозначно понятно, какая конкретная сущность
должна быть использована. О таких обозначениях говорят, что они являются
полиморфными.
Важным видом полиморфизма является полиморфизм имен: один и тот
же идентификатор, описанный в разных пространствах имен, обозначает раз-
личное: данные, алгоритмы и др. Здесь смысл идентификатора, или, что то
же, то, имя какой сущности используется, раскрывается посредством приме-
нения правил локализации имен (см. следующий параграф). Процедуры с од-
ним и тем же именем, но обозначающие разные алгоритмы, (в С++, и др.) —
разновидность полиморфизма имен, раскрываемого за счет привлечения так
376 ГЛАВА 8. ПОДПРОГРАММЫ

называемых сигнатур — информации о количестве и типах параметров. Это


в точности то же самое, что и полиморфизм операций в выражении, когда
конкретный смысл операции выявляется типами аргументов.
При рассуждениях о процедурах удобно рассматривать сигнатуру как часть
имени. И хотя такое рассмотрение несколько противоречит разграничению
контекстно-свободного синтаксиса и контекстных зависимостей, это проти-
воречие не распространяется далее конкретного синтаксиса языка.4 Для чи-
тателей, сведущих в объектно-ориентированном программировании, заме-
тим, что частью имени нельзя считать информацию о том, какая из одноимен-
ных виртуальных функций будет использована при вычислениях. Эта инфор-
мация извлекается в динамике вычислений из того, к какому классу относит-
ся действующий в данный момент объект, когда понятие именования уже не
рассматривается.5 Этот динамический полиморфизм зачастую рассматрива-
ют как единственный вид полиморфизма в языках программирования, что
ничем не мотивированно.
В качестве сквозного примера, развиваемого в последующих разделах,
приведем заголовки двух процедур, предназначенных для реализации ввода
с клавиатуры квадратной матрицы размера N на N , размещаемой в “объ-
емлющем” двумерном массиве, индексируемом значениями 1:NN, где NN —
константа, (см. Рис. 8.2).
Первая процедура с заголовком

не вырабатывает значения, вторая — вырабатывает значение, определяющее


void MyOwnMatr(Matrix M, int SizeM);

размер массива:

MyOwnMatr применима тогда, когда заранее известно значение N , а MyInpMatr —


int MyInpMatr(Matrix M);

в случаях, когда величину N требуется определять при вводе, и тогда выраба-


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

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

ектов.
8.3. ЛОКАЛИЗАЦИЯ ИМЕН 377

NN
Неиспользованная часть
массива

NN

Рис. 8.2. Представление матрицы размера N*N в массиве размера NN

ной программе), что предпочтительнее MyInpMatr, т. к. функция не связана


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

§ 8.3. КОНТЕКСТЫ И ОБСТАНОВКИ. ЛОКАЛИЗАЦИЯ ИМЕН

Тело подпрограммы в совокупности с ее локальным контекстом называ-


ется блоком. Процедура или размещается в тексте программы, или включа-
ется в нее другим способом, и, соответственно, попадает в некоторый другой
локальный контекст. Этот контекст становится глобальным для данной про-
граммной единицы — в теле подпрограммы появляется возможность исполь-
зовать данные и другие программные элементы из глобального контекста, в
котором она применена. Такое использование регламентируется так называ-
емыми правилами локализации имен.
Сосредоточимся сейчас в первую очередь на процедурах. Из сказанного
выше, в частности, следует, что глобальный контекст процедуры включает
в себя саму процедуру (именно это обстоятельство обеспечивает, к приме-
ру, возможность рекурсивного вызова). Внешние, перерабатываемые данные
378 ГЛАВА 8. ПОДПРОГРАММЫ

процедуры — это часть глобального контекста, фактически затрагиваемая


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

• описания локального контекста;

• тело.

В различных языках программирования описание процедуры может оформ-


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

1. константу NN — размер массива по измерениям;

2. переменную N типа int — фактический размер матрицы;

3. переменную Matr типа двумерный массив размера NN на NN компонент,


для определенности типа float.

Эти три объекта исчерпывают внешние перерабатываемые процедурами дан-


ные. Кроме перерабатываемых данных, в глобальном контексте представле-
ны два оператора #include, задающие внешние библиотечные средства.
Внутренние данные, т. е. локальный контекст, определяются при соста-
влении конкретных алгоритмов тел процедур. Прежде чем составлять эти ал-
горитмы, необходимо до конца уточнить интерфейс процедур. Поэтому далее
предполагается, что глобальный контекст MyOwnInpMatr и MyInpMatr пред-
ставлен следующими описаниями:
8.3. ЛОКАЛИЗАЦИЯ ИМЕН 379

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

const NN = 100;
(8.1)
typedef float Matrix[NN][NN];
Matrix Matr; //(*)
int N;

Это первый вариант глобального контекста. Когда потребуется оперирование


с несколькими матрицами, вместо строки с (*) в (8.1) используется:

Описание типа
Matrix Matr1,Matr2;

полезно для содержательного понимания того, что делается. К тому же оно


typedef float Matrix[NN][NN];

просто сокращает запись, когда требуется определять переменные, параме-


тры и т. д. одного и того же типа:
Matrix Matr1,Matr2; /**/
void MyOwnInpMatr(Matrix M, int SizeM);
int MyInpMatr(Matrix M);
Рассмотрим теперь соотношение обстановки и контекста. Мы говорили
об обстановке лишь для оператора цикла (см. п. 7.3.5). Для программы и под-
программы в целом обстановка — существено влияющая на нее часть кон-
текста, в котором она работает: служебные и библиотечные программы и пе-
ременные операционной среды и системы программирования. Кроме того,
в обстановку включаются призраки и такие более абстрактные и зачастую
прямо не представленные в программе сущности, как входные и выходные
потоки. От обстановки зависит поведение программы.
Для подпрограммы в обстановку может войти еще и доступная часть кон-
текста других подпрограмм, с которыми данная подпрограмма взаимодей-
ствует.
Для вызова процедуры как части некоторой программы обстановка есть
объединение составляющих:
a) контекст описания — то, что описано на том же уровне, что и сама про-
цедура, и на уровнях, статически охватывающих ее описание;
380 ГЛАВА 8. ПОДПРОГРАММЫ

b) контекст вызова — то, что описано на уровне точки вызова процедуры,


и на уровнях, статически охватывающих ее.

Про обстановку вычислений любого другого фрагмента осмысленно гово-


рить, когда он представляет собой логически замкнутую часть вычислений.
Для тела программы, подпрограммы или процедуры это условие выполня-
ется по определению. Определять формально обстановку было бы неразум-
но, поскольку это заслонило бы содержательную сущность понятия: разгра-
ничение между локализованными во фрагменте элементами вычислений и
внешними по отношению к нему элементами. Такое разграничение — часть
задачи анализа программы.
Обычно контекст описания всегда доступен процедуре. Она может упо-
треблять имена, декларированные в этом контексте. В современнных языках
имеются и средства ограничения доступа: явно указывается, что некоторые
из описаний требуют особых прав доступа, и, таким образом, они не предна-
значены для данной процедуры.
Как правило, в языках программирования контекст вызова (за исключе-
нием той его части, которая попадает в контекст описания), непосредственно
процедуре недоступен. Поэтому необходимо передать процедуре те элемен-
ты контекста вызова, которые должны обрабатываться (использоваться и/или
модифицироваться).
В настоящее время распространены два способа передачи данных кон-
текста вызова во внутренний контекст процедуры.
Определение 8.3.1. Параметризация — часть локальных данных процеду-
ры (называемая совокупностью формальных параметров) объявляется спе-
циально выделенной для передачи данных между внутренним контекстом
процедуры и контекстом ее вызова. Общие блоки —- некоторый контекст мо-
жет быть сделан частью внутреннего контекста нескольких фрагментов про-
граммы.
Конец определения 8.3.1.
Оба варианта передачи данных изображены на рис. 8.3. Серым цветом обо-
значены данные, совместно используемые тремя процедурами. Они предо-
ставляются четырьмя общими блоками и через передачу параметров. Стрел-
ки отражают фактическое обращение к данным. Из рисунка видно, что при
использовании общих блоков подпрограммы выступают равноправно, тогда
как при параметризации нет этого свойства. Поскольку g вызывается внутри
8.3. ЛОКАЛИЗАЦИЯ ИМЕН 381

Общий Общий Общий Общий


блок блок блок блок
1 2 3 4

main f g

параметры

параметры

Рис. 8.3. Общие блоки и параметры

f, а та внутри main, то через формальные параметры g может получать дан-


ные из двух контекстов, f из двух, а главная программа может иметь лишь
данные глобального контекста. Каждый из двух механизмов передачи дан-
ных вынуждает программиста думать о взаимоотношении подпрограмм по-
разному, следовательно, они концептуально противоречат друг другу, что при
совместном использовании проявляется в ошибках и в неоправданной запу-
танности программ.
Остановимся вначале на менее распространенной дисциплине взаимо-
связи вызова процедуры и других фрагментов программы — на общих бло-
ках.
Пример 8.3.2. Общие блоки появились в языке FORTRAN. В этом языке
тексты основной программы и подпрограмм полностью разделены. Но мож-
но в любой подпрограмме описать общий блок, например,

В другой подпрограмме может быть такое же описание, и тогда каждая из них


COMMON /COORD/ X,Y,Z

пользуется переменными X, Y, Z как элементами своего внутреннего контек-


ста. Но беда была в том, что совершенно не запрещалось написать в другой
программе

или даже
COMMON /COORD/ A(3)
382 ГЛАВА 8. ПОДПРОГРАММЫ

Общие блоки в языке FORTRAN понимаются лишь с точки зрения общей ма-
COMMON /COORD/ CHAR STR(10)

шинной памяти, разделяемой подпрограммами.


Конец примера 8.3.2.
В языке С++ имеется любопытный суррогат общих блоков, лишенный
большинства из недостатков, присущих примитивной концепции языка FORTRAN.
Если некоторый элемент класса определяется как static, то он разделяется
всеми экземплярами данного класса.
Проблема передачи параметров заслуживает отдельного рассмотрения, а
пока что мы займемся правилами разрешения конфликтов при объединении
контекстов (правилами локализации, см. определение 7.3.2).
В различных языках введены разные правила локализации. В языках с так
называемой блочной структурой (к которым относится, например, Pascal)
употребление имен языковых объектов регламентируется правилами, впер-
вые сформулированными в Algol 60:
a) если некоторое имя, употребляемое в теле процедуры, встречается среди
описаний ее локального контекста, то оно понимается в том смысле, в
котором оно определено в локальном контексте;
b) если некоторое имя, употребляемое в теле процедуры, не встречается сре-
ди описаний ее локального контекста, то делается попытка найти его в
объемлющем контексте (т. е. в том контексте, в рамках описаний которо-
го помещен текст процедуры), в той его части, которая непосредственно
предшествует описанию данной процедуры;
c) если имя, употребляемое в теле процедуры, не удается найти в соответ-
ствии с правилами (a) и (b), то делается попытка найти его в следующем
объемлющем контексте до тех пор, пока контекст поиска имени не ока-
зывается локальным контекстом процедуры. (Для С это правило не дей-
ствует, поскольку нельзя описывать функции внутри функций; в С++ кон-
струкцию описания метода с точки зрения локализации имен можно рас-
сматривать как задание уровня вложенности в описания класса).
d) если в результате выполнения действий, указанных в правилах (a), (b) и
(c) имя остается ненайденным, то оно считается не определенным, а его
использование — некорректным.
Перечисленные правила локализации имен языковых объектов зачастую до-
полняются еще одним соглашением (это сделано, например, в языке Pascal):
8.3. ЛОКАЛИЗАЦИЯ ИМЕН 383

e) поиск использованного в программе или подпрограмме имени осуществля-


ется среди тех описаний (какого-либо контекста), которые текстуально
предшествуют точке использования имени.
Правило (e) впервые появилось в связи с идеей так называемой однопроход-
ной трансляции (построения объектного кода за один просмотр текста про-
граммы), но в дальнейшем оказалось, что оно хорошо согласуется с прин-
ципами модульного и структурного программирования, и задача трансляции
отошла на второй план.
Правила локализации имен — второе по важности после именования сред-
ство языка, предопределяющее его возможности в части использования про-
цедур. Они отвечают на вопросы о регламенте взаимоотношения текста про-
цедуры и текста программы.
Для языка C/С++, где вложенных процедур нет, все представляется до-
вольно просто, и следующая схема программы иллюстрирует введенные по-
нятия.
Программа 8.3.1

#include <stdio.h>
const N = 100,
N1 = 30;
int a, b, c, x, y, z, u, v;
void P1();
void P2();

// Контексты программы:
// локальный: const N 100; N1 30;
// переменные a, b, c, x, y, z, u, v;
// процедуры P1, P2;
// глобальный:то, что предоставляет <stdio.h>

void main()
{
const N1 = 50;

// Контексты функции main:


int x, y, u, v;

// локальный: const N1 50;


// переменные x, y, u, v;
384 ГЛАВА 8. ПОДПРОГРАММЫ

// глобальный: const N 100;


// переменные a, b, c, z;
// процедуры P1, P2;
// то, что предоставляет <stdio.h>
// Внешние данные main: она ничего не принимает и не выдает
} // конец main
void P1()
{
const N1 = 40;

// Контексты функции P1:


int x, z, v;

// локальный: const N1 40;


// переменные x, z, v;
// глобальный: const N 100;
// переменные a, b, c, y, u;
// процедуры P1, P2;
// то, что предоставляет <stdio.h>
// локальной переменной x присваивается значение
// N1 = 40 — локальной константы
x = N1;

// переменной y, глобальной для P1 и локальной для


// main, присваивается значение суммы переменной
y = x + N;

// x, локальной для P1 и N = 100 — глобальной (из


// программы) константы
a = b + x + y; // глобальной переменной a присваивается
// сумма глобальных переменных b (из программы)
// и y (из P1) и локальной переменной x
// Внешние данные P1:
// переменные y, a, b; константа N
} // конец P1
void P2()

// Контексты функции P1:


{

// локальный: (пустой)
// глобальный: const N 100; N1 30;
// переменные a, b, c, x, y, z, u, v;
// процедуры P1, P2;
// то, что предоставляет <stdio.h>
x = N1;
8.3. ЛОКАЛИЗАЦИЯ ИМЕН 385

y = x + N;

// вызов процедуры P1, глобальной для P2


a = b + x + y;

// Внешние данные P2:


P1();

// переменные x, y, a, b; константы N1, N;


} // конец P2

Схема контекстов программы для языка, который допускает общий случай:


вложенные описания процедур — представляется чуть сложнее. Проиллю-
стрируем ее на примере языка Pascal.
Программа 8.3.2

program PROG;
const N = 100;

a, b, c : ...; { Для иллюстрации контекстов }


N1 = 30;

x, y, z : . . . ; { типы переменных }
var

u, v : . . . ; { несущественны.}
procedure P1 . . . ;
const N1 = 50;
var x, y : . . . ;
u, v : . . . ;
procedure Pp1 . . . ;
const N1 = 40;
var x, z : . . . ;

{Контексты процедуры Pp1:


v : ...;

локальный: const N1 = 40;

глобальный:
var x, z, v;
const N1 = 50;
var y, u, v;
procedure Pp1;
const N = 100;
var a, b, c;
procedure P1;
}

{ локальной переменной x присваивается значение


begin {Pp1}
x := N1;
386 ГЛАВА 8. ПОДПРОГРАММЫ

локальной константы N1 = 40 }
{ переменной y, глобальной для Pp1 и локальной
для P1, присваивается значение суммы переменной x,
y := x + N;

локальной для Pp1,


и глобальной (из основной программы)
константы N = 100 }
a := b + x + y; { глобальной переменной a присваивается
значение суммы глобальных переменных b
(из программы) и y (из P1) и локальной
переменной x }
{ Внешние данные процедуры Pp1:
переменные y, a, b; константа N }
end; {Pp1}

{Контексты процедуры Pp2:


procedure Pp2 . . . ;

локальный:
глобальный:
(пустой)

const N1 = 50;
var x, y, u, v;
procedure Pp2, Pp1;
const N = 100;
var a, b, c, z;
procedure P1; }

{ переменной x, глобальной для Pp2 и локальной


begin {Pp2}

для P1 присваивается значение константы N1 = 50


x := N1;

из процедуры P1 (глобальной для Pp2) }


{ переменной y из P1 присваивается значение
суммы переменной x из P1 и глобальной
y := x + N;

константы N = 100 (из основной программы) }


a := b + x + y; { глобальной переменной a присваивается
значение суммы глобальной переменной b
(из основной программы) и локальных для
P1 переменных x и y }
{ вызов процедуры Pp1, глобальной для Pp2, но
локальной для P1}
Pp1;

{ Внешние данные процедуры Pp2:


переменные x, y, a, b;
8.3. ЛОКАЛИЗАЦИЯ ИМЕН 387

константы N1, N; }

{ Контексты процедуры P1
end; {Pp2}

локальный: const N1 = 50;

глобальный: const N = 100;


var x, y, u, v;

var a,b,c, x,y;


procedure Pp2,Pp1,P1; }
begin {P1}
...
end; {P1}

{ Контексты процедуры P2
procedure P2 . . . ;

локальный: (пустой)
глобальный:
const N = 100; N1= 30;
var a,b,c, x,y,z, u,v;
procedure P2; P1; }
begin {P2}
...

{ Контексты программы PROG


end; {P2}

локальный:
const N = 100; N1 = 30;
var a,b,c, x,y,z, u,v;

глобальный: (пустой) }
procedure P2, P1;

begin {PROG}
...
end. {PROG}

Из приведенных схем видно, как глобальные имена экранируются локаль-


ными описаниями и, тем самым, становятся недоступными во вложенных
блоках.
Все сказанное выше про локализацию имен при практическом приме-
нении необходимо согласовывать с возможностями используемой системы
программирования. Применительно к языку С/С++ нужно иметь в виду на-
388 ГЛАВА 8. ПОДПРОГРАММЫ

личие препроцессора, который в принципе ‘не знает’ языка, но, тем не менее,
оперирует с именами программного текста. Среди прочего действия препро-
цессора включают в себя подстановку определенным образом оформленных
фрагментов вместо их имен, которые определяются операторами #define. В
результате в программном тексте появляются как нормальные, относящие-
ся к языку имена, так и имена обозначений текстовых фрагментов. Так вот,
если первые подчиняются только что определенным правилам локализации,
то вторые — нет. Они подчиняются правилам, которые “знает” препроцес-
сор: если имя определено оператором #define как обозначение некоторого
фрагмента, то это обозначение действует, сразу же после данного операто-
ра и до конца текста либо до нового оператора #define, переопределяющего
данное имя. Слово ‘действует’ надо понимать следующим образом: препро-
цессор подставляет обозначенный именем фрагмент в то место текста, в ко-
тором встречается данное имя. Нужно осознавать, что при вставке фрагмента
встречающиеся в нем имена приобретают смысл из контекста вставки (но не
описания, как если бы действовали правила (a)–(e)). Таким образом, работа
препроцессора может создать впечатление нарушения правил локализации
имен.6
Следующие два программных текста представляют собой описания на-
ших сквозных примеров процедур MyOwnInpMatr и MyInpMatr. Они демон-
стрируют возможности определения функций в глобальном контексте для
языка С. В данном случае предлагается использовать контекст с описанием
одной матрицы:

Первая функция должна вызываться, когда размерность Matr, т. е. перемен-


Matrix Matr;

ная N, заранее определена, вторая, когда требуется определить N, задать ей


нужное значение.
Программа 8.3.3
void MyOwnInpMatr()
{
int i,j;
printf("Enter matrix data:\n");
for(i=0; i < N; i++)
{

Если же пользоваться встраиваемыми функциями, то правила локализации имен сохра-


няются.
6
8.3. ЛОКАЛИЗАЦИЯ ИМЕН 389

printf("Row %3i:",i);
for(j=0; j < N; j++)
scanf("%f",& Matr [i][j]);
printf("\n");
}
}

Программа 8.3.4

int MyInpMatr()
{

Переменная используется для определения


int i,j,N=0;

окончания ввода первой строки матрицы


char c; //
//

printf("Enter matrix data:\n");


printf("Row 0:");
do
scanf("%f%c",& Matr [0][N++],&c);
while(c!=’\n’);
// scanf("\n");

for(i=1; i < N; i++)


{
printf("Row %2i:",i);
for(j=0; j < N; j++)
scanf("%f",& Matr [i][j]);
do
scanf("%c",&c);
while(c!=’\n’);
}
return N;
}

В описании локального контекста функции MyInpMatr определяется перемен-


ная N, что иллюстрирует пересечение глобального и локального контекстов
390 ГЛАВА 8. ПОДПРОГРАММЫ

по именам. Как было сказано выше, локальное N делает недоступным для


использования в теле функции глобального N, а потому все присваивания
значений локальной переменной N никак не влияют на значение внешней
переменной N.
Тело функции MyInpMatr демонстрирует, каким способом в С/С++ значе-
ние функции передается в точку ее вызова: оператор return N;
В ранних языках для этой цели применяется прием, который назовем ал-
головский возврат, т. к. впервые он появился в Algol 60: присваивание имени
функции (это не переменная, которую можно использовать!) вырабатывае-
мого значения. Этот прием сегодня можно считать устаревшим, но его при-
ходится сохранять, например, в языках линии Pascal для преемственности.
Следует, тем не менее, заметить, что алголовский возврат значения функции
в определенном смысле выразительнее: можно выработать значение, а после
этого продолжать другие вычисления функции. Но такая возможность всегда
достижима с помощью использования переменной нужного типа (в частно-
сти в Object Pascal для такой переменной вводится специальное обозначение
Result, и, тем самым атавизм языка Pascal может быть превращен в преиму-
щество).
Наслоение новых и старых подходов привело к тому, что в Object Pascal
возможные противоречия

ликвидируются на уровне прагматики.


Result := 17; F := 5;

Уже упоминалось, что функция MyInpMatr более автономна, чем проце-


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

§ 8.4. ВЫЗОВ ПРОЦЕДУРЫ

8.4.1. Семантика вызова процедуры

Цель данного параграфа состоит в том, чтобы дать представление о се-


мантике языковых конструкций, которые задают в программе выполнение
8.4. ВЫЗОВ ПРОЦЕДУРЫ 391

алгоритма процедуры (оператора процедуры и использования функции в ка-


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

• выполнение алгоритма процедуры;

• завершение выполнения процедуры.

На первой фазе абстрактный вычислитель языка

1. создает локальный контекст: обеспечивает возможность оперирова-


ния с системой контекстов, доступных для операторов тела вызывае-
мой процедуры, в частности, он выделяет память для локального кон-
текста процедуры;

2. осуществляет вход в подпрограмму: передает управление на тело про-


цедуры с запоминанием точки возврата, т. е. адреса, куда должно будет
передаваться управление после выполнения процедуры.
Вторая фаза заключается в

3. активизируется абстрактный вычислитель языка: выполняются опе-


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

запроцедуривание: превращение некоторого выражения или оператора в неявную процеду-


ру. Этот случай целесообразно рассмотреть отдельно, когда основа механизма вызова будет
изучена.
392 ГЛАВА 8. ПОДПРОГРАММЫ

4. уничтожает локальный контекст: возвращает состояние памяти, пред-


шествовавшее вызову процедуры (с точностью до изменений значений,
произведенных во внешнем контексте процедуры при выполнении вто-
рой фазы), в частности, он ликвидирует память, выделенную на первой
фазе для локального контекста;
5. передает управление в точку возврата.
Указанные шаги выполнения абстрактным вычислителем языка процес-
са вызова процедуры определяют семантику данной языковой конструкции.
Большинство моделей вычислений языков предписывает именно эти дей-
ствия для реализации вызова, хотя в некоторых случаях они могут быть вы-
рождены или дополнены. Наиболее заметны в этом отношении расхождения
моделей в части работы с контекстами (шаги 1 и 4). Так, во многих языках
предусматривается инициализация памяти, т. е. задание начальных значений
для переменных локального контекста. В С/С++ предусматривается иници-
ализация переменных, а в Паскале их значения считаются неопределенны-
ми. В простых языках, в которых процедуры не имеют локального контек-
ста, действия по подготовке памяти вырождены (например, в Basic). Иногда
предписывается необходимость сохранения локального контекста после за-
вершения вызова (описатель own Algol 60, который трактуется как средство,
предназначенное для передачи информации от вызова к вызову процедуры).
В этих случаях следует говорить не только о создании и уничтожении ло-
кальных контекстов, но и об упрятывании их, возможно, частичном, а также
о восстановлении контекстов.
При рассмотрении шагов 2 и 5 может возникнуть вопрос о том, где за-
поминается адрес возврата. С целью предоставить гибкость при разработ-
ке систем программирования и обеспечить возможность учета архитектуры
компьютера, эталонные определения языков, как правило, не фиксируют ре-
гламент хранения адреса возврата, но обычно он размещается в локальном
контексте, дополняя последний недоступным для пользователя элементом.
Иллюстрациями вызова процедур (функций) в С/С++ в рамках продолже-
ния сквозного примера могут служить следующие конструкции:

после которого (при условии корректного ввода) матрица Matr получает зна-
MyOwnInpMatr();

чения своих элементов, и

при выполнении которого кроме ввода матрицы Matr значение, выработанное


N = MyInpMatr();

функцией, присваивается глобальной переменной N.


8.4. ВЫЗОВ ПРОЦЕДУРЫ 393

8.4.2. Статическая и динамическая цепочки

Для тех, кто желает ограничить себя программированием на языках С/С++,


данный раздел, скорее всего, не представляет интереса, однако для глубокого
усвоения процессов, которые происходят в реальной вычислительной техни-
ке при вызовах процедур, представленный материал является необходимым.8
Из предыдущего видно, что вызов процедуры приводит к оперированию с
контекстами. В этой связи важно проследить взаимодействие контекстов, ди-
намически возникающих в ходе выполнения программы, и статических кон-
текстов, которые обсуждались ранее.
Возможность доступа к любым объектам в процедуре определяется до
выполнения программы в соответствии с механизмом согласования контек-
стов (см. параграф 8.4.1). При выполнении программы локальные контексты
порождаются и уничтожаются в порядке, который определяется динамиче-
ски: порядком вызовов процедур. Поскольку их имена являются элементами
контекстов, в которых они описываются, возможность вызова той или иной
процедуры (как и других объектов) регламентируется статической вложен-
ностью контекстов. За счет этого одновременно существуют локальные кон-
тексты всех процедур, вызванных в некоторый момент вычислений. Таким
образом, для каждой процедуры имеются:
• статическая последовательность контекстов доступа, называемая ста-
тической цепочкой процедур, и

• динамическая, определенная в момент вызова процедур, последователь-


ность контекстов, которые суть локальные контексты вызванных, но не
завершенных процедур в порядке их вызовов,— динамическая цепочка
процедур.

Приводимый ниже пример поясняет введенные понятия. Пусть имеется сле-


дующая схема программы, на которой многоточиями обозначены описания
программных объектов, входящих в контексты основной программы и про-
цедур. Справа от схемы отмечены локальные контексты, которые обозначе-
ны буквой K с последующим именем подпрограммы. Слева указаны номера
строк. Пример приводится для языка Pascal, для С/С++ он был бы вырожден-
ным из-за запрета вложенных описаний процедур.
В частности, логическое описание обработки исключительных ситуаций приводит к вы-
воду, что их корректное представление возможно, лишь если обработчики будут процедура-
8

ми, описанными внутри процедур.


394 ГЛАВА 8. ПОДПРОГРАММЫ

Программа 8.4.1
1. program PROG; KPROG
2. ... KPROG
3. procedure P; KP KPROG
4. ... KP
5. begin
6. ...
7. end; {P}
8. procedure Q; KQ KPROG
9. ... KQ
10. procedure Q1; KQ1 KQ
11. ... KQ1
12. begin
13. P;
14. end; {Q1}
15. procedure Q2; KQ2 KQ
16. ... KQ2
17. begin KQ2
18. Q1; KQ2
19. end; {Q2}
20. begin
21. Q2;
22. end; Q
23. begin
24. Q;
25. ...
26. P;
27. end.
Локальный контекст основной программы KPROG, являющийся глобальным
для процедур P и Q, включает среди прочего имена этих процедур. Это по-
зволяет задать вызов процедуры Q в теле программы (строка 24). Тем самым
при вызове Q будет образована динамическая цепочка

которая совпадает со статической цепочкой тела процедуры Q, образованной


KPROG, KQ,

путем погружения в контекст. KQ включает в себя имена процедур Q1 и Q2,


доступных для вызова в теле Q. Вызов Q2 (строка 21) приводит к образова-
нию динамической цепочки
8.4. ВЫЗОВ ПРОЦЕДУРЫ 395

которая опять совпадает со статической цепочкой тела процедуры Q2. Вызов


KPROG, KQ, KQ2,

Q1 в строке 18 правомерен, он приводит к динамической цепочке

которая не совпадает со статической цепочкой тела процедуры Q1:


KPROG, KQ, KQ2, KQ1,

(локальные данные из KQ2 не доступны!). В Q1 вызывается процедура P


KPROG, KQ, KQ1

(строка 13), в результате в момент выполнения этого вызова динамическая


цепочка подпрограмм примет вид

Статическая цепочка, регламентирующая доступ к данным в теле процедуры


KPROG, KQ, KQ2, KQ1, KP.

P, строка 4, есть

Статическая и динамическая цепочки при исполнении строки 25, не содер-


KPROG,

жащей вызова подпрограмм, состоят из контекста основной программы.


Когда исполняется вызов процедуры P в строке 26, статическая и дина-
мическая цепочки принимают вид

При возврате из процедуры из них удаляется имя KP, а при выходе из всей
KPROG, KP.

программы обе цепочки ликвидируются.


Приведенная иллюстрация показывает, что статические цепочки доступа
к данным оказываются фрагментами динамических цепочек. Очевидно, что
это свойство выполняется для любых вызовов процедур.
Семантика вызова процедур, представленная выше, восходит к модели
вычислений Algol 60; в Pascal, как и во многих других развитых языках про-
граммирования, с точностью до различий в локализации имен (которая к вы-
зову имеет косвенное отношение), она воспроизведена без изменений.
Как уже отмечалось, потребность в статических цепочках возникает, ко-
гда язык допускает вложенные описания процедур с правилами локализации
имен, которые приведены выше на примере Pascal. Реализация такой воз-
можности требует, чтобы в процессе выполнения программы в памяти со-
хранялись обе цепочки: одна для адресации данных, связанной с динамиче-
ской вложенностью, а другая — со статической вложенностью. Это обстоя-
тельство не очень привлекательно для разработчиков языков и трансляторов,
а потому зачастую они жертвуют возможностью статической локализации
имен (обычно путем запрета вложенных описаний процедур, реже — изме-
нением правил локализации) в пользу повышения эффективности рабочих
программ. Характерным примером такого рода является язык С/С++.
396 ГЛАВА 8. ПОДПРОГРАММЫ

8.4.3. Понятие экземпляра процедуры

При обсуждении процедур важно иметь ввиду следующие моменты. Во-


первых, каждое обращение к алгоритму процедуры — это запуск вычисле-
ний самостоятельного процесса, который приостанавливает породивший его
процесс. В частности, рекурсивное обращение к алгоритму порождает новый
экземпляр работающего в данный момент или приостановленного процес-
са. Как следствие, порождается новый комплект локальных данных процес-
са еще до окончания текущего процесса. Во-вторых, завершение вычислений
процесса процедуры возвращает вычисления в целом к той точке программы,
в которой вычисления были приостановлены для обращения к процедуре —
это свойство обращения к любому вспомогательному алгоритму.
Таким образом, имеет смысл говорить об экземплярах процедур и функ-
ций, существующих одновременно9 . Эти экземпляры обладают следующими
свойствами.
a) экземпляр характеризуется своим комплектом локальных данных и точ-
кой возврата;

b) порождение экземпляра — это создание его комплекта локальных дан-


ных, запоминание точки возврата и передача управления на начало про-
граммы алгоритма;

c) завершение экземпляра — это уничтожение его комплекта локальных дан-


ных и передача управления в точку возврата, а для функций, вдобавок, пе-
редача результата вычислений в вызвавшее выражение для продолжения
его вычисления;

d) из всех экземпляров рекурсивной процедуры или функции выделяется ак-


тивный экземпляр, который непосредственно выполняется в данный мо-
мент вычислений, все остальные экземпляры пассивны в том смысле, что
они ожидают продолжения своего выполнения после завершения актив-
ного экземпляра;
В литературе часто можно встретить термин активация процедуры, смыл которого в точ-
ности соответствует тому, что здесь названо экземпляром. Термин восходит к операцион-
9

ному взгляду на процедуру: когда при вычислении вызывается процедура, это приводит к
активизации операторов ее тела, но при этом не прекращаются, а лишь приостанавливают-
ся другие начатые действия. Таким образом, становятся активированными сразу несколько
вычислительных процессов.
8.4. ВЫЗОВ ПРОЦЕДУРЫ 397

e) порождение и завершение экземпляров строго соответствует стековой дис-


циплине: “первым порожден — последним завершен”.

Возможности, соответствующие механизму рекурсивного вызова, у нашего


абстрактного вычислителя имеются: это запоминание всего локального кон-
текста на стеке и порождение нового контекста, которое можно трактовать
как порождение нового экземпляра рекурсивно запускаемой подпрограммы.
Возможность одновременного существования нескольких экземпляров од-
ной и той же процедуры или функции в динамике выполнения программы,
при условии выполнения только что перечисленных требований, можно рас-
сматривать в качестве определения рекурсивности этой процедуры или функ-
ции.
При определении абстрактных вычислений было бы неправильно тракто-
вать вызов процедуры как преобразование дерева абстрактного синтаксиса:
замена конструкции вызова процедуры деревом, представляющим ее тело.
Причина не в том, что при этом необходимо оперировать потенциально бес-
конечными графами (например, при раскрытии рекурсивных вызовов): это
вполне реализуемо, поскольку в семантически правильной программе рекур-
сия всегда конечна. Серьезным доводом против трактовки вызова как заме-
ны является растворение экземпляра процедуры в дереве программы и, как
следствие, невозможность оперирования с экземплярами. А оперирование с
экземплярами процедур, в том числе и не ограничивающее себя рамками сте-
ка, — исключительно важный момент10 .
Однако, как всегда, нецелесообразность некоторого решения в общем слу-
чае не означает его нецелесообразности в частных. Например, в языке C/C++
имеются встраиваемые процедуры (inline-процедуры), семантика вызова ко-
торых — именно прямая текстовая подстановка11 тела процедуры вместо
конструкции вызова.
Осознав, что абстрактный вычислитель должен порождать именно экзем-
пляры процедур, необходимо далее ответить на вопрос, как запоминать экзем-
пляры, становящиеся временно пассивными, и как их активизировать, когда
Надо заметить, что в языке Algol 60 было принято именно подстановочное определение
семантики вызова. Неудобства, связанные с таким определением, выявились в работах по
10

реализации Algol 60, и стимулировали осознание концепции контекста.


Как всегда, такое определение некорректно, и некорректность явно оговорена в семан-
тике, в частности, языка C++: правила локализации имен для встраиваемой и обычной про-
11

цедур одни и те же, и в этом главное отличие встраиваемых процедур от препроцессорных


определений.
398 ГЛАВА 8. ПОДПРОГРАММЫ

завершается текущий активный экземпляр? Для этого есть две возможности.

• Присоединить локальный контекст порождаемого экземпляра к локаль-


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

• Организация специальной памяти для хранения локальных контекстов


процедур, к которой имеет доступ абстрактный вычислитель. Это —
прагматический вариант реализации. Если не фиксировать дисципли-
ну доступа к такой памяти, то можно реализовать различные варианты
оперирования с экземплярами вызываемой процедуры.

Здесь используется вторая возможность, и чаще всего тот ее частный случай,


когда память контекстов организована как стек. Абстрактный вычислитель,
обрабатывающий вызов процедуры, переходит к интерпретации дерева те-
ла процедуры и одновременно размещает на стеке локальный контекст. При
завершении экземпляра стек восстанавливается, а управление возвращается
в точку вызова. В данном случае стек предоставляет место для хранения и
динамической, и статической цепочки процедур.
В практике программирования понятие экземпляра подпрограммы не огра-
ничивается применением лишь в области рекурсивных алгоритмов. В част-
ности, одновременное существование нескольких экземпляров подпрограмм,
не подчиняющееся стековой дисциплине, является мощным средством про-
граммного моделирования. Здесь нет принципиальной разницы, выполняют-
ся ли эти подпрограммы последовательно, приостанавливаясь или продолжа-
ясь по какой-либо из внешних причин, или параллельно, когда на причины
приостановки, продолжения и завершения накладываются правила одновре-
менной работы нескольких процессоров. Важно одно, что существует и вы-
держивается определенная дисциплина активизации и взаимодействия про-
цессов, и стековая дисциплина — это существенный, но лишь один из многих
пример реально используемых дисциплин.
Выше (см. § 7.3.4) при обсуждении циклической обработки было опре-
делено понятие сопрограммного механизма взаимодействия процессов, при
котором несколько программных объектов поочередно передают друг другу
управление и поставляют данные для обработки. Эта важная с практической
8.4. ВЫЗОВ ПРОЦЕДУРЫ 399

точки зрения дисциплина может рассматриваться как вариант, альтернатив-


ный по отношению к стековой организации взаимодействий. Тем не менее,
часто, особенно в случаях последовательных вычислений, сопрограммное
взаимодействие моделируется обычной стековой дисциплиной.
Типичный пример — синтаксическое управление трансляцией. Здесь вы-
делен главный процесс — синтаксический анализатор. Когда ему это нужно,
он запрашивает лексический анализатор путем обыкновенного вызова про-
цедуры, называемой далее генератором лексемы, которая вырабатывает оче-
редную лексему. Генератор лексемы останавливает себя сам, когда лексема
сформирована. Это реализуется как обычный возврат из процедуры. Понят-
но, что каждая лексема вырабатывается отдельным экземпляром генератора,
который тут же уничтожается, когда надобность в нем пропадает.
В то же время, генератор лексемы работает в собственном контексте, основ-
ными данными которого являются поток (файл) входных символов и указа-
тель очередного символа этого потока. Можно естественно определить со-
стояние такого контекста (совокупность значений его переменных), которое
меняется при работе каждого экземпляра генератора, и, как следствие, есть
все основания рассматривать, пусть даже умозрительно, особую программ-
ную единицу, объединяющую процедуру генерации лексемы и указанный
контекст. Именно к ней обращается синтаксический анализатор за очередной
лексемой, а то, что такое обращение превращается в вызов процедуры, есть
ничто иное, как реализация требуемого действия. И именно эта программ-
ная единица связана с синтаксическим анализатором как с сопрограммой.
Аналогично можно выделить и другие сопрограммы транслятора: процесс
работы с таблицей имен, анализатор контекстных зависимостей, генератор
объектного кода и др. (конкретный набор сопрограмм зависит от архитекту-
ры транслятора).
При явном описании таких сопрограмм появляются определенные пре-
имущества:
• автономная и во многом независимая их разработка (зависимости сво-
дятся к определению протокола в качестве соглашения о взаимодей-
ствиях),
• осуществимость различных стратегий реализации каждой из сопро-
грамм,
• упрощение задачи распараллеливания.
400 ГЛАВА 8. ПОДПРОГРАММЫ

§ 8.5. ПАРАМЕТРИЗАЦИЯ

Параметризация — важнейший способ связи экземпляра программы с ди-


намическим контекстом. Данное понятие заслуживает серьезного анализа.
8.5.1. Назначение параметризации

Мы уже определяли параметризацию с точки зрения контекстов (см. § 8.3),


определим ее с точки зрения действий.
Определение 8.5.1. Параметризация — средства языка программирования,
которые позволяют явно сопоставлять некоторым именам из локального кон-
текста процедуры (формальным параметрам) данные из динамического кон-
текста вызова, называемые фактическими параметрами. Формальные пара-
метры в ходе выполнения вызова процедуры именуют фактические. Процесс
замены формальных параметров на фактические при вызове процедуры на-
зывается передачей параметров.
Конец определения 8.5.1.
С конкретно-синтаксической точки зрения параметры делятся на позицион-
ные и ключевые.
При позиционной форме совокупности формальных и фактических па-
раметров представляют собой упорядоченные списки, а связь между ними
задается путем взаимно однозначного соответствия между списками: каждо-
му формальному параметру соответствует фактический, имеющий тот же
порядковый номер, что и формальный параметр. При ключевой форме ка-
ждому параметру приписывается ключ — имя, по которому он распознается.
Фактический параметр, помеченный ключом, считается соответствующим
формальному параметру с данным ключом (часто для пометки используется
синтаксис, подобный оператору присваивания: <ключ>“=”<фактический па-
раметр>). Использование ключевой формы позволяет задавать фактические
параметры в произвольном порядке, делает наглядной частичную параме-
тризацию, когда фактические параметры устанавливаются не для всех фор-
мальных параметров, а только для некоторых. Тем самым вызов процедуры
с не полностью заданными параметрами превращается в заготовку для вы-
зова процедуры с меньшим числом параметров, что соответствует матема-
тическому понятию частичной функции и стилю функционального програ-
мирования. К сожалению, в существующих языках программирования это
средство развито слабо. Ключевая форма удобна, когда для формальных па-
раметров определено некоторое стандартное задание, и тогда урпоминание
некоторых параметров можно опускать при вызове подпрограммы для ис-
8.5. ПАРАМЕТРИЗАЦИЯ 401

полнения. Практически только для этой цели и используются ключевые па-


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

Вызов с ключевыми параметрами:


f(a+b,c[i],&d);

Две формы такого же вызова, когда значения, предполагаемые по умолча-


\includegraphics[scale=1.50, rotate=90]{ris2-1.eps}

нию, опускаются, и когда они задаются явно:


\includegraphics{ris2-1.eps}

Ключевой параметр-признак:
\includegraphics[scale=1.00, rotate=0]{ris2-1.eps}

Конец примера 8.5.2.


copy /b file1.dvi file2.dvi

Уменьшение привязанности процедур и функций к контексту — лишь одна


из задач, решаемых с помощью параметризации. Другая, не менее важная за-
дача, — это повышение гибкости программирования. Благодаря параметри-
зации можно объединять в процедуре не только совпадающие, повторяющи-
еся в основной программе вычисления, но и похожие вычисления, отличаю-
щиеся своими параметрами — программными объектами, которые конкре-
тизируют выполнение вычислений процедуры при ее вызове. При составле-
нии текста процедуры такие объекты должны быть тем или иным способом
обозначены; о таких обозначениях известно, что они употребляются в про-
цедуре для замещения конкретных объектов, используемых при вызове.
В большинстве языков программирования способ обозначения параме-
тров связывается с использованием идентификаторов в качестве имен фор-
мальных параметров: эти имена описываются в заголовках процедур. Как
402 ГЛАВА 8. ПОДПРОГРАММЫ

альтернатива идентификаторам, в некоторых языках используются порядко-


вые номера параметров (специально оформленные синтаксически, напри-
мер, выражения вида #5 в системе TEX). Этот путь в общем случае неудобен
из-за снижения уровня абстракций по сравнению с именами-идентификато-
рами, но целессообразен, когда мы задаем процедуру, которая может вызы-
ваться с очень большим нефиксированным числом параметров.
Имена формальных параметров пополняют локальный контекст проце-
дуры. В частности, поэтому описание, к примеру, локальной переменной с
именем, совпадающим с именем формального параметра, вызывает ошибку:
повторное описание имени.
В отличие от формальных, фактические параметры полностью относят-
ся к контексту вызова процедуры — они строятся с помощью средств этого
контекста как языковые конструкции, способные замещать формальные па-
раметры.
По своему назначению параметры подразделяются на три вида:

1. входные, in-параметры, которые поставляют значения для выполнения


процедуры;

2. выходные, out-параметры, которые принимают значения, вырабатыва-


емые в ходе выполнения процедуры, для внешнего использования;

3. совмещенные, входные и одновременно выходные, или inout-параме-


тры, которые и поставляют значения, и принимают их, т. е. совмещают
два предыдущих назначения.

Фактический in-параметр — это выражение того типа, которого требует со-


ответствующий формальный параметр. Небольшая оговорка здесь требуется
по поводу передачи подпрограмме в качестве параметров для внутреннего
использования процедур и функций, которые при обсуждении параметриза-
ции удобно рассматривать как значения специальных типов: процедурного и
функционального. Если в языке определить переменные этих типов, то они
будут принимать в качестве значений алгоритмы процедур или функций. Ни
в одном развитом языке эта концепция не доведена до логической завершен-
ности. Ее непоследовательно реализованные фрагменты имеются во многих
языках (Алгол-68, несколько более ограниченно — Ada, Turbo-Pascal).
Фактические out-параметры — это переменные, т. к. только они в состоя-
нии принимать значения. Если рассматривать операнды выражений как еще
одну возможность принимать значения, то подпрограмму-функцию можно
8.5. ПАРАМЕТРИЗАЦИЯ 403

считать частным случаем процедуры, со специально синтаксически выде-


ленным out-параметром, который предписано использовать в синтаксиче-
ской позиции операнда выражения, когда он представлен конструкцией вы-
зов функции.
Inout-параметры являются совмещением двух других видов параметров,
а потому их фактические параметры могут быть только переменными. Отли-
чие таких параметров от out-параметров лишь в том, что эти переменные
перед вызовом подпрограммы должны иметь определенные значения (как
выражения in-параметров).
Перечисленные виды параметров характеризуют параметризацию с точ-
ки зрения использования процедур. Такое рассмотрение достаточно для кор-
ректного употребления процедур: три вида параметров исчерпывают все слу-
чаи взаимодействия процедур с окружением, необходимые на практике. Это
обстоятельство, по-видимому, является главной причиной того, что в совре-
менных языках программирования (например, в Ada) виды параметров ука-
зываются явно при описании процедур. Такой подход к параметризации со-
гласуется с интуитивным пониманием назначения параметризации и ее воз-
можностей.
Что же касается языка С/С++, то для его параметризации выбрана кон-
цепция, максимально приближенная к тому, как реализуется передача пара-
метров на машинном уровне. В частности, в нем представлен лишь один ме-
ханизм параметризации, через который все указанные виды параметров мо-
делируются. Если это и недостаток, то маленький, поскольку к такому моде-
лированию быстро привыкают и даже не замечают его при практической ра-
боте. Есть еще одно свойство языка С/С++, облегчающее неаккуратное про-
граммирование: возможность описывать функцию с одним числом параме-
тров, а использовать с другим, но это свойство соотносится не с понятием
вида параметров, а со средствами оперирования с ними.
В Алголе 68, как и в С/С++ задается единый механизм параметризации,
но там он предлагается для экономии мышления, а не с точки зрения эффек-
тивной реализуемости. И хотя оказывается, что эти позиции не противоречат
друг другу (что наталкивает на мысль о согласовании принципа экономии
мышления с принципом экономии вычислений), все же явное определение
назначения параметров представляется более логичным.
Каждый фактический параметр должен быть таким, как того требует ал-
горитм процедуры. Эта характеристика параметризации называется согла-
сованностью типов фактических параметров с типами формальных параме-
тров. Правила согласования для разных языков различны в зависимости от
404 ГЛАВА 8. ПОДПРОГРАММЫ

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

procedure Copy ( in X, out Y );


begin
Y := X
end;

Она копирует входное значение в выходное. С точки зрения такой се-


мантики, кажутся вполне правомерными следующие вызовы Copy (в данном
примере используются имена, типы которых указаны как окончания иденти-
фикаторов):

Copy ( a_int, b_int );


Copy ( ’=’, c_char );

поскольку осмысленны присваивания переменным значения их типов. Но


уже

Copy ( a_float, b_int );


Copy ( 40, c_char );

корректны или нет в зависимости от того, законны ли присваивания, типы


которых естественно обозначить в данном языке int:=float и char:=int.
А что будет, если в теле процедуры встречается, например, операция де-
ления? Будет ли тогда первый вызов процедуры корректным, а второй нет?
Может быть, корректными фактическими параметрами, т. е. согласованны-
ми по типам с формальными параметрами, считать такие, набор операций
которых включает в себя всю совокупность используемых в теле процеду-
ры операций с соответствующим формальным параметром? Но тогда нужно
осознавать, что логическим следствием будет расширение этой совокупно-
сти вглубь вызовов процедур: если формальный параметр используется (в
теле процедуры) в качестве фактического параметра другой процедуры, то
8.5. ПАРАМЕТРИЗАЦИЯ 405

совокупность должна быть расширена операциями, используемыми с соот-


ветствующим формальным параметром этой процедуры. А есть еще случаи
динамики, когда при одних вычислениях формируется одна, а при других —
другая совокупность; есть и перегрузка операций, когда смысл имени опе-
рации зависит от того, с какими аргументами она вычисляется. И все это
осложняется в связи с внешними библиотечными процедурами, про которые
принципиально неизвестно, как они используют свои параметры.
Эти и подобные им рассуждения приводят к выводу о том, что нужно как-
то ограничить правила соответствия по сравнению с тем, что можно предпо-
ложить на первый взгляд. Таким образом, предлагается ограничиться потен-
циальной применимостью операций, нисколько не заботясь о том, с какими
операциями они фактически используются. Иными словами, вводятся следу-
ющие соглашения:
• используется статическая система типов переменных, которая в мо-
мент описания связывает описываемое имя с типом и никогда не по-
зволяет нарушать эту связь. В свою очередь, тип определяется как со-
вокупность значений и набор операций, вычислимых с этими значени-
ями;
• вводятся приведения значений типов, которые определяются как пра-
вила преобразования значения одного типа к значению другого. Эти
правила, вообще говоря, могут требовать вызова подпрограмм, перера-
батывающих исходное значение в значение целевого типа. Они опреде-
ляются только для разумных сочетаний пар типов (для каждого языка
своих). Они могут вызываться и неявно, в тех случаях, когда приве-
дение требуется по контексту, и явно, когда приведение указывается
программистом принудительно;
• значение фактического параметра, соответствующего in-параметру, при-
водится к типу, заданному для формального параметра, а значение фор-
мального out-параметра, вырабатываемое в ходе вычислений процеду-
ры, приводится к типу фактического параметра.
Благодаря этим соглашениям нет нужды знать реализации алгоритмов про-
цедур, т. е. тексты, задающие их), чтобы корректно (с точки зрения языка) вы-
зывать процедуры. Достаточно знать только типы формальных параметров.
Собственно говоря, одной из предпосылок определения типа набором опе-
раций, допустимых для его значений, было обеспечение с минимальными
затратами корректности вызовов процедур.
406 ГЛАВА 8. ПОДПРОГРАММЫ

Следует специально остановиться на операторе присваивания значений.


С концептуальной точки зрения этот оператор есть специальным образом
синтаксически оформленная процедура с двумя параметрами: in-параметр —
источник значения, и out-параметр — получатель значения (по своей сути это
та же процедура Copy, рассмотренная выше). Поэтому правила соответствия
типов в данной ситуации применяются в полной мере, и нет нужды что-либо
специально доопределять на этот счет. Иное дело, что в языках часто опреде-
ляются не средства, обеспечивающие назначение параметров, а механизмы
параметризации, и, если эти механизмы базируются на присваивании (как
в С/С++), то поневоле приходится говорить сначала о приведениях в выра-
жениях и присваиваниях, а затем переносить выработанные соглашения на
параметризацию.
Ориентация Pascal на максимально полную, какую можно было реали-
зовать в начале 70-х гг. XX вв., проверку программы до ее исполнения пре-
допределила в языке такие правила согласования типов, которые допуска-
ют вычисление всей информации о типах объектов программы в процессе
трансляции. В соответствии с этими правилами в Pascal требуется, чтобы
при подстановке каждого фактического параметра его тип совпадал или был
бы эквивалентен типу соответствующего формального параметра (т. е. опре-
делен при своем описании как равный типу формального параметра). В част-
ности, с этой целью язык Pascal требует, чтобы в описании формального
параметра его тип был бы указан своим именем. Приведения типов почти
всегда явные (есть единичные исключения из этого правила, например, пре-
образование целого к вещественному).

8.5.2. Полиморфизм и вызовы с переменным числом параметров

В языках С/С++/С# и Java жизненно важной, в частности, для объект-


но-ориентированного программирования является возможность иметь много
процедур с одинм и тем же именем, но разным обращением. Нужный вариант
процедуры выбирается по контексту вызова. Это называется полиморфизмом
процедур. В последние версии языка Object Pascal проникли некоторые эле-
менты полиморфизма, хотя там они не являются столь жизненно важными и
введены скорее для согласования с вышеупомянутыми языками.
На самом деле во всех традиционных языках программирования имеет-
ся полиморфизм, неявно присутствующий в определении выражений языка.
Дело в том, что одна и та же операция с аргументами разных типов реализу-
ется разными командами (процедурами). Это — полиморфизм операций, ча-
8.5. ПАРАМЕТРИЗАЦИЯ 407

сто (в особенности в литературе по языкам Алгол 68 и Ada) называемый их


перегрузкой. Поскольку, в частности, языки C++ и Java разрешают описание
существующих операций для новых типов аргументов (например, програм-
мист сам может поределить операции ввода-вывода << и >>для нового типа
данных), это также является актуальным не только для системных програм-
мистов.
Полиморфизм в широком смысле — возможность выбора конкретной
реализации некоторого имени в зависимости от контекста, в котором дан-
ное имя появилось. Выбор конкретной реализации полиморфного имени на-
зывается разрешением полиморфизма. Полиморфизм соответствует омони-
мии в естественных языках, когда, скажем, ‘стекло’ понимается совершенно
по-разному в синтаксических позициях существительного и глагола. Но да-
же синтаксическая позиция может не помогать разрешению омонимии, на-
пример, для случая слова ‘коса’. Поэтому в языках с широко встречающейся
омонимией (китайский, японский, английский) переходят к иероглифическо-
му письму12 , когда омонимы различаются по написанию. Уже это показывает,
насколько обоюдоострым оружием является неограниченный полиморфизм.
В связи с этим в языках программирования различается два вида поли-
морфизма: тот, который разрешается синтаксически (т. н. раннее связывание
реализации с именем), и тот, который разрешается уже в момент исполнения
программы (т. н. позднее связывание).
Разработанные методы синтаксического разрешения полиморфизма сво-
дятся в основном к тому, что конкретный вариант процедуры или операции
выбирается, исходя из типов ее параметров. Для процедур, кроме того, есть
еще и количество аргументов.
Например, если в языке C/C++ заданы три процедуры Role(), Role(Actor),
Role(Employee), где Actor и Employee — различные типы данных, то одно-
значно понятно, какая из них вызывается в следующих трех случаях (solist
переменная типа Actor, clerk — типа Employee):
Role();
Role(solist);
Role(clerk);

Но вот если описаны две процедуры

Convert(float) Complex и Convert(double) Complex,


Многие современные лингвисты трактуют английское письмо как иероглифическое. На-
пример, рассмотрите широко известный ряд слов ‘rite’, ‘write’, ‘right’, ‘wright’.
12
408 ГЛАВА 8. ПОДПРОГРАММЫ

то вызов Convert(15) выглядит двусмысленным, поскольку сначала 15 нуж-


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

complex(a+b) и string(a+b)

и такой полиморфизм синтаксически разрешим, но в прочих контекстах при


полной неупорядочсенности системы приведений типов он приводит к по-
стоянным конфликтам.
Позднее связывание отнюдь не обязательно требует глубокого семантиче-
ского анализа. Оно в рудиментарном виде было реализовано даже на уровне
системы команд процессоров Burroughs и Эльбрус, реализующих тегговую
архитектуру. Конкретная реализация арифметической операции выбиралась
в зависимости от тегов аргументов.
Динамическую подстановку методов в объектно-ориентированном про-
граммировании можно трактовать как некоторый вариант позднего свзяыва-
ния, о чем сказано в соответствующей главе.
Инициализированные параметры языка С++. Очень соблазнительно
при описании процедуры задать некоторым формальным параметрам соот-
ветствующие им фактические параметры, которые будут использованы при
вызовах в случаях, когда позиция соответствующего фактического параметра
не заполнена. Часто (как, например, в Ada, в командных языках, в языке TEX)
это оформляется через ключевую форму параметризации. В С++ такая воз-
можность реализуется в рамках позиционной формы передачи параметров с
учетом их порядка: опускаться могут лишь последние параметры вызова. За-
дается инициализация параметров помощью конструкции инициализатора,
В языке Алгол 68, где приведения типов сведены в наиболее строгую и последователь-
ную систему, просто было определено, какие из приведений допустимы при разрешении
13

полиморфизма операций (полиморфизма процедур там нет).


8.5. ПАРАМЕТРИЗАЦИЯ 409

которая является обычной для задания начальных значений любых перемен-


ных. Это связано с тем, что параметризация данного языка подчиняется ме-
ханизму передачи параметров по значению, а значит, описание формальных
параметров ничем не отличается от обычного описания локальных перемен-
ных.
Наличие инициализированных параметров позволяет вызывать одну и ту
же функцию с разным числом аргументов, что внешне похоже на полимор-
физм. Так,
void f ( int, int = 7 );
делает возможными вызовы и f(6), и f(6,5). При этом инициализация параме-
тра во втором вызове игнорируется. Соответствие параметров в случае ини-
циализации, которая не игнорируется, обеспечивается соглашением о сохра-
нении порядка неинициализированных параметров.
Понятно, что инициализация параметров не приводит к появлению ново-
го механизма параметризации. Это просто одно из прагматических соглаше-
ний, которое разработчики трансляторов могут иметь ввиду для составления
более качественного объектного кода.
Читатель может сам написать кучу двусмысленных вызовов при совмест-
ном использовании инициализированных параметров и полиморфизма: уж
эти-то две возможности можно решительно не рекомендовать использовать
для процедур с одним и тем же именем!
Несколько слов о возможности не фиксировать число параметров, пре-
доставляемой в С/С++. На уровне вызова процедуры эта ситуация выглядит
просто: все фактические параметры один за другим загружаются в стек. Тем
самым транслятор исходит из предположения о том, что формальных пара-
метров для идентификации загруженных в стек значений достаточно. А вся
нагрузка определения того, что лежит в стеке, а также решение задачи со-
поставления фактических параметров формальным переносится на уровень
алгоритма процедуры. В частности, если специально не позаботиться о раз-
боре загруженных значений, то можно просто потерять передаваемые дан-
ные либо использовать значение одного типа как значение совсем другого.
Стандартный прием работы с переменным числом фактических параме-
тров — использование массива, который оказывается инициализированным
ими. Часто применяется и такой прием: первый параметр задает формат то-
го, что за значения заданы в списке параметров, каково их количество. Распо-
знавание этого формата в алгоритме процедуры дает возможность правильно
использовать полученные значения. Примерами такого рода служат функции
410 ГЛАВА 8. ПОДПРОГРАММЫ

scanf и printf, требующие передачи им формата параметров в виде строки, но


это далеко не единственный вид формата.

8.5.3. Механизмы передачи параметров

Обычные механизмы языков программирования, используемые для пере-


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

2. передача параметра как переменной, при которой фактический пара-


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

3. передача параметра по наименованию, когда при исполнении вызова


подпрограммы на фазе выполнения ее алгоритма имитируется подмена
в теле подпрограммы всех вхождений имени формального параметра
на текстуальное представление фактического параметра;

4. передача параметра по ссылке, когда при исполнении вызова подпро-


граммы на фазе выполнения ее алгоритма формальный параметр заме-
няется на адрес фактического параметра.

5. передача параметра по необходимости, при которой фактическая пе-


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

Нельзя считать все эти механизмы взаимно независимыми. Обычно одни из


них выражаются через другие. Так, через передачу параметра по значению
реализуется ссылочная параметризация, которая по своей сути есть специ-
ально оформленный вариант передачи переменной.
Для пояснения введенных понятий ниже приводятся примеры, в которых
описание параметра задано в синтаксисе языков С/С++, Pascal или в подоб-
8.5. ПАРАМЕТРИЗАЦИЯ 411

ной им манере, когда указанные языки не предусматривают соответствую-


щий механизм.
Семантика параметра, передаваемого по значению (короче — параме-
тра-значения), с точностью до указанных в определении действий, ничем не
отличается от обычной переменной локального контекста. Данный механизм
совершенно точно реализует in-параметры, и в этой связи он представляет-
ся наиболее простым. Именно он является единственным механизмом зада-
ния параметризации в языке С/С++, с помощью которого моделируется все
остальные. Для такого моделирования, а также для максимально возможного
сокращения записи, приведения типов задействованы более чем на полную
мощность. Это обусловлено привязкой языка к машинному представлению
алгоритмов и данных.
В Pascal формальные параметры-значения указываются в заголовке под-
программы в том виде, который используется для описания переменных (опус-
кается служебное слово var):
procedure P ( x : Integer );
begin
a := x; x := 1;
end;
Для С/С++ этот пример (с точностью до описания вырабатываемого функ-
цией значения, т. е. служебного слова void) будет отличаться только на уровне
конкретного синтаксиса:
void P ( int x )
{
a = x; x = 1;
}
В заголовках процедур описан один формальный параметр, поименован-
ный как x. Использование процедуры P возможно с единственным фактиче-
ским параметром-выражением, вырабатывающим целочисленное значение
или с вещественным параметром-выражением, если язык определяет, что его
значение при присваивании округляется до целого — т. е. имеет место приве-
дение вещественного значения к целому.14 Интересно, что вызов процедуры
В Pascal это не так, хотя обратное автоматическое приведение целого к вещественному
значению задействовано. Можете считать это способом сделать так, чтобы программист за-
14

думался, как нужно преобразовать действительное в целое: округлить или усечь дробную
часть?
412 ГЛАВА 8. ПОДПРОГРАММЫ

для С/С++ и для Pascal в данном случае выгладит одинаково.


Пусть приведенныке выше описания погружены в контексты (для каждо-
го языка этот контекст определяется по-своему, но отличия несущественны)
с доступными целыми переменными a, b, c и массивом целых чисел M с ин-
дексом из интервала 0:3. Прокомментируем различные вызовы процедуры
(см. рис. 8.4)
Передача параметра как переменной (короче — параметра-перемен-
ной) в Pascal синтаксически оформляется пометкой описания параметра слу-
жебным словом var:

Если тело этой процедуры содержит те же операторы, что и P:


procedure Q ( var x : Integer );

то погружение данного описания в указанный выше контекст позволяет упо-


begin a := x; x := 1; end;

треблять процедуру, во-первых, для присваивания глобальной переменной a


значения некоторой другой переменной, передаваемой процедуре в качестве
фактического параметра, а во-вторых, — для здания единицы в качестве зна-
чения фактического параметра-переменной. Эта переменная должна иметь
целочисленный тип. Таким образом, операторы
Q(23); Q(2 + 2); и
Q(u); {u — нецелочисленная переменная}
являются некорректными, но допустимы вызовы, изображеные на рис. 8.5.
Последний оператор на рис. 8.5 заслуживает более пристального внима-
ния. Пусть значения элементов массива M равны 4, 3, 2, 1, а значение пере-
менной a равно 1. Тогда вызов Q(M[a]) корректен, он приведет к следующим
действиям. Сначала параметр-переменная x вычислится как M[a], следова-
тельно, как M[1], и именно M[a] будет рассматриваться в качестве фактиче-
ского параметра. Далее, после входа в подпрограмму выполняется присваи-
вание a значения M[1], т. е. a станет равно 3. Наконец, выполняется оператор
x:=1, который присваивает значение 1 переменной с индексом M[1].
На языке С/С++ передача параметра как переменной моделируется с по-
мощью работы с указателями. Это моделирование будет рассматриваться при
обсуждении механизма передачи параметров по ссылке.
Считается, что механизм передачи параметров-переменных реализует inout-
параметризацию. Однако есть принципиальное расхождение между этим ме-
ханизмом и требованиями к inout-параметрам. inout-параметр является од-
новременно и входным, и выходным параметром. Первое требует включения
имени формального параметра в локальный контекст процедуры, а второе —
8.5. ПАРАМЕТРИЗАЦИЯ 413

{ a принимает значение 23, присваиваемое параметру на фа-


зе выполнения алгоритма; другого (внешнего для процедуры)
P ( 23 );

эффекта вызов P не дает. }


P ( 23.2 ); {Это верно для языков, в которых предусматривается автома-
тическое приведение вещественного к целому. Соответствен-
но, в Pascal этот вызов ошибочен, а в С корректен. a прини-
мает значение 23, которое с точностью до округления выра-
батывается так же, как в предыдущем случае. }
{ корректно, если значение b до этого оператора было опре-
делено (для С/С++ данная оговорка лишена смысла, т. к.
P ( b );

язык предусматривает инициализацию значений перемен-


ных); значение b не изменяется! }
{ корректно, если значение a до этого оператора было опреде-
лено (см., однако, предыдущее замечание); этот вызов дает: a
P ( a );

P ( 2+2 ); { значение выражения 2+2 вычисляется до выполнения опе-


= a;}

раторов тела процедуры, т. е. на фазе подготовки выполнения


алгоритма. }
P ( M[2] ); { значение выражения M[2] вычисляется на фазе подготовки и
присваивается переменной a на фазе выполнения процедуры;
значение M[2] не изменяется! }
P ( M[a] ); {Для Pascal вызов корректен при корректности выражения
M[a], т. е. при 0 6 a 6 3; Для С/С++ вызов корректен всегда,
поскольку этот язык не берет на себя заботу о корректности
индексации массивов. Эффект — присваивание значения M[a]
переменной a; M[a] не изменяется! }
Рис. 8.4. Вызовы процедуры с различными параметрами
414 ГЛАВА 8. ПОДПРОГРАММЫ

{ если значение b до этого оператора определено, то это зна-


чение присваивается a, а b присваивается 1}
Q ( b );

{ значение, получаемое переменной a, есть 1, оно вырабаты-


вается вторым оператором тела процедуры }
Q ( a );

Переменная с индексом возможна в качестве параметра-


переменной:
Q (M[2]); { адрес переменной M[2] вычисляется до выполнения опера-
торов тела процедуры; значение M[2] станет значением пере-
менной a, затем M[2] получит новое значение — 1}
Q (M[a]); { вызов корректен при корректности вычисления адреса пе-
ременной M[a], т. е. при 0 6 a 6 3; эффект — присваивание
значения M[a] переменной a и присваивание значения 1 пере-
менной M[a’], где a’ — старое значение a}
Рис. 8.5. Новые возможности вызова при передаче параметра как переменной

выполнения специальных действий по присваиванию выходных значений на


фазе завершения выполнения процедуры. Что же касается параметров-пе-
ременных, то их передача никак не связана с пополнением локального кон-
текста, а специальные действия по присваиванию значений переменным не
предусматриваются. Таким образом, эффект, требуемый inout-параметриза-
цией, достигается лишь при последовательных вычислениях, однако при па-
раллельных либо распределенных вычислениях он может нарушаться, по-
скольку нигде не фиксируется момент, когда параметр-переменная получит
окончательное значение.
Передача параметра по наименованию (или, что то же — по имени)
восходит к самым ранним языкам программирования (FOTRTRAN, Algol 60
и др.). Этот механизм на первый взгляд не вполне удовлетворителен с точки
зрения надежности программирования, тем не менее, полезно сопоставить
его, в частности, с подстановкой параметров-переменных.
Пусть в условиях приводимых иллюстраций описана процедура с вызо-
вом параметра по имени:
procedure R ( name x : Integer );
begin
a := x; x := 1;
end;

Это описание в указанном выше контексте позволяет употреблять проце-


8.5. ПАРАМЕТРИЗАЦИЯ 415

дуру для присваивания глобальной переменной a значения некоторого вы-


ражения, поставляемого фактическим параметром. А так как во втором опе-
раторе формальный параметр x является получателем значения, фактический
параметр, подставляемый по наименованию (параметр-имя), может быть толь-
ко переменной. Если бы в теле процедуры не происходило присваивание зна-
чения параметру, то в качестве фактического параметра можно было бы упо-
треблять и выражение (в данном случае целого типа).
В результате выполнения процедуры переменная x получает значение 1.
Таким образом, операторы R(23); и R(2 + 2); некорректны, но не по причи-
не использования выражений, а из-за наличия в теле R присваивания x := 1.
Различные возможности вызова по имени проиллюстрированы на рис. 8.6.
Полезно подробнее сопоставить последний вызов процедуры R с Q(M[a]) при

{u — нецелочисленная переменная или выражение}


некорректен из-за спецификатора Integer в заголовке проце-
R(u);

дуры, и это, вообще говоря, отношения к механизму переда-


чи параметров не имеет, а означает просто несоответствие ти-
пов. Вместе с тем, допустимы
{ если значение b до этого оператора определено, то это зна-
чение присваивается a, а переменной b присваивается 1}
R( b );

{ значение, получаемое переменной a, есть 1, оно вырабаты-


вается вторым оператором тела процедуры }
R( a );

Эти вызовы имеют тот же эффект, что и соответствующие вы-


зовы процедуры Q. Допустимы и следующие вызовы проце-
дуры, соответствующие Q(M[2]); и Q(M[a]), но с совсем иным
эффектом во втором случае:
R(M[2]); { значение M[2] станет значением переменной a, затем M[2]
получит новое значение 1}
R(M[a]); { вызов всегда корректен, возможна ошибка при вычислении
значения M[a], если неверно 0 6 a 6 3. Результат первого
оператора тела — изменение значения переменной a. Если
для нового значения a верно 0 6 a 6 3, то корректен опера-
тор x:=1. Его эффект — присваивание значения 1 переменной
M[a], где у a новое значение }

Рис. 8.6. Вызов по наименованию

тех же значениях переменных. В соответствии с определением при вызове


R(M[a]) имитируется подмена параметра x на M[a]. Поэтому первый опера-
416 ГЛАВА 8. ПОДПРОГРАММЫ

тор тела процедуры, как и в случае Q(M[a]), присваивает значение 3 перемен-


ной a, но далее текстуальная замена x на M[a] в операторе x:=1 приводит к
присваиванию значения 1 переменной M[3] (а не M[1], как при Q(M[a])).
Из примеров видно, что достигается при подстановке параметра по на-
именованию: системой программирования обеспечивается такой эффект вы-
полнения вызова процедуры, как при выполнении тела процедуры, в котором
всякое вхождение в текст формального параметра заменено фрагментом тек-
ста, представляющего соответствующий фактический параметр.
Приведенное выше определение передачи параметров не совсем коррект-
но: понимаемое буквально, оно противоречит отмеченному общему прин-
ципу разделения контекстов: формальные параметры относятся к контексту
процедуры, а фактические к контексту ее вызова. Иллюстрацией служит при-
мер на рис. 8.7. При буквальном понимании определения, переменная a, по-

...
var a : Integer;
procedure P(name x : Integer);

{ Текстуальная замена x на a приводит }


begin

{ идентификатор a в контекст, в котором }


... x ...

procedure Q( . . . ); { он понимается как имя глобальной переменной a,}


end; { P }

var a, b : Integer; { в то время как в точке вызова }


begin { a понимается как локальная переменная процедуры }
. . . P ( a ); . . .
. . . P ( b ); . . .
end; { Q }
...

Рис. 8.7. Плохой вызов по наименованию

являющаяся при подстановке фактического параметра вместо x, оказывается


той переменной, которая описана в первой строке примера, а переменная b
(второй вызов P) и вовсе недоступна. С любой разумной точки зрения это не-
верно, и требуется уточнение, которое в декларативной форме выглядит как
конкретизация принципа разделения контекстов: имена из фрагментов, вста-
вляемых вместо формальных параметров, идентифицируются, исходя из их
доступности в контексте вызова, а не описания процедуры. Средством до-
8.5. ПАРАМЕТРИЗАЦИЯ 417

стижения этого может быть переименование переменных, т. е. замена всех


вхождений конфликтных имен новыми уникальными именами (в полном со-
ответствии с тем, как подобная процедура производится в логических систе-
мах при оперировании со свободными и связанными переменными).
В реализационном плане коллизии имен ликвидируются с помощью остро-
умного метода санков (thunk), предложенного Ингерманом еще для Algol 60.
Суть метода в том, что в объектном коде контекст вызова процедуры попол-
няется специальными подпрограммами для каждого фактического параметра
(именно они и называются санками). При исполнении вызова обращение к
параметру интерпретируется как вызов санка данного параметра.
Фрагменты текста программы, соответствующие фактическим параме-
трам, превращаются в анонимные процедуры thunk_параметра, которые за-
даются в контексте вызова процедуры. В этом контексте, в частности, кор-
ректны вычисления фактических параметров. Например, если у нас есть па-
раметр a[i+j], то он превращается в процедуру с глобальными переменными
a, i, j, вычисляющую ссылку на соответствующий элемент массива. Техни-
ческий вопрос, связанный с тем, что для каждого вызова нужно обеспечить
свои ‘имена’, используемые в теле, решается техническими же средствами,
зависящими от многих частных решений используемого языка. Вы можете
сами предложить несколько его реализаций на разных уровнях (см. упр. 1 и
следующее).
Среди существующих языков программирования, по-видимому, только
Алгол 68 позволяет описать данную схему без привлечения уровня машин-
ных команд либо хакерских приемов. Для этого надо описать параметры-
имена как процедурные параметры, т. е. такие, которые требуют использо-
вать в качестве фактических параметров процедуры или, как в данном слу-
чае, функции. Допускается это не только в Алголе 68, но уникально то, что
этот язык позволяет подставлять вместо таких параметров не только явно
описываемые процедуры, но и предложения, которые синтаксически могут
быть выведены из понятия <тело процедуры>. В результате на Алголе 68 мо-
дификация описания
P(name int x);
потребовала бы только замены спецификатора name на proc; а в вызовы про-
цедуры P пришлось бы добавить синтаксическое оформление <фрагмент>
как изображения процедуры15 , скажем, наше выражение превратилось бы
В первоначальной версии языка Алгол 68 было предусмотрено приведение любого выра-
жения к типу <процедура без параметров, вырабатывающая ТИП> (запроцедуривание). В
15
418 ГЛАВА 8. ПОДПРОГРАММЫ

в ref int: (a[i+j]). В других языках, например, в Pascal’е для моделирования


требуемого эффекта требуется для каждого подставляемого фрагмента спе-
циально описывать функцию с телом <фрагмент>.
Схема использования санков должна быть рассчитана и на то, что в теле
процедуры P могут быть присваивания значений параметрам-именам. По-
этому разберитесь с тем, какой тип должна иметь анонимная процедура, со-
здаваемая для формального параметра либо, если в теле процедуры есть и
чтение значения параметра-имени, и присваивание ему значения, готовьте
два разных санка. К слову, возможность задания одновременно нескольких
(в данном случае двух) программ, представляющих параметр в теле проце-
дуры в зависимости от контекста его использования, уникальна для передачи
параметров по наименованию: даже в Алголе 68 она не может быть явно вы-
ражена. Уже по этой причине неверно было бы говорить, что процедурные
параметры реализуют механизм передачи по наименованию. Суррогат такой
контекстно-зависимой интерпретации выражений появляется при использо-
вании объектов, когда конкретная фукнкция, исполняемая при вызове мето-
да или использования значения, описанного как property, появляется лишь
в последний момент, в зависимости от конкретного экземпляра объекта. Но,
конечно же, это не контекстная зависимость в чистой форме, и сомнительно,
совместима ли чистая контекстная зависимость интерпретации выражений
со структурным программированием или же это скорее (пока еще не исполь-
зуемый) атрибут сентенциального программирования.
Механизм передачи параметров по имени может претендовать на реали-
зацию inout-параметризации. Во многих случаях его эффект совпадает с эф-
фектом передачи параметра как переменной, а потому с теми же оговорками,
что приведены выше, можно говорить о моделировании данного назначения
параметра. Однако здесь ситуация еще сложнее, поскольку нельзя гаранти-
ровать и то, что параметр-имя сохранится в течение выполнения тела проце-
дуры.
Разработчики языков и систем программирования часто считают, что услож-
нения, связанные с параметрами-именами, ничем не оправданы, поскольку
большинство нужных для практики эффектов параметризации достижимы
другими средствами. Поэтому в современных языках явно механизм переда-
чи параметров-имен практически никогда не предусматривается.
Тем не менее механизм передачи по имени — единственный адекватный
окончательной версии языка запроцедуривание, имеющее наряду с несколькими хорошими
свойствами, массу вредных побочных эффектов, исчезло.
8.5. ПАРАМЕТРИЗАЦИЯ 419

механизм передачи параметров, соответствующих удаленным данным, не-


зависимо изменяемых другим процессом. Каждый раз, обращаясь к таким
данным, мы должны заново их запросить и не удивляться, если их значение
изменится.
Передача параметров по ссылке употребляется в практике програм-
мирования достаточно часто. Даже использование параметров-переменных
можно рассматривать как синтаксически облагороженное употребление ссы-
лок — можно не заботиться о различии употребления формального параме-
тра в двух синтаксических позициях: в качестве источника и получателя зна-
чения. В обсуждаемых примерах тело процедуры с параметром-ссылкой x
нуждается в корректировке:

a := взять_значение x;
begin

взять_адрес x := 1;
end;
Словосочетания “взять_значение” и “взять_адрес” использованы для ука-
зания на необходимость перехода от оперирования ссылками к уровню значе-
ний и адресов, которые при использовании переменных обычно осуществля-
ется автоматически. В некоторых языках переходы такого рода при опери-
ровании со ссылками также делаются автоматически (на самом деле здесь
используются правила приведения типов), что позволяет считать синонима-
ми понятия передачи параметров как переменных и по ссылке. Понятно, что
по поводу моделирования назначения параметризации механизмом параме-
тров-ссылок можно сказать все то же, что и про параметры-переменные.
Именно передача параметра по ссылке в С/С++ реализуется с помощью
механизма передачи по значению. В этом языке весьма развиты средства опе-
рирования с указателями, а потому авторы языка предпочли “забыть о лиш-
них” механизмах. В самом деле, заголовок функции

предписывает, что фактическим параметром может быть нечто, указываю-


void S ( int *x )

щее на целочисленное значение. В частности, выражение

при вычислении дает адрес переменной Variable. А это именно то, что нужно
&Variable

для передачи параметра по ссылке.


Передача параметра по необходимости — это скорее подход, чем кон-
кретный механизм. При передаче по необходимости параметр вычисляется
в тот момент, когда в теле процедуры в первый раз происходит обращение
420 ГЛАВА 8. ПОДПРОГРАММЫ

к нему. В частности, если входной параметр так и не используется, то его


вычисления просто не произойдет.
Передача по необходимости является частным проявлением так называе-
мой стратегии ленивых вычислений (lazy evaluations). Суть стратегии в том,
чтобы откладывать отдельные части вычисления до тех пор, пока мы хоть
что-либо можем сделать, не используя их результаты. Таким образом, дей-
ствия производятся лишь тогда, когда их результаты с необходимостью тре-
буются для дальнейшего продвижения процесса вычислений. В результате
можно (чуть-чуть нестрого) утверждать, что при стратегии ленивых вычи-
слений не будут произведены лишние действия. На самом деле так происхо-
дит только в случаях, когда анализ необходимости вычислений оказывается
намного эффективнее самих откладываемых вычислений.
У параметризации по необходимости три цели:
• обеспечение вычислений с некорректно заданными параметрами в тех
случаях, когда эти параметры фактически не будут использованы для
вычислений;
• обеспечение того, что мы используем наиболее ‘свежее’ значение па-
раметра, доступное к тому моменту, когда он фактически нужен для
вычислений.
• повышение эффективности вычислений.
Первая цель кажется несколько искусственной, но на самом деле ее важность
иллюстрирует вычисление логических операторов в C, где фактически ре-
ализована подстановка по необходимости. Когда мы вычисляем A&&B в C,
то вычисление B производится лишь после того, как вычислено A и получе-
но значение true. Из-за этого, в частности, корректен следующий оператор
(li x — интергральный логарифм x, который определен лишь при x > 0):
if (x>0&& li(x)>y) x+=epsilon; (8.2)
Операторы, подобные данному, часто применяются для организации вычи-
слений с одновременной проверкой корректности областей действия.
Вычисление логических операций в С поучительно сопоставить с их вы-
числением в Pascal, где стандарт языка, во-первых, предписывает совмест-
ность вычисления операндов (как для любых выражений — см. § 5.2), а во-
вторых, разрешает оптимизацию вычислений, т. е. прекращение их, когда ис-
тинность или ложность выражения выясняется до того, как все составляю-
щие выражения вычислены (есть прагматически устанавливаемый режим, в
8.5. ПАРАМЕТРИЗАЦИЯ 421

котором запрещается подобная оптимизация). В результате транслятору раз-


решаются любые перестановки операндов логических выражений, сохраня-
ющие их истинность или ложность, с тем, чтобы выбрать наиболее эффек-
тивный путь вычислений. Как следствие, пример, подобный (8.2) может ока-
заться некорректно вычислен в стандартном Pascal. Прагматическая полез-
ность такого употребления логических выражений приводит к тому, что в
большинстве систем программирования на языке Pascal принята стратегия
их вычисления, в точности соответствующая тому, что предписано стандар-
том С/С++.
Красноречивый пример-иллюстрация для второй цели параметризации
по необходимости — распределенные вычисления. Пусть программа работа-
ет с данными, доступ к которым возможен лишь через сеть и которые непре-
рывно модифицируются другой программой. Предположим, что наша про-
грамма не изменяет данные, но принимает решение на основе их анализа.
Это дает возможность оставить в стороне много трудных проблем, связанных
с организацией совместного доступа к данным. Но нам важно проанализи-
ровать ту информацию, которая соответствует реальному состоянию дел. И
поэтому параметр, соответствующий данным, хранящимся в другом месте
сети, естественно передавать по необходимости, а не по значению.
Тот же пример иллюстрирует и третью цель параметризации по необходи-
мости. Для установления связи и последующей работы с удаленными данны-
ми целесообразна передача по необходимости, поскольку, с одной стороны,
установление связи — операция долгая, а с другой — не исключено, что си-
туация сложится так, что без удаленных данных можно будет обойтись. Еще
один пример, когда анализ наступления необходимости тривиален, демон-
стрируют логические операции: &&, || и др. (смотри анализ первой цели).
То, что два естественных примера группируют цели попарно, показывает.
что три цели передачи по необходимости отлично согласованы друг с другом
и концепция имеет право на существование.
Рассмотрим, как соотносится механизм подстановки параметра по необ-
ходимости с другими механизмами параметризации.
Передача по необходимости моделирует входной параметр в том случае,
когда вычисления, идущие после вызова процедуры до момента использо-
вания параметра не изменяют передаваемый процедуре фактический пара-
метр. Не следует в связи с этим исключать предельной возможности, когда
параметр, передаваемый по необходимости, придется передавать в момент
вызова (т. е. как параметр-значение).
Что касается выходного назначения параметра, то здесь ситуация еще бо-
422 ГЛАВА 8. ПОДПРОГРАММЫ

лее сложная. Стратегия ленивых вычислений предписывает откладывание


определения выходного параметра до тех пор, пока не возникнет потреб-
ность в этом, а такая потребность может появляться уже после завершения
выполнения процедуры (в частности, при последовательных вычислениях
она с гарантией возникает после завершения). Как следствие, последний мо-
мент, когда еще возможно вычислить выходной параметр, — это возврат из
процедуры. Именно этот момент является предельным для откладывания не-
обходимости вычисления выходного параметра.
Иллюстрация возможных переносов подстановки параметра по необхо-
димости дана на рис. 8.8.
Вызов Использование Вычисление Возврат из Использование
процедуры in-параметра out-параметра процедуры out-параметра

Гарантия Гарантия
неизменности неизменности
Перенос назначения
необходимости

Рис. 8.8. Перенос назначения необходимости на более ранние сроки

Механизм передачи параметра по наименованию содержит некоторые чер-


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

при первом обращении к N должно приводить к дозагрузке стека, а при по-


value N : by need

следующих обращениях — к эффекту работы с обычной локальной перемен-


ной (санк становится ненужным). При первом обращении к N, определяемо-
му как параметр-переменная

аналогично в стек загружается ссылка на N.


var N : by need

Чтобы разобраться с вопросами параметризации, полезно познакомиться


8.5. ПАРАМЕТРИЗАЦИЯ 423

с тем, как представленные выше механизмы задаются в конкретных языках.


Константные параметры. Осознание того, что некоторые передаваемые
процедуре данные нуждаются в дополнительной защите, приводит разработ-
чиков языков программирования к введению специальных спецификаторов
параметров для таких случаев. Обычно говорят о константном поведении па-
раметра, или, короче, о константном параметре. Это понимается как огра-
ничение возможности изменения значения фактического параметра в теле
процедуры.
В Object Pascal константный параметр синтаксически оформляется сле-
дующим образом:
procedure P ( const <описание формального параметра> );
В руководстве по языку Object Pascal сказано, что константный параметр
описывает константу, доступную только в теле процедуры, получающую в
качестве значения значение выражения фактического параметра при вызове
процедуры или функции. Как и любой другой константе, константному па-
раметру нельзя присваивать значений (он не может употребляться в качестве
левой части оператора присваивания).
В С++ оформление константного параметра практически такое же:
void P ( const <описание формального параметра> );
Для неуказательных типов это означает в точности же эффект, что и для Object
Pascal, но уже по другой причине: здесь описывается некоторая область па-
мяти в стеке с ограничением возможности присваивания значений точно так
же, как и для любого локального константного описания.
Ситуация меняется, если речь идет об указателях. Из требования руковод-
ства по Object Pascal следует, что описание формального параметра указа-
тельного типа определяет переменную этого типа с именем, вводимым этим
описанием. Коль скоро она получает значение фактического параметра-ука-
зателя, это значение не может быть изменено. В С++ данная конструкция
определяет память, к которой можно обращаться с использованием имени
формального параметра. Как в случае обычного описания, так и при описа-
нии формального параметра спецификатор const защищает от изменения эту
память, а не значение самого указателя.
Представляют ли эти конструкции новый механизм параметризации?
Приведенное разъяснение явно показывает, что для обоих языков кон-
стантный параметр дает в точности то, что требуется от входных параметров
(с несколько различным пониманием того, что передается процедуре через
параметр), но без дополнительной прагматической нагрузки параметров-зна-
чений, связанной с образованием переменой, получающей копию передава-
424 ГЛАВА 8. ПОДПРОГРАММЫ

емого значения. Напротив, для Object Pascal тот факт, что константные па-
раметры не могут служить получателями в операторах присваивания, позво-
ляет, в частности, организовывать передачу процедуре ссылки, когда фак-
тические параметры оказываются переменными, с сохранением требуемой
семантики. Это снимает противоречие между желанием передавать входные
параметры-массивы по значению и стремлением к эффективности, которое
заставляет во избежание копирования больших массивов данных пользовать-
ся параметрами-переменными. А константный указательный параметр языка
С++ непосредственно приспособлен к такому приему оперирования с масси-
вами, структурами и другими составными типами.
Резюмируя обсуждение, можно сказать, что константные параметры есть
всего лишь иное прагматическое оформление входных параметров.
TURBO Pascal версии 7.0 и Object Pascal Delphi предлагают еще один спо-
соб параметризации, который может добиваться некоторых эффектов, ана-
логичных механизму передачи параметров по именам. Речь идет о так назы-
ваемых нетипизированных параметрах. Синтаксически они задаются с по-
мощью спецификации var, const или out имени формального параметра, за
которой не указывается тип параметра:

procedure Change_any( var x );


procedure TakeAnything(const C);
procedure GiveSomething(out Res);

Вместо x или Res при вызове процедуры может подставляться перемен-


ная любого типа, вместо С — произвольное выражение.
На самом деле нетипизированные параметры являются естественным обоб-
щением средств оперирования с нетипизированными указателями — другой
возможности языка, расширяющей Pascal, — на область параметризации.
Как уже отмечалось, механизм параметров-переменных есть просто синтак-
сически оформленный механизм ссылочной параметризации, и коль скоро
язык допускает работу с указателями, ссылающимися на объекты произволь-
ных типов, эта возможность должна быть представлена и в средствах пара-
метризации. Здесь проявилась ссылочная суть задания параметров-перемен-
ных, которая затушевывается синтаксически.
Содержательная работа с нетипизированными параметрами, как, впро-
чем, и с любыми нетипизированными указателями требует, чтобы на уров-
не их использования структура памяти, на которую указывает формальный
параметр (или нетипизированный указатель), была как-то определена. Это
8.5. ПАРАМЕТРИЗАЦИЯ 425

предпочтительнее всего сделать путем явного приведения типов: если в про-


грамме описан какой-либо тип T (в процедуре среди локальных ее описаний
или в другом доступном контексте), то для приведенного примера использо-
вание конструкции
T(x)
дает возможность пользоваться памятью, указываемой нетипизированным
параметром-переменной (или нетипизированным указателем), как обычным
объектом типа T. Так, в теле процедуры
procedure Change_any( var X );
type M = array [ 1 .. 5000] of Real;
begin
...
end;
правомерно записать оператор
M(X)[10] := 5.8;

Независимо от того, какого типа фактический параметр поставляется в


вызове
Change_Any( A );
значение 5.8 будет присвоено по адресу, который имел бы элемент массива
действительных чисел с номером 10, размещаемый по адресу A. Это очень
опасная возможность, поскольку контроль правильности вызова полностью
возлагается на программиста.
Другие средства работы с нетипизированными объектами также нена-
дежны, а когда они выносятся на внешне не всегда заметное оперирование с
параметрами, их опасность еще более возрастает.
Что касается языка С/С++, то он просто не требует чего-либо дополни-
тельного, чтобы завуалировать работу с нетипизированными указателями,
поскольку в этом языке средства такого рода даются программисту в явном
виде (см., например, обсуждение константных параметров).
Нетипизированные параметры-переменные системы TURBO Pascal де-
монстрируют, как происходит диффузия средств одних языков в другие. В
данном случае речь идет о расширении возможностей языка Pascal до того
уровня, который предлагается языком С изначально. К сожалению, это рас-
ширение противоречит одной из основных концепций языка Pascal: строго-
му статическому контролю типов.
426 ГЛАВА 8. ПОДПРОГРАММЫ

Задания для самопроверки

1. Предложите схему трансляции вызова по наименованию в вызовы язык-


ка высокого уровня, которым Вы пользуетесь (С++, Object Pascal, Java,
Ada или др.)

2. (Для тех, кто знает программирование на машинном языке) Предложи-


те способ реализации вызовов по наименованию в языке ассемблера.

8.5.4. Рекомендации по использованию параметров

Погружение процедуры в глобальный контекст и параметризация пре-


доставляют программисту довольно широкие возможности для реализации
связи процедуры с окружением, из-за чего у начинающего могут появиться и,
как показывает опыт, появляются определенные трудности. Обсуждение на-
значения параметризации и, в частности, разграничение видов параметров и
механизмов их передачи в некоторой степени помогают решению вопроса о
том, когда, как и какие применять параметры. Тем не менее, его недостаточ-
но для того, чтобы сделать выбор между параметризацией и погружением в
глобальный контекст. Неоднозначность представления in-, out- и inout-пара-
метров механизмами передачи параметров также затрудняет выбор. Таким
образом, представляется полезным дать общие рекомендации, чтобы сфор-
мировать соответствующие методологические критерии. Этой задаче посвя-
щаются материалы данного раздела.
Разговор об использовании различных возможностей задания данных, пе-
рерабатываемых процедурами, будет обоснованнее, если провести его на фо-
не обсуждения конкретных примеров. В настоящей работе для этого исполь-
зуются процедуры ввода матрицы MyOwnInpMatr и MyInpMatr.
Оправдана или нет в процедуре MyOwnInpMatr передача значения размера
матрицы по измерениям через глобальный контекст, как это сделано выше?
С точки зрения минимизации текста программы — да: любой параметр тре-
бует, чтобы его, во-первых, описывали при определении подпрограммы как
формальный параметр, а во-вторых, указывали в списке фактических пара-
метров. Но это близорукая позиция. Более основательный критерий — на-
дежность программы. С этой позиции предпочтительнее включить размер
матрицы в число параметров процедуры MyOwnInpMatr — учитывать требо-
вание задания значения переменной N перед вызовом процедуры проще, ко-
гда она упоминается в операторе вызова. Конечно, использование параметра
8.5. ПАРАМЕТРИЗАЦИЯ 427

несколько снизит эффективность программы, но стоит ли жертвовать надеж-


ностью ради сокращения программы на одну-две команды?
Другая связь процедуры MyOwnInpMatr с глобальным контекстом — упо-
требление (глобальной) переменной Matr. Как и с переменной N, явное упо-
минание среди параметров имени массива, получение значения которого есть
основная цель процедуры, могло бы увеличить надежность программы. Од-
нако если обсуждаемая процедура предназначена для ввода только одной ма-
трицы, то имя ее массива становится элементом системы понятий, с кото-
рыми оперирует использующая программа. Тогда предположение о том, что
можно “забыть” имя обрабатываемой матрицы в подпрограмме ввода ее зна-
чений становится умозрительным. Вывод: включение имени массива, пред-
ставляющего матрицу в программе, в список параметров процедуры нецеле-
сообразно. Вместе с тем, если процедура используется для ввода нескольких
матриц, то употребление имени представляющего массива как out-параме-
тра разумно для реализации данной возможности. Для языка С/С++ вопрос
о том, каким механизмом параметризации воспользоваться, не стоит: массив
здесь это просто область памяти, адрес которой есть значение переменной,
описанной как массив (т. е. такой, описание которой содержит квадратные
скобки). Иными словами, язык предписывает считать имена массивов ука-
зателями на них, а потому передача такого имени функции в качестве аргу-
мента по значению дает доступ к указываемой памяти: получается так, что
массив (как область памяти) передается по ссылке (с соответствующим мо-
делированием эффекта выходного параметра). Перенося эти рассуждения на
паскалеподобный язык, становится понятной причина, по которой Matr надо
передавать как переменную (т. е. по ссылке).
У процедуры MyOwnInpMatr есть еще одна, неявная связь с глобальным
контекстом. Это условие корректной ее работы: размерность матрицы не долж-
на превосходить значения константы NN. С точки зрения надежности про-
граммирования неявные связи подпрограмм с контекстом их погружения не-
желательны. В примере процедура MyOwnInpMatr не совсем надежна, т. к.
ее можно вызвать с нарушением условия корректности, а ошибка, к которой
приведет такой вызов, не будет своевременно диагностирована.
В языке Pascal есть средство для смягчения последствий такой ненадеж-
ности: возможность контроля значения переменной N за счет придания ей
типа 0..NN (в глобальном контексте). Тем самым при наличии связи процеду-
ры с глобальным контекстом через эту переменную будет контролироваться
именно та величина, которая вызывает ошибку размерности. В случае проце-
дуры с параметром-размерностью этот параметр должен получить тип 0..NN
428 ГЛАВА 8. ПОДПРОГРАММЫ

(разумеется, путем включения в глобальный контекст описания типа, напри-


мер,
type Index = 0..NN;
и спецификации параметра как Index).
Такое решение практически исчерпывает проблему, но далеко не всегда
условия корректности работы подпрограммы выразимы с помощью указа-
ния требуемых статических типов переменных и параметров. К сожалению,
задание типовой информации — единственное средство языка Pascal, кото-
рое можно использовать для спецификации значений объектов. В некоторых
языках возможности такого рода расширяются. Иногда, к примеру, допус-
кается определение предиката над параметрами, формулирующего условия
корректной работы подпрограммы. Но ни один формальный способ специ-
фикации условий на параметры даже теоретически полон быть не может, а
неформальный чреват и ошибками неправильного истолкования, и многими
другими опасностями.
Ситуация с С/С++ сложнее, т. к. для него не предусмотрены средства опре-
деления того, какие значения допустимы для описываемой переменной. Бо-
лее того, в этом языке не считается ошибкой выход индекса за пределы мас-
сива. Как следствие, такая некорректность приведет лишь к искажениям па-
мяти, расположенной после размещения массива. Причина тому — ориен-
тация на примитивно понимаемое максимально полное приближение языко-
вых средств к уровню машинного представления. Таким образом, здесь обя-
зательно нужен дополнительный контроль со стороны программиста. Что-
бы такой контроль был ‘съемным’ (включаемым и выключаемым при необ-
ходимости), можно реализовать его с помощью средств препроцессора. Но,
к сожалению, его возможности по теоретически и практически непреодоли-
мым причинам принципиально ограничены.
Для полноты обсуждения процедуры MyOwnInpMatr уместно упомянуть
о ее локальном контексте, т. е. о переменных i и j, которые с формальной
точки зрения допустимо вынести в глобальный контекст. Со всей определен-
ностью можно сказать, что такое решение ничем не оправдано: переменные
i и j имеют содержательный смысл только внутри тела процедуры, а потому
использование их как глобальных объектов только повысило бы зависимость
процедуры MyOwnInpMatr от глобального контекста. В результате программа
оказалась бы менее надежной. (С этой же точки зрения неразумно задание i и
j в качестве формальных параметров процедуры — оно потребовало бы ука-
зания соответствующих фактических параметров из глобального контекста,
8.5. ПАРАМЕТРИЗАЦИЯ 429

для которых и вовсе не удается дать содержательную трактовку).


Таким образом, в языке Pascal для процедуры MyOwnInpMatr целесообраз-
но определить параметр, имеющий тип Index и передающий размер матрицы
алгоритму из глобального контекста. В С/С++ приходится иметь дело с цело-
численным типом. С иллюстративными целями предполагается, что проце-
дура предназначена для ввода различных матриц. Для обеспечения этой воз-
можности необходимо модифицировать глобальный контекст: внести в него
описания типов, откорректировать описание массива Matr и добавить опи-
сание еще одного массива, значения элементов которого можно вводить с
помощью MyOwnInpMatr. В результате описание глобального контекста при-
нимает вид:
#include <stdio.h>
#include <stdlib.h>
const NN = 100;
typedef float Matrix[NN][NN];
Matrix Matr, Matr1;
int N;

Для паскалевского варианта программы нужны следующие описания:

const NN = 100;
type Index = 1..NN;
Matrix = array [ Index, Index ] of Real;
var N : Integer;
Matr, Matr1 : Matrix;

С учетом проведенного обсуждения С-вариант процедуры MyOwnInpMatr


описывается следующим образом:

Программа 8.5.1

void MyOwnInpMatr( Matrix M, int SizeM )


{
int i, j;
printf("Enter matrix data:\n");
for(i = 0; i < N; i++)
{
printf("Row %3i:",i);
for(j = 0; j < N; j++)
430 ГЛАВА 8. ПОДПРОГРАММЫ

scanf("%f",& M[i][j]);
printf("\n");
}
}

Процедура MyOwnInpMatr может быть использована, к примеру, для такого


задания матриц, описанных в глобальном контексте:

N = 10;
MyOwnInpMatr (Matr, N);
MyOwnInpMatr (Matr1, 5);

При условии корректности ввода в результате выполнения этих операто-


ров переменная N получит значение размера матрицы Matr по измерениям, в
массиве Matr будет заполнен левый верхний угол размера 10 на 10, а в масси-
ве Matr1 — угол размера 5 на 5 (для Matr1 переменная, фиксирующая размер
матрицы, не предусмотрена). Это логический взгляд на структуру данных.
На уровне модели вычислений языка С/С++ все выглядит несколько иначе.
Никаких “верхних углов” матрицы не существует. Вместо матрицы рассма-
тривается вектор указателей (например, Matr[i] для каждого i), ссылающихся
на векторы вещественных значений. Именно этого требует стандарт языка, и
не случайно, что выражение
M[i][j]
является эквивалентным следующему фрагменту, явно отражающему опери-
рование с указателями:
*(*(M+i)+j)
Следует отметить, что в языке Pascal логический взгляд на обсуждаемую
структуру данных вполне правомерно представлять себе и как реализаци-
онный, можно даже не задумываться над представлением матриц в памя-
ти (разумеется, квалифицированный программист это всегда знает, а потому
использует, и именно этим обстоятельством обычно аргументируют адепты
языка С отход от традиций абстрактного рассмотрения представлений струк-
тур данных).
В языке Pascal заголовок процедуры MyOwnInpMatr может быть предста-
влен следующим образом:

procedure MyOwnInpMatr (var M : Matrix; SizeM : Index );


8.5. ПАРАМЕТРИЗАЦИЯ 431

Здесь явно обозначены различные механизмы, использованные для первого


и второго параметров. Это становится возможным из-за того, что описание
массива в языке Pascal не привязывается к реализации доступа к его элемен-
там, имя переменной типа массив действительно обозначает совокупность
‘переменных с индексами’, и не требуется прибегать к понятию памяти для
уточнения семантики данных понятий. В результате в языке Pascal Matr и
Matr1 обозначают значения массивов, тогда как в С/С++/C# — адреса памя-
ти для размещения значений. Именно поэтому внешне схожие изображения
двух указанных формальных параметров в С/С++/C# на логическом уровне
рассмотрения приводят к двум разным механизмам параметризации. В дей-
ствительности здесь нет противоречий, поскольку первый аргумент функции
передается по значению (именно так, как и второй аргумент), но передава-
емое значение есть ссылка в данном случае на массив и все оказывается в
точности так, как в Pascal.
Иное дело, что Pascal предлагает возможность передачи параметра-мас-
сива как значения. Сделать это очень просто: удалить var из заголовка. Но к
чему это приведет? Как уже отмечалось, параметр-значение приводит к обра-
зованию локальной переменной, инициированной фактическим параметром.
Применительно к массивам оказывается, что копируется весь массив, что ча-
ще всего само по себе нерационально. Но, кроме того, любые модификации
параметра не приведут к изменению фактического массива. И, если это при-
менить к обсуждаемым алгоритмам, становится понятным, что переменные
Matr и Matr1 своих (незаданных!) значений не изменят!
Все, сказанное выше по поводу процедуры MyOwnInpMatr, с точностью
до различий в алгоритмах применимо и к функции MyInpMatr. Поэтому но-
вый текст подпрограммы не требует подробного обсуждения:

Программа 8.5.2

int MyInpMatr ( Matrix M )

// Переменная используется для


int i,j, SizeM = 0;

// определения окончания
char c;

// ввода первой строки матрицы

printf("Enter matrix data:\n");


432 ГЛАВА 8. ПОДПРОГРАММЫ

printf("Row 0:");
do
scanf("%f%c",&Matr[0][SizeM++],&c);
while (c!=’\n’);
for(i=1; i < SizeM; i++)
{
printf("Row %2i:",i);
for(j=0; j < N; j++)
scanf("%f",& Matr [i][j]);
do
scanf("%c",&c);
while(c!=’\n’);
}
return SizeM;
}

Для полноты картины ниже представлен заголовок функции MyInpMatr на


языке Pascal:

function MyInpMatr (var M : Matrix ) : Integer;

Понятно, что здесь параметр SizeM не имеет смысла — значение, которое он


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

A[0][0] + MyInpMatr(A)

и
MyInpMatr(A) + A[0][0]
Быть может, последний термин и противоречит содержательному представлению о том,
что цель функции — ввод матрицы, но в большинстве случаев дисциплинированного про-
16

граммирования результат, специфицированный в заголовке функции, можно и должно счи-


тать целью ее работы (основным результатом).
8.5. ПАРАМЕТРИЗАЦИЯ 433

если к моменту их выполнения A[0][0] равно единице, а первое вводимое при


работе функции число есть ноль? В большинстве языков и систем програм-
мирования, в том числе в С/С++ и Pascal специфицируется совместность вы-
числения операндов выражения (см. п. 1.2.2), а значит, порядок, в котором
выполнятся MyInpMatr(A) и A[0][0] не определен.
Дисциплинированный программист вместо неконтролируемого формаль-
ными правилами языка ограничения на использование функции MyInpMatr
(запрета выражений, вроде только что приведенных) определил бы для этой
цели процедуру со свойством функции MyInpMatr, которая вырабатывает фак-
тический размер матрицы в качестве значения специального out-параметра.
Здесь требуется именно out-параметр, а не inout, т. к. входное значение па-
раметра для обсуждаемого алгоритма безразлично.
Однако, как уже отмечалось, языки С/С++ и Pascal в определении параме-
тризации ориентируются не на потребности, а на механизмы передачи пара-
метров. Поэтому приходится выбирать тот механизм, который обеспечивает
указанное свойство. Таким механизмом для языка Pascal является вид пере-
дачи параметра как переменной:
procedure MyBestInpMatr (var M : Matrix; var SizeM : Index);
Для С/С++ заголовок MyBestInpMatr представляется следующим образом (обра-
тить внимание на описание второго параметра как ссылки):
void MyBestInpMatr (Matrix M,int *Index)
Окончательную подпрограмму предлагается составить самостоятельно пу-
тем переделки функции MyInpMatr в процедуру. Предлагается встроить в эту
процедуру проверку того, что при вводе строк матрицы пользователь задал
лишние числа. В таких случаях полезно выводить соответствующее преду-
преждение.
По-видимому, это лучшая из приведенных подпрограмм, решающих за-
дачу ввода матриц с определением их размера. Ее использование для ввода
матрицы Matr в языке Pascal имеет вид
MyBestInpMatr (Matr, N);

В С/С++ тот факт, что процедура определяет значение второго параметра,


подчеркивается символом &, смысл которого — указать, что используется
адрес, а не значение N:
MyBestInpMatr (Matr, &N);
434 ГЛАВА 8. ПОДПРОГРАММЫ

Хорошо ли то, что логически единообразные действия — присвоить значе-


ния переменной-массиву и простой переменной — изображаются по-разному?
Скорее всего, это следствие чрезмерной “заботы” о прозрачности (т. е. абсо-
лютной понятности) реализации для программиста.17
При употреблении данной процедуры невозможно задать матрицу, не опре-
деляя специальной переменной, в которой фиксируется ее размер, подобно
тому, как это делалось оператором

MyOwnInpMatr (Matr1, 5);

Вообще говоря, это не недостаток, а достоинство подпрограммы: процедура


MyBestInpMatr дисциплинирует работу с данными.
Если развивать заботу о надежности процедуры ввода матриц, то стоит
предусмотреть еще один вид контроля: отслеживание того, что происходит
при вводе. Уже отмечалась возможность сопровождать ввод предупредитель-
ными сообщениями, но утверждать, что именно это всегда полезно, было бы
опрометчиво. При некорректном вводе (попытка ввода недопустимого зна-
чения и др.) целесообразно перенести реакцию на ошибку в использующую
программу, оставив процедуре лишь передаче сообщения об этом в контекст
вызова процедуры (анализ того, что произошло, и что можно предпринять
при ошибке, — задача вызывающей программы). Стандартный прием реали-
зации таких сообщений — формирование кода ответа в качестве результата
работы процедуры. Одно и только одно значение такого кода говорит, что все
в порядке; все другие значения указывают на ошибку и ее причину. Пользо-
ватель, проверяя код ответа, может запланировать различные действия при
разных ошибках.
Варианты формирования кода ответа сводятся к следующему:

• выработка возвращаемого значения (тогда процедура MyBestInpMatr сно-


ва превращается в функцию);

• присваивание значения специальной переменной из глобального кон-


текста (тогда процедуру MyBestInpMatr вместе с этой переменной це-
лесообразно рассматривать в качестве специального модуля ввода, раз-
решающего доступ к этой переменной по чтению);
По количеству “медвежьих услуг” на каждую реальную услугу программирование, ви-
димо, находится вне конкуренции во всех областях человеческой практики.
17
8.5. ПАРАМЕТРИЗАЦИЯ 435

• использование специального out-параметра для передачи кода ответа


(наиболее гибкий вариант, но требующий дополнительных описаний
даже тогда, когда проверка кода ответа не требуется).
Все эти варианты дают одно и то же, а какой из них выбрать, диктуется усло-
виями использования процедуры ввода матриц (в сочетании с другими сред-
ствами из соответствующей библиотеки).
В простых случаях тип кода ответа — логический: истинное значение —
все нормально, ложное — ошибка. Когда требуется диагностика ошибок, це-
лесообразно в качестве типа кода ответа использовать перечисление:
type TansCode = (ok, er1, er2, unknown_er);

Это описание требуемого типа на языке Pascal. Для С/С++ оно выглядит сле-
дующим образом:
enum TansCode = {ok, er1, er2, unknown_er};

Использование кодов ответов — это еще один из рекомендуемых приемов


работы с процедурами с регламентацией зависимости их от контекста погру-
жения. При его применении важно четко придерживаться единообразия опе-
рирования с кодами ответов не только при его формировании, но и при после-
дующем распознавании результатов. Только тогда можно будет утверждать,
что этот прием приведет к сокращению мысленных усилий программистов.
Цель проведенного обсуждения вариантов реализации ввода матриц —
дать представление о том, о чем нужно заботиться при разработке своих про-
цедур и функций. Благодаря этому следующие методические рекомендации
приобретают некоторую мотивированность:
• Погружение процедуры в глобальный контекст и переработка данных
этого контекста, вообще говоря, более эффективны по сравнению с па-
раметризацией. В то же время оно делает почти невозможным исполь-
зование процедуры вне контекста погружения.
• Употребление переменной из глобального контекста не оправдано, ко-
гда в процедуре вместо нее может быть использована локальная пере-
менная. За счет этого не только снижается зависимость процедуры от
контекста погружения, но и повышаются ее надежность и понятность:
изменения локальных переменных не изменяют данные, остающиеся
после отработки процедуры, точно известно, что локальный объект не
имеет смысла вне подпрограммы.
436 ГЛАВА 8. ПОДПРОГРАММЫ

• То же относится и к другим программным объектам: типам, константам


и т. д. Они должны быть локализованы в контексте, в котором исполь-
зуются, а не в охватывающем контексте.

• Если элемент данных предназначен для совместного оперирования не-


скольких подпрограмм, то его можно рассматривать как часть общей
памяти этих подпрограмм, т. е. часть их общего контекста. Скорее все-
го, такой объект не должен передаваться как параметр. Исключение со-
ставляют случаи раздельной обработки нескольких таких объектов, и
тогда их следует передавать подпрограмме в качестве параметров.

• Использование значения, которое задает тот или иной путь вычислений


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

• При необходимости получить результат какого-либо вычисления с по-


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

• То же относится и к ситуации, когда результат используется в качестве


фактического параметра другой процедуры. Однако при ориентации на
Pascal следует помнить, что язык допускает лишь скалярные результа-
ты функций, а потому ограничивает возможности применения данного
средства.18

• При необходимости получения нескольких результатов процедуры воз-


можно, что логически выделяется один из них и этот результат требует-
ся использовать в качестве операнда выражения. В таком случае удобно
оформлять процедуру как функцию, специфицированную типом этого
18
В Object Pascal это ограничение снято.
8.5. ПАРАМЕТРИЗАЦИЯ 437

операнда (или совместимым с ним типом). Остальные результаты явля-


ются логическими out- или inout-параметрами, которые моделируются
механизмом передачи параметров-переменных или ссылок.

• Вообще говоря, использование функций с несколькими результатами


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

• Общая рекомендация по использованию подпрограмм в том, что ис-


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

8.5.5. Параметры-процедуры и параметры-функции. Процедурный тип

До сих пор обсуждалась параметризация, которая обеспечивает задание


вариантов вычислений посредством предоставления подпрограмме различ-
ных значений параметров (входные параметры), за счет поставки подпро-
граммой значений для внешних переменных (выходные параметры), а также
при совмещении этих двух способов (см. § 8.5.3). Многие языки програм-
мирования допускает еще один вид параметризации, который связывается с
передачей процедуре в качестве фактического параметра (другой) процеду-
ры или функции с тем, чтобы вызывать обозначенный параметром алгоритм
в нужные моменты вычислений, обеспечивая тем самым вариантность вычи-
сления подпрограммы. В этом принципиальное отличие параметра-функции
оть вызова функции в позиции фактического параметра-значения, где функ-
ция вычисляется до перехода к выполнению операторов тела подпрограммы.
Можно считать, что алгоритм, представленный своим именем, есть зна-
чение одного из процедурных типов, и тогда естественно говорить о входном
параметре-процедуре или даже о выходном алгоритме, который порождается
438 ГЛАВА 8. ПОДПРОГРАММЫ

в результате выполнения процедуры, имеющей соответствующий выходной


параметр. Последнее — это пока что программистская экзотика, поскольку
процедурные типы являются типами более высокого порядка по сравнению
с типами обычных значений, а с ними связано очень много проблем, в том
числе и теоретических. Совмещенные параметры еще экзотичнее, хотя ситу-
ации, когда они были бы полезными, легко себе представить. Скажем, про-
цедура получает в качестве параметра некоторый алгоритм и в результате
своей работы порождает другой алгоритм, который заменяет исходный ва-
риант, например, на основании знания о конкретных условиях его работы.
Таким образом, если считать нормальным оперирование с типами высоко-
го порядка,19 то процедурные параметры являются частным случаем ранее
рассмотренных назначений параметризации.
Когда нужно обеспечить возможность вызова алгоритмов-параметров по
ходу вычислений, можно было бы воспользоваться механизмом подстанов-
ки по имени. Это ближе всего к тому, что требуется. Но это слишком силь-
ное средство, т. к. для использования процедурного параметра не требуются
никакие окружающие его вычисления (вроде тех, что задаются реализацией
вызова по имени) и, соответственно, зависимость такого параметра от внеш-
него контекста минимальна. В реализационном плане самым приемлемым
вариантом процедур как входных параметров является передача ссылки на
фактический параметр-алгоритм, т. е. адреса передаваемой процедуры. По-
этому не случайно, что именно так задается процедурная параметризация в
языке С/С++, что не требует дополнительных механизмов, поскольку адрес
есть значение указательного типа, которым обладает соответствующий фор-
мальный параметр. Процедуры же как выходные параметры пока что не под-
держаны по сути современными системами программирования, и в лучшем
случае значением такого выходного параметра будет одна из фиксированно-
го конечного множества заранее описанных в программе процедур. Поэтому
и здесь решение С/С++ достаточно на современном этапе.20
Как в принципе и должно было бы быть, поскольку ввиду парадокса изобретателя умение
работать с сущностями высших порядков является единственным путем к снижению объема
19

программных систем для действительно сложных задач и, соответственно, единственным


путем к кардинальному повышению их надежности. Но на этом пути стоят, помимо чисто
технических, методологические и педагогические трудности: слишком мало людей оказы-
ваются подготовленными к восприятию сущностей высших порядков. . .
Если же процедуры станут полноправными значениями, то решение языка C/C++ сразу
станет неудовлетворительным даже для входных параметров, поскольку ими тогда могли бы
20

быть процедурные выражения, составленные при помощи композиции и частичной параме-


8.5. ПАРАМЕТРИЗАЦИЯ 439

Если заботиться о надежности, то приходится требовать от системы про-


граммирования сопутствующих действий, способствующих корректности ис-
пользования ссылок на процедуры. В первую очередь здесь речь идет об
обеспечении правильного вызова процедуры, передаваемой в качестве па-
раметра: должно быть соблюдено соответствие типов формальных и факти-
ческих параметров этого вызова, а также типа вырабатываемого результата
требованиям позиции вызова, если параметр-процедура является функцией.
Перед изучением существующих суррогатов средств задания и использо-
вания в подпрограммах параметров-процедур и параметров-функций умест-
но рассмотреть пример, демонстрирующий полезность использования ново-
го вида параметризации подпрограмм. Обсуждение этого примера покажет
способ введения в языках данного средства.
Пусть требуется вычислить следующее выражение:
sin(1) + sin(2) + · · · + sin(N )
f (N ) =
1 + 12 + · · · + N1
Видно, что как в числителе, так и в знаменателе дроби используется сумми-
рование, отличающиеся только видом общего члена. Если описана функция
double cm( double i)
{
return 1/i;
}

то аналогия между выражениями числителя и знаменателя подчеркивается


еще нагляднее:
sin(1) + sin(2) + · · · + sin(N )
f (N ) =
cm(1) + cm(2) + · · · + cm(N )
Уместно потребовать, чтобы язык позволял описать функцию суммирования,
которая умеет “работать” с различными общими членами. В С/С++ это дости-
гается с помощью использования типа указатель на функцию с одним пара-
метром в качестве типа формального параметра:

double sum (int N, double (*f)(double p))


{
double s = (*f)(1);

тризации.
440 ГЛАВА 8. ПОДПРОГРАММЫ

for ( double i = 2; i <= N ; i++ )


s += (*f)( i );
return s;
}

Нужно обратить внимание на то, как вызывается функция-параметр: (*f)(i).


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

double c = sum ( N, sin) / sum ( N, cm );

делает именно то, что нужно.


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

function sum (N : Integer; function M (X : Real) : Real) : Real;

указывает, что второй параметр является функцией. Это очень похоже на


С/С++. Но вот вызов этого параметра гораздо проще: M (<значение типа Real>).
Эта конструкция прямо указывает на обращение к процедуре или функции
без указания каких бы то ни было деталей реализации. То, что при таком вы-
зове никогда не произойдет коллизий со списками параметров, гарантируется
описанием формального параметра-подпрограммы, которое специфицирует
возможные фактические параметры.
В контексте приведенного описания правомерны выражения sum(N,sin)
и sum(N,cm), поскольку sin и cm являются функциями, вырабатывающими
значения вещественного типа с вещественным или целым (почему?) пара-
метром, как того требует спецификация формального параметра-функции M.
Следовательно, требуемое вычисление f(N) не отличается от того, как оно за-
писывается в С/С++.
По правилам языка Pascal формальный параметр-функция задается в спис-
ке параметров с помощью синтаксической конструкции <заголовок функ-
8.5. ПАРАМЕТРИЗАЦИЯ 441

ции>, которая определяет шаблон для всех функций, подходящих для под-
становки в качестве фактического параметра. Совершенно аналогично зада-
ется параметр-процедура, для которого шаблоном служит заголовок проце-
дуры. Употребляемое в таких заголовках имя подпрограммы-параметра обо-
значает ее для использования в конструкции вызова процедуры или функ-
ции. Оно является именем формального параметра. Заголовок процедуры
или функции, используемый для определения формального параметра, на-
зывается спецификацией параметра.
Как видно из примера, правило задания спецификации параметров-функ-
ций и процедур избыточно: оно требует указания имен формальных пара-
метров специфицируемого параметра-подпрограммы (в примере — X), то-
гда как необходимой и достаточной информацией являются лишь их типы (в
примере — Real). При выборе изображения для таких спецификаций автор
языка Pascal стремился к минимизации синтаксических понятий языка, а по-
тому отдал предпочтение указанному виду. В этой связи стоит отметить, что
многие другие системы программирования в подобных случаях не требуют
указания избыточных имен.
При трансляции заголовка процедуры, имеющей формальный параметр-
процедуру, для такого параметра отводится элемент памяти, который пред-
ставляет формальный параметр в теле процедуры. Ему при вызове обеспе-
чивается присваивание адреса фактического параметра-процедуры, и это по-
зволяет осуществлять вызов формального параметра с помощью тех же ко-
манд, которые задаются при обычном вызове процедуры. Таким образом,
этот механизм в точности соответствует механизму передачи параметра по
ссылке, который, как было указано на стр. 419, специальным образом оформ-
ляется синтаксически с той целью, чтобы изображение вызова обычной про-
цедуры ничем не отличалось от вызова формального параметра. В результате
при написании тела процедуры можно не замечать их различий и абстрагиро-
ваться от того, какие фактические параметры-процедуры будут подставлять-
ся вместо используемого формального параметра.
В языке Pascal передача процедуре алгоритма в качестве параметра под-
чиняется существенному ограничению: все параметры процедур, которые
используются в качестве фактических параметров, подставляемых вместо
формальных параметров-процедур, должны быть параметрами-значениями.
В частности, невозможно, чтобы у таких параметров были бы свои параме-
тры-процедуры. Введение такого ограничение разумно, поскольку без него
442 ГЛАВА 8. ПОДПРОГРАММЫ

возможности контроля параметризации неоправданно сужаются21 (об этом


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

Следует, однако, заметить, что на уровне семантики передачи процедурам


Object Pascal.

алгоритмов в качестве параметров модели вычислений стандартного и Object


Pascal почти совпадают. Их различия не касаются назначения и механизмов
передачи параметров-процедур, они связаны лишь с тем, что в Turbo Pascal
и Object Pascal процедурам и функциям пытаются придать смысл значений
так называемых процедурных типов. Язык допускает определение перемен-
ных таких типов и, следовательно, присваивание им ‘алгоритмических’ зна-
чений: тел процедур и функций, и уже, как следствие, он избавляется от спе-
циального определения таких конструкций, как формальный параметр-про-
цедура, спецификация формальных параметров-процедур и формальных па-
раметров-функций.
Попытка переноса спецификации параметров-процедур и параметров-функ-
ций на уровень типовой информации наиболее последовательно проводится
в языке Алгол 68, где декларируется, что подпрограмма есть (исполняемое)
значение процедурного или функционального типов (видов — в термино-
логии языка). В развитии Pascal подобное средство, представленное в Turbo
Pascal и Delphi, связывается с возможностью использования описаний такого
Более того, есть исключительно тонкие теоретические результаты, показывающие, что па-
раметр-процедура, в свою очередь принимающий параметры-процедуры, ведет себя прин-
21

ципиально по-другому, чем привычные программистам объекты низших типов. В частности,


структурный переход из тела такой процедуры может привести к неустранимым и исключи-
тельно трудно диагностируемым некорректностям.
8.5. ПАРАМЕТРИЗАЦИЯ 443

рода:

type Prt =procedure ( X : Integer; Y : Real );


Fut = function ( С : Integer ) : Real;

определяющих шаблоны:

• процедуры с двумя параметрами указанных типов и

• функции с одним параметром, вырабатывающей вещественное значе-


ние

(как и спецификации параметров-процедур стандартного Pascal, такие ша-


блоны имеют избыточные имена параметров: X, Y и С).
Такие описания вводят имена так называемых процедурных типов (в при-
мере — Prt и Fut). Их можно использовать для определения процедурных и
функциональных переменных:

var p : Prt;
f : Fut;

с которыми допускается естественное оперирование: присваивание ‘проце-


дурных’ значений (если источник значения имеет тип, соответствующий ша-
блону процедуры или функции), использование для вызова подпрограммы.
Следует заметить, что в качестве литеральных изображений значений про-
цедурных типов можно рассматривать описания процедур и функций, но с
одной оговоркой: такие литеральные изображения значений нельзя исполь-
зовать в качестве выражений (т. е. их нельзя присваивать процедурным и
функциональным переменным, они могут быть только описаниями обычных
процедур). Это кажется на первый взгляд чисто технической недоработкой,
связанной с тем, что в данной реализации информация о процедуре при при-
сваивании не дублируется. Но на самом деле такое присваивание ввело бы
авторов системы в плохо знакомый им мир значений высших типов, который
они и в страшно усеченной-то форме никогда не могли корректно реализо-
вать.22
Один из авторов книги, который всегда интересовался проблемой значений высших ти-
пов, пытался работать с ними в доступных ему версиях трансляторов. И трансляторы с Ал-
22

гола 68, и все версии расширений языка Pascal, с которыми ему приходилось иметь дело,
содержали в данном месте ошибки реализации.
444 ГЛАВА 8. ПОДПРОГРАММЫ

В реализационном плане процедурные и функциональные переменные в


первом приближении представляются как указательные переменные, кото-
рые в состоянии хранить в качестве значений адреса процедур и функций.
Поэтому не удивительно, что для С/С++ средства такого рода определены
естественно, т. е. как описание указателя (примеры см. в программе 10.2.8
в п. 10.2.5, где, в частности, показана одна из возможностей использования
указателей на процедуры). Но для данного языка также естественно, что кон-
троль использования подобных указателей лежит на программисте.
Синтаксис описания и использования процедурных параметров Object и
Turbo Pascal иллюстрируется следующим примером:

procedure PP (T : Prt);
...
begin
...
T (5, 5.5);
...
end; {PP}

и, соответственно, правомерны операторы PP(RP) и PP(p), если RP удовле-


творяет шаблону, определенному в описании Prt, а процедурное значение
(некорректное значение с точки зрения вызова PP(p) переменной p присвое-
но быть не может).
По своей природе реализованные в нынешних общедоступных системах
программирования процедурные значения являются ссылочными — ника-
кой переписи программного кода при присваивании таких значений не про-
исходит. Поэтому параметр-процедура и параметр-функция передаются как
значения, а присваивание соответствующим формальным параметрам этих
значений оказывается присваиванием ссылок.
С процедурными типами расширений Pascal связан ряд технических огра-
ничений, которые стоит признать достаточно разумными, поскольку они по-
зволяют до некоторой степени отгородиться от теоретических опасностей, о
которых, судя по всему, авторы системы умом даже не подозревали, но кото-
рые они нутром чуяли.
Для программирования наиболее существенными из них являются:

• Недопустимость использования в качестве значений процедурного ти-


па стандартных процедур и функций. Чисто прагматически это ограни-
чение связано с другим форматом библиотек, но с теоретической точки
8.5. ПАРАМЕТРИЗАЦИЯ 445

зрения оно также целесообразно, поскольку исходные процедуры про-


граммируются совершенно другим способом, с использованием осо-
бенностой конкретного процессора и конкретной реализации. Обойти
данное ограничение легко, построив свою процедуру или функцию с
телом из единственного оператора, вызывающего требуемую программ-
ную единицу.
• Процедуры и функции, которые могут подставляться в качестве фак-
тических параметров для формальных параметров процедурного типа,
должны быть оттранслированы в самом общем режиме (использование
так называемой дальней модели вызова подпрограмм, которое задает-
ся с помощью специального прагматического указания для компилято-
ра). Это ограничение прагматическое, оно существенно прежде всего
для Turbo Pascal, поскольку в Delphi такой режим трансляции процедур
стал стандартным по умолчанию, но оно также подкрепляется теоре-
тическими рассмотрениями.
• Процедуры и функции, используемые в качестве значений процедурно-
го типа, не могут быть вложенными, т. е. не могут находиться внутри
других подпрограмм (здесь создатели системы обошли серьезнейшую
теоретическую ловушку).
• Процедуры и функции, используемые в качестве значений процедурно-
го типа, не могут иметь прямых обращений к средствам операционной
системы (и здесь никакое решение не было бы теоретически удовле-
творительным, а практически удовлетворительные ослабления данно-
го ограничения пока неизвестны).
Задания для самопроверки
1. Самостоятельно модифицировать алгоритм ввода матриц, предусма-
тривая возможность ошибочного ввода.
2. Продумать другие варианты представлений очередей и алгоритмы функ-
ций и процедур для работы с ними (например, кольцевой буфер).
3. Почему на стр. 388 говорится в связи с препроцессором не о программе,
а о программном тексте?
4. Что будет, если из локального контекста функции MyInpMatr (см. про-
грамму 8.3.4) удалить описание переменной N?
446 ГЛАВА 8. ПОДПРОГРАММЫ

5. Нет никаких реализационных препятствий для обращения к формаль-


ному параметру по его вычисляемому порядковому номеру. Подумайте,
почему в хорошо определенных языках это средство не встречается, а,
скажем, в C, где оно осталось как рудимент, им во всех методиках ре-
шительно рекомендуют не пользоваться?
6. Как Вы думаете, каковы достоинства и недостатки идентификации па-
раметров их номерами?
7. Привести пример, когда при параллельных вычислениях передача па-
раметра-переменной и inout-параметризация приводят к разным ре-
зультатам.
8. Один из авторов книги в студенческие годы попал в ловушку, стоив-
шую ему лишней недели работы над программой. Одна из стандарт-
ных программ системы Algol-60 принимала параметр по имени, а ав-
тор подставил вместо него 0. В результате в разных местах программы
началась мистика: константа 0 получила другое значение!
Как вы думаете, должен ли транслятор извещать программиста о такой
ошибке? Если да, то какой алгоритм ее обнаружения Вы предложите?
9. На стр. 428 обсуждалсялокальный контекст процедуры MyOwnInpMatr
и делался вывод о нежелательности вынесения некоторых переменных
в глобальный контекст. Перепроверьте эти рассуждения с помощью по-
нятия операционной обстановки и операционного контекста.
10. Почему не стоит исключать возрождения аппарата параметров-имен?
Для каких языков он привлекателен?
11. Какие эффекты возникают при спецификации параметра как
var N : by need?

12. Эквивалентны ли спецификации

и
name N : by need

name N ?
13. Сопоставтьте параметры-функции с параметрами, ппередаваемыми по
наименованию. Приведите примеры одинаковых и различных эффек-
тов.
8.6. МОДУЛЬНОСТЬ TURBO PASCAL 447

РАЗВИТИЕ ЯЗЫКОВЫХ СРЕДСТВ МОДУЛЬНОСТИ В ЯЗЫКАХ ЛИ-


НИИ TURBO PASCAL
§ 8.6.

Понятие модульности является одним из центральных в программирова-


нии, и мы неоднократно обсуждали и будем обсуждать его различные аспек-
ты. Этот параграф целиком посвящен развитию идеи модуляризации. В ка-
честве базы мы выбрали язык Pascal и его диалектов и прямых потомков:
Turbo Pascal и Object Pascal. Они ярко демонстрируют историческую транс-
формацию и отнюдь не самое худшее воплощение этой идеи. Конечно, один
пример не в состоянии продемонстрировать все грани модуляризации. Но
он дает достаточно наглядное и убедительное представление данного вопро-
са. Выбор иного языка, к примеру С/С++, был бы не столь показательным,
поскольку его линия развития находится под еще большим давлением при-
митивно понимаемой прагматики, что стабильно приводит к стратегически
худшим решениям и весьма затушевывает картину. Что же касается мето-
дических положений, развиваемых ниже, и в частности, выбора проектных
решений, то, если оставаться в рамках традиционной модели вычислений,
этот материал совсем не зависит от языка программирования.
8.6.1. Поддержка модульности в стандартном языке Pascal

Ближайшая задача, решаемая ниже, — развитие примера работы с очере-


дями из § 8.1 в направлении реализации операционных подпрограмм некото-
рой специальной библиотеки. Требуется составить следующие подпрограм-
мы:
• StateQueue — функция, вырабатывающая значение состояния очере-
ди: Empty (пустая), Full (заполненная) или Normal (нормальная), в за-
висимости от наличия сохраняемых элементов;

• GetElQueue — функция, вырабатывающая значение элемента очереди


и извлекающая элемент из очереди, если состояние очереди (значение,
вырабатываемое функцией StateQueue) не Empty, и специальное, вы-
деленное значения типа элемент очереди NotEl в противном случае;

• PutElQueue — процедура, помещающая элемент-параметр в очередь


для хране-ния, если это возможно, т.е. когда состояние очереди (значе-
ние, вырабатываемое функцией StateQueue) не Full. Если очередь за-
полнена, то процедура должна выдавать сообщение об ошибке обра-
щения к ней.
448 ГЛАВА 8. ПОДПРОГРАММЫ

Прежде, чем приступить к реализации подпрограмм необходимо договорить-


ся о том, для хранения каких элементов будет использоваться библиотека.
Ограничиваясь иллюстративными целями, будем считать, что очередь пред-
назначена для хранения целочисленных значений, а значение NotEl равно ну-
лю.
Существует довольно много вариантов реализации очередей, приспосо-
бленных для использования в различных режимах. Среди режимов, опреде-
ляющих выбор конкретного решения, можно указать, с одной стороны, на
дисциплину доступа к данным, когда чередование записи и чтения информа-
ции более или менее равномерно, а с другой — на работу большими порци-
ями, при которой чередуются периоды с относительным преобладанием за-
писи с периодами относительно более частого чтения. В первом случае при
достаточно большом объеме памяти, отводимой для хранения элементов, си-
туации с переполнением очереди являются исключительными, во втором —
необходима особая забота об обработке переполнений. На выбор варианта
реализации очереди влияет и то, предусматривается или нет режим работы
с приоритетами: требуется ли обеспечить возможность пропускания элемен-
та “вперед”. Список подобных особенностей использования очередей легко
продолжить. Зачастую трудно заранее предвидеть поведение вычислитель-
ного процесса, а значит и оптимальную реализацию очереди. Именно поэто-
му весьма желательно явно разделять уровни использования и реализации:
если реализация окажется неудачной, смена ее не повлечет изменение ис-
пользующей программы.
В приводимой программе используется следующий подход к реализации
очередей. Хранилище элементов представляется массивом Container объема
MaxSize. Для указания компоненты массива, в которой при очередном обра-
щении к процедуре PutElQueue разместится элемент очереди, используется
переменнаяиндекс IB. Элемент, который должен быть извлечен из Container’а
при обращении к функции GetElQueue, указывается переменной-индексом
IE. Три структуры данных: Container, IB и IE являются основой реализацион-
ного представления очереди.
Все хранимые элементы очереди размещаются между компонентами, ин-
дексируемыми IB и IE (см. рис. 8.9a). Запись элемента влечет увеличение ин-
декса IB, а чтение — IE. Если после чтения элемента IE оказывается равным
IB, то это означает, что все элементы очереди прочитаны (состояние очере-
ди — Empty). Когда в результате записи элемента значения индекса IB дости-
гает своего максимума (MaxSize), то за счет произошедших ранее чтений из
очереди в массиве Container может оказаться свободное место в его начале,
8.6. МОДУЛЬНОСТЬ TURBO PASCAL 449

Свободные участки
Занятый участок

Container

IE

IB

(а) Конфигурация с одним занятым и двумя свободными участками

Занятые участки
Свободный участок

Container

IE

IB

(б) Конфигурация с одним свободным и двумя занятыми участками

Рис. 8.9. Представление очереди массивом и двумя индексами


450 ГЛАВА 8. ПОДПРОГРАММЫ

которое целесообразно переиспользовать. Это достигается сбрасыванием IB


в минимально возможное значение (в примере: IB := 1). Теперь заполнение
очереди возможно до тех пор, пока IB не станет равным IE (см. рис. 8.9б), и,
таким образом, состояние заполненности очереди (Full) есть равенство ин-
дексов IB и IE после записи элемента.
При чтении очередного элемента возможно, что IE указывает на послед-
ний элемент массива. Это означает, что после чтения он должен быть уста-
новлен на начало Container’а. В результате конфигурация занятости массива
снова принимает вид, изображенный на рис. 8.9а.
Нагляднее всего описанный процесс можно представить при помощи изо-
бражения массива Container в виде кольца, т. е. склеивая его начало с кон-
цом, благодаря чему особые случаи работы с индексами исключаются (см.
рис. 8.10). Следует обратить внимание на равенство IE = IB, которое дости-
IE
Container

IB

Рис. 8.10. Наглядное представление очереди как массива-кольца

гается в двух крайних случаях: когда очередь опустошается и когда она ста-
новится заполненной. Это равенство говорит, что очередь находится в одном
из состояний: Empty или Full. Какое из них фактически установилось, ясно,
если проанализировать, какая из операций GetElQueue или PutElQueue вы-
полнялась последней. Информация об этом сохраняется в переменных BFull
и BEmpty логического типа, которые должны рассматриваться в качестве со-
ставляющих представления очереди23 .
Заметим, что можно было не вводить две этих переменных, а вызывать соответствующую
реакцию на крайнее состояние в конце каждой из подпрограмм GetElQueue и PutElQueue.
23

Такое использование структурных переходов и реакций формально не противоречит струк-


турному программированию, но затрудняет модификацию программы.
8.6. МОДУЛЬНОСТЬ TURBO PASCAL 451

С учетом приведенного обсуждения реализация набора операционных


подпрограмм работы с очередью в их общем контексте на стандартном языке
Pascal описывается следующим образом. Кроме упомянутых выше подпро-
грамм в наборе представлена еще две процедуры. Первая из них — InitQueue —
задает начальные значения переменных, а вторая — IncI — предназначена
для изменения значения индексов с учетом сбрасывания (изменяемый ин-
декс передается в качестве параметра).

Программа 8.6.1 Очередь на чистом языке Pascal

const
MaxSize = 100; { public !!!}
NotEl = 0; { public !!!}

type
States = ( Full, Empty, Normal ); { public !!!}

var
Container : array [ 1..MaxSize ] of Integer; { private }
IB, IE : Integer; { private }
BFull, BEmpty : Boolean; { private }
procedure InitQueue; { public }
begin
IB := 1; IE := 1;
BFull := False; BEmpty := True;
end;

procedure IncI ( var I : Integer ); { private }


begin
if I = MaxSize
then I := 1
else I := I + 1
end;

procedure PutElQueue ( X : Integer ); { public }


begin
if not BFull
then
452 ГЛАВА 8. ПОДПРОГРАММЫ

begin
Container [ IB ] := X;
IncI ( IB );
BEmpty := False;
BFull := IB = IE

else Write (’Ошибка: переполнение’)


end

end;

function GetElQueue : Integer; { public }


begin
if not BEmpty
then
begin
GetElQueue := Container [ IE ];
IncI ( IE );
BEmpty := IB = IE;
BFull := False
end
else

Write (’Ошибка: чтение из пустой очереди’);


begin

GetElQueue := NotEl
end
end;

function StateQueue : States; { public }


begin
if BEmpty
then StateQueue := Empty
else if BFull
then StateQueue := Full
else StateQueue := Normal
end;

Как можно воспользоваться представленными только что средствами? Для


стандартного Pascal’я нет другой возможности, кроме размещения приве-
денного текста среди описаний использующей программы. Конечно, систе-
8.6. МОДУЛЬНОСТЬ TURBO PASCAL 453

мы программирования обычно позволяют строить пользовательские библио-


теки, и если это так, то в использующую программу достаточно встроить
заголовки процедур, а вместо тел их указать служебное слово external. Но
и тогда пользователь стандартного Pascal’я не сможет применять у себя в
программе непроцедурные описания (в приведенном тексте они помечены
символами !!!). Все это — следствие первоначальной концепции модульно-
сти, исходящей из определения библиотек как разрозненных наборов под-
программ.
Использование данного набора подпрограмм полностью автономно: оно
не требует знания того, какими структурами данных представлена очередь,
какие алгоритмы задействованы в ее реализации и т. д. Тем не менее, если
представленный выше текст встраивается в программу явно, то стандарт-
ный Pascal не запрещает обращаться в программе к данным и подпрограм-
мам, не предназначенным для общего использования. В приведенном тексте
строки, где описываются такие объекты, отмечены комментарием { private },
а строки, в которых вводятся объекты общего пользования — комментарием

Стандартный Pascal не в состоянии запретить обращение к реализации,


{ public }.

а значит, не поддерживается скрытие ее особенностей на уровне использо-


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

8.6.2. Модули в TURBO-системах программирования

Недостатки стандартного Pascal’я и других языков, являющихся прямы-


ми потомками Algol 60, в части модульности вполне очевидны: жесткая блоч-
ная структура препятствует явному выделению частей программы, предна-
значенных и не предназначенных для пользователя. В более поздних языках
вводятся специальные средства модуляризации. Ниже они рассматриваются
454 ГЛАВА 8. ПОДПРОГРАММЫ

для диалектов Pascal’я в системах программирования TURBO. Вполне сло-


жившимися эти средства можно считать для TURBO-Pascal’я версий 5.0 и
выше. С 5.5 версии язык включает в себя средства поддержки объектно-ори-
ентированного программирования, которое в настоящее время является наи-
более распространенным инструментом профессионального конструирова-
ния программ. В последующих версиях эти средства дополнены многими
полезными в прагматическом плане возможностями.
Ближайшая цель — показать, как можно разрозненную коллекцию опера-
ционных подпрограмм и их общего контекста превратить в модуль с четким
разделением уровней использования и реализации. Обсуждение ведется при-
менительно к языку TURBO-Pascal версии 6.0. Указываются дополнитель-
ные возможности, предоставляемые в версии 7.0.
Общая структура конструкции модуля в этих языках представляется сле-
дующей схемой:

unit <имя модуля>;

<описания видимых объектов>


interface

<описания скрытых объектов>


implementation

<инициализация: описание действий, которые


begin

необходимо выполнить перед использованием


основных средств модуля>
end.
В версии 7.0 эта схема, исходя из практических потребностей программи-
рования, расширена: появился дополнительный раздел модуля finalization:

unit <имя модуля>;

<описания видимых объектов>


interface

<описания скрытых объектов>


implementation

<инициализация: описание действий, которые


begin

необходимо выполнить перед использованием


основных средств модуля>
8.6. МОДУЛЬНОСТЬ TURBO PASCAL 455

<завершение: описание действий, которые


finalization

необходимо выполнить после использования


модуля>
end.

Имя модуля — это идентификатор, упоминание которого в специальном


разделе использующей программы, называемом uses, делает доступным ви-
димые объекты модуля.
Описания видимых объектов — это обычные описания констант, типов,
переменных и др., которые предоставляются пользователю. Их называют пре-
доставляемый контекст. Особо следует сказать об описании подпрограмм в
данном разделе. Поскольку от пользователя должны быть скрыты тела под-
программ, здесь перечисляются лишь их заголовки (в обычном синтаксисе
языка Pascal).
Помимо видимой части локального контекста модуля, он содержит неви-
димую — описания скрытых объектов. В разделе implementation заголовки
процедур и функций можно задавать либо сокращенно: служебным словом
procedure или function и именем подпрограммы (с учетом того, что полный
заголовок объявлен выше), либо в полной (обычной для Pascal’я) форме, со-
впадающей с заголовком подпрограммы из раздела interface.
Раздел инициализации описывает действия, которые необходимо выпол-
нить перед употреблением средств модуля в программе пользователя. Они
автоматически вызываются в программе перед выполнением всех ее опера-
торов. Раздел finalization в языке появился в ответ на потребность автомати-
ческого выполнения действий после работы модуля (например, отказ от за-
резервированной памяти). Если эти разделы не предусматривать, то на поль-
зовательский уровень придется выносить средства, которые для его логики
являются инородными.
На примере раздела инициализации мы имеем возможность сопоставить
варианты библиотеки с автоматически вызываемой инициализацией и без
нее, когда соответствующие действия пользователь должен указывать явно. В
варианте без инициализации пользователь должен вызвать процедуру InitQueue,
задающую начальные установки для работы с модулем. В альтернативном
варианте этот вызов можно скрыть путем выполнения его в разделе инициа-
лизации и исключения InitQueue из интерфейса.
Таким образом, средства работы с очередью оформляются в виде следу-
ющего модуля:
456 ГЛАВА 8. ПОДПРОГРАММЫ

Программа 8.6.2

unit IntQueue;

interface
const MaxSize = 100;
NotEl = 0;
type States = ( Full, Empty, Normal );
procedure PutElQueue ( X : Integer );
function GetElQueue : Integer;
function StateQueue : States;

implementation
var
Container : array [ 1..MaxSize ] of Integer;
IB, IE : Integer;
BFull, BEmpty : Boolean;

{ реализация процедур }
{ и функций }
procedure InitQueue;
...
procedure IncI ( var I : Integer ); { InitQueue, }
... { IncI, }

{ GetElQueue и }
procedure PutElQueue (X : Integer); { PutElQueue, }
...

{ в точности повторяет }
function GetElQueue : Integer; { StateQueue }

{ их тексты на }
...

{ стандартном Pascal’е }
function StateQueue : States;
...
begin
InitQueue;
end.

Пример реализации очереди демонстрирует стиль модуляризации в TURBO


Pascal’е, которой не слишком далеко ушел от стиля, достижимого в стандарт-
ном языке: появилась возможность не знать, как реализована используемая
очередь. Тем не менее, модуль IntQueue с потребительской точки зрения не
очень удобен: во-первых, он не позволяет работать с несколькими очередями,
а во-вторых, элементами очереди могут быть только целые числа.
8.6. МОДУЛЬНОСТЬ TURBO PASCAL 457

Первый недостаток может быть преодолен за счет специального програм-


мирования инициализации очереди, вынесения процедуры InitQueue в ин-
терфейсную часть модуля и дополнения его процедурой DoneQueue, уничто-
жающей очередь по окончании работы с нею. Следует позаботиться об иден-
тификации очередей, что в стиле стандарта языка Pascal естественнее всего
реализуется через дополнительный параметр каждой из подпрограмм. В ре-
ализации процедур InitQueue и DoneQueue целесообразно предусмотреть,
соответственно, порождение и ликвидацию массива Container и других пред-
ставляющих структур данных для каждого экземпляра очереди.
Второй недостаток при использовании средств модуляризации, предста-
вленных выше, и других возможностей стандартного Pascal’я преодолеть не
удается. В языках с так называемыми абстрактными типами данных (CLU,
Alphard и др.), а также в языке Ada, можно было бы воспользоваться параме-
трами-типами, заменив тип компонент массива Container и других структур,
принимающих значения хранимых в очереди элементов, именем формаль-
ного параметра, вместо которого при вызове модуля подставляются имена
конкретных типов. В TURBO Pascal’е вместо этого предусмотрены механиз-
мы наследования и полиморфизма, которые являются одним из ключевых
моментов объектно-ориентированного программирования.
Сделаем еще три замечания, характеризующих некоторые тонкости мо-
дульного программирования.
Во всех модульных расширениях языка Pascal возникает общая пробле-
ма конфликта имен. Если один и тот же идентификатор описан в нескольких
модулях, используемых программой, то транслятор должен разрешать кон-
фликт имен. Конечно же, есть соглашение по умолчанию, какое по порядку
описание считается главным, но им мы решительно не рекомендуем пользо-
ваться. Любое имя, встретившееся в модуле, может быть однозначно специ-
фицировано как

ThisUnit.Identifier

Этим лучше всего пользоваться, а для сокращения записи можно применить


конструкцию

with ThisUnit do begin . . . end;

В программе или модуле видимы лишь те имена, которые определены в


модулях, перечисленных в соответствующем предложении uses. Никакого
транзитивного замыкания простанства имен не производится.
458 ГЛАВА 8. ПОДПРОГРАММЫ

Если два модуля разрабатываются совместно, то каждый из них может


имет в своем операторе uses ссылку на другой. Это не считается ошибкой,
лишь если открытая структура зависимостей модулей остается сетью (гра-
фом без циклов). Поэтому целесообразно обе перекрестные ссылки упрятать
в раздел implementation.
Если обратиться к задаче спецификации предоставляемых модулем средств,
то придется констатировать, что возможности TURBO Pascal’я версии 5.0 в
этом отношении очень ограничены. Нет никакой поддержки независимого от
реализации описания и, как следствие, документация оказывается отделена
от программы. Впрочем, этим же недостатком страдают и все последующие
языки линии TURBO Pascal и Object Pascal. Единственное, что они могут
предложить — это рецепт использовать мнемоничные имена. А этого явно
недостаточно. Из нашего примера видно, что если б мы реализовывали не
очередь, а стек, то интерфейсная часть модуля отличалась бы только имена-
ми. Чтобы можно было бы увидеть, что именно реализовано в модуле, про-
граммист должен позаботиться о соответствующем описании дополнитель-
но. Здесь и ему, и пользователю его модуля придется обратиться к призра-
кам-переменным, призракам-процедурам или к иным призракам составляе-
мой программы.

8.6.3. Пример использования модуля

В качестве иллюстрации использования построенного абстрактного типа


данных приводится задача, требующая реализации линии задержки — спе-
циального приема программирования, основанного на накоплении порций
информации из потока для последующей их совместной обработки.
Требуется смоделировать поведение кота-рыболова, который знает, сколь-
ко грамм рыбы MaxW он может съесть за один день, при этом не позволяя
себе оставлять частично съеденную рыбину.
Пусть у кота есть последовательность садков для хранения рыбы. Стра-
тегия кота состоит в следующем. Если очередная поступившая рыба может
быть съедена в тот же день (суммарный вес уже накопленных в текущем сад-
ке и очередной не превосходит MaxW), то она помещается в текущий садок,
и тем самым величина веса рыб текущего садка CurW увеличивается. При
этом, если суммарный вес рыбы становится равным MaxW, то садок счита-
ется заполненным и далее рыбы размещаются в следующем садке. Когда сум-
марный вес рыб превышает норму, возможно, что в садке есть рыба, замена
которой на очередную позволит уложиться в норматив MaxW. В этом случае
8.6. МОДУЛЬНОСТЬ TURBO PASCAL 459

принимается решение об обмене рыб. Если же это невозможно, то садок счи-


тается загруженным (лучше недоесть, чем что-то оставить). В обоих случаях
одна из рыб — текущая или обмененная — помещается в следующий садок.
Для простоты считается, что обмен на две или более уже пойманных рыб
превосходит интеллектуальные силы кота. По исчерпании поступления рыб
последний из садков может оказаться неполным. Требуется напечатать по-
следовательность весов рыб, размещаемых в садках, и суммарные веса рыб
в каждом садке.
Задача решается путем ‘перекачки’ поступающей рыбы в очередь с одно-
временным накоплением суммарного веса. Как только последний превышает
норму, организуется очистка очереди, в ходе которой ищутся рыбы, способ-
ные заменить последний экземпляр из поступивших с улучшением показа-
теля насыщения. Найденные таким образом экземпляры обмениваются на
последний поступивший и начинают играть роль последнего поступившего.
При этом возможно выполнение серии обменов. В программе используют-
ся вспомогательные процедуры: InpW, организующая ввод входного потока
с текущим контролем, ClearQueue, очищающая очередь и распечатывающая
ее содержимое, и InpHandle, выполняющая все действия, связанные с обра-
боткой очередного входного числа.
Решение опирается на использование модуля IntQueue, т. е. демонстриру-
ются возможности модульной декомпозиции на базе языка Turbo Pascal 5.5,
а не более развитых версий. Это сделано по следующим причинам:
• Достаточность средств модуля для данной задачи, для которой нет не-
обходимости использовать несколько очередей, очереди с нецелочи-
сленными или разного типа элементами. Как следствие, прибегать, к
примеру, к объектно-ориентированным средствам особой нужды нет.

• Достаточность средств модуля для демонстрации возможностей средств


модуляризации всех языков линии Turbo Pascal. Как можно было уви-
деть из предыдущего обсуждения, эти средства у более развитых язы-
ков базируются на том же, что предлагалось в Turbo Pascal 5.5. Допол-
нительные средства принципиально картину не меняют.

• Учебными целевыми установками. Здесь имеется ввиду возможность


использования решения начинающими программистами, для которых
более развитые средства программирования целесообразно изучать по
мере освоения базовых средств программирования.
В программе используются вспомогательные процедуры: InpW, организую-
460 ГЛАВА 8. ПОДПРОГРАММЫ

щая ввод входного потока с текущим контролем, ClearQueue, очищающая


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

Программа 8.6.3

program WWW;

uses Crt, IntQueue;

const MaxW = 15;


var InpEl, CurW : Integer;

procedure InpW;
begin
repeat

Writeln ( ’Ошибка: слишком большой вес.’,


Read ( InpEl );

’ Повторите ввод’);
until InpEl <= MaxW
end;

procedure ClearQueue;
begin
while StateQueue <> Empty do
Write ( GetElQueue, ’ ’ );
end;

procedure InpHandle;{ Обрабатывает InpEl, корректирует CurW }


var S, Sn, R : Integer;
begin
S := CurW + InpEl;
if S < MaxW
then begin CurW := S;
PutElQueue ( InpEl );
end
else
if S = MaxW
8.6. МОДУЛЬНОСТЬ TURBO PASCAL 461

then
begin
ClearQueue;
Write ( InpEl, ’ ’);
Write ( ’(’, S, ’) ’);
CurW := 0;

else { S > MaxW - возможен обмен }


end

begin
while StateQueue <> Empty do begin
R := GetElQueue;
Sn:= S - R;

then { Отказ от обмена }


if ( Sn > MaxW ) or ( Sn <= CurW )

Write ( R, ’ ’ )
else begin

if Sn = MaxW then { Оптимизация }


Write ( InpEl, ’ ’ );

{ Завершение обмена }
ClearQueue;

InpEl := R;
CurW := Sn
end
end;
Write ( ’(’, CurW, ’) ’);
CurW := InpEl;
PutElQueue ( InpEl )
end
end;

begin

Writeln (’Поток входных значений:’);


CurW := 0;

while not eoln do


begin
InpW; InpHandle; end;
ClearQueue;
Writeln ( ’(’, CurW, ’) ’ );
462 ГЛАВА 8. ПОДПРОГРАММЫ

repeat until KeyPressed


end.

Решение данной задачи, вообще говоря, может базироваться не только на ис-


пользовании IntQueue. Так, для задержки поступающих элементов вполне
правомерна стековая дисциплина хранения данных. Чтобы реализовать эту
стратегию, можно чисто механически заменить в программе WWW вызовы
подпрограмм модуля IntQueue на вызовы подпрограмм подходящего моду-
ля работы со стеком (разумеется, со сменой используемого модуля). Новое
решение будет отличаться от приведенного лишь порядком установки кон-
тейнеров (съедения рыб котом). Читателю настоятельно рекомендуется убе-
диться в этом.
Анализ предложенного решения и сопоставление его с только что упомя-
нутым решением показывает, что сам по себе факт использования очереди
или стека не влияет на эффективность программы. Уровень эффективности
будет зависеть от качества реализаций этих двух структур данных. Как оче-
редь, так и стек обеспечивают правильную дисциплину обращения к памяти
для организации поиска кандидатов на обмен, что, собственно говоря, и есть
причина использования линии задержки в данной программе. Выбор между
стеком и очередью для данной задачи вполне произволен. Следует однако за-
метить, что очередь позволяет ‘почти’ сохранять в выводе порядок, в котором
вводятся элементы данных, тогда как стек делает его ‘почти’ произвольным.
Может быть, это обстоятельство склонит программиста в пользу очереди?
С точки зрения демонстрации модуляризации уместно заметить, что сме-
на одного модуля (IntQueue) на другой (работа со стеком или с очередью,
базирующейся на списковой организации) практически не приводит к из-
менению алгоритма. С точностью до имен процедур решение сохраняется.
Но это достигнуто благодаря тому, что модулем IntQueue на уровень исполь-
зования вынесены только необходимые возможности, никак не завязанные
на реализацию. Поддержка разделения контекстов языком программирова-
ния обеспечила защиту от употребления реализационно-зависимых средств.
В свою очередь, именно это обстоятельство позволяет провести анализ раз-
личных вариантов организации памяти.

Задания для самопроверки

1. В случае, когда у нас заранее известное количество очередей, можно


эффективно организовать работу, воспользовавшись секциями initialization
8.7. РЕКУРСИВНЫЕ ПРОГРАММЫ 463

и finalization модуля. Проделайте это для случая, когда в программе


всего одна очередь.
2. Реализовать стратегию кота, механически заменив в программе WWW
использование модуля IntQueue на использование некоторого модуля
для работы со стеком и вызовы подпрограмм модуля IntQueue на вызо-
вы подпрограмм модуля работы со стеком.
3. Чем новое решение будет отличаться от приведенного в тексте?

§ 8.7. РЕКУРСИВНЫЕ ПРОГРАММЫ

Методы составления рекурсивных алгоритмов занимают заметное место


среди средств, которыми пользуются программисты в своей работе. В то же
время, их освоение требует определенных навыков, которые базируются на
понимании процессов вычислений в целом. Главная цель настоящего раздела
в том, чтобы обеспечить возможность на практике ознакомиться с рекурси-
ей, сопоставить рекурсивность с другими, в некотором смысле родственны-
ми понятиями, в первую очередь, с итеративностью. В соответствии с этой
целью мы не будем здесь обсуждать рекурсию как метод программирования.
Этому посвящена глава в части III.
Если алгоритм построен таким образом, что в ходе его работы возможно
повторение некоторых действий посредством обращения к самому себе (т. е.
обращение к вычислениям, описанным этим алгоритмом), то такой алгоритм
называется рекурсивным.
При изучении этого понятия следует обратить внимание на две стороны
рекурсивности: способы задания рекурсивных алгоритмов (описание их на
языке программирования) и механизм выполнения рекурсивного алгоритма
в ходе работы программы.
Чтобы задать рекурсивный алгоритм, нужно уметь обозначать в его про-
грамме для повторяемых обращений. Пожалуй, единственным языковым сред-
ством такого рода являются операционные подпрограммы: обозначение ал-
горитма — имя процедуры или функции, снабженное списком фактических
параметров — используется при вызове подпрограммы для исполнения. Вы-
зов подпрограммы, помещенный непосредственно в тело подпрограммы (т. е.
в описание алгоритма на языке) приводит к активизации повторного выпол-
нения алгоритма еще до того, как первое выполнение подпрограммы не за-
вершено.
464 ГЛАВА 8. ПОДПРОГРАММЫ

Рассмотрим следующий пример. Пусть требуется вычислить N ! для не-


больших значений N . Определение этой функции как произведения

1 ∗ 2 ∗ ··· ∗ N

приводит к почти однозначному описанию требуемого алгоритма в виде ци-


кла — итеративная схема:
F := 1;
for I := 2 to N do F := F * I;
C*

F = 1;
for (int i = 2; i<= N; i++) F = F*I;

Если же N ! определяется рекуррентным соотношением:

1! = 1;
N ! = (N − 1)! ∗ N, при N > 1,

то для такого определения итеративная схема не очень естественна. Более


согласуется с ним следующее описание функции, которое называется рекур-
сивным алгоритмом, или, точнее, алгоритмом, построенным по рекурсивной
схеме:

Программа 8.7.1

function FACT ( N : Integer ) : Integer;


begin
if N < 2
then FACT := 1
else FACT := FACT ( N - 1 ) * N
end;
C

int FACT ( int N )


if (N<2) return 1;
else return ( FACT (N-1)*N);
}
8.7. РЕКУРСИВНЫЕ ПРОГРАММЫ 465

(Здесь для простоты функция N ! при неположительном целом аргументе до-


определена как единица).
Выполнение рекурсивных программ проще всего пояснить, отслеживая
процесс вычислений и сопоставляя его с процессом вычислений по итера-
тивной схеме. В рассматриваемом примере сопоставляются два способа вы-
числения N ! при N , равном четырем.
По итеративной схеме произведение 1 ∗ 2 ∗ 3 ∗ 4 вычисляется путем на-
капливания его в переменной F. F инициализируется единицей, затем при I,
равном 2, 3, и 4, увеличивается, соответственно, в 2, 3, и 4 раза (после выпол-
нения оператора F := F * I), и, как следствие, становится равной 2, 6 и 24. I,
равное 4 — последнее значение, при котором перевычисляется F, и именно
при этом значении I формируется последнее F (равное 24), которое является
результатом выполнения итеративной программы.
По рекурсивной схеме 4! вычисляется как произведение 3! на 4. Если 3!
вычислено, то 4! есть результат умножения этого значения на 4. Таким обра-
зом, чтобы подсчитать 4! по рекурсивной схеме, надо сначала вычислить 3!,
что требует вычисления 2!, для которого требуется предварительно вычи-
слить 1!. Последнее, согласно алгоритму, есть 1, т. к. N , равное 1, меньше,
чем 2. 2! есть 1! (уже вычисленное и равное 1), умноженное на 2. 3! — это
произведение 2 ∗ 3, где 2 = 2! = (3 − 1)!, а 3 — величина N , использованная
при вызове алгоритма для нахождения 3!. Наконец, коль скоро 3! получено,
можно продолжить вычисление 4! как 3! ∗ N при N = 4.

8.7.1. Сопоставление итеративной и рекурсивной схем

Главная цель сопоставления двух схем программирования заключается в


решении вопроса о том, когда в реальных программах использовать ту или
иную из них. Можно показать, что любой итеративный алгоритм допускает
трансформацию в рекурсивный и наоборот с сохранением вычислительной
эквивалентности. Любое математическое утверждение об эквивалентности
или об изоморфизме структур не следует понимать буквально: будто бы вы-
бор схемы или структуры данных непринципиален. Как правило, трансфор-
мации, предоставляемые доказательством эквивалентности, могут оказаться
сложными, к тому же они никак не учитывают заботы о модифицируемости
и чаще всего абстрагируются от вопросов сложности вычислений. Поэтому
для практического программирования весьма важно знать теоретические ре-
зультаты об эквивалентности либо изоморфности, но интерпретировать их
466 ГЛАВА 8. ПОДПРОГРАММЫ

сто̀ит как наличие нескольких альтернативных структур, выбор одной из ко-


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

Буквальное следование рекуррентному определению почти все-


гда таит в себе опасность повторного счета и фантастической по-
тери эффективности вычислений.

Следующие примеры демонстрируют такую опасность.


Пример 8.7.1. Пусть требуется вычислить N -ое число Фибоначчи, опреде-
ляемое соотношением:

Φ(1) = Φ(2) = 1;
Φ(N ) = Φ(N − 1) + Φ(N − 2), при N > 1.

Функцию
8.7. РЕКУРСИВНЫЕ ПРОГРАММЫ 467

Программа 8.7.2
function F ( N : Integer ) : Integer;
begin
if N < 2
then F := 1
else F := F ( N - 1 ) + F ( N - 2 )
end;
заданную в точном соответствии с рекуррентным соотношением, нельзя при-
знать удовлетворительной.
В самом деле, вычисление F(5) требует вычислить F(4) и F(3), которые
затем складываются. Каждое из них есть вызов F без передачи какой-либо
информации об истории вычислений. В результате F(3) считается дважды:
сначала в рамках вычисления F(4), т. е. в первом аргументе выражения F ( N
- 1 ) + F ( N - 2 ), а затем повторно в ходе вычисления F(5) при вычислении
второго аргумента этого же выражения. В свою очередь, F(2) вычисляется
уже три раза: дважды при каждом вызове F(3) и еще один раз в ходе вы-
числения F(4). Здесь повторный счет возник в связи с тем, что алгоритм не
предусматривает сохранения информации о ранее вычисленных значениях
F(K), 1 6 K < N .
Таким образом, вычисление чисел Фибоначчи, буквально следующее опре-
деляющему рекуррентному соотношению, приводит к неэффективности. Для
обучаемых рекомендуется выполнить упражнения 1–4.
Конец примера 8.7.1.
На примере функции F легко видеть, что в комплекте локальных данных под-
программы должна запоминаться точка возврата: в зависимости от того, вы-
звана функция как первый или второй операнд сложения, требуется обеспе-
чить тот или иной корректный возврат при завершении экземпляра.
Пример 8.7.2. Вычисление наибольшего общего делителя (НОД) двух нео-
трицательных чисел. НОД(x, y) определяется как максимальное целое, ко-
торое делит без остатка x и y. Алгоритм основан на следующих свойствах
функции НОД:

НОД(x, x) = x;
НОД(x, y) = НОД(x, y − x), если y > x.
468 ГЛАВА 8. ПОДПРОГРАММЫ

Программа 8.7.3
procedure MCD (x,y : Integer 0..MAXINT; var R : Integer);
begin
if x = y
then R := x
else if x > y
then MCD ( y, x, R ) {*}
else MCD ( x, y - x, R )
end;

void MCD ( int x, y, *int R) {


if (x==y) &R = x;
else if (x>y) MCD (y, x, &R);
else MCD (x, y - x, &R);
}

Замечания к процедуре MCD:


1. Приведенный алгоритм содержит ошибку: при вызове процедуры с пер-
вым или вторым параметром, равным нулю, произойдет ‘бесконечное’
порождение экземпляров (почему?). Обучаемым предлагается испра-
вить программу, например, с учетом того, что для любого x НОД(0, x) =
НОД(x, 0) = x.
2. Программа может быть улучшена за счет замены оператора MCD (y,x,R)
в строке { * } на MCD (y,x - y,R). Предлагается определить отличие двух
вариантов вычислений.
3. Для углубления понимания механизма рекурсии полезно проследить,
какие изменения процесса вычислений вызовет объявление x и y как
параметров-переменных.
4. Как вы думаете, почему в этой программе, внешне схожей с процедурой
вычисления чисел Фибоначчи, не происходит повторный счет?
Легко показать, что если некоторая функция для всех положительных целых
x и y обладает указанными свойствами, то она является функцией, вычисля-
ющей наибольший общий делитель двух чисел. Таким образом, с точностью
8.7. РЕКУРСИВНЫЕ ПРОГРАММЫ 469

до ошибки, отмеченной в замечании 1, процедура MCD корректно решает за-


дачу.
Конец примера 8.7.2.

8.7.2. Рекурсия через параметры

Понятие рекурсивной процедуры возникает в связи с осуществимостью


и полезностью применения в практике программирования приема, когда в
ходе вычислений одновременно существует несколько экземпляров одной и
той же программной единицы (см. п. 8.4.3). Как правило, различаются явная
рекурсия — вызов процедуры в ней самой, и взаимная (опосредованная) ре-
курсия — в тексте программы представлена последовательность процедур,
вызывающих друг друга и в конечном итоге вызывающих первую из проце-
дур последовательности.
Отличительной особенностью всех таких случаев рекурсии является то,
что она всегда может быть выявлена путем анализа текстов самих подпро-
грамм. При взаимной рекурсии этот анализ довольно сложен, но, тем не ме-
нее, вполне осуществим (обучаемым предлагается построить соответствую-
щий алгоритм).
В практике программирования встречается еще один способ задания ре-
курсивных алгоритмов, который не всегда может распознаваться на основа-
нии только такой информации. Этот случай связан с заданием рекурсии че-
рез параметры. В языках со строгой типизацией (например, в Pascal, где для
повышения надежности программирования вводится ограничение: в параме-
трах-функциях и параметрах-процедурах не допускаются иные параметры,
кроме параметров, передаваемых по значению) такая рекурсия невозможна.
Но если соответствующие ограничения на параметризацию не накладывают-
ся, появляется возможность задавать процедуры, становящиеся рекурсивны-
ми через параметры.
Пусть, к примеру, описана процедура P с параметром-процедурой Q:
procedure P ( procedure Q );
begin

{ В этой строке с точки зрения языка Pascal содержится }


...

{ошибка, т. к. в определении параметра Q не указано, что им }


Q(R);

{является процедура с параметром }


...
end;
470 ГЛАВА 8. ПОДПРОГРАММЫ

Вызов такой процедуры корректен, если в качестве ее фактического пара-


метра используется процедура с параметром. В противном случае ошибочен
оператор Q(R). Отсутствие в спецификации процедуры P информации о ви-
де возможных параметров параметра-процедуры Q не позволяет контролиро-
вать, какие фактические параметры могут подставляться вместо формальной
процедуры Q. В частности, оказывается допустимым оператор P(S), если в
программе имеется описание
{Как и в описании P,}
{ здесь не специфицированы параметры}
procedure S ( procedure T );

{формальной процедуры T, т. е. с точки}


{ зрения языка Pascal T может вызываться }
{ только без параметров }
begin
...
end;
Если употребление оператора P(S) не приводит к опосредованной рекур-
сии P, то вычисление этого оператора не дает рекурсии. В то же время, сама
процедура P годится для использования в качестве своего фактического па-
раметра. Вызов P(P) формально не является ошибочным (в рамках сделан-
ных предположений, но не с точки зрения Pascal). Однако его вычисление
может привести к выполнению оператора Q(R), где Q — формальный пара-
метр-процедура, и, следовательно, к порождению еще одного экземпляра P
до завершения выполнения предыдущего экземпляра, т. е. к рекурсии P. Та-
ким образом, в одном случае обращение к P является рекурсивным, а в дру-
гом — нет. И ситуации, когда P надо транслировать как рекурсивную, а когда
как нерекурсивную, не распознаваемы путем анализа текстов процедур.
Если процедуры, подобные P, предлагаются в качестве библиотечных, ко-
гда в принципе невозможно предусмотреть заранее, как будут употребляться
подпрограммы, то это может привести к существенной потере эффективно-
сти библиотеки. Следует отметить, что алгоритмы таких подпрограмм труд-
ны для понимания. Все это приводит к тому, что рекурсия через параметры
редко используется в практике программирования. Поэтому зачастую в язы-
ках не разрешается употреблять параметры-подпрограммы. Однако прямо-
линейные запреты средств такого рода затрудняют задание полезных алго-
ритмов, в которых желательно подключение внешних вычислений для вы-
полнения тех или иных действий (классический пример — вычисление опре-
деленных интегралов, когда подынтегральная функция является параметром).
8.7. РЕКУРСИВНЫЕ ПРОГРАММЫ 471

Гибкий путь преодоления недостатков рекурсии через параметры — это бо-


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

procedure PS ( Npar : Integer; procedure T ); { Это не Паскаль! }


...
begin
...
case Npar of
0 : T;
1 : T ( x );
2 : T ( x, y );
end;
...
end; {PS}

в одной программе правомерны операторы PS(0,PR0); PS(1,PR1); и PS(2,PR2);


где PR0, PR1 и PR2 — имена процедур с 0, 1 и 2 параметрами соответ-
ственно. Однако цена такой ‘выразительности’ чрезмерна: из-за нее исчезает
возможность контроля до выполнения программы ошибочных обращений к
процедуре: PS(0,PR1), PS(2,PR1) и др. Существеннее, что эта возможность
исключается и в более обычных ситуациях.
Для разработчиков различных языков цена ограничений, которые надо
вводить на параметризацию с целью повышения надежности, различна. До-
пускаемая ими свобода спецификации параметров колеблется от простого
обозначения и игнорирования возникающих трудностей (как в Алголе 60 и
приведенной иллюстрации) до полного запрета употреблять параметры-под-
программы.
К примеру, способ введения параметров-процедур и параметров-функ-
ций в расширения языка Pascal (см. п. 8.5.5) может привести к рекурсии че-
рез параметры, которая, однако, кажется контролируемой, поскольку соот-
ветствующий программный тип с необходимостью должен быть определен
в описании типов рекурсивно. Пусть, к примеру:

type fff = function( N : Integer; M : fff ) : Real;


472 ГЛАВА 8. ПОДПРОГРАММЫ

Здесь имя типа fff использовано для спецификации формального параметра


M. Допустимые для M фактические параметры в силу определения fff должны
удовлетворять шаблону, введенному при описании fff. В результате функции
типа fff рекурсивны непосредственно или взаимно.
Правомерна, по крайней мере, формально, еще одна возможность, не при-
водящая к рекурсии: когда такая функция имеет фиктивный параметр, т. е. не
используемый в ее теле:

{ Параметр M не используется }
function mm ( X : Integer; M : fff ) : Real;

{ в теле функции mm. }


begin
mm := X - 1
end;

Такая возможность, хотя и кажется избыточной, заслуживает отдельного


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

8.7.3. Пример для самостоятельного анализа

В настоящем разделе приводится пример программы с рекурсивными про-


цедурами, который предлагается для самостоятельного анализа. Программа
предназначена для распечатки всех последовательностей, составляемых из
символов “1”, “2” и “3”, среди которых знаком “+” помечаются удовлетворя-
ющие некотрому условию, которое Вам предстоит выяснить самим.
Для решения задачи выбран рекурсивный алгоритм, но это не означает,
что ее нельзя решить без использования рекурсии.
Программа 8.7.4

program SEQ;

const NN = 100;
K = 5;
Ms = ’3’;
MaxNstr = 23;
var i, N : Integer;
S : packed array [ 1 .. NN ] of Char;
sim: packed array [ 1 .. K ] of Char;
8.7. РЕКУРСИВНЫЕ ПРОГРАММЫ 473

M, Nseq, MaxNseq, Nstr : Integer;

procedure Init;
begin
for i := 1 to N do
S [i] := ’1’;
M := N;
end; { Init }

function Next : Boolean;


begin
if S [ M ] = Ms
then if M > 1
then begin
S [ M ] := ’1’;
M := M - 1; Next := Next; M := M + 1
end
else Next := false
else begin S [ M ] := Succ ( S [ M ] ); Next := true end
end; {Next }

procedure Inpt;
var ch : Char;

Writeln ( ’Введите проверяемую строку’ );


begin

N := 0;
repeat Read ( ch ); N := N + 1; S [ N ] := ch
until ( N >= NN ) or ( ch = #13 );
N := N - 1
end; { Inpt }
function Good1 ( ib, ie : Integer) : Boolean;
var i, l : Integer;
gl : Boolean;
function Eq ( i, j, l : Integer) : Boolean;
var imax : Integer;
r : Boolean;
begin
l := l - 1;
474 ГЛАВА 8. ПОДПРОГРАММЫ

imax := i + l;
r := ( l >= 0 ) and ( imax <= N ) and ( j + l <= N );
while r and ( i <= imax ) do
begin
r := S [i] = S [j];
i := i + 1; j := j + 1;
end;
Eq := r
end; { Eq }
begin
if ib + 1 = ie
then Good1 := TRUE
else if Good1 ( ib, ie - 1 )
then begin
gl := true; l := ( ie - ib ) div 2;
while gl and ( l > 0 ) do
begin
gl := not Eq ( ie-l-l, ie-l, l );
l := l - 1
end;
Good1 := gl
end
else Good1 := false
end; { Good1 }

begin { Главная программа }


Write (’Введите длину последовательностей ’ ); Readln ( N );
Init;
Nstr:= 0; Nseq := 0; MaxNseq := 80 div ( N + 2 );
repeat
if Good1 ( 1, N )
then Write (’+’)
else Write (’ ’);
for i := 1 to N do Write ( S [ i ] ); Write (’ ’);
Nseq := Nseq + 1;
if Nseq >= MaxNseq then begin
Nseq := 0; Nstr := Nstr + 1;
Writeln
8.8. ПРОЦЕДУРЫ В РАЗНЫХ МОДЕЛЯХ ВЫЧИСЛЕНИЙ 475

end;
if Nstr >= MaxNstr then begin Nstr := 0; Readln end
until not Next;
Writeln;
end.

§ 8.8. ПРОЦЕДУРЫ В РАЗНЫХ МОДЕЛЯХ ВЫЧИСЛЕНИЙ

При обсуждении подпрограмм, представленном в настоящей главе, бы-


ли подробно рассмотрены все аспекты работы с ними как с операционными
единицами программы и единицами декомпозиции. В целом это рассмотре-
ние ограничивалось лишь стилем структурного программирования, в рамках
которого, собственно говоря, понятие процедуры и приобрело современное
содержание. Если же взглянуть на это понятие шире, попытаться распростра-
нить его на другие стили, то окажется, что некоторые из рассмотренных вы-
ше положений нуждаются в корректировке. Причина тому очевидна: стили,
относящиеся к другим моделям вычислений, вкладывают свое содержание
как в операционные, так и в декомпозиционные аспекты автономно рассма-
триваемых подпрограмм.
Даже самое близкое к структурному стилю программирование от состоя-
ний приводит к несколько отличному от устоявшегося понятию процедуры.
Этому случаю оказывается противопоказана рекурсия. В самом деле, с точ-
ки зрения модели вычислений программирование от состояний есть конеч-
ный автомат, которому не нужны понятия экземпляров процедур, локального
контекста и др., тогда как для структурного понимания процедуры они необ-
ходимы. То, что это действительно так, подтверждается ранними языками
программирования Fortran IV и Basic, которые в бо̀льшей степени, чем все
их наследники, соответствуют данному стилю. Предписываемые в них меха-
низмы реализации подпрограмм явно указывают на то, что о рекурсивности
здесь речи нет.24 В Basic стиль программирования от состояний поддержива-
ется наиболее последовательно: нет не только рекурсии, но и само понятие
Отсутствие рекурсивности в ранних языках часто объясняют неразвитостью методов
трансляции, необходимостью работать в очень жестких ресурсных ограничениях и другими
24

подобными мотивами. Однако есть контрпример: язык LISP, рекурсивные механизмы кото-
рого органичны для функционального стиля. Это один из самых старых языков програм-
мирования, применявшийся в тех же самых жестких условиях. В этом контексте следует
отметить Algol 60, разработчики которого пусть не во всем, пусть лишь на уровне идей, но
уже понимали, что потребуется не только программирование от состояний, но и значительно
476 ГЛАВА 8. ПОДПРОГРАММЫ

процедуры подменено командами GOSUB и RETURN, которые выполняют-


ся, соответственно, как переход по метке с запоминанием адреса возврата и
как переход по этому запомненному адресу. С точностью до использования
специальных регистров это именно то, что делается в обычном ассемблер-
ном коде.
Различное отношение двух родственных стилей к рекурсии можно счи-
тать следствием различий в трактовке структуры информационного простран-
ства программы (см. § 3.3). Процедурность в программировании от состоя-
ний исходит из того, что информационное пространство является общим для
всех подзадач, из решений которых складывается решение основной зада-
чи. Отсюда следует, что в данном случае утверждение о том, что процедура
выполняется в каком-то контексте, в первом приближении становится триви-
альным — все программные единицы вычисляются в одном и том же контек-
сте.25 Как было показано, в структурном программировании ситуация иная.
Понятие контекста выполнения процедуры как частный случай вложенно-
сти контекстов является основой и реализационного механизма, и методики
декомпозиции программы.
Другая трактовка процедур требутеся и в ООП (см. соответствующую
главу). Тут основным является понятие предоставляемого интерфейса, и воз-
никает принципиально новое понятие делегирования действий.
Если для столь родственных моделей вычислений обнаруживаются раз-
личия процедур, то что говорить о других, нетрадиционных языках и сти-
лях, на которых мы здесь и остановимся. Но предварительно надо отметить,
что традиционное понятие процедуры сформировалось под значительным
влиянием математических методов, которые использовались и используются
до сих пор при исследовании вопросов вычислимости. По этой причине не-
удивительно, что процедуры функционального стиля (т. е. функции) похожи
на то, что применяется в языках структурного программирования. Можно
утверждать, что разработчики структурных и других ранних языков поста-
рались спроецировать функциональные возможности на фон Неймановскую
модель вычислений. При этом они посчитали не имеющим смысла, как про-
тиворечащую ранним представлениям об эффективности, реализацию таких
свойств функций, как частичная параметризация, как оперирование с типа-
ми высоких порядков, как понятие предела и т. д. Практически за рамка-
ми рассмотрения осталась концепция значения, без которой оперирование
более развитые стили.
25
Более подробное рассмотрение данного вопроса см. в § 10.1.
8.8. ПРОЦЕДУРЫ В РАЗНЫХ МОДЕЛЯХ ВЫЧИСЛЕНИЙ 477

с функциями (суперпозиция, комбинирование и др.) просто бессодержатель-


но. Вместо них языки предлагают суррогаты, со всех сторон “обвешанные”
прагматическими соглашениями. Это было бы не так страшно, если бы не
стихийная стандартизация концептуально недоработанных средств, в резуль-
тате чего плохие традиции сохраняются и множатся во вполне современных
системах.
Рассмотренные варианты процедур не выходят за рамки нетрадицион-
ных моделей вычислений, абсолютизируя строго последовательный ход вы-
числительного процесса, императивность и пассивность памяти. И хотя они
заметно различаются, но не кардинально. Новые качества процедурности не-
избежно появляются в системах, которые пытаются внедрить идею изъяви-
тельного наклонения в языки программирования. Рассмотрим их в той же
последовательности, что и ранее (см. § 3.1).
1. Системы продукций. Процедуры в таких системах должны приме-
няться для следующих целей:

• Они объединяют логически связанные правила вывода, применя-


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

Примером продуктивной реализации идеи может служить язык Рефал


(см. § 13.1), в котором аналогом процедуры являются правила с совпа-
дающим детерминативом. Такие процедуры применяются как для объ-
единенного описания однотипных сопоставлений, так и для выполне-
ния внешних, в частности, арифметических действий.
478 ГЛАВА 8. ПОДПРОГРАММЫ

2. Системы функций. Как уже упоминалось, форма задания традицион-


ных процедур во многом унаследована от функциональной схемы, ко-
торая оказалась существенно сужена при проецировании на фон Ней-
мановскую модель. Указанные выше возможности (частичная параме-
тризация, оперирование с типами высоких порядков, понятие предель-
ного перехода и т. д.), реализуемые в противовес (а чаще в дополнение)
к императивным средствам, характеризуют операционный аспект про-
цедурности в системах функционального программирования. Особен-
ностью процедур функциональных систем является возможность гиб-
кой реализации параметризации, в частности, параметризации по не-
обходимости, размножения параметров и др.
Эти же возможности способствуют развитию средств декомпозиции
функциональных систем, позволяя конструировать зависимости меж-
ду данными с весьма сложной структурой. Как правило, подобное кон-
струирование оформляется в виде библиотечных средств, которые ока-
зываются в состоянии достаточно естественно описывать весьма аб-
страктные взаимоотношения между данными. Иллюстрацией тому мо-
жет служить библиотечная надстройка над языком Lisp CLOS (Common
Lisp Object System), которая по существу представляет собой функци-
ональный объектно-ориентированный язык с иными и в некоторых от-
ношениях более широкими возможностями, нежели то, что предлагают
развитые операционные объектные языки.26

3. Коммутационные системы. Процедура как элемент декомпозиции в


таких системах задается как полный подграф коммутационного графа,
обладающий замкнутостью относительно выполняемого в этом под-
графе действия, т. е. как подсистема с входами и выходами, к которым
ведут и из которых исходят дуги, связывающие ее с другими вершина-
Справедливости ради следует отметить, что успех CLOS объясняется не только функци-
ональностью LISPа, но и двумя другими свойствами этого языка:
26

• идентичность структуры перерабатываемых данных и структуры программы,


• возможность определения так называемых списков свойств атомарных единиц струк-
туры данных или программы.
Первое из этих свойств органично сочетается с функциональностью и может даже счи-
таться характерным для этого стиля (оно соответствует трактовке данных как нульместных
функций), второе — более универсально и согласуется с объектной установкой на совмест-
ное определение данных и операций.
8.8. ПРОЦЕДУРЫ В РАЗНЫХ МОДЕЛЯХ ВЫЧИСЛЕНИЙ 479

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

• Блокировка вычислений до тех пор, пока не будут поставлены все


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

Поскольку ациклические коммутационные системы есть одна из форм


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

4. Ассоциативные системы как вариантная форма коммутационных си-


стем при задании вычислений переносят акцент на активность данных,
которые ‘знают’, в каких действиях они должны участвовать (это зна-
ние заключено в коде ключа, сопровождающего значение). Соответ-
ственно, основной единицей декомпозиции ассоциативной программы
становятся множества данных, ключи которых (или фрагменты ключей)
можно рассматривать в качестве указателей на процедуры (имен про-
480 ГЛАВА 8. ПОДПРОГРАММЫ

цедур), подобные методам объектно-ориентированного стиля програм-


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

• Подготовка вызова: действия, собирающие параметры в виде спе-


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

Прекращение выполнения процедуры (аналог возврата) может быть ав-


томатическим, если это действие предписано для выполнения, когда
готовы выходные параметры, либо принудительным, как срабатывание
специально проверяемого условия. В обоих случаях необходимо осво-
бождение номера активации, ликвидация локальных данных и переда-
точной структуры.
Из описания процедурного механизма видно, что в нем особое вни-
мание уделяется асинхронному параллельному вычислению всего, что
может быть вычислено по мере готовности операндов, а не путем при-
нудительной либо неявной передачи управления. Таким образом, упра-
вление в ассоциативных системах подчиняется потокам данных (dataflow-
вычисления).
8.8. ПРОЦЕДУРЫ В РАЗНЫХ МОДЕЛЯХ ВЫЧИСЛЕНИЙ 481

5. Аксиоматические системы. Аналогом процедуры в такой системе слу-


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

Из обсуждения нетрадиционных процедур видно, что в их механизмах


проявляется необходимость параллельного и совместного исполнения, пред-
писываемого либо поощряемого соответствующими моделями вычислений.

Задания для самопроверки

1. Устранить неэффективность рекурсивного алгоритма для числе Фибо-


наччи 8.7.2 путем преобразования программы, направленного на запо-
минание вычисляемых значений.

2. Устранить неэффективность рекурсивного алгоритма для числе Фибо-


наччи 8.7.2 путем перехода к итеративной программе.

3. Устранить неэффективность рекурсивного алгоритма для числе Фибо-


наччи 8.7.2 путем использования теоретических результатов, дающих
другое определение чисел Фибоначчи.

4. Сравнить четыре решения задачи про числа Фибоначчи.


Глава 9

Структуры данных

В данной главе анализируется, как организуются структуры перерабаты-


ваемых программой данных.
Первой причиной появления структур данных является то, что система-
тизация и структурирование для сложной информации всегда идут рука об
руку. Поскольку управление и данные взаимосвязаны, иерархически струк-
турируя управление, мы должны соответственно структурировать и данные.
Другая причина связана с тем, что реальные объекты представляются агре-
гатами взаимосвязанных характеристик. Например, хранить координаты точ-
ки пространства как три отдельные переменные — провоцировать ошибки в
программе. Следовательно, в языке необходимо обеспечивать возможность
агрегирования (соединения) данных. А для вычислителя нужно обеспечить,
чтобы в конце концов и аргументы, и результат вычислений сводились на
уровень атомарных объектов. Поэтому агрегаты необходимо уметь разъеди-
нять на составные части.
Третья причина в том, что при составлении программ требуется задавать
регулярные действия для подобно описываемых и подобно интерпретируе-
мых объектов, и хранить промежуточные результаты вычислений, при этом
далеко не всегда можно обойтись атомарными единицами данных.
В последующих параграфах обсуждаются вопросы задания, использова-
ния и хранения структурированных данных, а также средства, предлагаемые
для этого языками программирования.

§ 9.1. ОБЩИЕ КОНЦЕПЦИИ СТРУКТУРИРОВАНИЯ ДАННЫХ

9.1.1. Структура программы и структуры данных

482
9.1. СТРУКТУРИРОВАНИЕ ДАННЫХ 483

Система данных, перерабатываемых программой, базируется на простых


и неделимых понятиях — первичных элементах системы. Выбор базы пер-
вичных элементов зависит от точки зрения на программу, в частности, от
того, что считается существенным и чем можно временно пренебречь. Аб-
стракция рассмотрения в значительной степени определяет и взгляд на дан-
ные. В то же время, различные уровни абстрактности данных требуют раз-
личных языковых средств, иногда своего языка описания обработки. Это ка-
сается и средств оперирования, и структур данных. Реальная моделируемая
программой жизнь содержит не целые, вещественные и иные числа или мас-
сивы, а вполне конкретные объекты, представляемые в программе числами,
структурами, массивами и т. п. Выбор представления сущностей предопре-
деляет реализацию конкретных операций, и чаще всего успех либо неудачу
программного проекта.
Поэтому, если задача с самого начала ставится корректно, правильно го-
ворить о выборе языка программирования, соответствующего решаемой за-
даче. Но часто выбор языка навязан внешними обстоятельствами. В любом
случае программист должен осознавать:
• на какие средства описания он вправе рассчитывать и чего не может
требовать в данной ситуации;

• какие абстракции являются идеально подходящими для решаемой за-


дачи, и как это идеальное моделируется в реальном языке.

Ответ на эти вопросы существенно зависит от квалификации самого про-


граммиста. Первая часть этого положения — вопрос специального образова-
ния программиста, вторая — общей теоретической подготовки.
Обычный язык программирования предлагает широкий ассортимент воз-
можностей организации данных. Следовательно, перед программистом сто-
ит задача выбора не только абстрактного, но и конкретного представления
данных. Видимость, что здесь больше свободы, чем при составлении алго-
ритмов, обманчива. Налицо взаимосвязи и взаимовлияние. Фиксация струк-
тур данных определяет, будет ли хорошо работать тот или иной алгоритм. С
другой стороны, идеально подходящие для алгоритма объекты — не только
образы конкретных объектов, но и прообразы программной структуры дан-
ных, которая не может быть выбрана произвольно. Традиционные критерии
эффективности по времени вычислений и занимаемой памяти ныне являют-
ся отнюдь не самыми важными. На выбор и алгоритмов, и структур данных
существенно влияют ответы на следующие вопросы:
484 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

• насколько понятен сделанный выбор,


• каков уровень адекватности получающегося решения,
• велика ли трудоемкость распространения решения на другие ситуации
и его модификации при изменений условий задачи,
• каковы границы возможного для таких модификаций?
И это далеко не полный перечень аспектов задачи выбора структуры данных
и соответствующих ей алгоритмов.
Единственный универсальный рецепт, который здесь можно дать, это по-
вышение уровня абстрактности рассмотрения задачи с последующей транс-
формацией выбранного в конкретные структуры. Поэтому важно системати-
ческое изучение возможностей наиболее употребляемых структур данных.
Предварительно стоит обратить внимание на одну особенность вопроса:
общность подходов к структурированию данных и действий, которая обусло-
влена двумя обстоятельствами:
• целостностью процесса разработки программ, для которого структури-
рование данных и действий есть две неразрывные составляющие;
• взаимосвязанностью представлений данных и процессов их обработки.
В первом приближении параллелизм данных и действий в программирова-
нии можно охарактеризовать следующим образом:
• Как данные, так и алгоритмы — структурные конструкции, составляе-
мые программистом для тех или иных целей.
Это положение универсально, т. е. не зависит от используемого язы-
ка и стиля программирования, а два последующих связаны в первую
очередь со структурным программированием.
• Атомам алгоритмов соответствуют атомарные данные, для которых опре-
делены непосредственные действия вычислителя. Из атомов строятся
структуры.
Для алгоритмов это — последовательно, совместно или параллельно
выполняемые операторы, разветвления, циклы и др., а также процеду-
ры.
Для структур данных это — массивы, записи и др., а также классы объ-
ектов.
9.1. СТРУКТУРИРОВАНИЕ ДАННЫХ 485

• Структурирование действий (в особенности с использованием проце-


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

9.1.2. Базовые структуры данных и конструкторы структур

В большинстве языков программирования предлагается некоторый набор


базовых типов и конструкторов, которые позволяют строить новые структу-
ры из уже существующих. Естественно, что как базовые структуры, так и
конструкторы для разных языков программирования различны.
Определение 9.1.1. Базовые структуры данных — изначально определен-
ные в абстрактном вычислителе языка структуры данных, т. е. базовые объ-
екты, которыми оперирует абстрактный вычислитель. Типы, описывающие
базовые структуры и операции вычислителя над базовыми структурами, на-
зываются базовыми типами данных.
Конструктор структуры данных — правило построения новых струк-
тур данных из существующих структур. Конструкторы языка предоставляют
возможность программисту составлять свои представления (абстрактных)
данных.
Конструктор типа данных — это конструктор структуры данных плюс
правила, позволяющие определять операции для новых структур данных.1
Конец определения 9.1.1.
Основанием системы структурирования данных (т. е. подхода к данным
в языке программирования) служат базовые элементы данных — то, из чего
строится вся система, включая базовые структуры. В качестве базы теорети-
чески может быть выбран булевский тип: все записи в ячейках памяти можно
представлять как кортежи булевских значений; все вычисления в реальных
компьютерах на самом деле строятся путем моделирования операций через
первичные логические действия. Однако даже если игнорировать чрезмер-
ную сложность такого построения, есть и другие причины, почему булевский
В рамках современных требований к конструкторам типов считается правильным со-
вместное определение структуры данных и операций для них (методов). Это одно из ключе-
1

вых положений объектно-ориентированного программирования.


486 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

тип не считается единственным базовым типом системы структурирования


данных:
• средства оперирования с данными, в настоящее время предоставляе-
мые машинными языками, а тем более общеупотребительными языка-
ми программирования, не связаны (за редкими исключениями) с логи-
ческой моделью вычисления операций, поскольку ориентированы на
обработку практически более важных типов данных;
• при моделировании реальности сначала требуется понять, как предста-
влять конкретные объекты структурами данных вычислителя, следо-
вательно, описание данных программы строится сверху вниз, посред-
ством декомпозиции, а не составляется из примитивных элементов.
В истории развития языков был период, когда пытались строить специфици-
рующие модели вычислителей на основе редукции до уровня битов (т. е. на
базе логического типа).2 Иллюстрацией этого направления может служить
язык APL, который провозглашал принцип конструирования всего, что нуж-
но, из битовых наборов с помощью подходящих средств композиции. Мно-
гочисленные описания моделей вычислений реальных компьютеров на этом
языке убеждают только в том, что ни проверить корректность модели, ни
добиться ее понятности не удается. Причина очевидна: для описаний нуж-
ны более абстрактные структуры данных, а конструкторы данных на бито-
вом уровне не подходят для композиции абстрактных структур — на каждом
уровне требуются свои средства.
Таким образом, выбор базового уровня не является ни случайным, ни
минимально необходимым. Он должен соответствовать командам модели
вычислений для данного языка. Программист может свободно пользоваться
базовыми типами для описания переменных, констант, параметров и др. в
соответствии с регламентами и правилами языка.
В операционных алгоритмических языках естественной базовой абстрак-
цией является использование тех объектов, для которых характерно следую-
щее:
• обычные вычисления с этими объектами укладываются в системы ко-
манд реальных компьютеров; и
Здесь речь идет не о логическом проектировании оборудования, при котором подобные
модели весьма полезны (пример — язык DDL), а о предъявлении формальных описаний вы-
2

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


9.1. СТРУКТУРИРОВАНИЕ ДАННЫХ 487

• их комбинации достаточно просто использовать для представлений ре-


альных объектов прикладных областей.

Следовательно, можно сформулировать первые два принципа систем струк-


турирования данных, безусловно принятые для всех языков программирова-
ния:

1. В качестве базовых структур данных, предоставляемых языком, выби-


раются такие структуры, которые однозначно представляются на ма-
шинном уровне большинства общеупотребительных компьютеров.

2. Все структуры данных, как предоставляемые языком, так и те, кото-


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

Эти принципы хорошо согласуются с устройством базовых средств опериро-


вания, т. е. с операционными конструкциями, которые однозначно соответ-
ствуют командам и типовым последовательностям команд.
Следует отметить и различия. Базовые средства программирования “на-
рабатываются”, т. е. языковые формы получают те последовательности ко-
манд, которые хорошо себя зарекомендовали (использовались наиболее ча-
сто, отвечают требованиям защиты и т. п.). Базовые же структуры данных
выбираются более жестко: значения, хранимые в регистрах, не могут быть
произвольными, машинные команды понимают лишь ограниченное количе-
ство базовых типов и развиваются медленнее языков программирования.
Параллели между структурами данных и структурами оперирования на-
блюдаются и в возможности построения одних структур с помощью других.
Базовых средств оперирования для представления всех нужных алгорит-
мов не хватает, и уже поэтому появилось понятие процедуры (в ее опера-
ционном аспекте). Точно так же для представления объектов, с которыми
приходится иметь дело при решении реальных программистских задач, базо-
вых структур данных не хватает, и приходится вводить конструкторы новых
структур данных. Первой структурой данных, появившейся в языках про-
граммирования, стал массив, обеспечивающий осуществимость именно тех
видов вычислительных работ, для которых первоначально и предназначались
компьютеры. Само понятие конструктора было осознано позднее.
Конструкторы данных не должны приводить к тому, чтобы нарушались
первые два принципа систем структурирования данных. Таким образом, мож-
488 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

но сформулировать следующие два принципа систем структурирования дан-


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

9.1.3. Операции над вновь определяемыми типами данных

В ранних языках операции для новых данных ограничивались селекто-


рами — операциями, выделяющими из сложного типа составляющие части
его реализации. Для конструируемого значения необходимо уметь выделять
его компоненты, иначе исчезнет сводимость к базовым операциям. Абстракт-
ное рассмотрение конструируемых структур данных было возможно лишь на
уровне процедур с параметрами, обозначающими эти структуры.
Идея, проведенная в концепции объектно-ориентированного программи-
рования, отражает подход, когда совместно со структурой данных фиксиру-
ются операции — методы обработки определяемых объектов. Чтобы рассма-
тривать новые данные абстрактно, без привязки к конкретному представле-
нию значения, селекторы компонент просто вредны. Поэтому использование
селекторов ограничивается теми частями программы, которые задают реали-
зации методов.
Введение новых типов данных требует системной проработки, и сначала
нужно ответить на следующие принципиальные вопросы, касающиеся реше-
ний о данных:
a) как и какие из существующих стандартных средств распространяются на
новые данные;
b) какие способы приведения между старыми и новыми типами предусма-
триваются вообще, и какие из них определяются по умолчанию.
c) где и как будут доступны операции, определяемые для новых данных;
9.1. СТРУКТУРИРОВАНИЕ ДАННЫХ 489

d) как новые операции согласуются с процедурными средствами данного


языка (в частности, для новых данных необходимо обеспечить механизмы
параметризации, представленные в языке);

Эти вопросы решаются вместе с вопросами размещения конструируемых


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

Способы приведения различны для разных языков, но реально всегда пре-


Pascal).

доставляется возможность их явного задания, например, с помощи указания


имени типа, к которому нужно привести значение. Варианты неявных при-
ведений зависят от того, насколько строг контроль типов в данном языке.
Именно так нужно подходить к конкретным приведениям, предусматрива-
емым в том или ином случае. Концепция, когда ‘все приводится ко всему’,
все еще имеет своих защитников среди ассемблерных программистов, осо-
бенно тех, кто пишет маленькие, автономно работающие и практически не
модифицируемые модули. Но если говорить о надежном коллективном про-
граммировании, то задача строгого контроля типов занимает важное место.
Обсуждая базовые структуры данных и конструкторы, нужно затронуть
вопрос о способах задания литералов и, вообще, изображения значений, как
базовых, так и структурных данных. Разнобой, который при этом демонстри-
руют языки, свидетельствует об отсуствии достаточно обоснованных крите-
риев выбора того или иного решения.
Литералы обладают атрибутом, исключительно существенным для вы-
числений, — фиксированным значением, независимым от контекста. Значе-
ния, а не изображения значений, воспринимаются абстрактным вычислите-
лем, выполняющим программу. Принципиально, что значения как атрибуты
фиксируются уже при реализации языка, которая задает их в соответствии с
возможностями конкретного вычислителя моделировать действия абстракт-
ного вычислителя.
Есть ряд моментов, которые нужно учитывать:

a) Литералы воспринимаются человеком, следовательно, они должны быть


для него понятны. Поэтому в качестве изображений значений использу-
ются числа в привычной нотации, строки, идентификаторы, осмысленные
с точки зрения естественного языка.
490 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

b) По причине реальной неперечислимости всех возможных значений боль-


шинства базовых типов и необходимости естественных для человека пра-
вил записи изображений значений, возникают общие синтаксические по-
нятия: число, строка и др.

c) Неестественность использования литералов для большинства конструи-


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

d) Необходим учет назначения данных (пример из С/С++ — логические зна-


чения, которые все-таки пришлось определить, хотя первоначально язык
обходился без них).

Говоря о распространении стандартных средств программирования на но-


вые структуры, в первую очередь следует обсудить присваивание, которое
часто рассматривается как универсальная операция копирования любых зна-
чений,4 и потому при конструировании структур данных ее распространяют
на новые значения чаще всего.
Так сделано, к примеру, в Pascal. Распространение присваивания здесь
означает, что с каждым конструктором типа связана подпрограмма присваи-
вания с двумя параметрами, указывающими на источник и получатель значе-
ния новых типов данных. В С/С++ с новым типом ассоциируется указатель-
ный тип и все оперирование с конструируемыми значениями осуществляется
Поясним пункт c). Типы значений, определяемые в программе, обычно принадлежат ее
локальному контексту и не известны в среде, в которой работает программа, а возможность
3

передавать информацию о них — весьма ограничена и плохо разработана. Как следствие,


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

ми качествами этой модели являются пассивность памяти и единственность активного ком-


понента, называемого процессором. Следовательно, только присваивание значений может
изменить состояние вычислителя. Для других моделей универсальность присваивания не
обязана сохраняться. Более того, как отмечал еще Бэкус, именно присваивание в качестве
основы смены состояний является препятствием для повышения производительности вы-
числительных систем (см. § 1.2 про традиционные языки).
9.1. СТРУКТУРИРОВАНИЕ ДАННЫХ 491

посредством доступа к ним через указатели. Присваивание указателей при-


водит лишь к получению нескольких ссылок на общую память (см. рис. 7.6;
если необходимо копирование, то об этом следует особо позаботиться, на-
пример, разработать специальную процедуру).
Что касается других операций, то чаще всего они не распространяются на
конструируемые структуры данных. Упомянем несколько исключений. Из-
вестны попытки распространять арифметические операции на массивы, что
демонстрирует, например, язык Альфа, разработанный в Новосибирске. В
неоднократно упомянутом языке APL определяется механизм, который по-
зволяет единообразно распространять существующие операции на значения
и переменные новых типов.
В современных языках новые операции обычно определяются с помощью
процедур. Возможно синтаксическое оформление вызовов этих процедур как
операций (С/С++, Алгол-68).
Присваивания указателей — основа согласования новых операций с про-
цедурными механизмами. В некоторых случаях, когда основные конструк-
торы языка продуцируют структуры, к которым можно обращаться только
посредством указателей, осуществим подход, не требующий распростране-
ния присваивания значений. В этом случае программы получаются короче,
чем при явном оперировании как со значениями, так и с указателями, но они
требуют более мощной поддержки со стороны системы программирования.
В таких языках прибегают к автоматическому распределению памяти: она
выделяется при создании объекта и уничтожается, когда надобность в объ-
екте исчезает. Для переиспользования памяти требуется механизм ее утили-
зации (например, сборка мусора). Хотя модель вычислений языка усложня-
ется, программист может иметь упрощенное представление о вычислениях,
достаточно согласованное с реальным представлением о процессах. Харак-
терный пример — язык Java. Примечательно, что, даже не имея высокой ква-
лификации в программировании, на этом языке можно писать приемлемые
для практического использования программы. Это — прямое следствие по-
вышения семантического уровня базовых средств языка (в том числе кон-
струирования данных (объектов) и присваивания) в сочетании с механизма-
ми автоматического распределения памяти и сборки мусора, заложенными в
модель вычислений языка.
По существу модель вычислений языка Java основана на том, что память
абстрактного вычислителя потенциально бесконечна, а сборка мусора есть
механизм проецирования бесконечной памяти на конечную память реально-
го вычислителя. Понятно, что далеко не все корректные в Java вычисления
492 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

можно спроецировать таким образом. Но это уже область прагматических


ограничений реализаций языка.
Сопоставляя разные подходы к присваиванию, видим, что выявляются
два взгляда на понятие переменной:
• переменная трактуется как языковой объект, который обладает атрибу-
том, называемым значением. Этот атрибут может меняться в динамике
вычислений (пример такого подхода — язык Pascal);
• переменная трактуется как адрес (или обозначение адреса, если речь
идет об имени), указывающий на область памяти, где могут размещать-
ся разные значения, как правило, но совсем не обязательно одного и
того же типа (пример такого подхода — язык С/С++).
Это различие взглядов ведет к другим отличиям языков в части оперирования
со значениями и переменными, в подходах к присваиванию, параметризации
и приведениям, в отношении к указателям, что в конечном итоге приводит к
разным системам типов данных.
В качестве примера еще одного подхода к понятиям переменной и зна-
чения уместно привести их трактовку в языке LUCID, в которой явно фигу-
рирует время. Переменная в этом языке рассматривается как последователь-
ность элементарных значений во все моменты ее жизни. При такой трактовке
основными операциями оказываются соотношения между значениями пере-
менных в разные моменты времени. Например, фрагмент

for all t C[t] = 5; (9.1)

задает константу со значением 5, а соотношения

V[first] = 0; (9.2)
for all t > first V[t] = V[t-1] + C;

определяют в условиях первого фрагмента, что переменная V изменяется во


времени как арифметическая прогрессия с разностью, равной C = 5.
В данной иллюстрации, чтобы представить понятие переменной в опера-
ционном стиле, мы намеренно отошли весьма далеко от нотации, принятой
в строго функциональном языке LUCID, которая точно соответствует этому
стилю программирования. В естественной нотации фрагмент (9.1) записы-
вается просто как
9.1. СТРУКТУРИРОВАНИЕ ДАННЫХ 493

а соотношения (9.2) — как


C = 5;

(fby — follow by, “следуют через”). Никакого индексирования временем нет.


V = 0 fby V + C;

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

9.1.4. Типизация и стили

Базовые структуры данных языков выбираются, в первую очередь, исходя


из потребностей стиля. Как следствие, проблема их реализуемости решается
в рамках реализуемости модели вычислений языка в целом. Типизация для
разных стилей определяется практически всегда, хотя и понимается неоди-
наково.
Например, для функционального типа естественной, хотя и не единствен-
но возможной, концепцией типа данных может служить соглашение о том,
что данные считаются нульместными функциями, а система типов строит-
ся на базе операции применения функции к ее аргументам. Таким образом
строится система типов, соответствующая математическому понятию типов
функций (см. Приложение А).
Для объектно-ориентированного стиля требуется поддержка порождения
и уничтожения данных (объектов) в динамике выполнения программы. Как
следствие, появляются специальные методы конструкторов, семантика ко-
торых сводится к созданию объекта и выполнению ряда сопутствующих (в
том числе и явно описываемых программистом) действий. Соответственно,
для уничтожения объекта появляются методы деструкторов, выполняющих
выбранные программистом действия, необходимые для корректного удале-
ния всех следов объекта.
Динамическое конструирование объектов позволяет более адекватно мо-
делировать реальность, рассматривать и фиксировать требования к новым
структурам с точки зрения решаемых задач.
Здесь на первый план выступают критерии работы с новыми структура-
ми данных как с едиными абстрактными объектами. Требуется, чтобы спо-
собы оперирования с новыми данными не снижали уровня абстрактности их
494 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

рассмотрения программистом. Конкретизируя положение, что для работы с


построенными конструктором данными должны применяться только те опе-
рации, которые явно определены для новой структуры данных, ООП дела-
ет первые шаги к такому абстрактному подходу. Уже отмечалось (см. § 12.2
и § 12.1), что в связи с объектной декомпозицией полезно в качестве типа
объекта рассматривать имя, используемое для обозначения конкретного про-
граммного интерфейса, и, таким образом, объект может обладать нескольки-
ми типами, или, что то же, реализовывать несколько интерфейсов. Эта по-
пытка развития понятие типа появляется в связи с потребностью регламен-
тировать ситуации, в которых можно обращаться к объекту, и разграничи-
вать их. Более последовательной попыткой такого рода является концепция
абстрактных типов данных.
9.1.5. Абстрактные типы данных

Этот взгляд на конструкторы структур в значительной степени отвлека-


ется от рассмотрения новых структуры как чего-то, составленного из имею-
щихся структур.
Абстрактные типы данных (АТД) —теоретическая концепция, в которой
пытаются полностью освободиться от явного задания представления данных
и заменить его на порождение представления по свойствам операций, требу-
емым для нового типа данных.
Приведем требования, выставляемые в абстрактных типах данных, в со-
поставлении с требованиями ООП.
1. Операции с новыми значениями и переменными должны порождать
определение структуры новых значений. Эта структура называется пред-
ставлением конструируемого типа.
Это требование удовлетворяется в ООП в другой форме: операции опре-
деляются совместно и одновременно с определением структуры новых
значений.

2. Соотношения между операциями должны полностью определять все


возможные эффекты использования значений и переменных конструи-
руемого типа — поведение объектов конструируемого типа.
Не может быть и речи об удовлетворении этого требования в современ-
ном ООП.

3. Определение поведения не должно опираться на информацию о пред-


ставлении конструируемого типа.
9.1. СТРУКТУРИРОВАНИЕ ДАННЫХ 495

Требование частично удовлетворяется в ООП, но даже в принципе не


может быть выполнено полностью.

4. Сведения о реализации операций конструируемого типа не должны быть


доступными на уровне использования, чтобы исключить возможность
вложить в использующую программу информацию о том, как устроен
конструируемый тип — экранирование реализации.
В принципе декларируется в ООП, но в реальности нарушается на ка-
ждом шагу. Более того, при использовании шаблонов проектирования,
семантически связывающих объекты разных типов (классов) опреде-
ленными отношениями, регламентированное нарушение этого требо-
вания даже предписывается. Пример с делегированием (см. § 12.1)
лишь одна из иллюстраций такого рода, когда явно указывается, что
один объект должен содержать в своем представлении ссылку на дру-
гой объект.

5. Должна быть определена функция абстракции [53] A, отображающая


объекты представления (rep) в абстрактные объекты A:

A : rep → A

Это отображение должно быть согласовано с использованием опера-


ций, применимых к A, т. е. для любой операции f , действующей в обла-
сти абстрактных объектов, ее реализация frep , действующая на предста-
влении A, должна удовлетворять диаграмме

A −−−→ A0
f
x x
  0
A A (9.3)
repA −−−→ repA0
frep

A0 и repA0 — области значений функций f и frep , соответственно. По


существу это требование означает возможность независимого от пред-
ставления оперирования над абстрактными объектами.
Безусловно, почти всегда функция абстракции является призраком.
Объектно-ориентированный подход с самого начала отказывается от
данного требования. Упоминавшееся выше делегирование (см. § 12.1) —
496 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

пример, показывающий, как нарушается независимость объектного ти-


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

Приведенные требования выражают суть концепции АТД, которая, начиная с


середины семидесятых годов, интенсивно развивается в теоретическом аспек-
те. Несмотря на очевидные преимущества концепции абстрактных типов,
есть ряд трудностей при реальном ее воплощении.
Главная трудность обусловлена теоретическими ограничениями: абсолют-
но формальное описание абстрактного типа, зависящего от ранее постро-
енных конкретных типов, недостижимо. Любой подход к описанию типа,
претендующий на формальную строгость и полноту, должен использовать те
понятия, которые использовались при построении представлений исходных
типов, или понятия, которые описывают эти представления. Иными слова-
ми, если новый тип пополняет сложившуюся систему типов программы, то
невозможно описать его поведение без привязки к какому-либо описанию
представления существуующих типов.
Этот исключительно сложный теоретический вывод базируется на тех же
идеях, что теорема Геделя о неполноте, и так же жестко указывает на границы
применимости подхода. Таким образом, нужно либо отказаться от целей,
на которые концепция абстрактных типов данных претендовала, либо не
базироваться на привычных типах, даже на типе целых чисел.
Внимание!
Средства конструирования абстрактных понятий сверху, путем конкре-
тизации и композиций общих концепций, плохо совместимы со средствами
построения новых понятий снизу, из базовых конкретных операций и кон-
кретных данных. См. § 3.3.
Это предупреждение выражает еще одно концептуальное противоречие.
9.1. СТРУКТУРИРОВАНИЕ ДАННЫХ 497

В идеале абстрактные высокоуровневые концепции надо было бы реали-


зовывать в языках, вообще не содержащих конкретных типов данных и кон-
кретных понятий, а уже затем получившиеся шаблоны применять к конкрет-
ным конструкциям. Блестящий эксперимент Б. Лисков с языком АТД CLU
[53] имел своей ахиллесовой пятой именно недостаточное осознание необ-
ходимости жестких и даже жестоких решений при последовательном прове-
дении нового метода, в нем пытались совместить восходящий и нисходящий
подходы.
Приведенное выше предупреждение связано со следующим практическим
наблюдением. Есть два пути построения структуры данных программы:

1. наращивание сложившейся структуры путем добавления новых значе-


ний и переменных, относящихся к конструируемым типам; и

2. определение абстрактных структур, моделирующих реальные объек-


ты, и построение для этих структур подходящих представлений, обес-
печивающих осуществимость такого моделирования.
Первый подход называется восходящим, а второй — нисходящим проекти-
рованием. На практике обычно применяется ‘смешанная’ стратегия, когда
сначала проектирование ведется нисходящими методами, затем осуществи-
мость моделирования проверяется на основе восходящих построений, и да-
лее проводится ‘синтез’ двух подходов (т. е. начинаются нерегламентирован-
ные итерации переходов от одних методов к другим). Именно на этапе ‘син-
теза’ начинаются неприятности в разработке большинства программных си-
стем. Поэтому чем жестче средства программирования будут разделять вос-
ходящее и нисходящее движение, тем лучше. Их средства, по крайней мере,
не будут путаться в голове и в тексте программы.
Можно заметить, что АТД в идеале полностью принадлежит нисходяще-
му проектированию, а ООП пытается совместить нисходящее и восходящее.
Перечислим другие трудности, связанные с проблемой перехода от тео-
ретической осуществимости к практической реализации:

• не получается разработать удобный для практики язык спецификаций


типов данных,

• конструкторы абстрактных типов довольно громоздки,

• не удается описать поведение объектов типа в зависимости от динами-


ки вычислительного процесса.
498 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

В методологическом плане требования концепции абстрактных типов дан-


ных положительно повлияли на систему типов данных в языках программи-
рования и в программах. К примеру, современные системы типов языков про-
граммирования часто стремятся строить уже не в привязке к базовым сред-
ствам конкретных вычислителей, а на основе математически (или системно
и логически) обоснованной системы понятий, которая уже затем трансфор-
мируется в понятия языка конкретного вычислителя. Далеко не всегда такая
трансформация проходит безболезненно, но все-таки она дает возможность
добиться резкого повышения качества программ, если рассматривать их как
элементы всей системы человеческой деятельности.
Абстрактную точку зрения на понятие типа данных следует рассматри-
вать как средство декомпозиции задач. Но это средство высокого уровня. Да-
же описать поведение объекта полностью независимо от представления не-
льзя без высокой логико-алгебраической культуры. Только тогда можно полу-
чить абстрактное представление, такое, чтобы любая его реализация, удовле-
творяющая спецификации, гарантировала бы корректность использования.
Кроме того, если говорить о спецификации, которая описывает не только
математические свойства, но и требования к эффективности, то нынешняя
теория еще только начинает заниматься подобными вопросами, и поэтому
связь ресурсных требований с абстрактным представлением остается декла-
ративной, описанной так же нестрого, как семантика в традиционных описа-
ниях языков программирования. Выбор конкретного представления, которое
должно удовлетворять ресурсным требованиям, вступает в противоречие с
высокоуровневыми операциями, никак не связанными при нынешнем их по-
нимании ни с памятью, отводимой под объекты, ни с временем исполнения.
Эта сторона спецификаций остается тем, что очень трудно проверить.
В связи со всем этим стоит заметить, что и объектный подход совсем не
преуспел в решении задачи отдельного от реализации описания типа и в со-
гласовании абстрактных и ресурсных требований.
Если исходить из потребностей абстрагирования, то заманчиво поставить
вопрос о существовании универсального типа типов, из которого все нужные
типы строились бы путем вычисления операций этого типа с типовыми зна-
чениями. Однако построения такого рода — прямой путь к логическим пара-
доксам теории множеств или комбинаторной логики (к примеру, к парадоксу
лжеца и парадоксу Рассела, см. [63]).
В последующих разделах обсуждаются средства построения систем ти-
пов данных, предлагаемые различными языками, с учетом общих положе-
ний, изложенных выше.
9.2. БАЗОВЫЕ И ВЫВОДИМЫЕ ТИПЫ 499

§ 9.2. БАЗОВЫЕ И ВЫВОДИМЫЕ ТИПЫ

9.2.1. Перечисления

Простейшие из абстрактно представленных базовых структур данных за-


даются просто перечислением значений.
Определение 9.2.1. Перечисление является конечным линейно упорядочен-
ным множеством изображений значений (литералов).
Для перечислений определены следующие константы и операции.

• Элементы first и last — константы, представляющие первый и послед-


ний элементы перечисления.

• Отношения < и = — двуместные операции, задающие соотвественно


порядок и равенство на перечислении.

• succ и pred — частичные одноместные операции, вычисляющие сле-


дующее и предыдущее значения своего аргумента; succ не определено
для last, а pred — для first, соответственно.

Конец определения 9.2.1.


Через операции порядка и равенства определяются также >, 6, > и 6=.
Изображения значений перечислений могут быть заданы априори или
определяться программистом. Априорное задание фиксируется определени-
ем языка, либо посредством явно выписанного в определении перечня изо-
бражений, либо с помощью правил построения изображений из символов.
Например, для булевого типа литералы true и false явно заданы языком. Для
целого типа есть соответствующие правила составления изображений чисел
из цифр, знаков плюс и минус.
Конкретно-синтаксические правила задания программистом перечисля-
емого типа различны, но все они сводятся к тому или иному способу зада-
ния набора литералов. Обычно литералы, определяемые программистом, —
идентификаторы (Pascal, С/С++ и др.). Имена литералов дополняют контекст,
а потому должны подчиняться правилам локализации имен данного языка.
Можно сказать, что литералы выбираются из множества правомерных в дан-
ном контексте имен.
Рассмотрим некоторые преобразования перечислений как типов данных.
Из одного перечисления можно построить другое путем образования от-
резка: нужно просто указать два значения из данного набора, одно из которых
500 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

объявляется first, а другое — last для нового перечисления (новые first6last в


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

Конструктор отрезка показывает важный класс конструкторов типов, ко-


§ 9.1.5).

торые в некотором смысле ограничивают тип, не усложняя его структуры.


Типы, получаемые при помощи ограничений других типов, часто называют-
ся выводимыми из них.
Если есть два перечисления, то можно построить инъективное отобра-
жение меньшего по числу элементов в большее, естественно согласованное
с упорядоченностью типов. Это очень важное для типов перечислений ото-
бражение называется вложением. Образ элемента a типа T1 при вложении в
тип T2 можно считать представлением a в типе T2.
Вложение любого перечисления в множество значений целого типа явля-
ется стандартным реализационным представлением данного типа. Но вот ка-
кое из вложений выбрать — не всегда очевидно. Некоторые языки оставляют
это на усмотрение разработчиков систем программирования (Pascal). В этом
случае стандарт языка не фиксирует преобразования между различными пе-
речислениями, но и не запрещает их. В других (С/С++/C#) допускается явное
задание отображений между перечислениями (в С/С++/C# — отображение в
тип беззнаковых целых; по умолчанию для него устанавливается, что first
отображается в нуль).
Имея вложение одного перечисления в другое, можно говорить и об обрат-
ном (частичном) отображении: оно определено, если для значения существу-
ет прообраз вложения, и не определено в противном случае. Требования од-
нородности диктуют запрет явного использовании в программе нескольких
различных вложений и их обращений для одной и той же пары перечислений.
Для отрезков перечислений можно говорить о естественном вложении, кото-
рое сохраняет литеральные изображения. Корректно определенный язык не
допускает иных вложений и их обращений между перечислением и его от-
резком, что не исключает задания разных вложений для них в другие пере-
числения.
Вложения перечислений — основа для приведений объектов данных ти-
пов. В С/С++/C# такие приведения явно задавать не требуется, там перечи-
сления низводятся до уровня беззнакового целого. В языках со строгим ста-
тическим контролем типов (например, в Pascal и Ada) такие приведения нуж-
9.2. БАЗОВЫЕ И ВЫВОДИМЫЕ ТИПЫ 501

но указывать явно.
Указанных выше операций достаточно для использования перечислений
в программах. Для повышения выразительности полезно согласование этих
операций с циклами for (в двух вариантах: по возрастанию и по убыванию)
и for all (порядок перебора не фиксируется). Естественно, что по значениям
типа перечисления можно организовывать выбирающие операторные кон-
струкции. С точностью до оператора for all так сделано в Pascal.

9.2.2. Булевский тип

Этот тип есть перечисление специального вида. Множество его значений


состоит из двух элементов, для которых языком фиксированы изображения:
True и False. Уместно заметить, что это не означает эквивалентности булев-
ского типа какому-либо перечислению из двух элементов, хотя бы потому,
что операции succ и pred для булевских значений не употребляются. Логи-
ческий тип в хорошо сконструированных языках не приводится ни к каким
другим (в C++, как мы уже видели, логический тип без всякого предупрежде-
ния может приведен к целому).
Обычный набор операций булевского типа дополняет стандартные опера-
ции логическими связками. Смысл такого расширения в том, что они легко
(однозначно и эффективно) реализуются непосредственно командами любо-
го конкретного вычислителя и соответствуют задаче разветвления вычисле-
ний, а потому есть прямой резон предоставить их программисту в качестве
языкового оформления использования команд вычислителя.
По этой причине логический тип был подробно разобран в главе 5, по-
священной операторам выбора.
9.2.3. Тип литерных

Этот тип есть перечисление специального вида, наиболее близкое к об-


щему определению: все операции заданы именно так, как предписывается
для любых типов перечислений. Мощность множества значений стандарт-
ного литерного типа 256, что соответствует байтовому кодированию литер.
В качестве вложения в тип целых чисел для литерных значений задается ото-
бражение на отрезок 0..255, что прямо указывает на кодировки литер как на
представление данного типа.
Для литерных значений вводятся правила изображения. Обычно это за-
ключенный в кавычки символ. Но есть значения, не представимые символа-
ми. Для них, а также для кавычек в качестве символа, используются правила
502 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

задания изображений специального вида. Часто такие правила распростра-


няются на все литерные значения, из-за чего в языке появляется синонимия
и излишняя привязка к реализации, в частности, к конкретным кодировкам
литер.5
В связи с распространением в последние годы международной кодиров-
ки Unicode, в которой символ представляется двумя байтами, тип символь-
ных значений приобрел напарника для представления символов Unicode. Для
этой кодировки все перечисленные проблемы обостряются, так как почти на-
верняка многие из символов Unicode приходится представлять числовыми
кодами.
9.2.4. Тип целых

Целые — это перечисления, для которых языком вводятся специальные


обозначения литералов: значений целых типов. В соответствии с машинной
арифметикой и разрядной сеткой часто определяют несколько вариантов це-
лых, каждый из которых соотносится со своим регистровым представлени-
ем: байтовые, двухбайтовые, беззнаковые и др. Считать ли все варианты це-
лых соответствующими отрезками “самых больших” целых, представимых в
данном языке или системе программирования, дело вкуса. Необходимо отме-
тить, что реализация оперирования с целыми каждого вида не делается путем
приведения операндов к “самым большим” целым с последующим обратным
приведением к требуемому формату, да и человек скорее склонен восприни-
мать различные целые типы как отрезки типа-призрака всех целых чисел,
который непредставим в машине из-за своей бесконечности.
Наличие вариантов целых мотивируется в первую очередь тем, что си-
стемы команд предлагают различные способы работы, выбираемые исходя
из конкретных особенностей значений. Объединение различных целых ти-
пов дает возможность повысить строгость описания языка, одновременно
упростив его. Как следствие появляется возможность проведения вычисле-
ний с гарантированной надежностью, даже когда промежуточные результаты
выходят за границы представления, определенные для сохраняемых резуль-
татов. Для стандартных представлений целых чисел надежности гарантиро-
вать не удается: к примеру, сложение и вычитание машинных целых не под-
чиняется законам математики (произведение ненулевых чисел, в частности,
может дать 0).
Русские программисты прекрасно знают неприятности, которые доставляют многчислен-
ные, не согласованные друг с другом, кодировки русских букв.
5
9.2. БАЗОВЫЕ И ВЫВОДИМЫЕ ТИПЫ 503

Для расширения стандартного набора операций целого типа языковыми


образами арифметических машинных команд (например, командами сдвига)
есть те же мотивы, что и для булевского типа: система команд конкретного
вычислителя.
Современные языки иногда предлагают средства управления представле-
нием различных числовых типов, ориентированные на повышение точности
и корректности вычислений. Это нужно, например, для обеспечения перено-
симости программ и для гарантированных вычислений6 . Применительно к
целым средства такого рода сводятся к заданию области значений для типа.
Вот как это делается в языке Ada:

– область фиксирована
type MyInt1 is INTEGER range 0..1000;

– область динамически вычисляется!!!


type MyInt2 is INTEGER range 0..V;

– любое неотрицательное целое,


type MyInt3 is INTEGER range 0..INTEGER’LAST;

– допустимое реализацией
Транслятор использует информацию об областях значений следующим
образом. Описание языка предписывает системам программирования обес-
печить математическую корректность вычислений в заданной области, но
при условии, что значения не превышают допустимых величин INTEGER’FIRST
и INTEGER’LAST. Для малых значений это указание означает, что можно ис-
пользовать короткие целые, для больших значений предписывается модели-
ровать арифметику, если вычислитель не позволяет соответствующие дей-
ствия выполнить стандартными командами.
Интересен случай динамических вычислений области. Он осмыслен для
динамически возникающих переменных и констант, область которых опре-
деляется в момент появления описания их типа (после этого, к примеру, из-
менение переменной V не повлияет на диапазон данных, тип которых опи-
сывается как MyInt3).
В Аде есть еще одно полезное свойство определения типов, применимое
и к типу целых. Можно описать новый тип, который имеет поведение уже
существующего типа, но им не является:
type MyNewInt is new INTEGER;
Гарантированные (доказательные) вычисления — вычислительные эксперименты, стро-
го обосновывающие оценки некоторой величины, например, α > 2 или 0.49999 < disp <
6

0.50001.
504 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

Это описание позволяет определять переменные и константы, которые, к при-


меру, нельзя складывать с целыми, но можно между собой. Транслятор дол-
жен контролировать указанное различие. Применение данного средства на-
кладывает, например, запрет на «сложение ‘яблок’ и ‘людей’».7
Это отличает новые типы от тех, которые описываются без специфика-
тора new и называются в Ada подтипами одного и того же типа. Примеры
подтипов: MyInt1, MyInt2 и MyInt3. Применительно к целым подтипы — это
просто отрезки “самого большого целого типа”, и неявные приведения в оба
направления для них возможны, тогда как для новых типов приведение мо-
жет задаваться только явной квалификацией (указанием имени типа).
Оперирование с новыми типами и с областями предусматривается не толь-
ко для целых типов. Эти средства, безусловно, являются конструкторами ти-
пов. Они до некоторой степени отвечают потребности программистов при-
ближению записи программ к прикладному уровню, а значит, служат целям
повышения абстрактности программирования на данном языке.

9.2.5. Вещественные типы

Системы команд большинства компьютеров обеспечивают возможности


оперирования с числами с плавающей точкой. Во многих случаях предусма-
тривается и оперирование с числами с фиксированной точкой. Естественно,
эти возможности предоставляются в языках с помощью специальных базо-
вых типов, именуемых как вещественные (real) в двух видах: плавающие
(float) и фиксированные (fixed). Первые характеризуются относительной точ-
ностью, задаваемой как число значащих цифр, вторые — абсолютной точно-
стью, задаваемой с помощью минимальной величины, на которую они могут
различаться. Другая характеристика — это область допустимых значений,
т. е. их диапазон: минимальное и максимальное представимые числа.
Уже из этих характеристик следует, что о вещественных типах нельзя го-
ворить как о математических вещественных числах. В то же время, на соот-
ветствующем уровне абстракции полезно не замечать различий, поскольку
представление вещественных чисел в виде
( знак_числа, знак_порядка : (’+’, ’-’);
порядок : INTEGER range 0..размер_порядка;

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

размерностей — рекламный трюк, используемый в руководствах по Ada.


9.2. БАЗОВЫЕ И ВЫВОДИМЫЕ ТИПЫ 505

мантисса : INTEGER range 0..размер_мантиссы )

совсем не отражает то, о чем думают, когда оперируют с вещественными чи-


слами.
Вместе с тем, совсем игнорировать представление нельзя, т. к. иначе на-
капливаемые ошибки вычислений могут быстро напомнить о неточной при-
роде оперирования с вещественными числами. Эта противоречивая двой-
ственность данного типа объясняет и наличие двух вариантов вещественного
типа в языках программирования, и другие особенности средств оперирова-
ния с ними. В частности, согласно с математическому смыслу, вещественные
значения плотно упорядочены отношением “<”:

∀p∀q(p < q ⇒ ∃x(p < x&x < q)),

но конечность множества всех машинных вещественных чисел в принципе


позволяет говорить о следующем и предыдущем числе для данного значения.
Однако математическая бессмысленность этих понятий не позволяет апелли-
ровать к подобным свойствами. Переход к дискретному континууму привел
бы к утрате важных качеств машинного моделирования реальных процессов.
Это более важно, чем неоднозначность определения следующего и предыду-
щего числа и чем различия вычислений, которые бы проявились на разных
вычислительных машинах.
Говорить об отрезках вещественных значений можно, но в несколько ином
смысле, чем для перечислений. В частности, необходим запрет на все сужде-
ния и опирающиеся на них средства, которые связаны с мощностью отрезка.
Отрезкам отводится роль задания областей, в которых лежат значения под-
типов вещественного типа. Это средство представлено в Ada:
– область фиксирована,
– число значащих цифр = 10
type MyFloat is digits 10 range 0..1000;

– область фиксирована,
– числа различимы с шагом 0.01
type MyFixed is delta 0.01 range 0..1000;

За счет подобных описаний можно добиться передачи программе сведе-


ний о точности представления вещественных чисел, но, однако, это не гаран-
тирует от ошибок вычислений и их накапливания. Например, оператор

(X_Fixed_1 — переменная, описанная как фиксированная с одной значащей


X_Fixed_1 := 1.1 + 2.1 + 3.1;

цифрой после десятичной точки) может вызвать ошибку в ‘гарантирован-


ном’ дельтой знаке, если внутреннее представление чисел реализуется на
506 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

четырехбитных регистрах и сложение осуществляется при помощи команд


с плавающей точкой. Примечательно, что Ada разрешает такое, поскольку
разработчики этого языка были вынуждены следовать требованиям реали-
зации поддержки представления с фиксированной точкой. Решить же про-
блему поддержки вычислений выражений с фиксированной точкой в нота-
ции этого языка не представляется возможным: поскольку литеральные изо-
бражения значений двух вариантов вещественного типа совпадают, нельзя
узнать, какой вид вычислений подходит для того или иного выражения. Если
это узнать можно, то вполне рационально для фиксированных веществен-
ных выражений воспользоваться (точной!) целочисленной арифметикой пу-
тем превращения значений в целые числа (нужно просто умножить число
на соответствующую степень десяти или, что то же, перенести точку вправо
на несколько знаков) с последующим обратным превращением. Так реали-
зуется фиксированная арифметика в языке FORT. При использовании веще-
ственных с фиксированной точкой следует четко осознавать, что в результате
вычислений могут появляться переполнения.
Арифметика с плавающей точкой более устойчива к проблеме перепол-
нения. Эта форма вычислений реализуется в оборудовании практически все-
гда (исключения составляют специализированные процессоры). Но точность
вычислений приходится контролировать специально.
В языках вслед за аппаратурой популярна конструкция длинных, или двой-
ной (тройной и т. д.) точности вещественных. Это удобно, когда конкрет-
ный вычислитель действительно обладает соответствующими форматами.
Но если они не предусмотрены, то обычный компилятор просто проигнори-
рует указание кратности длины (в Алголе-68 прямо предписывается посту-
пать именно так). В результате у программиста сохраняется только иллюзия
повышенной точности вычислений.
Как уже упоминалось, бессмысленно говорить о перечислении всех ве-
щественных значений (для типа с фиксированной точкой это возможно, но
только если его арифметика реализована корректно). Тем не менее, рассма-
тривая задание области значений как пары чисел, максимально и минималь-
но допустимых, можно считать определенным вложение такой области в “аб-
страктное” перечисление-призрак. В этом случае исходный набор операций,
определенный для перечислений, сужается: из него исключаются succ и pred
как противоречащие аксиоме непрерывности.
Для вещественных типов построение новых типов с наследуемым от ро-
дительского типа поведением, но не допускающих неявные приведения, не
менее актуально, чем для типа целых. По этой причине конструкция
9.2. БАЗОВЫЕ И ВЫВОДИМЫЕ ТИПЫ 507

в языке Ада играет наиболее важную роль, когда T — один из веществен-


type MyNewT is new T;

ных типов или его подтипов. Она дает возможность программисту осуще-
ствлять контролируемое компилятором “разделение мер”, фигурирующих в
содержательной задаче. Таким образом, имеет место удобное и недорогое
средство дополнительного контроля. Почему это средство редко встретишь
в практических языках? Во многом это объясняется традициями, но не толь-
ко они причина. К примеру, для С/С++ с его концепцией приведений просто
нет места для так называемых “новых” типов, которые все равно останутся
полностью эквивалентными свои базовым типам. В этом плане некоторые
дополнительные средства контроля предоставляются в рамках методологии
объектно-ориентированного программирования.
Что касается абстрактно-синтаксической структуры выводимых типов, то
в качестве примера рассмотрим рис. 9.1, на котором в приведена конструк-

... ...

Переменная Переменная

Тип = отрезок int


Тип = int range

first = 1 last = 10

a) b)

Рис. 9.1. Описание выводимого типа

ция <Переменной>, имеющей в качестве значения атрибута <Тип> один из


встроенных типов (a), и <Переменной> выводимого типа (b). Таким образом,
здесь развивается лишь ветвь, соответствующая атрибутам типа.
В ходе обработки описания при вычислении конструктора достраивает-
ся ветвь атрибутов компоненты, а при обработке обращения (к переменной,
508 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

процедуре и т. п.) она используется. В этих случаях достаточно заранее по-


строенных шаблонов добавляемых ветвей.

§ 9.3. СТРУКТУРНЫЕ ТИПЫ

С абстрактной точки зрения структура рассмотренных в предыдущем раз-


деле типов не существует. Это означает, что их битовое представление во
всех нормальных случаях игнорируется: ни одна операция в разумно устро-
енном языке не дает доступ к отдельным битам этого представления. Впро-
чем, с этой точки зрения есть смысл проанализировать так называемый ма-
шинно-ориентированный подход, достойным представителем которого явля-
ется С/С++. По существу в них нет неструктурных типов, а то, что какие-
то объекты называются целыми, литерными и т. п., означает только одно: в
большинстве контекстов они могут рассматриваться такими. Но когда нуж-
но, разработчик программы имеет право мгновенно изменить точку зрения
на объект. Строго говоря, здесь имеет место использование структурных кон-
структоров, в частности, конструктора объединения (см. § 9.3.3) в худшем
из вариантов. Это следствие принципа однородности памяти. Все меняет-
ся, когда используется, к примеру, тегированная память. Для нее смена типа
хранимых данных в регистре — нормальная операция машинного уровня, и,
как следствие, приходится вводить в машинно-ориентированные языки стро-
гую типизацию данных (жесткие условия для неявных приведений — см. на-
пример, Эль-76 — автокод многопроцессорного вычислительного комплекса
Эльбрус).
В противоположность неструктурным структурные типы предусматрива-
ют доступ к компонентам, из которых они состоят. В этом смысле такие ти-
пы всегда задаются конструкторами структур данных со стандартными (для
конструктора) селекторами компонент.
Абстрактная точка зрения на структурные типы требует рекурсивного опре-
деления их конструкторов: компоненты — это объекты произвольных типов,
в том числе и такие, которые построены с использованием данного конструк-
тора. Но здесь не появляется “дурная” бесконечность: цепочка вложенности
применения конструкторов всегда статическая и всегда завершается базовы-
ми (неструктурными) типами (сравнить с бекусо-науровой формой). Иное
дело, когда рекурсивно определяется тип в программе. Это тот случай, ко-
гда нельзя удовлетворительно трактовать возможность компонента объекта
иметь тот же самый тип, что и определяемый. Обычное соглашение для дан-
9.3. СТРУКТУРНЫЕ ТИПЫ 509

ной ситуации — трактовка рекурсивности как указателя на объект определя-


емого типа. Поэтому, как правило, в языках для такой вложенности исполь-
зуются явные обозначения указателя.
Несколько слов о соотношении понятий типа и структуры данных. Струк-
тура — это определение того, из чего и как строятся данные некоторого типа.
Таким образом, типом данных логично называть множество значений, ко-
торые задаются одним и тем же конструктором структуры данных. Следуя
принципам абстракции, нужно различать употребление значения как струк-
турного объекта, т. е. состоящего из более простых элементов, и как единого
объекта оперирования, когда сведений о структуре значения не требуются.
В первом случае мы говорим о структурах данных, а во втором — о типах.
В точном соответствии с таким разграничением можно говорить о структу-
ре и типе переменной, способной принимать значения. Но по отношению к
переменной чаще употребляется термин тип, поскольку основные действия
с переменной — принимать и извлекать значение в целом — относится к аб-
страктному, а не структурному рассмотрению значения.
С абстрактной точки зрения и значения, и переменные одного типа ха-
рактеризуются тем, в вычислениях каких операций они могут участвовать
как аргументы и/или результаты (здесь понятие операции трактуется рас-
ширительно: оно охватывает как сами операции, так процедуры и функции,
определяемые для данного типа). В этом смысле, имея в виду традицион-
ные способы спецификации параметров, можно отождествить тип значения
с областью значений соответствующего аргумента операции. То же про тип
результата.
Данная характеристика внешне очень похожа на математическое опреде-
ление областей определения и значения функции. Однако стоит указать на
существенное отличие. Для математической функции эти области точно ука-
зывают на то, из чего может быть вычислена функция, и что можно получить
в итоге таких вычислений. Программистские области аргументов и резуль-
тата — это чисто синтаксические понятия, никакого отношения к функци-
ональному отображению не имеющие. Для согласования спецификаций па-
раметров с математическими областями в языке должны быть средства опи-
сания таких областей, т. е. гибкие возможности оперирования с типами. В
частности, требуются динамические вычисления над типами как объектами
оперирования (например, для выражения соотношений). Однако традицион-
ные системы типов языков программирования исходят из концепции стати-
ческого контроля типов: вынесение проверки типовой правильности употре-
бления переменных и значений на уровень трансляции. Эта чисто синтакси-
510 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

ческая позиция просто не оставляет места для задания динамических вычи-


слений типов.
9.3.1. Наборы компонент

Определение 9.3.1. Конструктор общего вида, назначение которого есть опре-


деление набора или кортежа именованных значений-компонент, называется
набором компонент. Если T1, . . . , Tn — типы компонент, а Ki : Ti — обозна-
чение того, что компонента с именем Ki имеет тип Ti, то кортеж (упорядочен-
ных) компонент обозначается как:

а набор (неупорядоченых):
(K1 : T1; . . . ; Kn : Tn)

Конец определения 9.3.1.


(K1 : T1, . . . , Kn : Tn)

Это определение не полно с точки зрения программирования, потому что в


нем ничего не сказано ни о том, может ли меняться количество компонент и
как оно задается, ни о том, что такое селектор.
Чтобы провести требуемые уточнения, необходимо разграничить случаи:
• статический набор, когда число компонент фиксируется при опреде-
лении;
• динамический набор, когда число компонент фиксируется как значение
некоторого объекта и не может быть изменено после применения кон-
структора для определения структуры данных;
• гибкий набор, когда число компонент при определении не фиксирует-
ся, а объекты с конструируемой структурой данных могут в динамике
выполнения программы состоять из разного числа компонент.
Для определения селекторов следует выделить атрибуты, идентифицирую-
щие компоненты набора, возможно, через посредство некоторых функций,
зависящих от этих атрибутов:
a) порядковый номер компонент в наборе, если набор упорядочен;
b) статическое имя компонент в наборе, если компонентам приписаны фик-
сированные имена;
c) динамическое имя компоненты в наборе, если имена компонент могут из-
меняться при использовании объектов, построенных с помощью конструк-
тора набора;
9.3. СТРУКТУРНЫЕ ТИПЫ 511

d) тип компоненты, если по крайней мере некоторые из компонент разли-


чаются своими типами;
e) значения теговых компонент, которые могут давать идентификационные
сведения о других компонентах;
f) специальные атрибуты компонент, смысл и назначение которых для се-
лектирования на абстрактном уровне набора компонент не определяются.
Теперь можно дать абстрактное определение селектора.
Определение 9.3.2. Универсальный селектор — пара частичных функций
Sv и Sa с одной и той же областью определения. Sv отображает некоторое
подмножество значений идентифицирующих атрибутов компонент в значе-
ния компонент набора, Sa отображает то же подмножество в переменные,
содержащие значения Sa .
Конец определения 9.3.2.
При желании можно считать результат Sa адресом, что соответствует трак-
товке структурных конструкторов в языке С/С++. Чаще всего обе эти функ-
ции рассматриваются совместно, и выбор одной из них определяется синтак-
сической позицией: что требуется в данный момент вычисления програм-
мы — адрес или значение. Иными словами, можно говорить о вычислении
компоненты Sa , об извлечении значения компоненты Sv и о приведении ком-
поненты, определенной функцией Sa , к значению Sv . В тех случаях, когда
язык позволяет связывать с указанными вариантами доступа к компонентам
различные алгоритмы, явно определяются две функции доступа: для чтения
и для записи атрибута-значения. Это называется программируемым досту-
пом к данным. Примитивнейший вид программируемого доступа демонстри-
руют компоненты со спецификатором property в объектно-ориентированных
языках, где задаются функции Sa и Sv .
Программируемый доступ является одним из средств повышения уровня
абстрактности. В самом деле, когда пишутся программы для Sa и Sv , опре-
деляются два алгоритма, работу которых допустимо трактовать и конкретно
синтаксически оформлять как активизацию селекторов. При этом можно да-
леко уйти от понятия фактического доступа к реальным данным.
Разумное языковое решение программируемго доступа — специальная
конструкция, в которой можно описать свой локальный контекст и два ал-
горитма селектирования, работающие в этом контексте. Удобно параметри-
зировать эту конструкцию в соответствии с абстракцией программируемого
512 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

доступа. Например, можно определять скалярный, векторный, матричный и


другие виды доступа, которые на уровне использования будут оформлять-
ся подобно обращению к соответствующим структурам данных. Это было
в значительной степени реализовано в таких языках программирования, как
Bliss, Ярмо, Эль 76.
К сожалению, в современных объектно-ориентированных языках сред-
ства программируемого доступа даются в самом примитивном виде. Причи-
на тому — банальная неосведомленность разработчиков сегодняшних объ-
ектно-ориентированных языков8 .
Какое оперирование с компонентами можно предусмотреть на уровне уни-
версального конструктора наборов? Прежде всего, приходят на ум средства
циклической обработки: совместный цикл for all для всех компонентов набо-
ра. Но такой цикл целесообразен, лишь если все компоненты имеют одина-
ковые типы и подобную прагматику. Ведь цикл предполагает однородность
выполняемых действий, а компоненты набора могут иметь и разные типы, и
совершенно разную прагматику, что уничтожает все достоинства цикла.
Безусловно, полезна функция, вычисляющая размер и характеристики на-
бора. Часто ее суррогат предоставляется системами программирования как
зависимая от реализации функция размера sizeof, вычисляющая размер па-
мяти, необходимой для размещения набора.
Конструктор набора компонент является полностью абстрактным, и в ре-
альных программистских построениях он почти никогда не появляется явно.
Он может быть приспособлен для выражения любых употребляемых в прак-
тике программирования структурных конструкторов. Для этого надо лишь
конкретизировать то, что представлено в конструкторе набора в обобщенном
виде. Некоторые трудности возникают только при представлении рекурсив-
ных структур данных, поскольку их определение требует привлечения до-
полнительных понятий, связанных с семантикой выполнения (в частности,
памяти, адресации и др.).
Поэтому вместо конструктора набора используются конкретные конструк-
торы, которые могут рассматриваться как конкретизации абстрактного кон-
структора набора компонент для частных вариантов построения структур-
ных типов. При конкретизации уточняются функции Sv и Sa и вводятся необ-
ходимые ограничения. Кроме того, определяется конкретно-синтаксическая
форма конструктора и его селекторов.
8
Ignorance is power.
(G. Orwell, ‘1984’)
9.3. СТРУКТУРНЫЕ ТИПЫ 513

Абстрактно-синтаксическая форма любого такого конкретного конструк-


тора является ограничением абстрактно-синтаксического представления кон-
структора набора компонент, а поведение абстрактного вычислителя для всех
конкретных конструкторов наследуется (с ограничениями) от конструктора
набора компонент. Поэтому имеет смысл описать это поведение один раз в
общем виде. Это построение является типичным примером применения при-
зраков (в данном случае — конструктора набора компонент, рассматриваемо-
го в качестве призрака) для формулировки общих свойств различных конкре-
тизаций абстрактного понятия.
Предварительно опишем те аспекты поведения абстрактного вычислите-
ля, которые являются общими для оперирования с любыми языковыми кон-
струкциями, имеющими тип: для переменной, процедуры и др. Как было уже
замечено, у такой конструкции имеется атрибут <Тип>, который может быть
извлечен абстрактным вычислителем для тех или иных действий.
Для встроенных в язык типов единственное действие абстрактного вы-
числителя с атрибутом <Тип> — извлечение его значения, т. е. типа кон-
струкции. Все остальное, что может потребоваться (к примеру, активизация
приведения) стандартизовано языком и выполняется как атомарная коман-
да вычислителя. Уже для выводимых типов атрибут <Тип> более сложен: он
имеет составляющие, которые доступны для действий вычислителя. Однако
и здесь нет смысла определять оперирование специально, поскольку струк-
тура атрибута <Тип> зафиксирована языком и никогда не меняется. Так что
средства работы абстрактного вычислителя с базовыми и выводимыми типа-
ми, в частности, задание значений составляющих и использование значений
составляющих, можно рассматривать как библиотечные функции, специфи-
цированные языком.
Ситуация с конструкторами, порождающими структурные типы, несколь-
ко сложнее, чем для выводимых, поскольку в ходе обработки описания типа
при вычислении конструктора должна быть достроена ветвь, которая опреде-
ляется программистом. Более того, тип компоненты может быть представлен
именем типа, описанного ранее (в некоторых языках, возможно, что позже),
или же он описывается непосредственно в данном конструкторе. Во всех слу-
чаях должна быть обеспечена доступность селекторов компонент.
Как и ранее, мы не будем вдаваться в детали распознавания требуемой
структуры по конкретному синтаксису. В соответствии с определением на-
бора компонентов в результате распознавания должно быть построено место
присоединения дерева набора компонентов у атрибута <Тип> (см. рис. 9.2, где
это место определяется для <Переменной>), к которому подсоединен один
514 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

...

Переменная

Тип = Конструктор
Статический набор
Динамический набор
Гибкий набор

Рис. 9.2. Структурная переменная

из вариантов структуры набора (см. рис. 9.3). Схемы для компонент набора
приведены на рис. 9.4.
Значением атрибута <Тип> может быть структурный или неструктурный
тип, представленный своим деревом либо именем. Если язык допускает толь-
ко статические типы, то в результате трансляции имена типов могут быть
заменены соответствующими им деревьями, которые используются для кон-
троля правильности задания оперирования с типизированными данными и
при необходимости для встраивания подпрограмм нужных приведений. В
статически типизированном языке после выполнения этих действий типовая
информация нигде не используется, и надобность в атрибуте <Тип> исчезает.
Для языков, которые допускают динамические вычисления с типами (хотя
бы в самой рудиментарной форме) атрибут <Тип> не может быть ликвидиро-
ван. Он (быть может, в трансформированной форме) используется в процессе
вычислений. Например, в объектно-ориентированных языках это ссылка на
таблицу виртуальных методов объекта.
Новым по отношению к рассмотренным ранее абстрактно-синтаксиче-
ским структурам у конструктора набора компонент является то, что дере-
во строится как часть атрибутной информации вершины. Эта ситуация ана-
логична процедурам: в данном случае атрибут фиксирует структуру, кото-
рая должна использоваться при вычислениях в качестве программы для аб-
страктного вычислителя.
Все теоретически возможное разнообразие вариантов набора компонент
в нынешних языках не используется. Конкретные формы конструкторов дан-
9.3. СТРУКТУРНЫЕ ТИПЫ 515

Конструктор Конструктор Конструктор


Динамический Динамический
Статический упорядоченный неупорядоченный
набор набор набор

+ +
Компонент 1 … Компонент N

Компонент Компонент Компонент Компонент


Схемы для каждого N
(a) (b) (C)

Конструктор Конструктор
Динамический Динамический
упорядоченный неупорядоченный
набор набор

* *
Компонент

(d) (e)

Рис. 9.3. Структура набора

ных выбираются из соображений согласованности с операционными едини-


цами программ. Используемые на практике варианты — это то, что действи-
тельно необходимо для представления данных, адекватного стилям струк-
турного програмирования и ООП.

9.3.2. Записи

Этот конструктор представляет в программах структуру, в которой реа-


лизуется математическое понятие декартового произведения множеств зна-
чений компонентов. В программировании он называется конструктором за-
писей (Pascal) или конструктором структур (С/С++, Алгол-68). Компонен-
ты записи называются полями. Каждое поле имеет собственное имя и явно
516 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

Компонент набора Компонент набора


Неструктурный Структурный
компонент компонент

? ? ? Конструктор
Имя Тип Имя
комп онента компонента компонента Статический набор
Динамический набор
Гибкий набор

Рис. 9.4. Схемы для компонент набора

указываемый тип значений, которые оно может принимать. Как следствие,


число компонентов-полей всегда статически определено.
Существуют две точки зрения на понятие записи, касающиеся упорядо-
ченности ее полей. Первая отражает абстрактный подход, согласно которо-
му совокупность полей для их использования не требует знания порядка их
следования в описании, вторая связывает порядок следования полей в опи-
сании с их порядком размещения в памяти, т. е. восходит к реализационному
представлению. В С/С++ структуры — это упорядоченный набор, а стандарт
языка Pascal разрешает при реализации размещать поля записи в любом по-
рядке. Но независимо от представления, порядковый номер поля не исполь-
зуется для селектирования. По причинам, описанным в § 9.3.1, никогда не
предлагаются средства организации цикла по полям: запись объединяет, как
правило, разнотипные и прагматически разнородные компоненты, их обра-
ботку не получается описывать единообразно.
Таким образом, селекторы полей для записи сужают возможности аб-
страктных селекторов: единственный их идентификационный аргумент —
это имя поля. Традиционное обозначение для такого селектора
<имя записи> “.” <имя поля>.
В С/С++ есть возможность наложения записи (структуры) на произвольную
область памяти. Она предоставляется как результат вычисления указателей.
В этом случае можно опускать имя записи при задании селектора, а вместо
него и последующей точки писать “->”. Такой способ может приводить к дву-
смысленности, когда указатель способен ссылаться на экземпляры различ-
9.3. СТРУКТУРНЫЕ ТИПЫ 517

ных записей.
Средства оперирования для записей расширяются за счет оператора при-
соединения with, который позволяет однократно указывать запись, к которой
применяются селекторы для извлечения полей:
with <имя записи> do <оператор /* фрагмент */>
В результате имена полей пополняют контекст фрагмента, подчиняясь прави-
лам локализации имен. В частности, имена полей становятся приоритетными
при поиске определяющих вхождений идентификаторов в программе.
Оператор присоединения является средством скорописи для языка, но
очень часто он помогает повысить выразительность программ. Для фрагмен-
та после do он задает дополнительный контекст выполнения, причем поиме-
нованный (именем записи). Т. е. фрагмент может использовать поля в каче-
стве добавочной системы понятий. Полезность этого качества хорошо по-
нимали разработчики языка Симула-67, появившегося, когда записей еще не
придумали. Они отразили его в конструкции префикс-класса для (операцион-
ного) блока, тем самым дали возможность наиболее выразительно предста-
влять в программе средства, описанные при определении объектов. В С++,
как и в других языках того же рода, объектная ориентированность строит-
ся на базе понятия записи (структуры), но нет присоединений, которые мог-
ли бы в манере префикс-классов явно выделять то, что доступно от объекта
(в Object Pascal такая возможность есть). Мотивируется это тем, что из-за
возможного перемешивания селектирования и присоединения трудно разби-
раться с контекстами фрагмента, которые пополняются без явного разграни-
чения, какие имена к чему относятся.
Представление записей в программе допускает следующие варианты:
• стандартное, когда каждое поле получает собственный адрес. В этом
случае время доступа к полю оказывается минимальным;

• со свободной упаковкой, когда привязка полей к регистрам осуществля-


ется с учетом побайтового их разбиения, что несколько замедляет до-
ступ по сравнению со стандартным случаем, если вычислитель не до-
пускает байтовой адресации;

• с жесткой упаковкой, когда размер памяти, выделяемой под поле, стро-


го соответствует необходимому для размещения самого большого зна-
чения числу разрядов;

• ссылочное, когда некоторые компоненты записи задаются как другие


записи уровня представления, состоящие из двух полей:
518 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

– указатель на представление поля исходной записи,


– представление поля исходной записи, которое размещается в обла-
сти памяти, доступной через указатель.
В ссылочном представлении поле исходной записи заменено указателем на
то место, где хранятся его значения. Это удобно в двух случаях:
• когда для разных экземпляров записи значения поля, требующего мно-
го памяти, могут повторяться,
• когда память, нужная для значения поля, не имеет фиксированного раз-
мера.
В качестве примера первого рода можно рассмотреть записи в базе дан-
ных, а в качестве примера второго рода — поля типа variant.
На абстрактном уровне рассмотрения, как и всегда, представления нераз-
личимы. Поэтому, к примеру, в ссылочном представлении указатель на по-
ле является невидимым и не участвует в оперировании. Задача реализации
представления состоит в том, чтобы обеспечить доступ к полям на абстракт-
ном уровне.
И опять мы сталкиваемся с проблемой, обозначенной при сопоставлении
концепции абстрактных типов данных с объектно-ориентированным програм-
мированием. Корректно решить в языке указанную задачу, игнорируя требо-
вание существования функции абстракции (см. п. 9.1.5), не представляется
возможным.
В связи с типом записей полезно проследить аналогию между двумя ви-
дами структур программы: операционной и данных, т. е. указать параллели
и расхождения между понятием записи и последовательным (совместным)
выполнением операторов.
(1). Оператор как элемент операционной абстракции играет ту же роль, что
и поле, рассматриваемое как элемент абстракции данных.
(2). Последовательность операторов как синтаксическая единица аналогич-
на списку полей записи, конкретизирующих список компонентов уни-
версального набора.
(3). Исполнение последовательности операторов может быть последователь-
ным, параллельным, а также параллельно-совместным и последователь-
но-совместным. Именно последний вариант соответствует понятию про-
извольного доступа к полям записи.
9.3. СТРУКТУРНЫЕ ТИПЫ 519

(4). Именование. Для операторов — это метки, к которым можно переходить.


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

(5). Любой переход к оператору для его выполнения аналогичен вычисле-


нию селектора записи.

(6). Время жизни оператора есть его выполнение после перехода к нему. Вре-
мя жизни записи шире, оно включает выполнение всего контекста, в ко-
тором запись описана. Здесь расхождение полное.

(7). Активность оператора во время его жизни (при исполнении) противопо-


ложна пассивности полей записи.

(8). Операторы используют данные, тогда как записи не используют операто-


ры. Однако при объектно-ориентированном подходе или при функцио-
нальном программировании операторы в некоторой степени становятся
данными.

9.3.3. Объединения

Определение объединения мало отличается от записи на уровне конкрет-


ного синтаксиса, к примеру, в С/С++ оно задается служебным словом union
вместо struct. Однако семантика этих двух конструкторов принципиально
различна. При объединении явно указывается, что компоненты в реализа-
ционном представлении объединения сливаются, налагаются друг на друга.
Соответственно, здесь речи нет о селектировании всех объединенных ком-
понент как отдельных объектов, можно выделить лишь один из вариантов
налагаемых компонент.
Каким способом можно обеспечить эту установку, используя возможно-
сти универсального конструктора набора компонент? Объединение строится
как статический неупорядоченный набор. Из этого следует, что для опреде-
ления селекторов можно воспользоваться именами компонент, типами ком-
понент или значениями теговых компонент. Соответственно есть три вида
объединений.
• Размеченное — вводятся разные наименования полей, употребление
которых в операции селектирования определяет трактовку компонен-
520 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

та (case <тип> языка Pascal; считается, что множество значений этого


типа достаточно для отображения в него объединяемых полей). При-
мер:
union ( A : T1, B : T2 );

• Неразмеченное, когда одно и то же поле трактуется в разных смыслах в


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

• Тегированное, когда используется специальное поле, значение которо-


го указывает на то, как трактуются объединяемые компоненты (case
<описатель поля> языка Pascal):

зн1 : A : T1,
case tag : Tp of

зн2 : B : T2);
end;

Легко видеть, что тегированное объединение — это запись, состоящая из


двух полей:
(<тег>, <размеченное или неразмеченное объединение>).
Этот вид объединений выделяется явно по той причине, что он наиболее на-
дежен и удобен в работе9 .
Есть еще один вид объединений, часто предлагаемый в языках програм-
мирования. Это так называемые вариантные записи — комбинация двух кон-
структоров: записей и объединения. Тег, если он есть, последнее поле перед
объединением, которое размещается после постоянной (безвариантной) ча-
сти записи (причины такого ограничения — чисто реализационные).
Таким образом, функции Sv и Sa дают возможность по-разному тракто-
вать одну и ту же память в зависимости от того, как идентифицируется ком-
понента.
9
По причине изоморфности логической дизъюнкции в конструктивной логике.
9.3. СТРУКТУРНЫЕ ТИПЫ 521

С прагматической точки зрения здесь речь идет о том, что программист


принимает обязательство никогда не обращаться к объединяемым компонен-
там одновременно в нескольких смыслах (т. е. с разными типами). Языки
программирования поддерживают эту точку зрения, определяя указанные вы-
ше виды объединений. В этом суть обсуждаемой структуры данных.
Ничто не запрещает (хотя и не поощряет!) реализацию, когда в памяти
действительно представлены все компоненты объединения. Но в этой реали-
зации придется каждое присваивание значения одной компоненте сопрово-
ждать подходящим изменением значений других компонент, что не соответ-
ствует прагматике и логическому смыслу объединения.
В соответствии со смыслом объединения, правильно располагать объеди-
няемые компоненты по одному и тому же адресу и трактовать их так, как того
требует ситуация. Однако это решение не со всем сочетается. Так, обычное
для него автоматическое приведение значений совсем не то, что чаще всего
нужно. Наглядный пример: перевод декартовых координат в полярные, когда
оба представления — пара вещественных чисел, а потому автоматическое
приведение тривиально, но лишено всякого смысла.
При объединении неприводимых значений обычно память выделяется по
максимуму либо используется ссылочная структура (пример — тип variant).
Как оформляются селекторы компонент в языках программирования? В
зависимости от того, какие виды объединений предусматриваются, возмож-
ны следующие решения:

• имя поля — применяется для размеченного объединения;

• тип поля — неразмеченное объединение (обычно выглядит как явное


приведение);

• тег — обычно сопровождается оператором выбора вариантов действий.

В контексте обсуждения объединений уместно рассмотреть некоторые сред-


ства распределения памяти между программными единицами: общие блоки,
см. определение 8.3.1. Например, на языке FORTRAN при описании процеду-
ры можно указать, что некоторые ее данные являются общими, т. е. принад-
лежащими к определенному COMMON-блоку (см. пример 8.3.2). При этом
структура общих данных может быть различна. Язык согласует структуры
на уровне размещения данных в COMMON-блоке так, что при использовании
их разными процедурами происходит обращение к одним и тем же (“общим”)
адресам. Отметим, что дисциплинированное использование COMMON-блоков,
522 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

когда данные налагаются, но не перекрывают друг на друга, есть объедине-


ние.10 В этом случае имя COMMON-блока — это имя конструируемой струк-
туры данных, имена общих данных в различных процедурах — поля-вариан-
ты объединения. Вопрос о выборе нужного варианта даже не стоит, посколь-
ку в каждой процедуре виден лишь один вариант COMMON-блока. Хотя син-
таксически (и даже семантически, если иметь ввиду недисциплинированное
использование COMMON-блоков) все это не выглядит как применение уни-
версального конструктора, суть от этого не меняется.
Рассмотрим вариантные записи в сопоставлении с оператором выбора.
Здесь, как и для ранее приведенной аналогии между записями и последова-
тельным совместным выполнением операторов можно найти много общего
и отметить расхождения.

(1). Основа определения аналогии та же, что в предыдущем случае. В обоих


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

(2). Последовательность операторов как синтаксическая единица аналогич-


на списку полей записи, конкретизирующих список компонент универ-
сального набора.

(3). Исполнение последовательности операторов, входящих в состав одно-


го из вариантов оператора выбора, исключает выполнение остальных (в
С/С++ приходится это моделировать). Именно так осуществляется вы-
бор поля объединения, т. е. с гарантиями единственности варианта до-
ступа к совмещенным полям записи.

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

(5). Любой переход к оператору для его выполнения аналогичен вычисле-


нию селектора.
Например, при обсуждении массивов мы увидим, что за счет наложения векторов на ма-
трицы можно добиться повышения эффективности работы с разреженными массивами.
10
9.3. СТРУКТУРНЫЕ ТИПЫ 523

(6). Соответствие между временем жизни оператора и временем жизни ва-


риантной записи то же, что и в предыдущем случае (простых записей):
расхождение полное.

(7). То же про активность оператора во время его жизни (при исполнении) и


ее противоположность пассивности полей записи.

(8). Как и в предыдущем случае, имеется асимметрия: данные используются


оператором, а оператор невозможно использовать в вариантной записи.
Однако расхождение здесь сильнее, т. к. нет аналога объектно-ориенти-
рованному расширению понятия вариантной записи.

Если говорить о восходящем построении структур данных, то понятно, что,


наряду с перечислениями, записи и объединения — это самые простые кон-
структоры структур данных. На это указывал еще Хоор в статье «Структур-
ная организация данных» [29]. При условии, что компоненты записей и объ-
единений элементарны, справедливо следующее:

a) каждый элемент данных занимает фиксированный и, как правило, очень


небольшой объем памяти, который линейно зависит от длины определе-
ния;

b) необходимая память может быть выделена экономно, без использования


сложных механизмов распределения памяти;

c) наиболее частые операции достаточно просто и естественно реализуются


с помощью команд вычислительных машин или их последовательностей;

d) для представления этих структур не требуются указатели (ссылки, адре-


са), а потому облегчается решение задач обмена между внешней и опера-
тивной памятью;

e) выбор естественного представления прост.

Но большинство из указанных преимуществ рассыпается, когда базой для


этих конструкторов являются другие, возможно, более сложные типы.
524 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

9.3.4. Массивы

Упорядоченный набор компонент одного и того же типа, для которых не


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

• упорядоченность компонент, и как следствие,

• селектирование с помощью порядкового номера компонента — индек-


сирование.
Другой взгляд на массивы — порядковый номер рассматривается как часть
(атрибут) компоненты, который вычисляется при селектировании. Он несколь-
ко абстрактнее и позволяет переносить понятие массива на случаи, когда в
качестве индекса используются не целые, а какие-либо иные значения, на-
пример:
• Перечисление, в частности, их отрезки (Pascal).

• Отрезок любого линейно упорядоченного типа, причем не обязательно


перечисления (т. е. операции succ и pred могут отсутствовать). Реаль-
но используются строки, даты и т. п. Это называется ассоциативными
массивами.

• Абстрактное индексирование: любая совокупность попарно различ-


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

• область изменения индексов (тип индексов, нижняя и верхняя граница,


т. е. отрезок типа) и

• тип компонент.
9.3. СТРУКТУРНЫЕ ТИПЫ 525

Если тип индексов всегда один и тот же (обычно отрезок целых неотрица-
тельных, как в С/С++), то он может опускаться.
Массивы бывают статическими, динамическими и гибкими. Для статиче-
ских массивов границы изменения индексов задаются статически вычисли-
мыми выражениями (состоящими из констант и переменных периода ком-
пиляции), для динамических массивов — выражениями, значения которых
должны быть вычислены до выполнения определения типа массивов. Про
гибкие массивы говорят, что они имеют подвижные границы, т. е. такие, ко-
торые могут изменяться в процессе оперирования с массивом.
Подвижные границы чаще всего указываются специальным служебным
словом. Если значение нижних границ всех массивов стандартизовано (в С/С++
это 0), оно опускается. В С/С++ задание верхней границы индекса массива —
это указание границ безопасного обращения к элементам массива. В строгом
смысле этого слова она границей не является, поскольку никакой ошибки не
будет, если индексное выражение выйдет за указанный предел. Последнее в
точности соответствует трактовке массивов как указателей на область памя-
ти.
Массив есть таблично задаваемая функция, переводящая область ин-
дексов в область компонент. Если D — область изменения индексов (одно-
мерная для векторов или многомерная в других случаях), а R — множество
(область) значений компонент, то

M : D → R.

Точнее, это семейство функций, если иметь в виду возможность присваивать


компоненте новое значение. Хоар эту операцию называет выборочным обно-
влением массива.
Как правило, в качестве имени этой функции (семейства функций) выби-
рают имя переменной типа массив.
Чтобы полностью специфицировать значение типа массив, нужно ука-
зать для каждого элемента D значение из R. Этот выбор делается независимо
для каждого элемента массива. В результате

мощность(D → R) = мощность(R)мощность(D) .

Таким образом, при представлении моделируемых объектов массивами за-


даются все значения для всех индексов, тогда как это может противоречить
реальности (массив дней месяца).
526 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

Не следует путать последнее с так называемыми разреженными масси-


вами, у которых для большинства элементов принимается стандартное зна-
чение (как правило, 0; пример — диагональная матрица), либо большинство
элементов не используется (случаи, подобные дням месяца, — это издерж-
ки неадекватности абстрактной структуры конкретному содержанию). С аб-
страктной точки зрения разреженный массив ничем не отличается от обыч-
ного (чаще гибкого) массива. Поэтому было бы правильно (как в языке Java)
не делать различий между ними при изображении массивов в программе.
Различия появляются, когда выбираются представления массивов. Так, для
диагональной матрицы (D) естественным представлением является вектор
~ rep ) со следующим доступом (i, j — индексные выражения):
(D

Здесь exception — указание того, что с разными индексами D[i,j] употреблять-


D[i,j] = (i == j? Drep[i]: exception);

ся не должна. Возможно, что это присваивание в некоторых программах ра-


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

D[i, j] :
read(i, j) = (SV (D, i, j)) =
if i > j then(SV (D, j, i))
else(SV (D~ rep , ϕ(i, j)); (9.4)
write(i, j, x) = (SA (D, i, j) := x) =
if i > j then(SA (D, j, i) := x)
else(SA (D~ rep , ϕ(i, j)) := x)

Здесь ϕ(i, j) — некоторая функция (зависящая от порядка, в котором предста-


влены элементы матрице в векторе), которая вычисляет расположение (i,j)-
того элемента матрицы (D) в векторе D ~ rep .
В плане реализации доступа, смешивающего многомерные и линейные
представления массивов, преуспел Фортран с его COMMON-блоками и опе-
ратором EQUIVALENCE.
Наиболее распространенное представление массивов — такое, когда ка-
ждому элементу отводится одно или несколько машинных слов. Каждый эле-
мент адресуется непосредственно. Это представление называется стандарт-
ным.
9.3. СТРУКТУРНЫЕ ТИПЫ 527

a[0]
a[1]
a
...

Здесь и далее закраской выделена заполненная часть машинного слова; то,


a[n]

что осталось без закраски, — неинформативная часть (т. е. потери памяти).


Следующее представление называется представлением со свободной упа-
ковкой:
a[0] a[1]
a[2] a[3]
a
...

Когда нужно экономить память, применяют представления с жесткой упа-


a[n]

ковкой (см. рис. 9.5). Когда разрабатывается представление для массивов,

A: A[0] A[1] A[2]


A[5] A[3] A[4]
A[6] A[7]
...

A[n]

Рис. 9.5. Упакованное представление массива

компоненты которых сами являются массивами, асто используется ссылоч-


ное представление (см. рис. 9.6).
В случаях разреженных и ассоциативных массивов нужно гораздо боль-
шее искусство выбора представлений. В обоих случаях надо учитывать, что
со многими компонентами работа не производится. В случае разреженного
массива к тому же заранее известно, что далеко не все компоненты массива
фактически используются, а потому не нужно их хранить.
Примером такого представления может служить решетчатое представле-
ние (см. рис. 9.7).
Примером представления, подходящего для ассоциативных массивов, явля-
ется ключевое представление, когда по индексу вычисляется ключ, а уже по
ключу размещается значение (см. рис. 9.8). Здесь двойная стрелка означает
528 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

Первое измерение:

Второе
измерение

Рис. 9.6. Ссылочное представление

один из вариантов представления записи

(<ключ, если для него есть компонент>, Ki ),

которая в ключевом представлении заменяет исходный компонент разрежен-


ного массива.
Таким образом, доступ к компоненте становится двухступенчатым:

1. выбор нужной компоненты в новом наборе (логически это снова мас-


сив, но уже плотный) и

2. извлечение Ki из записи (второе поле).

Старый индекс для такого представления называется ключом, а порядковые


номера компонентов нового набора — индексами записей нового набора.
При ключевом доступе возникает задача контроля целостности: в но-
вом наборе не должны появляться записи с одинаковыми ключами, и нужно
контролировать, не нарушается ли упорядоченность ключей. Ключевое пред-
ставление можно рассматривать как следующий по отношению к исходному
массиву уровень абстракции (более нижний, реализационный), который, в
свою очередь, допускает различные реализации двухступенчатого доступа.
Указанная запись может представляться стандартным способом, со свобод-
ной упаковкой (реже) или с использованием ссылок (очень часто). С учетом
упорядоченности ключей (обычное свойство для ассоциативных массивов —
9.3. СТРУКТУРНЫЕ ТИПЫ 529

A:

d2 d2’
Остаток

этого ряда
d1 Нет значения
A[d1,d2’]
A[d1,d2]
… Остаток
d1’ этого ряда
A[d1’,d2’]
A[d1’,d2]
… … …
Граничная Остаток этого Остаток этого
цепочка d1 столбца столбца

Рис. 9.7. Решетчатое представление массива

см. выше) целесообразно предложить организацию индексов, приспособлен-


ную для дихотомического поиска. Здесь возможны варианты:

1. Индексы представляются как последовательность регистров со ссыл-


ками на Ключи, которая соответствует упорядоченности ключей — пря-
молинейная реализация.

2. По значениям ключей вместо массива Индексы строится B-дерево, обес-


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

3. Используется расстановка (хеширование), суть которой сводится к сле-


дующему. Определяется расстановочное поле (хеш-таблица) — мас-
сив фиксированного размера m, который будет заполняться ссылками
на группы компонентов исходного разреженного массива (записей но-
вого представления). Эти группы далее называются гроздями. На мно-
жестве всех возможных значений ключей определяется функция рас-
530 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

Индексы Ключи Значения


Ki1
Ki2
Нет значения
Ki3

Kij
… Kin

Рис. 9.8. Ключевое представление массива

становки11 (хеш-функция) со значениями в множестве номеров гроздей


Термин ‘хеширование’ неудачен как в русском, так и в английском языке (переводится
примерно как “что-либо мелко порубленное, нарезанное”. Сам метод изобретен в СССР не-
11

зависимо от зарубежных аналогов и назван вполне логично расстановкой. Сопутствующие


понятия также получили вполне адекватные наименования. Однако после ужасного (в смы-
сле терминов) перевода замечательной книги Д. Гриса [26] расстановка стала замещаться
хешированием, и сегодня русский термин некоторым кажется старомодным.
Впервые этот метод (примитивный вариант) предложен Петерсоном [87]. А. П. Ершов
использовал собственный вариант метода в 1967 году в очень развитом для своего времени
трансляторе [32]. Хорошее изложение этого метода см. в [46].

Ключ1 K1

Ключ1L K1L Ключ1R K1R

Ключ1LL K1LL Ключ1LR K1LR Ключ1RR K1RR

................................................

Рис. 9.9. B-дерево


9.3. СТРУКТУРНЫЕ ТИПЫ 531

Расстановочное поле Ключi11 Ki11


Ключi12 Ki12
1. Ссылка на гроздь 1 ...
2. Ссылка на гроздь 2 Ключi1g1 Ki1g1
3. Ссылка на гроздь 3 nil
Ключi21 Ki21
Нет значений с Ключi22 Ki22
… соответствующи- ...
ми ключами Ключi2g2 Ki2g2
m Ссылка на гроздь m ...

Ключim1 Kim1
Ключim2 Kim2
...
Ключimgm Kimgm

Рис. 9.10. Расстановка (хеширование)

от 0 до m − 1 так, чтобы выполнялись условия перемешивания:

(a) для каждого значения ключа существует значение функции рас-


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

Устройство грозди может быть любым (массив, список). Их взаимное


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

(a) Многоуровневая расстановка. По существу, это двухступенчатый


доступ, т. е. работа с массивом массивов. Эффективность хеширо-
вания при этом резко падает. Используется, когда надо привлекать
внешнюю память.
532 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

(b) Расстановка по составному ключу. Два или более значения ин-


дексных полей — составной ключ — дают возможность практи-
чески всегда однозначно выделить значение массива. Можно счи-
тать, что здесь используются новые ‘большие’ значения. Здесь,
как правило, может нарушаться равномерность функции расста-
новки, тем не менее этот способ зачастую используется.
(c) Вырожденный случай 1: размер расстановочного поля равен чи-
слу значений ключей. Нет нужды хранить значения ключей в ка-
честве компонентов наборов, представляющих абстрактный уро-
вень (они вычисляются однозначно). Если размер расстановочно-
го поля больше числа значений ключей, то имеет место бессмы-
сленная трата памяти, но этот вырожденный случай порою встре-
чается при использовании слишком мощного механизма для еще
не развившейся базы данных.
(d) Вырожденный случай 2: размер расстановочного поля равен еди-
нице. Это означает, что гроздь — весь массив, и он представляет-
ся списком, последовательностью и иным способом. Понятно, что
нет смысла в расстановочной функции, но иногда этот вырожден-
ный случай возникает на начальных этапах развития большой ба-
зы данных, в которой в дальнейшем без расстановки не обойтись.

4. Локально плотное представление. Оно используется в том случае, если


нам заранее известна глобальная информация о строении массива, что
часто случается в ходе больших вычислений с большими, но достаточ-
но регулярно организованными, матрицами.
Пусть имеется массив (матрица) следующего вида
 


A 0 B  (9.5)
 
0 C 0
Пусть WA , WB , WC и HAB , HC — числа элементов в ненулевых блоках
матрицы по горизонтали и вертикали, соответственно, элементы строк
и столбцов матрицы нумеруются с единицы. Тогда можно построить
9.3. СТРУКТУРНЫЕ ТИПЫ 533

следующую функцию:
если(0 < k 6 WA )&(0 < l 6 HAB )

если(WA + WC < k 6 WA + WB + WC )&(0 < l 6 HAB )



 1,

если(WA < k 6 WA + WC )&(HAB < l 6 HC + HAB )
2,
F (k, 1) =
exception в остальных случаях

 3,

Она может рассматриваться в качестве функции расстановки для сле-


дующего программируемого доступа:

D [k, l]=
( case F(k, l) of
1 : DA [k, l];
2 : DB [k - WA - WC , l - HAB ];
3 : DC [k - WC , l - HAB ];
end)

Ситуацию иллюстрирует рис. 9.11. В данном случае даже не потребо-


валось вводить расстановочное поле как структуру данных.

F ( k,l ) А
В

Рис. 9.11. Функция расстановки

Для всех вариантов массивов циклы всех видов допустимы, причем они ре-
ализуются достаточно эффективно. Также для всех размеров массивов не-
обходимо иметь функцию вычисления размера массива, более того, отсут-
ствие такой встроенной функции в стандартных языках программирования
является, пожалуй, одним из видов глупости, передающейся из поколение в
поколение.
534 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

9.3.5. Множества

Множество — это тип данных, значения которого представляют множе-


ства значений другого, ранее определенного типа, называемого базовым ти-
пом множества. Особенностью конструктора set of T является то, что опре-
деляемые с его помощью объекты в высшей степени зависят от количества
элементов в типе T и в принципе не содержат в себе в качестве компонент
значения базового типа. В данном отношении конструктор множеств отли-
чается от ранее рассмотренных нами конструкторов, которые интегрировали
значения базовых типов в определяемые ими структуры данных. Правда, са-
мое тупое из приходящих на ум представлений множества: перечень всех
элементов данного множества ({зн1 , . . . , знn } : T ) , — конечно, содержало
бы эти элементы. Но недостатки данного представления очевидны (тем не
менее, в одном из упражнений Вам предлагается найти случаи, когда такое
представление множества оптимально, и подумать, как его организовать в
таких случаях).
Другое представление множества естественно подсказывается изоморф-
ной алгебре множеств математической структурой функций M : T → {0, 1}.12
Поскольку функция на конечном множестве, в свою очередь, то же самое, что
массив, то множество может быть представлено как массив булевских значе-
ний.
type Set_T=array[T] of Boolean;
Просим извинения у любителей C/C++/C#, но примитивная логическая
структура этих языков не позволяет выразить данную идею столь же просто и
ясно. А в какие ловушки ведут попытки держаться слишком близко к деталям
реализации, мы уже видели.13
Поскольку операции алгебры множеств над множествами A и B сводятся
к логическим операциям над значениями предикатов x ∈ A x ∈ B, вычисле-
ние объединений, пересечений и других операций булевой алгебры сводится
к циклу по всем элементам T с применением соотвествующих булевых опера-
ций к их элементам. Проверка равенства множеств, включения одного мно-
жества в другое и подсчет числа элементов множества также производится
Еще раз повторим, что изоморфизм математических понятий исключительно важен для
программиста, поскольку изоморфные структуры эквивалентны лишь с математической точ-
12

ки зрения, на практике они дают различные представления понятий.


Впрочем, так же в любой области человеческой деятельности. Попытка прямо достичь
цели чаще всего ведет в тупик, а победа, даже если она одержана, оказывается пирровой.
13
9.3. СТРУКТУРНЫЕ ТИПЫ 535

за один проход по массиву.


Таким образом, выделяются основные операции типа множеств (x, x1,. . .
x2, y — значения базового типа T, S, S1, S2 — значения или переменные типа

1. x ∈ S — предикат;
set of T ):

2. S1 = S2 — предикат;

3. S1 ∩ S2 — пересечение;

4. S1 ∪ S2 — объединение;

5. S1 − S2 — дополнение;

6. S1 ⊆ S2 — предикат включение;

7. |S| — размер множества (число элементов).


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

(II) небольшое n, меньшее величины k ∗ Cell, где k — размер не очень боль-


шого массива, затраты памяти на который можно считать приемлемы-
ми.

(III) конечное, но довольно большое n, когда несколько прямо построенных


массивов приведут к неприемлемому расходу памяти;

(IV) величина n не определяется как конечное с точки зрения машинной


памяти целое число.
Случай (I) наиболее легок с практической точки зрения: он реализуется наи-
более эффективно, красиво и концептуально естественно на уровне машин-
ной логики. При этом используются шкалы — битовые наборы, размещае-
мые в ячейках. Каждый бит шкалы указывает наличие или отсутствие того
536 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

или иного признака. В частности, пустое множество — шкала из всех нулей,


множество всех возможных значений базового типа представляется шкалой
из всех единиц. Указанные выше операции вычисляются почти что на уровне
пары машинных команд.
К примеру, если S1 и S2 — множества-шкалы со следующими значени-
ями битов (нулевые значения не показаны), то их объединение вычисляется
как логическая дизъюнкция шкал:

A: 1 1 1 1 1 1 1 1
B: 1 1 1 1 1 1
A ∪ B: 1 1 1 1 1 1 1 1 1 1 1 1

В изображениях множеств вместо круглых скобок, унаследованных от на-


бора компонентов, часто используют фигурные скобки.14
Случай (II) ненамного сложнее, но технические детали, которые придет-
ся использовать, несколько снижают эффективность реализации, следующей
принципам случая (I).
Случай (III) требует перехода от представления множества шкалой к мас-
сивам или спискам. В зависимости от мощностей множеств, с которыми пред-
полагается иметь дело, а также от фактически нужных в данной обработке
операций, могут оказаться полезными плотные или разреженные массивы,
представляющие, соответственно плотные или разреженные множества, для
работы с которыми применяются те же методы, что и для массивов. Но есть
нюанс, связанный с тем, что в массивах явно задается индексирование, тогда
как для множеств оно избыточно. Отсюда, во-первых, возможность более эф-
фективного расположения значений (какого?), а во-вторых, надстройку, свя-
занную с заданием индексирования, можно либо исключить вовсе, либо сде-
лать удобнее для выполнения нужных операций.
В данном случае есть три варианта представления:

• хранение значений — явный перечень элементов множества, например,


в виде списка;

• хранение ссылок на значения — эту лучше, когда возможных значений


не очень много, и их размер превышает размер памяти, требуемый для
ссылки;
14
В языке Pascal используются квадратные скобки.
9.3. СТРУКТУРНЫЕ ТИПЫ 537

• хранение кодов значений — использование таблицы кодов (функция


перекодировки — каждому значению однозначно сопоставляется его
код) может оказаться экономнее явного перечня элементов (шкала —
это частный случай данного метода, ссылка — другой частный случай).
Случай (IV). Вообще говоря, здесь требуется реализация символических вы-
числений15 (не путать с символьными!), в которых объектами оперирова-
ния являются утверждения, т. е. предикаты, формирующие множества. Та-
ким образом, явное представление множеств элементов, мест элементов или
каких бы то ни было значений базового типа либо их заменителей не требует-
ся. Более того, оно было бы ограничительным по отношению к бесконечным
множествам.
В языке Pascal есть только минимально необходимый набор операций над
множествами. Формально базовый тип множества в этом языке и его расши-
рениях должен быть перечислением. Под единой синтаксической оболочкой
в этом языке кроются представления множеств, представления коотрых соот-
ветствуют случаям (I) и (II). Этого хватает даже для множеств всех коротких
целых чисел.
В языке Pascal и его расширениях имеются литералы для изображения
множеств. Например, множество четырех приемлемых состояний выхода из
подпрограммы может быть изображено как
const goodend=
[OK, User_break, Suspended_and_saved, Negative_result].

Пожалуй, небольшие множества — это единственное ‘гармоничное’ сред-


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

вычислить в рамках символических вычислений — это преобразование символических вы-


ражений, направленные на их сокращение, на доказательство их свойств и т. п. В отличие от
символических вычислений символьные вычисления имеют дело с данными символьного и
строкового типов.
538 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

ем изображений пустого множества (иногда и полного) литеральные изобра-


жения множеств невозможны, когда литеральные изображения для значений
базового типа не предусмотрены.
Некоторые из перечисленных операций требуют более одной команды,
но, тем не менее, остаются простыми для реализации. В ряде случаев могут
потребоваться специальные константные шкалы, определяемые статически.
Почему в С/С++ этого средства в явном виде нет? Ответ: есть побитовые
операции, и поэтому реализовывать то, что можно сделать с их помощью,
сочли уже ненужным.
Случай (IV) может показаться экзотическим, имеющим лишь академиче-
ский интерес. По этой причине его редко можно встретить в практических
языках. Но на самом деле этот случай часто встречается на практике. Напри-
мер, области на рисунке являются множествами точек, а точек слишком мно-
го. Подобные множества приходится моделировать подручными средствами,
а потому не всегда видна их концептуальная природа, что затрудняет пони-
мание программ.
Предикаты, формирующие множества, полезны не только при работе с
потенциально неограниченными множествами. В частности, они расширяют
выразительность оперирования со сложно устроенными множествами. От-
сюда вполне разумно предложить еще одну операцию для множественных
типов данных:
{x ∈ S|P (x)} — формирование множества с помощью предикатного филь-
тра P (x).
Как реализовать эту возможность? Первый приходящий на ум путь: сим-
волические вычисления над множествами. Символическое вычисление пред-
полагают выполнение действий над специально представленными обозначе-
ниями (в данном случае — множеств и их элементов), а не над конкретными
значениями, которые, быть√ может, и существуют-то только в идеальном ма-
тематическом смысле (π, 2 и др.). Их можно трактовать как абстрактно-син-
таксическое представление конкретно-синтаксического представления язы-
ка изображения предикатов.
Язык программирования, в котором можно оперировать с потенциально
бесконечными множествами — Setl.
Интересно, что в первых версиях языка, разработанных Дж. Шварцем для
советско-американского проекта, эта возможность не была декларирована.
Она появилась в языке только тогда, когда новосибирскими программиста-
ми было предложено представление бесконечных множеств, т. е. реализация
случая (IV). Реализационная стратегия этого проекта (который уместно на-
9.3. СТРУКТУРНЫЕ ТИПЫ 539

звать сибирским Сетлом) опиралась именно на символические вычисления.


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

НОД(x, y) =
max {n|1 6 n&n 6 min{x, y}&∃k1, k2 (x = k1 ∗ n&y = k2 ∗ n)}

Из этого определения (оператора языка Setl) извлечь, к примеру, алгоритм


Евклида невозможно.
Задача разграничения эффективных и неэффективных описаний алгорит-
мов в языках, подобных Setl, решена не была. Быть может, по этой причине
языковые формы оперирования с бесконечными множествами не получили
развития в практических языках программирования.

Задания для самопроверки

1. Дать пример, когда разделение единиц измерения, предоставляемое ап-


паратом новых типов языка Ada, помогает находить ошибки в програм-
ме.

2. Какие достоинства и недостатки статической системы типов Вы може-


те указать?

3. Построить один из вариантов размещения диагональной матрицы в век-


торе и функцию ϕ, от него зависящую.

4. Для каких массивов целесообразно решеточное представление?

5. Почему прямолинейная реализация ключей неэффективна для боль-


ших массивов?

6. Когда целесообразно представлять множество перечнем его элементов?


Как целесообразно организовать этот перечень в предложенных Вами
случаях?

7. Если для базового типа определен линейный порядок, уточнить пред-


ставление и эффективно реализовать следующие операции над множе-
ствами:
540 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

(a) min(S) — минимальный элемент, содержащийся в S;


(b) max(S) — максимальный элемент, содержащийся в S;
(c) succ(S,x) — если x ∈ S, то элемент, обладающий свойством
y ∈ S&x < y&∀z (x < z ⇒ y 6 z),
если таковой существует, иначе не определено;
(d) pred(S,x) — определяется двойственно succ;
(e) (x1 . . x2 ) — множество
{y ∈ T | x1 6 y&y 6 x2} .

8. Построить на С++/C# средства работы с небольшими множествами. С


помощью объектно-ориентированных возможностей языка это делает-
ся достаточно изящно.
9. Постройте пакет для работы с большими множествами, когда произ-
водится хранение значений — явный перечень элементов множества,
например, в виде списка.
10. Постройте пакет для работы с большими множествами, когда происхо-
дит хранение ссылок на значения — это лучше, когда возможных значе-
ний не очень много, и их размер превышает размер памяти, требуемый
для ссылки.
11. Постройте пакет для работы с большими множествами, когда проис-
ходит хранение кодов значений с использованием таблицы кодов или
функции перекодировки. Это может оказаться экономнее явного переч-
ня элементов (шкала — это частный случай данного метода, ссылка —
другой частный случай).
12. Постройте свой собственный пакет для работы с большими множества-
ми, обладающими некоторыми общими свойствами. Явно сформули-
руйте ограничения на применение данного пакета. Обязательно вос-
пользуйтесь оптимизацией представления, связанной с тем, что в мно-
жестве нам не нужно хранить конкретные значения элементов.
13. Какими недостатками обладает реализация областей на рисунке как
множества точек экрана (что является не слишком большим множе-
ством и может быть представлено шкалой)?
9.4. РЕКУРСИВНЫЕ СТРУКТУРЫ ДАННЫХ 541

§ 9.4. РЕКУРСИВНЫЕ СТРУКТУРЫ ДАННЫХ

Пусть R = (K1 : T1 , . . . , Kn : Tn ) — универсальный набор компонент,


одна или несколько из которых определены с использованием R. Тогда R
называется рекурсивной структурой данных.
В этом определении речь идет об использовании самого R, а не конструк-
тора, с помощью которого эта структура построена. Это разные сущности. К
примеру,
array [I1] of array [I2] of A
это рекурсивно определяемая, но не рекурсивная структура данных. В дан-
ном фрагменте указано, что компонентами массива с индексами из I1 явля-
ются компоненты, которые сами строятся как массивы.16
В общем случае рекурсивные структуры данных определяют произволь-
ные графовые структуры.
При задании только одного конструктора в определении трудно описать
структуру, и фактически несколько рекурсивных структур данных всегда за-
даются одновременно, так что структура R задается несколькими выраже-
ниями над типами:
type T1 = E1(T1,. . . ,Tk);
...
Tk = Ek(T1,. . . ,Tk);

Здесь определяется сразу несколько (k) типов. Эти соотношения трактуются


Явно оговорим некоторые тонкости. Возможен взгляд на массив и как на рекурсивную
структуру данных. Массив как рекурсивная структура данных можно определить, например,
16

таким образом:
Базис рекурсии.
M = array [] of A — массив с компонентами типа A с нулевым числом компонентов;
Шаг рекурсии.
Если M — массив с n или меньшим количеством компонентов типа A, то M M array [n+1] of
A — массив, у которого n + 1 компонентов типа A, где M — специальная операция над типа-
ми, предназначенная для добавления элементов в набор. Таким образом, в данном примере
определяется тип массивов с подвижной верхней границей.
Определение матрицы, т. е. двумерного массива, в языке С/С++/C# — это просто сокраще-
ние для того, чтобы сказать, что вводится указатель на указатель на компоненты некоторого
скалярного типа (это другой конструктор).
542 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

как задание системы типовых уравнений, решаемых следующим образом:

T10 = B1 ; . . . ; Tk0 = Bk ;
T1m = E1 (T1m−1 , . . . , Tkm−1 ); . . . ; Tkm = Ek (T1m−1 , . . . , Tkm−1 );
T1 = lim T1m ; . . . ; Tk = lim Tkm .
m→∞ m→∞

где Bi — заранее определенные базисные типы, если такое решение возмож-


но. Таким образом, определение рекурсивной структуры над типами следует
общему определению рекурсии в теории программирования: (T1 ; . . . ; Tk ) —
наименьшая неподвижная точка системы операторов (E1 ; . . . ; Ek ):

T1 = E1 (T1 , . . . , Tk );
...
Tk = Ek (T1 , . . . , Tk );
T10 ⊆ T11 ⊆ · · · ⊆ T1 ;
...;
Tk ⊆ Tk1 ⊆ · · · ⊆ Tk ;
0

Конечно же, как всегда при рекурсивных определениях, могут получиться


бессмысленные типовые уравнения, к примеру:

type X = X; Y = ^Y; Z = array [I] of Z;

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

• задавать описания индуктивно, как процесс порождения структуры;

• указывать в описаниях заместители рекурсивных данных, способные


представлять (обозначать) саму структуру либо свидетельство ее от-
сутствия.
9.4. РЕКУРСИВНЫЕ СТРУКТУРЫ ДАННЫХ 543

Первое решение чаще всего предлагается языками, которые обеспечива-


ют непосредственное оперирование с рекурсивными структурами данных,
рассматриваемыми в качестве базовых языковых структур данных. Конкре-
тизации таких структур для реальной обработки задаются как варианты ба-
зовой структуры. Как правило, это специализированная обработка (КС-грам-
матики и синтаксический анализ, выражения Рефала и др.)
Для традиционных языков обычным является второе решение. Оно в точ-
ности соответствует реализации рекурсивных структур с помощью указате-
лей. Именно указательные значения и переменные используются в качестве
заместителей рекурсивных данных. Применяются либо универсальные ука-
затели — любой адрес может быть их значением, либо типизированные ука-
затели — их значениями могут быть только ссылки на объекты заданного ти-
па. Первый вариант восходит к адресам традиционных архитектур с однород-
ной памятью. Он менее надежен, т. к. не предполагается контроль соответ-
ствия типов. Второй вариант более точно отражает потребность определения
рекурсивных структур данных. Наиболее последовательно он представлен в
языке Алгол 68.
Несмотря на противоположность решений индуктивного и заместитель-
ного задания рекурсивных структур данных, они могут сочетаться в одном
языке. Правда, как часто бывает в подобных ситуациях, при этом вполне ве-
роятны концептуальные просчеты, что также демонстрирует история, и яр-
кая иллюстрация тому — Lisp с его базовой списочной структурой, дополнен-
ной возможностями, которые эквивалентны традиционному оперированию с
указателями. Обсуждение методологических ошибок Lisp’а проведено в сле-
дующем разделе.
Формальную осмысленность списочных структур относительно существу-
ющего аппарата распределения памяти, в отличие от рекурсивных процедур,
легко задать чисто формальными правилами, впервые точно сформулирован-
ными в Алголе 68.

1. Любой рекурсивно определяемый ссылочный тип осмыслен, посколь-


ку он имеет универсальный элемент nil и поскольку память, отведенная
на указатель, имеет стандартный размер.

2. Рекурсивное использование типа осмысленно, если при раскрытии си-


стемы конструкторов на любом пути от типа к его рекурсивному ис-
пользованию встретится осмысленный тип.

Таким образом, рекурсивные структуры существеннейшим образом задей-


544 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

ствуют аппарат указателей.

9.4.1. Списочные структуры

Списки являются одной из важнейших ‘реальных’ математических струк-


тур, используемых в программировании. Абстрактное определение списков
элеентов произвольной природы см. в Определении A.6.1.
В современном программировании наиболее распространенной реализа-
цией этого абстрактного определения являются списки в стиле языка LISP,
которые строятся с помощью следующей конструкции (T — имя типа эле-
ментов):
Sequence(T) = (Head : T, Tail : *Sequence(T)); (9.6)
Выбор этих двух компонент производится по их именам. Как было сказано
в предыдущем параграфе, для корректности такой конструкции требуется,
чтобы рекурсивная ссылка на список была заменена ссылкой на указатель
(ref T — Алгол 68, ^T — Pascal, *T — C++/C#) Иначе конструкция понималась
бы как требование на бесконечную память для потенциально бесконечной
графовой структуры.
Часто такую конструкцию называют также линейный список, или после-
довательность. Мы специально записали здесь уравнение для типа данных с
параметром, поскольку на самом деле мы определили шаблон типов данных,
годящийся для последовательностей с произвольным типом элементов.
Конструкция (9.6) неполна в двух отношениях. Во-первых, необходимо
указать на возможность существования пустых списков (пустой список в тра-
диционной нотации LISP обозначается NIL):

Ts0 = {( )};

Это дополнение не снимается существованием указателя-джокера nil, по-


скольку NIL — вполне респектабельное значение, ссылку на которое отнюдь
не всегда нужно трактовать как ошибку.
Во-вторых, собственно списки отличаются от последовательностей тем,
что элементами списков могут быть сами списки. Таким образом, типовое
уравнение приобретает вид
(
NIL
List(T) = (9.7)
(union Head : (H0 : T, H1 : ∗List(T)), Tail : ∗List(T));
9.4. РЕКУРСИВНЫЕ СТРУКТУРЫ ДАННЫХ 545

В исходном языке LISP были приняты два предположения (часто автоматиче-


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

2. (A1,A2,A3,. . . ,An)=(A1,(A2,(A3,(. . . ,An). . . ))).


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

(x (y z)) x

y z

((x) (y z))

x y z

Рис. 9.12. Структура списка в языке LISP

С современных позиций оба приведенных выше соглашения выглядят до


крайности морально устарелыми.17 В нынешнем стандартном диалекте язы-
ка Common Lisp они сохранены для совместимости со старыми программа-
ми, но основной определяющей функцией для списков стала функция (list
Во избежание недоразумений!!!
Критика некоторых решений LISP в данной книге не является стремлением сделать ‘над-
17

стройку сверху’. Еще раз повторим, что любое решение, принятое человеком, содержит в
себе в качестве атрибута человека ошибку. Гениальные решения, типа LISP, не являются
546 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

. . . ). Теперь можно записать, например, (list ’a ’b ’c nil) и получить результат


(A B C NIL), не равный ни (A B C), ни (A (B (C NIL))).
Основная операция над списками, не выводимая из описания структуры
данных — добавление нового элемента:
если S = ();


(x),
Add (S,x) = (S.Head x), если S = (S.Head);
(S.Head Add(S.Tail, x) в остальных случаях.

Заметим, что определенный нами тип данных List(T) не совпадает со спис-


ками, индуктивно определенными в A.6.1. В частности, ему удовлетворяют
кольцевые списки, пример которых приведен на рис. 9.13.

S = (x y S) ...

x y

Рис. 9.13. Кольцевой список в языке LISP


Замечание. В языке LISP кольцевой список может получиться только как
результат вычислений и, вообще говоря, кольцо невозможно изобразить, по-
этому “S” и “=” на рис. 9.13 не следует рассматривать в качестве символов
языка.
Рекурсивный тип, описывающий произвольные списки, можно опреде-
лить следующим образом:
List1(T) = Safearray(union(Arc : *List1(T), Atom : T));
Здесь Safearray — гибкий одномерный массив. Обозначение взято со стр. 309,
где описана реализация таких массивов в современных системах распреде-
ленного программирования.
исключением. Нельзя молиться на то, что сотворено человеком, как на Священное Писа-
ние, и воспринимать как ересь любые отступления от частностей даже самых гениальных
решений. Точно так же, увидев недостаток или даже прямую ошибку, нельзя сразу же стано-
виться в позу высокомерного пренебрежения и игнорировать имеющиеся в данном решении
находки. Тем более это верно потому, что перечисленные недостатки LISP являются скорее
мелкими шероховатостями, имеющими полное оправдание для тех условий, в которых со-
здавался язык.
9.4. РЕКУРСИВНЫЕ СТРУКТУРЫ ДАННЫХ 547

Для списков необходимо определить операцию конкатенации (ограни-


читься присоединением одного элемента в данном случае уже нецелесообраз-
но): 
Conc S1, S2) = (KS1 S1 S2 S2
1 , . . . , KnS1 , K1 , . . . , KnS2 ;

В разных языках принята разная нотация для изображения списков. Ча-


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

Graph(T1, T2) = (T1, *Safearray((*Graph(T1, T2), T2)).

9.4.2. Указательные типы

В современном практическом ООП сложные системы объектов появля-


ются на каждом шагу, в частности, в визуальных средах. Списки и указате-
ли являются стандартным способом взаимосвязи систем объектов. Поэтому
рассмотрим организацию систем объектов в более явной взаимосвязи с ны-
нешней их реализацией и с понятием адреса.
Вначале детализируем вопрос об определении обычного списка (тем бо-
лее, что взаимосвязи объектов часто идут именно через списки объектов).
Пусть имеется специальный примитивный тип pointer, значения которого
есть произвольные адреса. Нетипизированный указатель можно рассматри-
вать как типизированный, но с базовым типом, совпадающим с объединени-
ем всех возможных типов (последнее не является конструкцией языка, по-
скольку явно определить такое объединение невозможно). Такие типы пре-
дусмотрены, в частности, в C++/C#, Java и Object Pascal.
Имеются некоторые тонкости, ткотрые не очень важны для дальнейшего,
но заслуживают упоминания. В С/С++/C# и Java специальной нотации для
указателей не требуется, т. к. одновременно с определением типа определя-
ется и указатель для него (спецификатор ‘*’), а следовательно, в определе-
нии структуры, содержащей указатель на нее саму, нет ничего нового: эта
конструкция попадает под формальное определение рекурсивной структуры
548 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

данных. В данном языке указатель относится к базовым типам, тогда как в


Pascal и в большинстве других языков (во всех языках со строгой статиче-
ской типизацией) указатель — это конструктор структуры данных (подобный
образованию отрезка из перечисления), имеющий единственный аргумент:
имя своего базового типа.
Теперь
record Inf : T, Next : pointer; end
описывает структуру, которая может представлять список, т. к. pointer, ука-
зывая куда угодно, способен на то, чтобы указывать на точно такую же за-
пись, как та, что описывается. Однако фактически он может указывать не
только на такую запись, поэтому нельзя утверждать соответствующая память
будет иметь описываемую структуру. По существу, приведенная запись не
должна рассматриваться в качестве рекурсивной структуры данных, хо-
тя она и может применяться, и применяется для реализации рекурсивных
структур. Более того, такая запись с нетипизированным указателем часто
реализует в C++/C#, Object Pascal и Java список объектов различных типов,
правда, здесь есть одна тонкость: на первом месте стоит также указатель.
type ListofObjects=record Inf , Next, Prev: pointer; end
Конечно же, здесь нет никаких признаков (за исключением имен) того, что
Next и Prev указывают на такие же структуры. Ничем не помогает здесь огра-
ничение множества допустимых значений произвольного указателя до мно-
жества значений данного типа, ничего нового не обнаруживается. К примеру,
описание
type R = record Inf: pointer, Next, Prev : ^R; end
можно считать простым сужением предыдущего описания записи с указате-
лем. Однако при описании R фактически используется R, а значит, оно по-
падает под формальное определение рекурсивной структуры данных. Тем не
менее самая прагматически важная характеристика структуры данных — то,
что для каждого ее конкретного экземпляра должно быть выполнено
(
r.Next.Prev = &r;
(9.8)
r.Prev.Next = &r;
никак не отражено в определении типа.18 Чисто прагматически этот недоста-
ток обходится тем, что определяются три различных конструктора такого ти-
Смотрите упражнение 5 в качестве комментария к только что приведенным соотношени-
ям.
18
9.4. РЕКУРСИВНЫЕ СТРУКТУРЫ ДАННЫХ 549

па, соответствующие вставке нового элемента в начало, в конец и в середину


списка, и деструктор, который, удаляя объект, одновременно перекоммути-
рует ссылки.
Рассмотренный пример показывает, почему от тематики АТД никуда не
деться, поскольку любые чисто технологические ограничения остаются на
практике благими пожеланиями, пока они как следует не поддержаны.
Можно считать, что при помощи конструктора указателя создаются про-
сто переменные того типа, на который указывается, поскольку прагматиче-
ские функции указателей в точности те же, что и переменных. Имя перемен-
ной либо указатель на значение используется
(a) как аргумент при извлечении значения;

(b) как аргумент при обновлении значения.

Лишь в языке Алгол 68 этот семантический и прагматический изоморфизм


был отражен и сущнсти ‘переменная’ и ‘указатель на значение’ были отожде-
ствлены. Это позволило определить общую (семантически естественную и
полезную) операцию приведения значений: разыменование. Разыменование
активизируется, как и любое приведение типов, в зависимости от синтак-
сической позиции; в частности, в левой части присваивания разыменование
происходит лишь для снятия лишних косвенных ссылок, в правой части, вну-
три выражения, оно, как правило, продолжается до получения аргумента ба-
зового типа.
В нынешних языках разыменование всегда указывается явно (какая тро-
гательная аккуратность, особенно в сопоставлении с тем, какие грубые при-
ведения производятся неявно!). Например, в языке Pascal для получения зна-
чения из указателя нужно написать <имя>^, в C/C++/C# — *<имя>.
В языках Pascal, С++/C#, Java для создания новых указателей с одновре-
менным отведением памяти для значения базового типа имеется оператор
new. Практически это — инициализация переменной именно как перемен-
ной, а не значения, которое содержит соответствующая локация. Соответ-
ственно, удаление указателя вместе с именуемым значением производится
при помощи оператора dispose (или delete, в зависимости от языка). В язы-
ке Java) пытаются по мере возможности обойтись без явного уничтожения
указателей, заменяя это неявной сборкой мусора.
Конечно же, явное оперирование с указателями влечет (и практически
всегда на практике приводит к) нерегулярностям в структуре памяти, в част-
ности, к появлению нескольких имен у одного и того же значения, и к другим
550 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

неприятностям (см. рис. 7.6, на котором была показана получающаяся ситу-


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

9.4.3. Специальные рекурсивные структуры

Пусть
В = ( K1 , . . . , Kn : (В,T) ); n > 0
И пусть на операции формирования структуры и селекции компонентов на-
кладываются различные ограничения. Тогда получаются разные специаль-
ные варианты рекурсивных структур данных. Не все они общеупотребитель-
ны, но на некоторых стоит остановиться.
Стек и очередь
Стек
Push (S : В, E : T) = Push (( K1S , . . . , KnS ),E) ⇒
S = S + E = ( K1S , . . . , KnS , E );
Pop (S : В) : E ⇒
if S = ( K1S , . . . , KnS , E ) // или S.n > 0, что то же
then { S = ( K1S , . . . , KnS ), result = E }

Empty (S : В) : Boolean ⇒ result ( S = ());


else result = Error;

Это неограниченный стек, который строится как линейный список со спе-


циальными операциями. Графически его можно представить следующим обра-
зом (см. рис. 9.14).
Очередь
Все почти также.

Insert (S : В, E : T) = Insert (( KS1, ..., KSn, ),E) ⇒


9.4. РЕКУРСИВНЫЕ СТРУКТУРЫ ДАННЫХ

...
551

E KS n KS 2 KS 1

Начало, Конец

Рис. 9.14. Стек

S = S + E = ( K1S , . . . , KnS , E );
Delete (S : В) : E ⇒
if S = ( E, K1S , . . . , KnS ) // или S.n > 0, что то же
then { S = ( K1S , . . . , KnS ), result = E }

Empty (S : В) : Boolean ⇒ result ( S = ());


else result = Error;

Это неограниченная очередь. Графически ее можно представить следу-


ющим образом (см. рис. 9.15). Это еще не все, т. к. не определена операция

KS 1 ... KS 2 KS n E

Начало Конец

Рис. 9.15. Очередь

генерация стека или очереди в их начальном состоянии:


Init (S : В) : В ⇒ s = ();

Абстрактные последовательности и файлы


Линейный список можно рассматривать как основу для построения аб-
страктной последовательности, если определить (наряду с операцией Add)
регламент создания и доступа к компонентам (проделать самостоятельно!).
Т. е. все, как для стека и очереди. Но все эти структуры являются абстракт-
ными с той точки зрения, что допускают различные представления. Ограни-
чения, налагаемые на доступ, позволяют говорить о любой реализации.
Есть два частных случая такой конкретизации для последовательностей,
которые обычно имеют в виду, когда говорят о языках и системах програм-
мирования:
• последовательности (иногда описываются как специальный конструк-
тор с соответствующими операциями) — употребляются редко, чаще
всего эта структура отождествляется со следующей;
552 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

• файлы — последовательности, сохраняемые на внешней памяти (часто


по способу доступа к файлам причисляют различные внешние устрой-
ства, по которым программа получает данные и которым она их пере-
дает — связь с потоками).
Главное свойство — чтение и запись подряд (значит, есть операции дать сле-
дующий, записать очередной, установить в начало и др.).
Файлы Паскаля — самый необходимый минимум. Но еще есть понятие
буфера, через который осуществляется перекачка данных. В пределах буфе-
ра — произвольный доступ.
Файлы С/С++/C# те же возможности, что дает обычная система програм-
мирования (рассказать).
Ограничения доступа:
• только по чтению;
• только для записи;
• по чтению с возможностью дозаписи в конец (два указателя точек до-
ступа).
К примеру, обратное чтение было бы странным.
Цикл вида for x from S do, где S — последовательность или файл.

9.4.4. Деревья

Tree (T), где T — базовый тип:


L : T — Tree (T) — лист;

Если tr : Tree(T), то R(tr) — корень и все дерево (причем за счет рекурсивности


B1, ..., Bm : Tree (T) ⇒ (B1, ..., Bm) — Tree (T).

это верно и для всех ветвей).


B(tr, i : integer) объект типа Tree(T), такой что

Опять можно не говорить про указатели, но пока это только для дере-
tr = (. . . , B(tr, i),. . . )

вьев, подобным изображенным на рис. 9.16. А как задать стрелки в обратном


направлении? Нужно определить операцию родитель P(x), т. е. рекурсивно
найти ιr ∃i B(r, i) = x.
Разумеется, представление приведет к указательным структурам, напри-
мер, вида, изображенного на рис. 9.17. Для доступа к родителям от детей
нужны дополнительные ссылки, как на рис. 9.18.
9.4. РЕКУРСИВНЫЕ СТРУКТУРЫ ДАННЫХ 553

Рис. 9.16. Дерево


Корень: Inf (Inf — информационное поле)

Ссылки на братьев: ...

Inf Inf
Ветви одного уровня:

Рис. 9.17. Списочное представление дерева

Задания для самопроверки


1. Приведенное нами формальное определение корректности рекурсив-
ного задания структуры не исключает некоторых содержательно бес-
смысленных случаев. Приведите примеры абсурдных, но формально
правильных с точки зрения транслируемости, рекурсивных определе-
ний типов.
2. Какие еще необычные списки удовлетворяют определнию List(T)?
3. Определите в качестве рекурсивной структуры данных оснащенные муль-
тиграф (неориентированный), орграф и граф. Что мешает представле-
нию орграфа и графа?
4. Запишите построенные определения типов на Вашем языке програм-
мирования (C++/C#, Object Pascal, Ada, Java).
554 ГЛАВА 9. СТРУКТУРЫ ДАННЫХ

Inf

Ссылки на ...
родителей

Inf Inf

Рис. 9.18. Двунаправленное дерево

5. В соотношениях (9.8) есть ошибка. Найдите и исправьте ее.

6. Постройте что определяются три различных конструктора типа линей-


ный список, соответствующие вставке нового элемента в начало, в ко-
нец и в середину списка, и деструктор, который, удаляя объект, одно-
временно перекоммутирует ссылки.
Часть III

Методы программирования

555
556

Профессионализм программиста во многом определяется тем, какими ме-


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

• Опыт разработки концептуально и конструктивно сложных программ,


когда приходится выбирать и применять те или иные методы (только
он может превратить новичка в профессионала).

• Широкий взгляд на всю сферу программирования, когда излюбленные


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

• Умение видеть взаимосвязи создаваемых Вами конструкций с фунда-


ментальными теоретическими и методологическими идеями. Даже на
весьма среднем уровне это превращает совокупность знаний в систе-
му и дает Вам реальную возможность подняться на уровень Метода и
Стратегии с уровня Комбинирования и Тактики. А уж если Вы овладе-
ли этим на достаточно высоком уровне. . . (но это требует широкого и
глубокого образования и высокого исходного уровня интеллекта).

• Как следствие из всего предыдущего, умение видеть суть задач, пере-


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

• Владение тонкостями алгоритмов и методов из Вашей профессиональ-


ной области, например, объектно-ориентированного подхода (только
это позволит Вам хорошо реализовать Ваши хорошие идеи).
Известно, что в науке по-настоящему творческие личности меняют направление работы
раз в пять-семь лет.
19
557

• И, наконец, опыт неудач! Рим был более велик, чем куча других антич-
ных государств, не потому, что он умел побеждать (это умели и другие),
но потому, что он умел переносить тяжелейшие поражения и неудачи,
и часто превращать победу противника в Пиррову или Ганнибаловы.
Пока Вы не потерпели серьезную неудачу и не встали на ноги после
нее, переоценив свои ценности, Вы не станете специалистом высшего
класса.20
В данном изложении мы, конечно, не ставим цели осветить все основ-
ные методы программирования — любые попытки такого рода пока что в
принципе ведут к энциклопедии и коллекции, а не к структуре знаний. На-
ша задача в том, чтобы помочь ориентироваться в применении методов (в
том числе и в первую очередь неописанных здесь!), в том, чтобы увидеть, как
метод может быть представлен в формах языка программирования. Мы пы-
таемся показать это на наиболее характерных примерах, в которых идея кон-
струирования программы не заслонялась бы излишними алгоритмическими
трудностями21 .
Понятие метода программирования в известной мере соотносится с по-
нятием стиля: для каждого стиля характерны свои методы. Но соотношение
между стилями и методами не следует сводить к утверждению о характерных
методах. Стиль программирования — это, прежде всего, образ мышления: «я
выбираю данную систему понятий, чтобы на ее основе строить программные
решения». Здесь нет явной привязки к какой-либо задаче. Метод же опреде-
ляет направление движения от задачи к решению. Он налагается на стиль,
т. е. формулируется в рамках системы первичных понятий стиля. Иногда та-
кое наложение осуществимо просто и естественно, и тогда можно говорить
о естественности метода для данного стиля. Иногда метод не вписывается в
стиль: приходится прибегать к новым понятиям, выходящим за рамки стиля,
от других понятий отказываться, и именно это сигнализирует о несоответ-
ствии метода стилю. Среди рассмотренных в главе 3 стилей есть такие, кото-
рые поощряют и даже пытаются предписывать использование определенных
методов или классов методов. Так, программирование от событий, постули-
рующее явное отделение в вычислительном процессе генерации событий от
За одного битого двух небитых дают
(Русская поговорка).
20

Сами по себе алгоритмические трудности тоже заслуживают отдельного систематиче-


ского и глубокого курса. В системе отечественного университетского образования обычно
21

на это нацелены курсы дискретной математики и вычислительных методов.


558

их обработки, поощряет использование только таких методов, которые согла-


суются с таким разделением (примером может служить метод конечных авто-
матов — см. § 10.1). Но разработчик остается свободным: стили, которым он
следует при реализации и генерации, и обработки, могут выбираться, исходя
из соображений, не связанных с программированием от событий. Более то-
го, он свободен даже выбрать метод, отнюдь не поощряемый стилем, и затем
гордиться тем, как он виртуозно преодолел возникшие трудности (слишком
часто героизм оказывается следствием ранее допущенных просчетов).
Наиболее естественная языковая поддержка метода достигается в язы-
ке соответствующего стиля. Но это не значит, что использование метода в
других языках невозможно. Напротив, типична ситуация, когда приходится
моделировать стиль: если непосредственное применение метода недостижи-
мо, программист подменяет его прямую реализацию тем, что предлагается в
языке. Показать такие приемы программирования — еще одна задача, решае-
мая в данной части. Но далеко не всегда моделирование метода, тяготеющего
к определенному стилю, средствами другого стиля оказывается продуктив-
ным, и именно в подобных случаях мы говорим о несовместимости стилей.
Следует заметить, что стили, альтернативные структурному программирова-
нию, традиционно противопоставлялись ему — рассматривались как вред-
ные. Наша цель в связи с этим — выявить действительный и мнимый анта-
гонизм стилей, и тем самым дать практикующему программисту методи-
ческую базу для принятия решений о том, стоит ли в конкретной ситуации
пользоваться методами разных стилей, и как это безболезненно делать.
Мы стремимся показать не просто сегодняшнее (не дошедшее до уровня
зрелости практически ни в одной из областей программирования и инфор-
матики) состояние, а пути, которыми развивались принятые решения, что-
бы увидеть побудительные причины развития (либо не развития)языковых
средств поддержки методов. В некоторых случаях подобные средства дей-
ствительно появляются, в других — они пока что остаются на уровне кон-
цепций и академических экспериментов, в третьих — они появлялись, были
отброшены и преданы анафеме, но, согласно законам развития, к ним придет-
ся вернуться на новом витке в новом виде. Квалифицированному програм-
мисту очень важно уметь различать такие случаи, а также понимать, почему
то или иное средство применяется или не применяется в конкретном языке
и в конкретной методике программирования.
Мы почти совсем не рассматриваем стиль объектно-ориентированного
программирования. Этот стиль и связанные с ним методы программирова-
ния и проектирования заслуживают специального изучения, выходящего за
559

рамки нашего обсуждения стилей в целом, поскольку сегодня это — основ-


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

Методы программирования от
состояний

Есть много программистских задач, которые удобно решать с помощью


методов, формализацией которых могут служить диаграммы состояний и пе-
реходов. Если при анализе задачи удается выявить набор состояний описы-
ваемого процесса, условия перехода между состояниями, и действия, ассо-
циированные с состояниями, то эту задачу уместно решать одним из таких
методов. При анализе таких методов можно применять конечные автоматы
Мура.
Теоретически эквивалентные альтернативные варианты этих методов —
когда действия ассоциируются с переходами (по аналогии с автоматами Ми-
ли). Какой из вариантов выбирать, зависит от задачи. Из общих соображений
можно сказать, что связывание действий с переходами более гибко (можно
раздробить действие одного состояния по переходам), но если такая гибкость
приводит к дублированию информации или просто не нужна, то на первый
план выступают другие критерии, например, целесообразность концентри-
рованного описания действий.
Мы начнем с примеров таких задач и иллюстрации техники решений в
разных случаях. Затем разбирается теоретическая основа и следствия из нее,
которые нужны для применения на практике, а затем возвращаемся к технике
программирования и показываем различные реализации программирования
от состояний.
Очень часто диаграммы переходов порождают структуру конечного авто-
мата (см. определение A.4.1). В этих случаях программирование от состоя-
ний вне конкуренции по сравнению с другими стилями. Но порою оно при-

560
10.1. ОСНОВНЫЕ СТРУКТУРЫ 561

менимо и тогда, когда диаграмма перехода соответствует более сложной вы-


числительной модели. Словом, если сохраняется инвариант “Действия гло-
бальны, условия локальны”, то задачу уместно решать с помощью мето-
дов, основывающихся на понятии диаграмм переходов и конечного автомата.
Есть много вариаций методов программирования от состояний. Более то-
го, ‘уместно’ не всегда означает ‘лучше всего’. Поэтому программирование
от состояний удобно для показа на примерах того, как варьируются практи-
ческие методы решения логически и математически вроде бы однородных
задач. Небольшое изменение в ресурсных ограничениях — и, хотя старые
методы, как правило, остаются уместными, но лучше перейти к другим.

ОСНОВНЫЕ СТРУКТУРЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯ-


НИЙ
§ 10.1.

Информационное пространство всех блоков и процедур при программи-


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

ЗАДАЧА ПОДСЧЕТА ДЛИН СЛОВ ТЕКСТА И ЗАДАНИЕ КОНЕЧ-


НОГО АВТОМАТА
§ 10.2.

10.2.1. Постановка задачи и первичный анализ

Пусть требуется решить следующую задачу. Словом называется любая


непустая последовательность букв латинского алфавита (для простоты —
Интересно, что необходимость развитых средств поддержки модуляризации программ
первоначально провозглашалась для для структурного программирования, но оказалась пре-
1

красно приспособлена к совсем другому стилю. Впрочем, в современных программах самый


верхний (часто полуавтоматически генерируемый системами визуального программирова-
ния и поэтому обычно мало интересный для программистов) этаж структуры программ, как
правило, записан именно в стиле программирования от состояний.
562 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

Система

S1 S2

SS2
SS1

SS3

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

Рис. 10.1. Информационное пространство систем


10.2. ПОДСЧЕТ ДЛИН СЛОВ 563

только строчных букв). Перепечатать из входной последовательности сим-


волов все слова в следующем виде:
<слово> - <длина слова><конец строки печати>
Эту довольно простую задачу можно решать по-разному. Ближайшая цель —
построить решение, исходя из представления вычислительного процесса на-
бором состояний с переходами.
Для решения задачи входную последовательность символов естественно
считать потоком, читаемым слева направо, пока не закончится входная стро-
ка. При чтении букв, небукв и конца строки действия, которые необходимо
выполнять, различны. Кроме того, различны действия для первой буквы и
для последующих букв слова.
Здесь уже сделаны неявные предположения об алгоритме:
• слово рассматривается как вложенный во входную последовательность
поток;

• обработка слова будет происходить по схеме с инициализацией, включа-


ющей генерацию первого элемента потока-слова.

Можно считать, что эти предположения выяснены в результате анализа за-


дачи, и что они вытекают из сопоставления разных вариантов обработки. Из
этих предположений следует, что в данном случае задача чисто автоматная,
и поэтому построение диаграммы переходов и конечного автомата — одно и
то же.

10.2.2. Построение графа состояний

Решение, которое исходит из применения метода конечного автомата, тре-


бует, чтобы был определен набор состояний. Различные состояния выделя-
ются прежде всего из-за различия действий-реакций на чтение символов.
Вот полный перечень вариантов таких действий:
1. Символ буква: инициализировать обработку слова
(счетчику длины слова присвоить единицу).
2. Символ буква: продолжить обработку слова
(счетчик длины слова увеличить на единицу).
3. Символ не буква: закончить обработку слова
(напечатать длину прочитанного слова).
4. Символ не буква: пропустить символ.
5. Символ конец строки: закончить обработку слова и
564 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

завершить процесс.
6. Символ конец строки: завершить процесс.

Есть еще одно действие, не отраженное в этом списке, но подразумевае-


мое: чтение очередного символа, которое должно выполняться, когда пере-
ход срабатывает (соответстующее сдвигу входной ленты конечного автома-
та). Для данного примера и для большинства других автоматных примеров
нет смысла указывать его явно, поскольку оно автоматически сопоставляет-
ся каждому переходу и полностью соответствует математической модели. Но
возможны и другие ситуации:
• используется ‘отрицательное’ условие перехода, которое отсылает к
состоянию, предусматривающему, к примеру, детализирующий анализ
контекста передаваемого символа с последующими переходами (с чте-
нием символов) к другим состояниям;
• условие перехода связывается не с анализом символа, а с проверкой
тех или иных данных окружения, непосредственно не имеющих дело с
чтением из потока.
Внимание!
Наличие нестандартных (не требующих чтения) переходов, вообще говоря,
усложняет конструкцию таблицы переходов и математическую модель. Они
являются потенциальными источниками бесконечного зацикливания без про-
движения по потоку или выхода за пределы модели конечного автомата. По-
этому, если возможно, их стоит исключать за счет, например, укрупнения
действий. Однако переходы без чтения порою неизбежны, а порою они ка-
жутся полезны, например, для логического выделения действий, ведущего к
принципиальному упрощению схем.
Теоретически первая ситуация означает переход по пустому (несуществу-
ющему) символу. Оно исключается довольно просто: строятся сквозные пе-
реходы, минующее промежуточное состояние. Это, возможно, потребует кор-
ректировки действий. В результате построенный конечный автомат может
стать менее наглядным.
С точки зрения исключения переходов вторая ситуация сложнее. Она мо-
жет означать, что используется добавочный, явно не выделенный, поток об-
рабатываемых данных или на самом деле модель вычислений не автомати-
ная, и тогда исключение переходов просто недопустимо. Возможно, что не-
стандартные переходы использованы для вполне стандартизованных целей,
10.2. ПОДСЧЕТ ДЛИН СЛОВ 565

и по этой причине их исключение нежелательно, поскольку приводит к неяс-


ности программы. Но если подобных оснований нет, то следует постараться
объединить действия, связанные с нестандартным переходом и с последую-
щими переходами, чтобы исключить переходы без чтения.
Из перечня действий для рассматриваемой задачи следует, что должно
быть, как минимум, два класса состояний, имеющие разные действия-реак-
ции.
Следующий методический прием — это определение начального состо-
яния, т. е. того, в котором вычислительный процесс активизируется. Нужно
указать, какие действия возможны в данном состоянии, и какие условия пе-
рехода в нем должны быть.
Для рассматриваемого случая в начальном состоянии (St1), возможны
действия:
1 с переходом в другое состояние — St2 (поскольку следующая буква
требует другой реакции),
4 с сохранением состояния и
6 завершение обработки.

Символ
буква Инициализировать
обработку слова

Вход Символ не буква St2


St1

Символ конец
строки
Завершить процесс

Выход

Рис. 10.2. Начало построения графа состояний

Результат только что проведенного анализа можно представить в виде гра-


фа, изображенного на рис. 10.2. С каждой дугой графа связано две пометки:
566 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

символ-условие перехода и ассоциированное действие. Вход и выход обозна-


чаются черными прямоугольными блоками. При желании их можно считать
специальными состояниями, с которыми ассоциированы действия инициа-
лизации и завершения процесса. Состояние St2 изображено штриховой ли-
нией, поскольку анализ его еще не проведен.
Таким образом появляются новые состояния (в примере — St2). Для ка-
ждого из них следует определить условия перехода и соответствующие им
действия.
В рассматриваем случае в состоянии St2 возможны действия:
2 с сохранением состояния,
3 с переходом в другое состояние, которому дается временное имя St3 и
5 окончание слова и всего потока, завершение обработки.
После этого построения проверяется, не является новое состояние копией
уже имеющегося как по действиям, так и по переходам. Если это так, то но-
вое состояние можно отождествить (склеить) с одним из ранее построенных.
Вновь появляющиеся состояния анализируются аналогично.
Для решаемой задачи легко выяснить, что состояние St3 требует тех же
действий и переходов, что и St1. Следовательно, возможна склейка этих двух
состояний и построение графа завершается, так как новых состояний нет
(см. рис. 10.3). При желании, чтобы не дублировать действия “Завершить
процесс” можно еще определить еще одно, заключительное состояние, с вы-
ходом из которого будет ассоциировано это действие (один раз!). Понятно,
что это состояние будет иметь нестандартный переход.
Представленный метод выявления состояний конечного автомата годится
для не очень больших задач, когда строящийся граф остается обозримым. Он
может быть улучшен, если в ходе анализа задачи заранее удается выявить со-
держательно обособленные состояния и переходы между ними. Так, для дан-
ной задачи вполне очевидно, что обрабатывающая программа должна рабо-
тать по-разному, когда слово еще не началось, и когда уже прочитаны его пер-
вые символы. Иными словами, вычислительный процесс может находиться
в двух состояниях: ожидание начала слова (St1) и ожидание окончания слова
(St2). Для каждого из них можно указать набор действий, ассоциированных
с переходами, которые явно зависят от содержательного смысла состояний.
Содержательное выделение состояний удобно. В частности, можно себе
позволить строить недетерминированный конечный автомат, т. е. такой, у ко-
торого есть состояния с разными переходами, определяемыми одним и тем
10.2. ПОДСЧЕТ ДЛИН СЛОВ 567

Продолжить
Символ обработку слова
Символ буква
буква Инициализировать
обработку слова

Вход Символ не буква St2


St1

Символ конец
Символ конец Символ не буква строки
строки
Закончить обработку слова
Завершить процесс Завершить процесс

Выход

Рис. 10.3. Полученный граф состояний

же условием. Абстрактная точка зрения на такую ситуацию состоит в том,


что при вычислениях выбирается любой из переходов.2 Недетерминирован-
ный конечный автомат может быть автоматически преобразован в детерми-
нированный, при этом появляются новые состояния, которые не влияют на
логическое представление алгоритма.

10.2.3. Табличное представление графа состояний конечного автома-


та

Графическое или иное представление графа состояний конечного авто-


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

поскольку часто после выполнения одного перехода неясно, продвинулись ли мы к цели, и


нужно дойти до явно тупиковой ситуации. Практически здесь возникает одна из форм сен-
тенциального программирования, и представление ее недетерминированной таблицей со-
стояний и переходов скорее случайная теоретическая аналогия, сбивающая с толку на прак-
тике.
568 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

горитму перемещения по графу состояний от одного состояния к другому с


выполнением ассоциированных действий.
Различают визуальные представления диаграмм переходов, которые пред-
назначены для человека, и их программные представления, которые предна-
значены для выполнения программ конечных автоматов. Требования к визу-
альным представлениям — понятность для человека, статическая проверя-
емость свойств; требования к программным представлениям — осуществи-
мость эффективной обработки, заданной конечными автоматами; общее тре-
бование к представлениям обоих видов — однозначное соответствие меж-
ду ними, позволяющее утверждать пошаговую вычислительную эквивалент-
ность.
Какое представление графа состояний лучше всего использовать? Как все-
гда, ответ на этот вопрос зависит от сложности графа, статичности либо ди-
намичности его и того, для каких целей требуется спецификация в виде графа
состояний. Понятно, что важнейшей для нас промежуточной целью является
программа на алгоритмическом языке. Но подходы к построению такой про-
граммы могут быть различными. Существует две принципиально различные
позиции:
• трансляционная — структура управления программы определяется гра-
фом состояний;

• интерпретационная — строится специальная программа, которая вос-


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

2. Условие (срабатывания) перехода — логическое выражение или слу-


жебное слово failure, которое указывает на действие, выполняемое, если
ни одно из условий не срабатывает;

3. Действие, ассоциированное с переходом — последовательность опера-


торов, выполняемая, когда условие перехода истинно;

4. Адрес перехода — наименование состояния-преемника.


10.2. ПОДСЧЕТ ДЛИН СЛОВ 569

char symbol; // Как ранее, чтение потока символов неявное


int cnt; . . . // Инициализация St1
St1 ’a’<=symbol && printf ("%c", symbol); cnt = 1; St2

/* Так как нужно печатать только сло-


symbol <= ’z’

’z’<Symbol) &&*/ ва, действия не заполняются */


/*(symbol<’a’|| St1

// Переход не требует чтения,


symbol!=’\n’

// symbol == ’\n’ не нужно читать


failure exit

St2 ’a’<=symbol && printf ("%c", symbol);cnt++; St2


symbol <= ’z’
/*(symbol<’a’|| printf ("- %i\n", Cnt); St1
’z’<symbol) &&*/
symbol != ’\n’

отсутствие return 0; // Считать данную секцию та-


failure printf ("- %i\n", Cnt); exit

условия — ис- блицы


exit /*

тина, но без // состоянием или нет — дело вкуса


неявного чтения
потока */
Таблица 10.1. Табличное представление графа состояний
570 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

Кроме того, определяется специальная (первая) строка, в которой поме-


щаются операторы, инициирующие процесс, и адрес перехода начального
состояния.
Таблица 10.1 есть просто более формализованное представление рисунка
10.3. При желании табличное представление можно рассматривать как свое-
образный язык программирования, для которого может быть построен транс-
лятор или интерпретатор в точном соответствии с обозначенными выше по-
зициями по отношению к конечной цели решения задачи, т. е. к реализации
программы на алгоритмическом языке. Семантика вычислений на этом язы-
ке понятна. Рассматривается контекст некоторой программы на языке С/C++
(или на другом языке традиционного типа), в этом контексте определяется
значение переменной Entry (все множество ее значений совпадает с множе-
ством наименований состояний) и выполняется следующее:
1. Выбрать вход в таблицу, соответствующий значению Entry (текущее
состояние).
2. Если Условие отсутствует, то перейти к шагу 4.
3. Вычислить совместно все Условия срабатывания переходов (клетки
второй колонки для данного входа):
(a) Если среди результатов есть значения True, то выбрать одну из
строк, Условие которой истинно, и перейти к шагу 4 для выбран-
ной строки3 ;
(b) Если все значения Условий ложны и есть строка failure, то вы-
брать эту строку и перейти к шагу 4 для нее;
(c) Если все значения Условий ложны и нет строки failure, то завер-
шить вычисления автомата.

4. Выполнить Действие.
5. В качестве значения переменной Entry установить наименование со-
стояния-преемника (из четвертой колонки таблицы). Перейти к шагу
1.
В этом пункте может быть заложено недетерминированное поведение автомата. Очень
часто при применении метода конечных автоматов возможность недетерминированного по-
3

ведения игнорируется: оно исключается путем задания последовательных проверок и выбо-


ра первого из действий, для которого значение условия есть True. Это очень похоже на то,
как определены условные выражения и оператор переключения в языке С/С++.
10.2. ПОДСЧЕТ ДЛИН СЛОВ 571

Для данного примера указанной семантики вычислений достаточно (обра-


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

• start — указание действий, которые выполняются до основных дей-


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

• finish — указание действий, которые выполняются после основных


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

Можно также определять локальные для состояний данные (в примере такие


данные определены только для начального состояния), но это должно быть
согласовано с правилами локализации имен языка программирования и с об-
щим стилем, в котором написана программа. Заметим, что локальные данные
всех состояний конечного автомата должны быть помещены в общий кон-
текст, а приписывание их к конкретным состояниям является ограничением
видимости, подобным тому, о котором говорилось при рассмотрении моду-
лей в § 12.2 (еще раз прослеживается аналогия между модулями и состояни-
ями!). Это прагматически следует из того положения, что работа конечного
автомата не требует привлечения памяти.
Мы рассмотрим многие виды табличных языков, которые подходят для
описания различных типов автоматных алгоритмов. А пока займемся реше-
нием вопроса о том, как запрограммировать требуемые действия, когда нет
возможности воспользоваться таблицей непосредственно. Вот что можно де-
лать с таблицами, представляющими конечный автомат:

1. Оттранслировать вручную;

2. Отдать препроцессору для превращения в нормальную программу;

3. Использовать интерпретатор.

Рассмотрим последовательно эти три возможности.


572 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

10.2.4. Ручная трансляция диаграмм переходов

Решения 1 и 2 ставят следующий вопрос: во что транслировать таблицу


переходов? Вариантов может быть очень много. Ниже рассматриваются лишь
некоторые из них.
Вариант 1: считать St1 и St2 функциями, реализующими все действия
состояний.
Программа 10.2.1 Длины слов: рекурсия

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

char symbol; // Переменная для чтения потока


// Переменная для счета длин слов (из анализа
// возможных решений задачи)
int cnt;

void St2 (); // Предописание функции

void St1 ()
{
if (’a’<= symbol && symbol <= ’z’)
{
printf ("%c", symbol);
cnt = 1;
symbol = getchar ();
St2 ();
}
else if ( symbol != ’\n’ )
{
symbol = getchar ();
St1 ();
}
}
void St2 ()
{ if (’a’<= symbol && symbol <= ’z’)
{
printf ("%c",symbol);
10.2. ПОДСЧЕТ ДЛИН СЛОВ 573

cnt += 1;
symbol = getchar ();
St2 ();
}
else if ( symbol != ’\n’ )
{
printf (" - %i\n", cnt);
symbol = getchar ();
St1 ();
}
else printf (" - %i\n", cnt);
}
void main( void )
{
symbol = getchar ();
St1 ();
}

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

Программа 10.2.2 Длины слов: явный цикл обработки потока

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

char symbol;
int cnt;

enum States { St1, St2 } State;


void main( void )
574 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

{
State = St1;
while ( (symbol = getchar ()) != ’n’)
switch ( State )
{ case St1: if (’a’<=symbol && symbol <= ’z’)
{ printf ("%c", symbol);
cnt = 1;
State = St2;
}
else // if (symbol != ’\n’)
// State = St1;
// else if (symbol == ’\n’) return;
break;
case St2: if (’a’<=symbol && symbol <= ’z’)
{ printf ("%c", symbol);
cnt++; // State = St2;
}
else if (symbol != ’\n’)
{ printf (" - %i\n", cnt);
State = St1;
}
else
{ printf (" - %i\n", cnt);
return;
}
}
}

Лучше ли программа 10.2.2? Да, но только если ограничиться представлен-


ным выше критерием. Программа точно соответствует табличному предста-
влению автомата, почти все дублирования действий, связанные с таким пред-
ставлением, ликвидированы. Однако остается неудовлетворенность тем, что
появилось лишнее действие: выбор выполняемого фрагмента по значению
переменной State.
Если ограничиться управляющими средствами методологии структурно-
го программирования, то это — непреодолимое препятствие. Попытки при-
менить циклы внутри фрагментов, помеченных как case St1: и case St2: при-
водят лишь к уменьшению наглядности по сравнению с табличным предста-
10.2. ПОДСЧЕТ ДЛИН СЛОВ 575

влением.
Более технологичный вариант предыдущей программы можно постро-
ить, если каждый фрагмент программного кода, который отвечает за реализа-
цию действий, относящихся к разным состояниям, (разделы таблицы, поме-
ченные метками состояний), оформить в виде процедуры. Но чтобы не прий-
ти к варианту 1, необходимо помнить, что вызов такой процедуры не должен
приводить к выделению для нее собственной (локальной) области памяти —
конечный автомат работает с общей памятью!
При использовании многих языков (и систем программирования) выпол-
нения этого условия нельзя добиться, и остается только примиряться с тем,
что вызовы процедуры приводят к заниманию новой памяти. В качестве сред-
ства, пригодного для решения проблемы, обычно предлагается использовать
макрогенерацию (пример — препроцессор С). Для макрогенератора проце-
дуры, им обрабатываемые, становятся макросредствами:

— описание процедуры — определение макроса, или, что то же, тело ма-


кроса, а

— вызов процедуры — макровызов, или, что то же, использование макро-


са.

Обработка определения макроса — это создание из тела макроса текстовой


заготовки для последующего встраивания ее вместо макровызова (параме-
тризация макросредств, хотя и приводит к затруднениям для препроцессора,
также возможна).
Однако внешний для языка препроцессор приводит к большой проблеме,
связанной с идентификацией возможных ошибок в теле макросов, поскольку
препроцессор не в состоянии различать контексты вызова и описания проце-
дур. По этой причине в развитых языках предлагаются иные средства, пред-
ставляющие собой гибрид между процедурным механизмом и макрогенера-
цией. В С++/C# это встраиваемые (inline) функции. И именно они наиболее
точно применимы для решаемой задачи. Описывать что-либо внутри встра-
иваемых процедур можно, понимая, что при этом образуется лишь собствен-
ная область видимости имен, а не область памяти.
Есть еще один вопрос, который требуется решить при таком подходе, —
выбор типа результата функций-состояний и способа возвращения результа-
та. Естественный результат такой функции — это новое состояние, в которое
попадает автомат при переходе. Он может быть представлен, например, гло-
бальной переменной (такой как State в программе 10.2.2, которой присваива-
576 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

ется новое значение при выполнении функции-состояния). Но элегантнее не


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

Программа 10.2.3 Длины слов: использование процедур для описания со-


стояний

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

char symbol;
int cnt;
enum States { St1, St2, 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 St1;
}
else return Exit;
}

inline States f_St2 ()


{
10.2. ПОДСЧЕТ ДЛИН СЛОВ 577

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); return Exit;
}
}

void main( void )


{
symbol = getchar ();
State = St1;
for (;;)
{ switch ( State ) {
case St1: State = f_St1 ();
break;
case St2: State = f_St2 ();
break;
default: return;
}
}
}

Следует обратить внимание на то, что в данной программе явный цикл об-
работки потока исчез. Но стоит ли из-за этого считать вариант 3 ухудшени-
ем предыдущего? Конечно, нет. Программа наглядна, строго соответствует
таблице автомата. Можно сказать, что граф автомата определяет распреде-
ленную по состояниям схему потоковой обработки. И это нисколько не хуже
схем с явными циклами.
Проанализировав граф автомата или таблицу, можно заметить, что в дан-
ном примере наряду с циклом потоковой обработки имеется еще один цикл:
передачи управления между состояниями. Это причина, из-за которой в двух
578 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

предыдущих вариантах появились присваивание переменной State значения


и выбор выполняемого фрагмента по этому значению.
Особенность последовательностей действий

State = <значение>;
switch ( State )

которая выполняется всякий раз в конце действий, ассоциированных с состо-


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

Программа 10.2.4 Длины слов: состояния — метки в программе

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

char symbol;
int cnt;
10.2. ПОДСЧЕТ ДЛИН СЛОВ 579

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’) */ return;

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);
return;
}
}

10.2.5. Представления, ориентированные на автоматические преобра-


зования диаграмм переходов

Принятие решения об автоматическом оперировании с табличным пред-


ставлением влечет за собой требование строгого определения структур дан-
ных, программно представляющих конечный автомат. Это справедливо как
для варианта автоматической трансляции, так и для интерпретации. Задание
диаграммы конечного автомата зависит от стратегии дальнейшей обработки.
В частности, если предполагается предварительное построение текстового
580 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

представления таблицы, которое в дальнейшем преобразуется к виду, при-


способленному к конкретной работе, то вполне приемлемо, к примеру, такое
(универсальное) текстовое представление:
Программа 10.2.5 Программа в виде размеченного текста

_1 char symbol; // Чтение потока символов неявное

// Инициализация
int cnt;
... _4 St1 _5
_1 St1 _2 ’a’<=symbol && symbol <= ’z’ _3 printf ("%c", symbol);
cnt = 1; _4 St2 _5
_1 _2 //symbol <’a’ && ’z’< symbol && symbol != ’\n’

//Так как нужно печатать только слова,


_3

//действий нет _4 St1 _5

// symbol == ’\n’ не нужно читать


_1 _2 failure _3
_4 exit _5
_1 St2 _2 ’a’<=symbol && symbol <= ’z’ _3 printf ("%c", symbol);
cnt++; _4 St2 _5
_1 _2 //(symbol <’a’||’z’< symbol)&&*/ symbol!=’\n’
_3 printf (" - %i\n", Cnt); _4 St1 _5

// отсутствие условия — истина, но без неявного чтения потока


_1 _2 failure _3 printf (" - %i\n", Cnt); _4 exit _5
_1 exit _2
_3 return 0; _4 _5

Здесь “_i”, i = 1, . . . , 5, обозначают позиции таблицы. Эти сочетания симво-


лов, нигде в нормальной программе не встречающиеся, легко могут распо-
знаваться препроцессором. Размещаемые между ними последовательности
символов разносятся по соответствующим полям нужной структуры, которая
в зависимости от выбранной стратегии интерпретируется либо транслирует-
ся. С помощью этих сочетаний осуществляется разметка текста, которая
дает возможность распознавать и осмысливать его фрагменты. Стоит обра-
тить внимание, что за счет специального взаимного расположения символов
в данном тексте представляемая им таблица автомата вполне просто усма-
тривается взглядом. Если нет более подходящего представления, то данную
структуру вполне можно рекомендовать для ввода.
Однако непосредственная интерпретация универсального текстового раз-
меченного представления довольно затруднительна. Предпочтительнее, что-
бы единицами интерпретации были бы сами поля таблицы. Вообще гово-
10.2. ПОДСЧЕТ ДЛИН СЛОВ 581

ря, этому противоречит наличие в таблице полей, значения которых — фраг-


менты исполняемого кода на языке программирования (такая запись удобна
для человека, т. к. для заполнения таблицы не приходится прибегать ни к ка-
ким дополнительным соглашениям). На самом деле противоречие возникает
только для поля действий, поскольку последовательности символов между
“_2” и “_3” имеют ясно выраженную семантику: это проверка условия. Если
всегда рассматривать условия как проверку того, чему равен входной сим-
вол, то вполне понятны, легко задаются, распознаются и интерпретируют-
ся специальные обозначения: перечисления значений, их диапазоны и т. д.
Трактовка этих обозначений не вызывает осложнений, а потому подобные
приемы кодирования применяются довольно часто.
Что касается полей действий, то их представление для большинства язы-
ков программирования данное обстоятельство является непреодолимым пре-
пятствием: кодирование действий очень усложняет обработку. Но если в язы-
ке можно задавать подпрограммы как данные, то описание структуры табли-
цы, которое будет согласовано с дальнейшей трансляцией или интерпрета-
цией, возможно. Обсуждение проблем одновременной интерпретации полей
данных и полей действий будет продолжено в контексте произвольной раз-
метки текстов (т. е. без привязки к данному представлению таблицы).
Пусть тип T_StructTableLine описывает структуру одной строки таблицы,
а вся таблица представлена в виде массива Table таких структур. Пусть далее
iSt — переменная, используемая для индексирования массива Table, некото-
рые из ее значений представляют состояния конечного автомата (но не все, а
только те, к которым определен переход!). Наконец, пусть

обработчик строки (ее интерпретатор или транслятор), ‘понимающий’ струк-


int handler ( int );

туру T_StructTableLine, который специфицируется как функция, при выпол-


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

iSt = 0;
do {
iSt = handler ( iSt );
}
582 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

while ( OUTSIDE ( iSt ) );

Понятно, что выбор массива в качестве представления автомата неприн-


ципиален. Допустимо, а иногда и предпочтительнее, работать со списком
строк. Также непринципиален способ вычисления предиката OUTSIDE, про-
веряющего условие окончания цикла, хотя он и зависит от выбранного пред-
ставления автомата. Возможность не рассматривать явно имена состояний,
напротив, принципиальна: тот факт, что строки таблицы сгруппированы во-
круг (поименованных) состояний, ничего не значит ни для обработки, ни для
интерпретации, поскольку у конечного автомата следующее состояние все-
гда определяется явно.
Логически функция handler, интерпретирующая таблицу, описывается до-
вольно просто. Для правильной таблицы, не содержащей состояний, в кото-
рых возможны неопределенные переходы (неверно, что дизъюнкция условий
истинна), она строится из следующих шагов:

1. Извлечь значение Table[iSt];

2. Активизировать вычисление условия обрабатываемой строки табли-


цы;

3. Если условие истинно, то

(a) активизировать подпрограмму действий,


(b) читать очередной символ,
(c) завершить handler со значением, извлекаемым из поля следую-
щего состояния;

4. Если условие ложно, то

(a) iSt++;,
(b) Перейти к 1.

Эту простую схему несколько утяжеляют детали, которые нужно предусмо-


треть для заключительных состояний (проделать самостоятельно!).
Таким образом, нужно научиться описывать структуру строки таблицы.
На языке C/C++/С# эта задача может решаться довольно просто:
10.2. ПОДСЧЕТ ДЛИН СЛОВ 583

// поле для имени состояния избыточно


struct T_StructTableLine

// поле условия
{

// поле действия
int (*condition)();

// поле перехода: индекс строки таблицы,


void (*action)();

// которая будет обрабатываться следующей,


int ref;

// или признак завершения процесса


}

Сама таблица описывается следующим образом:

или
T_StructTableLine Table[]

T_StructTableLine Table [Размер таблицы]


если значения строк задаются вне этого описания.
Однако сразу же видно ограничивающее соглашение, которое пришлось
принять в связи с особенностями языка реализации: все условия и все дей-
ствия оформлены как функции, тогда как задача по своей сути этого не требу-
ет. Более того, отделенное от таблицы описание функций, как того требуют и
синтаксис, и семантика С/С++/C#, резко снижает выразительность предста-
вления. Таким образом, для данного языка нельзя избежать оторванных от
представления таблицы описаний функций:

— условие в Table[1]
и
int Cond_St1_1()

void Act_St1_1() — действие в Table[1] (строка 1 состояния St1);

— условие в Table[2]
и
int Cond_St1_2()

void Act_St1_2() — действие в Table[2] (строка 2 состояния St1);

и т. д.
Другим неприятным моментом данного представления является то, что
не удается единообразно с действиями представить их общий контекст (нуле-
вая строка таблицы), а поскольку инициализацию естественно рассматривать
как задание предварительных действий, готовящих собственно обработку, то
ее предпочтительнее соединять с контекстом, а не с функциями, реализую-
щими проверки и действия.
Последний недостаток неустраним ни в каком языке, если проверки и
действия трактовать как процедуры, которые работают в общем контексте,
584 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

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

Программа 10.2.6

/* инициализация */, 1},


Table[]= {
{{return true;},
/*St1*/ {{return ’a’<=symbol &&
symbol <= ’z’;}, {printf ("%c", symbol);
cnt = 1;}, 4},

/* Так как нужно печатать


только слова, действия не заполняются */,
{{return symbol != ’\n’;},

1},

/* Переход не требует
чтения, symbol == ’\n’ не нужно читать */, 0},
{{return true},

/*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}
};

Несколько нагляднее и ближе к табличному заданию то же самое может быть


записано на Алголе 68 (фактически представлен не точный перевод на этот
язык, а некий гибрид его с С/С++, к тому же избавленный от ряда специфич-
ных деталей):

Программа 10.2.7
10.2. ПОДСЧЕТ ДЛИН СЛОВ 585

[0: ] структ ( имя проц Condition = ()лог,


имя проц Action = ()ничто,
цел iRef

((истина), co инициализация co), 1),


)= (

(‘a’<=symbol и symbol<=‘z’;), (печатать(symbol); cnt = 1;),


( co St1: co

((symbol <> ‘\n’;), co пустые действия co), 1),


4),

((истина), (co пустые действия co), 0),

(’a’<=symbol и symbol<=‘z’;), (печатать(symbol); cnt += 1;),


( co St2: co

4),
(symbol <> ‘\n’;), (печатать(symbol+‘- \n’,cnt), 1),
((истина), (печатать(symbol+‘- \n’,cnt), 0)
);

Символ co переключает между режимами комментария и основного текста.


Эта запись таблицы более наглядна за счет того, что:

• есть возможность описывать процедурный тип (вид, по терминологии


Алгола 68);

• есть возможность литерального изображения процедурных значений;

• проще выглядит так называемое “замкнутое предложение, вырабаты-


вающее значение”.

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

a) для программиста сохраняется наглядность;

b) при исполнении достигается алгоритмичность;

c) легко разносить фрагменты таблицы по разным участкам программы;


586 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

d) можно жертвовать наглядностью результирующего текста, поскольку фак-


тически с ним никто, кроме транслятора не работает (пример: вставка но-
вых строк в таблицу — проблема для человека, но не для препроцессора);

e) по той же причине можно жертвовать даже структурностью результирую-


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

Это решение часто является приемлемым, но недостатков оно не лишено.


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

• Когда читать? Поскольку, прочитанный символ используется после его


распознавания (например, он может выводиться на экране), естествен-
но “не терять” его, пока выполняется действие. Следовательно, читать
очередной символ целесообразно после отработки действия.

• Как поступать в начале процесса: должен ли быть прочитан символ пе-


ред первым вызовом функции handler? Понятно, что чтение символов
является часть функциональности этой функции, и, если в ней реализо-
вать чтение до вызовов условия и действия, то это противоречит преды-
дущему соглашению, а противоположное решение делает нестандарт-
ным начало процесса. В предлагаемой программе принято решение об
особой обработке нулевой строки. Поскольку, являясь инициализаци-
ей, она действительно особая, это соответствует сделанному соглаше-
нию.
10.2. ПОДСЧЕТ ДЛИН СЛОВ 587

• Как поступать в конце процесса? Чтение заключительного символа (’\n’)


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

Программа 10.2.8 Длины слов: интерпретатор конечного автомата

// переменная для чтения потока символов


#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;

// или признак завершения процесса


}

Table[]={ {c0,a0,1},// таблица инициализируется статически,


T_StructTableLine

// альтернативное решение — специальная


// функция задания начальных значений.
{c1,a1,4},

// Оно более гибкое, но менее


{c2,a0,1},

// эффективно.
{c0,a0,0},

// О наглядности см. комментарий


{c1,a2,4},

// в тексте.
{c2,a3,1},
{c0,a3,0}};
588 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

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 true;}


int c1(){return ’a’<=symbol && symbol <= ’z’;}
int c2(){return symbol != ’\n’;}

void a0(){}
void a1(){printf ("%c", symbol);cnt = 1;}
void a2(){printf ("%c", symbol);cnt++;}
void a3(){printf ("- %i\n", cnt);}

10.2.6. Обсуждение решения

Как уже говорилось, функция подразумевает выделение памяти (в стеке),


следовательно, вызовы Table[i].condition() и Table[i].action() нарушают теоре-
тический постулат о том, что конечному автомату не требуется динамическая
память. Но фактически здесь нет никакого противоречия:
• Во-первых, теоретическое “не требуется” совсем не обязательно долж-
но буквально реализовываться. Напротив, на каждом уровне требуются
свои средства.
10.2. ПОДСЧЕТ ДЛИН СЛОВ 589

• Во-вторых, не исключается, что транслятор, проанализировав транс-


лируемый текст, сможет построить код, который учитывает фактиче-
ские режимы работы функций, и, как следствие, будут заменены вызо-
вы функций на прямые переходы к нужным фрагментам.
• В-третьих, при желании можно было отказаться от задания условий и
действий функциями, а вместо этого составить специальные “неиспол-
няемые” фрагменты и задать переходы к ним взамен вызовов. Послед-
ний путь — ручное кодирование предыдущей возможности и, в то же
время, реализация ранее указанного подхода (см. пункт e) на стр. 586).
На первый взгляд может показаться, что задача оптимизации вызовов в С/С++/C#
системах программирования решается с помощью прагматической директи-
вы inline. В самом деле, использование встраиваемых (inline) функции име-
ет целью подмену вызова размещением кода тела функции в точке вызова.
Однако в данном случае это не приведет к желаемому: компилятору просто
некуда вставлять код функции, а потому директива inline будет проигнори-
рована.
Если только что отмеченные недостатки метода автоматического встраи-
вания таблиц в программы можно считать не очень значительными, то сле-
дующая критика уже принципиальна. Более того, приводимые далее замеча-
ния нужно рассматривать в качестве базы для развития подхода в целом и
выработки адекватного технологического (и значит, в определенном смысле
универсального) метода.
Как уже отмечалось, фрагменты, описывающие условия и действия в та-
блице конечного автомата и реализованные как процедурные вставки, с точ-
ки зрения препроцессора (не препроцессора системы программирования, а
специального преобразователя, генерирующего представление таблицы для
интерпретации) являются нераспознаваемыми данными, которые он просто
пропускает без изменений для другой обработки С-компилятором. Интер-
претатор же, наоборот, не в силах сам исполнять процедуры, а потому трак-
тует ссылки на них (в соответствующих полях) как данные. Таким образом,
условия и действия в таблице двойственны: они являются одновременно и
данными, и программными фрагментами. Автоматический преобразователь
таблицы, не понимая языка таких двойственных данных, пытается, тем не
менее, их объединить с обычными данными (в рассматриваемом случае это
индексы строк переходов) в одной структуре.
На первый взгляд кажется, что ситуация существенно упростилась бы в
языке, в котором есть возможность воспринимать данные как выполнимые
590 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

фрагменты. Пример такого рода — команда “Вычислить строку”. Эта коман-


да давала бы возможность интерпретатору понимать программы-данные, не
оставляя их нераспознанными. Подобная команда порою встречается в язы-
ках скриптов, ориентированных на интерпретацию. А в языке LISP, в кото-
ром структуры данных и программы просто совпадают, указанная двойствен-
ность не возникает. Но опыт показывает, что столь общее решение чревато
столь же общими и вездесущими неприятностями, и нужно искать частный
его вариант, адекватный для данной задачи.
Разрыв между программами и данными довольно просто ликвидирует-
ся для поля условия, если все условия срабатывания переходов сводятся к
проверкам символов (как в обсуждаемой задаче). Достаточно закодировать
варианты проверок и вместо алгоритмической записи указывать лишь то, ка-
кие значения входного символа вызывают тот или иной переход. В частно-
сти, в рассматриваемом примере, это решение можно оформить в виде пе-
речислений символов срабатывания или множеств символов, а задача опре-
деления попадания входного символа в такие множества вполне реализуема
в функции handler. Поэтому замечание о двойственности более точно от-
носить лишь к действиям, которые являются программными фрагментами
принципиально. Реализация кодирования проверок требуется чаще всего, и
ее можно рассматривать в качестве стандартного приема программирования
(см., к примеру, синтаксические таблицы в § 10.3).
Метод решения задач с помощью построения конечных автоматов являет-
ся довольно удобным, когда речь идет об обработке потоков. Чтобы освоить
его, предлагаем самостоятельно решить этим методом ряд задач такого ро-
да и оценить решения количественно: сколько потребуется состояний, какое
число действий, ассоциированных с переходами, требуется. Полезно опре-
делить различные конечные автоматы для одной и той же задачи и сравнить
эффективность полученных решений. Для самостоятельного решения задач,
которые вполне подходят для применения данного метода, мы рекомендуем:

• Подсчитать число вхождений нескольких заданных заранее слов во вход-


ную последовательность (к подобной задаче мы вскоре вернемся) и

• Преобразовать входной текст, в котором неразличимы заглавные и строч-


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

Может оказаться, что вы почувствуете необходимость привлечения средств,


10.2. ПОДСЧЕТ ДЛИН СЛОВ 591

выводящих решения за рамки данного метода. Это не страшно, нужно только


четко осознавать, где кончается использование конечного автомата и начина-
ется применение другого подходящего метода.
Обсуждая метод конечных автоматов, полезно сравнить, как решалась бы
наша задача при использовании стилей, отличных от программирования от
состояний. При сопоставлении вариантов, предложенных выше для подсчета
длин слов, показано, что стиль структурного программирования плохо под-
ходит для этой задачи: как минимум, он приводит к избыточным вычислени-
ям.
Если обратиться к сентенциальному программированию, то для решения
в этом стиле нужно отказаться от соглашения об обработке потока, заме-
нив его описанием структуры перерабатываемых данных, и в терминах та-
кой структуры формулировать задание. Это осуществимо, например, можно
определить структурное понятие слова и способ вычисления его длины. Но
не окажется ли подобная структура чрезмерно сложной или вычислительно
избыточной? Для сентенциального стиля характерно оставлять за рамками
рассмотрения вопросы распознавания структуры, но в реальной программе
их так или иначе приходится решать, а значит, надо учитывать соответствую-
щие расходы. Применительно к нашей задаче оказывается, что эти расходы
слишком обременительны по сравнению с основной задачей. Конечно же,
эти рассуждения не учитывают затраты человеческого разума на разработ-
ку соответствующих представлений. Попросту говоря, реализация языковой
поддержки сентенциального стиля, выполненная однажды, может повлечь
за собой экономию умственных усилий при многократном использовании
этой поддержки. Но в данной задаче использование сентенциального стиля
(конечно, если оно не сводится к использованию стандартного метода, зало-
женного в библиотеку), не дает экономии и мыслительных ресурсов. Такой
стиль целесообразен для более логически сложных задач, работающих с бо-
лее сложными структурами данных.
С точностью до того, чем заменяется соглашение о потоке, рассуждения
по поводу сентенциального стиля можно перенести на функциональное ре-
шение. Здесь базовое соглашение становится совсем уж далеким от исход-
ной постановки задачи. Этот стиль плохо сочетается с понятиями состояния,
перехода и действия. Общим для двух стилей недостатком решения могут
стать трудности декомпозиции задачи: явное отделение действий от распо-
знавания слов. Очень часто именно это преимущество метода конечных ав-
томатов оказывается решающим в пользу его применения в конкретной си-
туации. Поэтому при разработке сентенциальных и функциональных систем
592 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

программирования нужна специальная забота не только о поддержке стиля,


но и о том, каким образом будет достигаться подключение к программе мо-
дулей, написанных в иных стилях. Впрочем, это же можно сказать вообще о
любых системах программирования.
Следующие два стиля (программирование от событий и от приоритетов)
вполне годятся для прямолинейной реализации конечного автомата. Во мно-
гом они сформировались под влиянием обсуждаемого метода, когда целесо-
образно вынесение конечного автомата на уровень универсального средства,
чтобы отделить его функционирование от другой обработки. Применитель-
но к задаче о длинах слов, как и во многих других ситуациях применения
обсуждаемого метода, это обстоятельство решающей роли не играет, а пото-
му указанное преимущество не является принципиальным. Иными словами,
использование данных стилей оправдано, если универсальный механизм их
поддержки уже имеется.
Метод конечных автоматов вполне сочетается с объектно-ориентирован-
ным стилем. Можно сказать, что система объектов, динамически продуци-
руемая программой данного стиля, является одной из возможных реализа-
ций конечного автомата с потенциально неограниченным числом состояний,
представляемых объектами. При таком взгляде на объектную систему анало-
гом переходов между состояниями служат сообщения, передаваемые меж-
ду объектами. Конечно, это далеко не единственная трактовка, более того,
в задачах, которые неестественно решать данным методом, она оказывается
вредной, противоречащей, например, взгляду на объекты как на одновремен-
но активные единицы выполнения программы. И это обстоятельство следует
рассматривать в качестве границы сочетаемости объектно-ориентированно-
го стиля и метода конечных автоматов.
Разница между объектной средой и конечным автоматом только в том,
что объекты могут возникать (и уничтожаться) динамически, а число строк
таблицы равно суммарному числу переходов для всех состояний и фиксиро-
вано до вычислений. Но эта разница и дает качественный рост мощности
объектно-ориентированного подхода, указывая, когда целесообразно пред-
ставлять таблицы состояний объектами.
Применительно к конкретной задаче о длинах слов объектно-ориентиро-
ванное задание автомата возможно, но для решения вопроса об автоматиза-
ции перевода табличного представления в программное само по себе оно ни-
чего не дает. В схеме с функцией handler двойственность программ и данных
по-прежнему затрудняет построение и интерпретацию.
10.3. СИНТАКСИЧЕСКИЕ ТАБЛИЦЫ 593

§ 10.3. СИНТАКСИЧЕСКИЕ ТАБЛИЦЫ

10.3.1. Расширение сферы применения конечных автоматов: анали-


затор как система связанных конечных автоматов

Привлекательность метода конечных автоматов является побудительной


причиной попыток его применения в тех ситуациях, которые формально вы-
ходят за рамки возможностей конечных автоматов. Один класс таких ситуа-
ций, связанный с построением систем объектов объектно-ориентированной
программы как автомата, был упомянут выше. Расширение сферы примене-
ния конечных автоматов в этом случае достигается за счет потенциально не-
ограниченного множества объектов-состояний.
В данном параграфе мы обсуждаем другой путь расширения метода. Ключе-
вым свойством его является снабжение автомата памятью. Речь идет не о той
памяти, которая используется в действиях, сопутствующих переходам, —
она есть часть обработки и к вычислительной мощности автомата отноше-
ния не имеет. Расширение конечного автомата как алгоритма распознавания
за счет памяти связывается с модификацией команд перехода из состояния
в состояние: теперь при выполнении команды может запоминаться какая-
либо информация, а при выборе перехода используются не только сведения
из входного потока, но и запомненные данные.
Если автомат снабжается стековой памятью, то говорят об автоматах со
стековой памятью4 , вычислительная мощность которых больше обычных
конечных автоматов, но меньше мощности машины Тьюринга. Именно такие
автоматы служат теоретической основой разработок синтаксических анали-
заторов контекстно-свободных языков. По существу все анализаторы, осно-
ванные на автоматах со стековой памятью, отличаются друг от друга лишь
стратегиями работы со стеком.
В русскоязычной литературе для этого понятия часто употребляется термин автомат с
магазинной памятью, который отражает разграничение между стеками и магазинами. Под
4

магазином понимается память, доступ к которой по чтению-извлечению и для записи огра-


ничен только последним употребленным элементом (это стек в обычным для англоязычной
литературы понимании). В таком случае, стек — это память, для которой определены опе-
рации чтения любого записанного элемента и извлечения только последнего из запомненных
элементов. Доказано, что с помощью магазина можно промоделировать стек, а потому тео-
ретически достаточно только одного понятия, т. е. магазина, как наиболее простого. Вместе
с тем, на практике разграничение между стеками и магазинами существенно, а потому сто-
ило бы использовать оба понятия. Однако вслед за англоязычной традицией сегодня чаще
всего используется термин стек.
594 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

Мы рассмотрим только одну из этих стратегий, которая допускает интер-


претацию, сохраняющую прежний смысл конечного автомата. Вместо явной
дополнительной памяти в системе одновременно действует несколько про-
цессов: экземпляров различных автоматов, подчиняющихся стековой дис-
циплине: активизация нового экземпляра автомата приводит к приостанов-
ке предыдущего автомата (запоминанию его состояния), а окончание работы
автомата — к возобновлению приостановленного автомата с передачей ему
сведений о том, как отработал завершающийся процесс. Внимательный чи-
татель тут же заметит, что эта трактовка повторяет то, что ранее мы называли
процедурным механизмом, но только для процедур специального вида, кото-
рые реализуют конечные автоматы.
Эти сведения (значение True, если вызванный автомат распознал строку
успешно, и False в противном случае) используются для выбора очередно-
го состояния, в которое переходит автомат. Использование автоматов со сте-
ковой памятью частично нарушает инвариант стиля программирования от
состояний: Действия глобальны, условия локальны, поскольку передава-
емая через стек информация, которая управляет поведением автомата, дела-
ется локальной. Конечно же, регламентированный отход от стиля не является
крамолой, и именно на этом пути появляются различные стили программи-
рования как варианты базовых стилей. Но в настоящей главе нас в первую
очередь интересует направление, в максимальной степени соответствующее
стилю программирования от состояний. По этой причине мы и рассматрива-
ем системы конечных автоматов, связанных стеком.
На первый взгляд может показаться, что такие системы еще дальше уво-
дят от программирования от состояний, чем автоматы со стековой памятью.
Однако требование корректно определять вычисления системы неизбежно
приводит к тому, что потоки, которые перерабатываются вызываемыми в од-
ном состоянии экземплярами автоматов, должны быть строго локальными
для них. В самом деле, если допустить, что передаваемый поток является
общим, то чтение из него должно быть независимым для каждого из экзем-
пляров, а это противоречит дисциплине обработки потока. При успешном
окончании работы экземпляра очередной читаемый символ должен следо-
вать за подстрокой, которая распознана отработавшим экземпляром, т. е. для
продолжения действий используется тот поток, который был передан этому
экземпляру. Что же касается неуспешно завершившихся экземпляров, то их
локальные потоки ликвидируются. Обычным способом реализации такого
расщепления потока и локализации его потомков по вызываемым экземпля-
рам автоматов является организация возвратов по обрабатываемой строке.
10.3. СИНТАКСИЧЕСКИЕ ТАБЛИЦЫ 595

Таким образом, для систем конечных автоматов локальность условий игра-


ет более значительную роль, чем хранимые на стеке данные для автоматов со
стековой памятью. Именно поэтому класс систем конечных автоматов, свя-
занных стеком, значительно шире класса всех автоматов со стековой памя-
тью: он моделирует машины Тьюринга. Этот класс оказывается эффектив-
ным и содержательным для описания достаточно широкого класса алгорит-
мов, в частности анализаторов контекстно-свободных языков. Но путь ис-
пользования систем конечных автоматов общего вида уже ведет к сентенци-
альному программированию. Если же оставаться в рамках программирова-
ния от состояний, то нужно сузить обсуждаемый класс, рассматривая лишь
такие системы, которые позволяют работать при выполнении инварианта это-
го стиля. Для конкретной задачи синтаксического анализа такими являют-
ся системы, которые в состоянии работать без возвратов по обрабатываемой
строке.
Переход от автоматов в возвратами к безвозвратной схеме и условия ее
реализации рассмотрены Д. Кнутом в статье [Top-down analysis], в которой
формализуется класс так называемых нисходящих методов синтаксическо-
го анализа. Примечательно, что этот подход хорошо ложится в русло стиля
программирования от состояний, что будет показано в ближайших разделах.
10.3.2. Задача анализа простых выражений

Рассмотрим задачу, которая достаточно часто встречается на практике.


Она возникает в каждой программе, требующей распознавания и анализа
сложных структур исходных данных, прежде всего, таких, которые зада-
ны в виде последовательностей символов. Например, в каждом транслято-
ре требуется распознавать контекстно-свободную структуру анализируемого
текста. Именно в таком частном случае был осознан и кодифицирован опи-
сываемый ниже общий метод анализа.
Задача решается путем построения комплекта конечных автоматов, ка-
ждый из которых в состоянии распознать во входном потоке последователь-
ность символов, удовлетворяющую определению соответствующей конструк-
ции языка. В ходе такого распознавания автомату может потребоваться обра-
титься к другому автомату, который в состоянии выяснить наличие или отсут-
ствие во входном потоке вложенной конструкции. Чтобы обеспечить взаимо-
действие автоматов, нужно договориться о том, что они продуцируют в каче-
стве выхода, и как этот выход будет использован. Поскольку распознавание
вложенной конструкции для использующего автомата концептуально подоб-
но распознаванию лексемы (в нашем примере — символа), целесообразно
596 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

сделать эти две ситуации унифицированными. Иными словами, обращение


к используемому автомату должно быть представлено как условие перехода
из состояния в состояние (см. рис. 10.4, на котором использование автомата

Действие S12
a

b S2
S1
Действие S13

<Вложенная
конструкция>
Действие S14
S3

S4

Рис. 10.4. Обращение к автомату для распознавания вложенной конструкции

изображено в виде его имени в угловых скобках).


Однако только что представленное прямое решение не является до кон-
ца продуманным. Здесь не учитывается, что при распознавании вложенной
конструкции, вообще говоря, должно быть продвижение по входному потоку.
По определению условия срабатывания перехода это означает, что, если кон-
струкция не распознана, то использующий автомат должен получить вход-
ной поток в том виде, в котором он был непосредственно перед обращением
к автомату. В результате придется моделировать двойное движение по пото-
ку. Это возможно (например, с помощью дополнительной памяти), однако
неэффективно.5 По этой причине обычно принимаются дополнительные со-
Когда приходится прибегать к такому моделированию, это сигнал о том, что метод ко-
нечных автоматов для решаемой задачи начинает давать сбои. В таких случаях, быть может,
5
10.3. СИНТАКСИЧЕСКИЕ ТАБЛИЦЫ 597

глашения, которые отражают стратегию обработки потока, которая, в свою


очередь, делает так, что все экземпляры автоматов оказываются в состоянии
работать с одним потоком.
В качестве одного из естественных соглашений можно указать на упо-
рядоченность проверок условий срабатывания: обращение к другому авто-
мату делается только тогда, когда все простые варианты переходов отбрако-
ваны. Чаще всего этого соглашения недостаточно, например, когда вариан-
ты включают распознавание нескольких конструкций, но в ряде случаев оно
позволяет сделать систему автоматов работоспособной. Более строгий под-
ход — ограничиваться применением метода ситуациями, когда все вариан-
ты переходов разделяются: множества символов, с которых могут начинать-
ся вложенные конструкции, не пересекаются между собой и с множеством
меток простых переходов. В этом случае по первому символу входного по-
тока можно детерминировать выбор простого перехода или перехода к рас-
познаванию соответствующей вложенной конструкции.6 Тогда за счет загля-
дывания вперед можно решить, надо ли фактически обращаться к дополни-
тельному автомату.
Принятие этих соглашений влечет за собой неприятное следствие: нача-
ло конструкции обнаружено, обращение к дополнительному автомату про-
изошло, но он оказался не в состоянии распознать конструкцию. Это случай,
когда входной поток не соответствует требуемой структуре, иными словами,
в нем обнаружена ошибка. Таким образом, унифицированное для простых
переходов и для переходов к конструкциям представление должно предусма-
тривать в проверках возможность не двух, а трех исходов (для простых пе-
реходов возможны только первые два варианта исходов):
• T, если автомат завершил распознавание конструкции успешно;
• F, если автомат обнаружил без продвижения по входному потоку, что
данная конструкция отсутствует;
• E, если начато распознавание конструкции (т. е. произошло продвиже-
ние по входному потоку), но автомат завершил работу, выяснив, что
естественнее перейти к сентенциальному решению задачи, приспособленному к организа-
ции недетерминированного движения к цели и возвратам к пройденному, когда очередная
попытка оказывается неудачной.
Если допускается, что конструкция порождается как пустая последовательность симво-
лов, то условие детерминирования необходимо дополнить требованием отсутствия пересе-
6

чения указанных множеств с множеством символов, возможно следующих за конструкцией.


598 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

данная конструкция неправильна.

Выстраивание проверок в последовательность для выяснения условия сра-


батывания перехода дает дополнительную возможность повышения вырази-
тельности метода. Она связана с тем, что становится осмысленным припи-
сывать действия не только успешным переходам, но и двум другим исходам
проверок (см. рис. 10.5, на котором упорядоченные проверки условий сра-

ДействиеTS12
a
T

F ДействиеFSнеa
bT S2
S1
F

ДействиеFSнеb
<Вложенная констру кция> ДействиеTS13
E F T
ДействиеTS14

S3
ДействиеESне<Вложенная конс трукция> S4


Действие FSне<Вложенная констру кция>

Рис. 10.5. Автомат с упорядоченными проверками условий срабатывания пе-


реходов

батывания переходов помечены символами исходов и вынесены за изобра-


жение состояния). Разумеется, с точки зрения теории вычислительная мощ-
ность автомата не меняется, однако для логики конкретных программ полез-
но иметь такую возможность. Заметим, что она потребует другого формата
таблиц, представляющих конечный автомат, нежели те, которые мы исполь-
зовали при решении задачи о длинах слов. Как и прежде, мы не будем опи-
сывать структуру таблиц в общем виде, а представим их на примере решения
задачи синтаксического анализа простых выражений.
Рассмотрим синтаксическое определение, порождающее простейшие вы-
10.3. СИНТАКСИЧЕСКИЕ ТАБЛИЦЫ 599

ражения языка С и оператор присваивания.


<оператор присваивания> ::=
<идентификатор> “=” <выражение>
<выражение> ::=
<множитель> [ “+” <выражение> |“-” <выражение>]*
(10.1)
<множитель> ::=
<терм> [ “*” <множитель> |“/” <множитель> ]*
<терм> ::= <идентификатор> |“(” <выражение> “)”
<идентификатор> ::= [ “a” |“b” |“c” ]
Эти правила несколько отличаются от используемых при определении вы-
ражений и приоритетов операций языка С. В прежнем стиле выражение и
множитель определялись в соответствии с правилами вычисления выраже-
ний:
<выражение> ::= <множитель> |
<выражение> [ “+” <множитель> |“-” <множитель>]*
(10.2)
<множитель> ::= <терм> |
<множитель> [ “*” <терм> |“/” <терм>]*
тогда как по определению (10.1) выражения с операциями одного уровня ста-
новятся правоассоциативными (сравните два вывода строки "a-b-c" в двух
грамматиках). Чуть ниже станет понятно, почему здесь выбраны “плохие”
правила, а также разъяснено, как их можно чисто формально исправить. Важ-
но подчеркнуть, что способ исправления демонстрирует метод работы безот-
носительно данного примера.
Система конечных автоматов для анализа контекстно-свободных языков
по их грамматике строится достаточно прямо. Для каждого нетерминального
символа составляется таблица — диаграмма автомата, отражающая все ва-
рианты его работы. Так, для присваивания строится следующая диаграмма
(см. таблицу 10.2). Первая строка таблицы — заголовок автомата. Она содер-
жит имя автомата, по которому происходит обращение к нему (мы исполь-
зуем угловые скобки, чтобы выделить имена автоматов явно и подчеркнуть
их связь с нетерминальными символами грамматики). Остальные строки та-
блицы описывают работу автомата.
Первая колонка используется для организации переходов между состо-
яниями (аналог первой колонки таблицы 10.1). Из-за простоты структуры
получающихся таблиц надобность в явном именовании всех состояний ка-
ждого автомата отпадает: большинство переходов при синтаксическом ана-
600 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

<оператор присваивания>
<идентификатор> T /* определен получатель значения */
F /* это не оператор присваивания */
E /* это лишняя графа — см. ниже */
F
E
“=” T

T /* определен источник значения */


F E

F /* ошибка: нет выражения */


<выражение> T

E /* ошибка в выражении (распространя-


E

ется на оператор) */
E

Таблица 10.2. Диаграмма для присваивания

лизе — это переходы к следующей строке, а для указания других переходов


оказывается достаточно простых меток.
Вторая колонка, которая используется для задания так называемых кон-
ституент, т. е. составляющих синтаксического правила (терминальных и
нетерминальных символов), аналогична второй колонке таблицы 10.1, т. е.
условиям срабатывания переходов. Но здесь нецелесообразно задавать про-
верки явно: само вхождение конституенты указывает на то, что надо сравнить
ее с символом входного потока или вызвать другой конечный автомат. В ре-
зультате выполнения такого сравнения должен быть выработан один из трех
исходов: T, F или E. Три или два (для терминальных символов) варианта ис-
хода выделены как три подстроки таблицы с соответствующими пометками
в третьей колонке.
Четвертая колонка предназначена для задания действия, ассоциирован-
ного с переходом, а точнее — c вариантом исхода проверки.
Наконец, пятая колонка, называемая полем продолжения, используется,
чтобы указать, куда должен переходить автомат в дальнейшем:
a) незаполненность поля продолжения означает переход к следующей стро-
ке;

b) если в поле продолжения указана метка (в нашей нотации задаются число-


вые метки), то следующее состояние автомата — это та строка, которая
помечена данной меткой (если такой строки нет, то таблица составлена
некорректно);

c) если в поле продолжения задан один из трех исходов T, F или E, то это


10.3. СИНТАКСИЧЕСКИЕ ТАБЛИЦЫ 601

означает, что текущий автомат завершает свою работу с указанным исхо-


дом, который передается автомату, вызвавшему данный автомат;

d) другое заполнение поля продолжения считается некорректным.


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

<выражение>

/* распознано, что выражения нет */


<множитель> T

/* ошибка в множителе */
F F
E E
2 “+” T

/* зацикливание */
F 1

/* после “+” нет выражения */


<выражение> T 2

/* ошибка в выражении */
F E
E E

/* распознано выражение */
1 “-” T
F T

/* после “-” нет выражения */


<выражение> T 2

/* ошибка в выражении */
F E
E E

Таблица 10.3. Диаграмма для выражения

10.3.3. Синтаксические таблицы и рекурсивный спуск


602 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

<множитель> /* все аналогично */


<терм> T
F F

T /* пример того, что можно объединить


E E

два вызова понятия (ср. c <выраже-


2 “*” 0

ние>) */
F 1
1 “/” T
F T
0 <множитель> T 2
F E
E E

Таблица 10.4. Диаграмма для множителя

Приведенная система таблиц может быть без труда систематически пере-


писана в комплект рекурсивных процедур Например, для оператора присва-
ивания можно написать следующую функцию, закодировав T, F и E целыми
константами:
Программа 10.3.1

int Assignment()
{
switch (Identifier())
{
case T:
if (current_symbol= =‘=‘)
{ get_next_symbol();
switch (Expression())
{
case T: return T;
case F: return F;
case E: return E;
}
}
else return E;
}
10.3. СИНТАКСИЧЕСКИЕ ТАБЛИЦЫ 603

/* терм — идентификатор */
<терм>

/* возможен другой вариант */


<идентификатор> T T
F

/* далее должны быть <выражение> и


E E
“(” T

/* распознано, что терма нет */


“)” */
F F
<выражение> T
F E

/* терм — идентификатор */
E E
“)” T T
F E

Таблица 10.5. Диаграмма для терма


<идентификатор>
“a” T T
F
“b” T T
F
“c” T T
F F

Таблица 10.6. Диаграмма для идентификатора

case F: return F;
case E: return E;
}
}

Функция Assignment вызывает функцию Identifier и проверяет ее исход (три


варианта для оператора switch). Эти операторы и все другие действия, включая
выставление соответствующих исходов, в точности повторяют то, что запи-
сано в таблице 10.2. В принципе при составлении таких функций вполне
можно обойтись без таблиц, руководствуясь лишь грамматическими прави-
лами. В этом случае мы получим в точности то же самое решение, которое
было представлено в § 11.6 при обсуждении метода рекурсивного спуска,
включая появляющиеся в процедурах циклы. Однако есть причины, по кото-
рым диаграммное решение предпочтительнее:
604 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

a) наглядность табличных диаграмм гораздо выше, чем при прямом кодиро-


вании правил в методе рекурсивного спуска;
b) появляется возможность отделить в тексте программы часть, ответствен-
ную за контекстно-свободный анализ, от других частей транслирующей
системы: от анализа контекстных зависимостей и от генерации следую-
щего представления транслируемой программы. Таким образом, метод
расширенного конечного автомата можно рассматривать как средство де-
композиции задачи;
c) ничто не препятствует и трансляционной реализации структуры управле-
ния программы (см. выше), которой по существу является рекурсивный
спуск, и построению специальной программы, которая будет восприни-
мать представление таблиц как задание для интерпретации. В ряде случа-
ев одно из этих решений может оказаться предпочтительнее;
d) отдельное описание синтаксиса может быть отдельно проверено, в част-
ности, с помощью достаточно простой программы, исследующей свой-
ства системы автоматов. В частности, довольно легко выяснить левую
бесконечную рекурсию, которая возникнет, если исходное определение
грамматики содержит леворекурсивные нетерминальные символы7 .

10.3.4. Преобразования грамматики, сохраняющие язык. Вычислитель-


ная мощность синтаксических таблиц

Последнее замечание в перечне из предыдущего раздела заслуживает осо-


бого рассмотрения. Заметим, что леворекурсивные нетерминальные симво-
лы — непреодолимое препятствие для обсуждаемого метода. Если допустить
их, то ни при каких условиях не удастся достичь разделяемости правил грам-
матики (см. стр. 595), а значит, не удастся сохранить соглашение о едином по-
токе для всех конечных автоматов системы. Именно поэтому для иллюстра-
ции мы выбрали определение простых выражений в виде (10.2), посколь-
ку согласованное с порядком вычислений определение (10.3) привело бы к
построению некорректных таблиц (к рекурсивному зацикливанию процесса
анализа).
Нетерминальный символ A называется леворекурсивным, если возможен вывод вида
A → ∗Aα, где α — любая строка из терминальных и нетерминальных символов. В этом
7

случае выбранный метод анализа ведет к зацикливанию работы автомата без продвижению
по анализируемой строке, т. е. к бесконечному зацикливанию.
10.3. СИНТАКСИЧЕСКИЕ ТАБЛИЦЫ 605

А можно ли с помощью данного метода убить сразу двух зайцев? Ока-


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

• действия, которые надо выполнять после анализа, и которые заданы как


функции от исходной грамматики, должны быть согласованы с новой
грамматикой.

Если первое условие ставит чисто формальные рамки (к тому же не очень


строгие, т. к. вполне допустимо выбрать новую грамматику так, чтобы поро-
ждаемый язык включал исходный, и применить фильтрацию на уровне дей-
ствий), то второе может вызывать затруднения. Но в нашем случае это не так.
Вместо (10.3) можно воспользоваться для выражения и множителя следую-
щими правилами:
<выражение> ::= <множитель> [ “+” <множитель> |
“−” <множитель>]*
<множитель> ::= <терм> [ “*” <терм> |“/” <терм> ]*

которые, очевидно, порождают тот же язык.


(10.3)

В качестве второго условия в данном обсуждении используется согласо-


вание грамматики с порядком вычислений. В реализующей программе это
легко достигается, поскольку оперирование полностью сосредоточено в дей-
ствиях каждого из автоматов (см. таблицы 10.7 и 10.8). В данном случае нет
никаких противоречий в том, чтобы разработчики сами выбирали порядок
действий.
В последних двух таблицах прямая рекурсия удалена. Вместо нее исполь-
зованы циклы. Хорошо это или нет? Хорошо с точки зрения эффективности и
возможности задавать нужный порядок. Плохо, если это ведет к нарушению
понимаемости. В данном случае это не так, но, вообще говоря, преобразова-
ние грамматики часто отрицательно сказывается на наглядности программы,
По вычислительной мощности синтаксические таблицы (даже без строк с
E-переходами и действий) равносильны машине Тьюринга. Но нерегламен-
тированное их использование может вызвать значительные трудности при
606 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

<выражение>

/* распознано, что выражения нет */


<множитель> T

/* ошибка в множителе */
F F
E E
2 “+” T

/* зацикливание */
F 1

/* после “+” нет множителя */


<множитель> T 2

/* ошибка в множителе */
F E
E E

/* распознано выражение */
1 “-” T
F T

/* после “-” нет множителя */


<множитель> T 2

/* ошибка в множителе */
F E
E E

Таблица 10.7. Модифицированная диаграмма для выражения

<множитель> /* все аналогично */


<терм> T
F F

T /* пример того, что можно объединить


E E

два вызова понятия (ср. <выражение>)


2 “*” 0

*/
F 1
1 “/” T
F T
0 <терм> T 2
F E
E E

Таблица 10.8. Модифицированная диаграмма для множителя


10.3. СИНТАКСИЧЕСКИЕ ТАБЛИЦЫ 607

понимании программ. В этой связи стоит посмотреть, как распознается не


контекстно-свободный язык
{an bn cn | 0 6 n}
с помощью синтаксических таблиц, снабжаемых соответствующими действи-
ями (таблица 10.9).

/* возвращает -1, если строка не принадлежит {an bn cn | 0 6


int Parser_aNbNcN ();

n}, и N, если она содержит по N букв a, b и c в нужном порядке


*/
int I = 0; J;
1 “a” T I++; 1
F J = I;
2 “b” T J--; 2
F if (J != 0) return ( -1 );
3 “c” T J++; 3
F if (I != J) return ( -1 );else return ( I );

Таблица 10.9. Распознаватель не контекстно-свободного языка

И последнее замечание. Синтаксические таблицы не обязательно связы-


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

10.3.5. Построение графа состояний


608 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

В §10.2 и 10.3 мы обсуждали возможности организации таблиц как сред-


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

2. В таблице конечного автомата не указываются исходы проверки усло-


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

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

4. В таблице конечного автомата возможны выделенные элементы — усло-


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

5. У двух таблично заданных автоматов разная распознающая сила. Син-


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

В связи с последним замечанием уместно подчеркнуть что, как мы могли


убедиться, разные варианты трансляции таблиц оказываются неравнознач-
ными с точки зрения эффективности. Так, вариант 1, реализующий конечный
10.4. ДИАГРАММЫ СОСТОЯНИЙ И ПЕРЕХОДОВ. ИХ СВЯЗЬ С МАТЕМАТИЧЕСКИМИ МОДЕЛЯМИ609

автомат с помощью функций-состояний (см. программу 10.2.1), не годится


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

ДИАГРАММЫ СОСТОЯНИЙ И ПЕРЕХОДОВ.


ИХ СВЯЗЬ С МАТЕМАТИЧЕСКИМИ МОДЕЛЯМИ
§ 10.4.

Вначале сделаем несколько методологических замечаний.


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

b) теоретическое (математическое) представление, которое предназначе-


но для анализа свойств системы и ее преобразований; его можно рассма-
тривать как формализацию задачи;

c) алгоритмическое представление, связывающее визуальное и теоретиче-


ское представление с программой и являющееся спецификацией програм-
мы;

d) программное представление, которое наконец-то предназначено для ис-


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

• к математическим представлениям: понятность для специалиста и на-


личие мощной системы преобразований;8

• общее требование к первым двум пунктам: статическая проверяемость


свойств;
Таким образом, в принципе математическое представление избыточно, если у нас нет воз-
можности эффективно использовать его для преобразования алгоритма. Но оно зачастую
8

играет еще и другие роли, одна из которых отмечена в следующем пункте, а вторая — за-
щита принятых решений от неквалифицированной критики и убеждение заказчика и/или
начальника и оппонентов в их обоснованности.
610 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

• к алгоритмическим представлениям: ясность для программиста;

• к программным представлениям: осуществимость эффективной обра-


ботки;

• общее требование к паре соседних представлений: достаточно простое


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

Теперь перейдем к более систематическому рассмотрению тех структур,


которые лежат в основе программирования от состояний. Мы заметили, что
общее визуальное представление алгоритмической спецификации, которая
затем перерабатывается в программу, написанную в стиле от состояний —
граф состояний и переходов.
Определение 10.4.1. Граф состояний и переходов, называемый также диа-
граммой переходов — нагруженный ориентированный граф G. Каждой вер-
шине графа G сопоставлено наименование состояния, а каждой дуге — усло-
вие.
Конец определения 10.4.1.
Примеры таких диаграмм можно посмотреть в предыдущем параграфе. Усло-
вие AB, сопоставленное дуге, ведущей из a в b, содержательно интерпрети-
руется следующим образом. При выполнении AB в состоянии a управление
передается состоянию b (или же в другом смысле осуществляется переход по
данной дуге).
Когда граф состояний и переходов используется для документирования
программы, наименования состояний, как правило, совпадают с именами про-
цедур, выполняющихся в данном состоянии.
Прежде всего, рассмотрим подробнее вопрос, по какой причине в про-
граммистских работах диаграммы переодов и конечные автоматы употре-
бляются почти как синонимы. Вычисления, соответствующие диаграммам
переходов, могут быть представлены циклом, телом которого является по-
следовательность трех действий:

Исполнение процедуры, сопоставленной состоянию;


Проверка условий, сопоставленных выходящим дугам,
выбор дуги, соответствующей истинному условию;
Переход к состоянию, в которое ведет выбранная дуга;
10.4. ДИАГРАММЫ СОСТОЯНИЙ И ПЕРЕХОДОВ. ИХ СВЯЗЬ С МАТЕМАТИЧЕСКИМИ МОДЕЛЯМИ611

Если отвлечься от конкретных действий, выполняемых в состояниях (при-


рода которых может быть очень сложной), и перейти к схеме программ, то
мы получаем схему Янова (см. § A.3), и, более того, схему Янова частного
вида. Если далее упростить условия на дугах, то мы можем закодировать их
символами алфавита, мощность которого не больше мощности множества
состояний на диаграмме. При этом коды всех дуг, выходящих из данного со-
стояния, будут различаться. Далее мы можем построить конечный автомат,
моделирующий поведение схемы Янова в следующем смысле:

1. Состояния автомата обозначаются натуральными числами i и взаимно-


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

2. Символы входного алфавита обозначаются теми же натуральными ча-


слами;

3. В программной матрице автомата элемент Aij = k тогда и только тогда,


когда в диаграмме переходов дуга, начинающаяся в состоянии, код ко-
торого i, и закодированная буквой j, ведет в состояние, закодированное
числом k.

Для каждого исполнения схемы Янова, соответствующей нашей диаграмме


переходов, существует входная лента построенного нами автомата, порожда-
ющая ту же последовательность состояний.
Таким образом, математический переход от диаграмм состояний к конеч-
ным автоматам весьма прям и весьма груб. Это типичное приближение пер-
вого порядка9 . Приближения первого порядка являются легче всего исследу-
емыми математическими моделями систем. Сделанные при их построении
огрубления часто приводят к тому, что, базируясь на таком приближении,
приходят к ложным выводам о поведении системы. Например, приближени-
ем первого порядка функции sin в точке 0 является прямая y = x. Поведение
этой прямой и поведение синуса на достаточно больших отрезках не имеют
ничего общего даже с качественной точки зрения. Такой эффект огрубления
Понятие приближения первого порядка, приближения второго порядка и т. д. появились
при применении разложений функций в ряд Тейлора для получения приближенных реше-
9

ний систем. В общем случае системного анализа эти понятия служат полезным маяком и
порождают аналогичную, но, конечно, не формализованную и неформализуемую, систему
оценок для нечисленных математических моделей.
612 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

возникает и в наших системах, формализуемых диаграммами переходов10 .


Тем не менее, как и многие другие математические модели первого порядка,
автоматная аналогия диаграмм переходов дает много полезного для обраще-
ния с диаграммами и получающимися программами, если ее не абсолюти-
зировать. В частности, методы преобразования и оптимизации автоматов ча-
сто могут прямо применяться для преобразования и оптимизации программ,
описанных диаграммами переходов.
Мы уже рассмотрели один случай, когда структура, по виду совершенно
аналогичная диаграммам переходов, на самом деле имеет другую интерпре-
тацию. А именно, это — синтаксические таблицы. В синтаксических табли-
цах на самом деле мы имеем рекурсию, замаскированную тем, что аргумен-
ты рекурсивного обращения к процедурам всегда вычисляются стандартно:
это — текущее подвыражение. Здесь годятся методы преобразования авто-
матов, связанные с экономией состояний, но методы декомпозиции и ком-
позиции уже не идут, поскольку в данном случае автомат на самом деле не
конечный, а магазинный, и прямо представить последовательности его со-
стояний в виде полугруппы не удается.
Это лишь первый из отмеченных нами случаев, когда поверхностные ана-
логии, связанные с совпадением грубейших представлений и (иногда вдоба-
вок еще и) грубейших математических моделей ведут в концептуальные ту-
пики.

§ 10.5. ПРОГРАММНЫЕ ПРЕДСТАВЛЕНИЯ ГРАФА СОСТОЯНИЙ

Отметим, что программные представления графа состояний очень сильно


зависят от динамики данного графа. Стоит выделить четыре подслучая.
1. Состояния и таблица переходов жестко заданы постановкой задачи (на-
пример, такова задача синтаксического анализа). Тогда практически все-
гда лучшее программное представление — goto, независимо от разме-
ра таблицы.

2. Состояния и таблица переходов пересматриваются, но фиксированы


между двумя модификациями задачи. Тогда при небольших размерах
таблицы по-прежнему предпочтительней всего реализация через пере-
ходы, а при достаточно больших необходима ее декомпозиция, в связи
Диаграммы переходов можно считать моделями второго порядка. Хотя действия по-преж-
нему остаются неконкретизированными, условия переходов выписываются более детально.
10
10.5. ПРОГРАММНЫЕ ПРЕДСТАВЛЕНИЯ ГРАФА СОСТОЯНИЙ 613

с чем часто целесообразно представление состояний объектами.

3. Состояния и таблица переходов динамически порождаются перед вы-


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

4. ‘Живая таблица’: модифицируется в ходе исполнения. Пока что дать


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

В контексте этих ситуаций следует рассматривать и работу с табличным пред-


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

10.5.1. Требования к автоматической трансляции таблиц

Серьезный недостаток предложенного в § 10.2.5 решения задачи автома-


тического преобразования диаграмм конечного автомата в программу свя-
зан с идеей препроцессорного построения, удобного для обработки предста-
вления диаграмм переходов. Игнорирование обратной связи между исход-
ным представлением автомата и его интерпретируемым представлением по-
рождает проблемы. Если не рассматривать развитие программы, то отсле-
живать ее не нужно. Но как только встает вопрос о хотя бы минимальных
переделках, возникает проблема:
614 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

• внесение изменений в диаграмму не влечет за собой автоматического


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

• поиск и диагностика ошибок (речь идет не только о синтаксисе) воз-


можны лишь на уровне интерпретируемого представления, что проти-
воречит осмысливанию программы в прежних терминах;

• наличие программы, не зависящей от исходного представления, про-


воцирует на ее модификации, которые не будут перенесены в исходное
представление. В результате концептуальная и реализационная модели
расходятся между собой.

Таким образом, предложенное решение не является технологичным. Оно мо-


жет быть распространено на другие ситуации лишь для очень узкого круга
задач.
Все, сказанное выше, в полной мере может быть отнесено и к синтакси-
ческим таблицам. В обоих случаях задача разработки технологии включает в
себя создание и использование инструментов поддержки всех этапов, из ко-
торых состоит технологическая цепочка оперирования с таблицами. Ключе-
вым моментом является выбор формата представления таблиц, приспосо-
бленный как к ручной, так и к автоматической обработке.
Есть смысл попытаться отыскать универсальное решение. Если плюс к
этому оно будет стандартизовано, то появляется возможность создать техно-
логию. Сегодня предпосылки для такого решения существуют: использова-
ние так называемых языков разметки текстов и, в первую очередь, XML.

10.5.2. Языки разметки и автоматическая трансляция таблиц

При обсуждении Программы 10.2.5 мы указывали, что предложенный


текст является размеченным символами _i, i = 1, . . . , 5. Было показано, как
от размеченного текста переходить к программе на языке С++ или к интер-
претации таблицы. У такой разметки только один недостаток: она не стандар-
тизована, а потому о распространении далее среды разработчиков системы,
предназначенной для поддержки применения таблиц, говорить не приходит-
ся.
10.5. ПРОГРАММНЫЕ ПРЕДСТАВЛЕНИЯ ГРАФА СОСТОЯНИЙ 615

В настоящее время широко применяются следующие языки разметки, ко-


торые можно считать стандартными:
• TEX и в первую очередь надстройка над ним LATEX - для типографской
подготовки текстов,

• HTML — стандартный язык разметки текстов в Интернет и

• XML — язык разметки текстов в Интернет, ориентированный на актив-


ную работу с ними.

Их основное назначение связывается со структурированным представлени-


ем визуализации информации. С точки зрения автоматической трансляции
таблиц целесообразно обсудить алгоритмический аспект этих языков, т. е.
ответить, в какой мере разметка текста позволяет описывать требуемые пре-
образования. Но предварительно зададимся вопросами о том, правомерно ли
языки разметки считать языками программирования, и если да, то какие сти-
ли программирования они в принципе могут поддерживать.
Построение разного рода визуализаций текстов и сопутствующей им ин-
формации — определяющая функция языков разметки, которая реализуется
путем размещения разметочных команд вместе с предъявляемой информа-
цией. Эти команды являются управляющими сигналами для некоего вычи-
слителя, который отвечает за визуализацию. Внутреннее совместное пред-
ставление команд и данных для их выполнения естественно рассматривать
в качестве операционной и одновременно информационной среды вычисли-
теля-визуализатора. В первую очередь совмещение данных и задания на их
обработку отличает язык разметки от языка программирования.
Пример 10.5.1. Текст данного примера в языке разметки LATEX (точнее, в
том пакете определений над ним, который разработан для верстки данного
текста и других работ одного из авторов) для автоматического размещения
заголовка, создания идентификатора, по которому в дальнейшем можно ссы-
латься на Пример 10.5.1, форматирования тела примера и отработки его кон-
ца, окружен командами

и \end{example}.
\begin{example}\label{markupeample0}

Для того, чтобы напечатать первую из этих команд, пришлось применить


дополнительную разметку (чтобы команда была проинтерпретирована как
часть текста)
\verb|\begin{example}\label{markupeample0}|\\.
616 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

И, наконец, конечно же, предыдущая строка также потребовала дополнитель-


ной разметки, но во избежание бесконечного регресса на этом закончим.
Конец примера 10.5.1.
Для употребляющихся в Интернет языков разметки визуализатором слу-
жит браузер. Но визуализация — далеко не единственное вычисление, кото-
рое можно связывать с операционно-информационной средой браузера. Над
ней можно задавать совершенно различные обобщенные вычисления. Эти вы-
числения могут быть либо совмещены в одном процессе, либо разнесены во
времени, что непринципиально, но они всегда могут быть эксплицированы
при рассмотрении оперирования, задаваемого языком разметки. Примени-
тельно к числовой разметке, использованной в § 10.2.5 для задания таблицы
конечного автомата, правомерно трактовать разметку как команды трех ви-
дов вычислений:
• визуализация таблицы — трактовка разметочных символов как команд,
предписывающих взаимное расположение фрагментов текста для его
интерфейсного предъявления;
• трансляция таблицы — автоматическое построение программы, вы-
полняющей действия автомата;
• интерпретация таблицы — трактовка разметочных символов как ко-
манд, выполняющих действия с операндами, в качестве которых ис-
пользуются текстовые фрагменты.
Подобные вычисления можно определять для всякого языка разметки, рас-
сматривая подходящие вычислители для нее. Иными словами, речь идет о
программировании на языке разметки. Наиболее полно эта концепция про-
слеживается в технологии XML/XSL.
Если обратиться к стилю программирования, который в принципе может
поддерживать язык разметки, то это сентенциальное программирование. Ха-
рактерным видом вычислений этого стиля являются не просто продуциро-
вание новых данных с сохранением операндов, а глобальные трансформа-
ции данных, существенно зависящие от контекста. Мы еще будем обсуждать
данный стиль безотносительно задачи оперирования с таблично заданными
автоматами, а пока заметим, что в определенном смысле любой сентенци-
альный язык можно рассматривать в качестве языка разметки перераба-
тываемых данных. Иногда такая разметка явно расставляется по структу-
ре данных (пример — язык Рефал), иногда она выносится в специальную
10.5. ПРОГРАММНЫЕ ПРЕДСТАВЛЕНИЯ ГРАФА СОСТОЯНИЙ 617

структуру (Prolog), но в обоих случаях контекст, определяющий выполнение


команд, и замена данных путем порождения результатов команд остаются.
Вполне закономерна тенденция, прослеживающаяся в развитии языков раз-
метки, отделять распознавание структуры текста — сентенциальная часть
обработки — от использования этой структуры для задания действий, кото-
рые определяют обобщенные вычисления.11
Обзор языков разметки с точки зрения выделения обобщенных вычисле-
ний естественно поместить в более позднюю главу, посвященную сентенци-
альному программированию, поскольку и вычисления, и структуры, задан-
ные этими языками, естественно соотносятся в первую очередь с методами
сентенциального программирования. Но, как можно было предполагать уже
после знакомства с синтаксическими таблицами, некоторые из аспектов об-
работки структурированных текстов и обобщенных вычислений над ними
естественно обсудить именно в связи с автоматами и методами программи-
рования от состояний. Поэтому ниже описывается конкретное решение за-
дачи автоматической трансформации таблиц конечного автомата с использо-
ванием наиболее подходящего для этих целей языка XML (см. книгу [55] и
§13.3.2).12

10.5.3. Автоматное преобразование структурированных текстов

Если описывать тексты в современных языках разметки, типа LATEX [43]


или XML то возникает задача описывать и программировать преобразования
таких текстов. Пакеты здесь являются паллиативом, и на самом деле решени-
ями являются специализированные языки преобразований текстов либо со-
ответствующие методики программирования, поддержанные автоматизиро-
ванным преобразованием спецификаций в программу. Здесь мы рассмотрим
методику, базирующуюся на автоматах.
Применим возможности системы XML/XSL к нашей конкретной задаче:
описание конечного автомата.
Программа 10.5.1
Как мы уже имели возможность убедиться (см., например, § 3.5 и 3.6), тенденция отде-
ления распознавания от обработки характерна и для других стилей программирования. Но
11

для сентенциального стиля она оказывается решающей.


То, что мы ссылаемся на более поздний параграф, — осознанное решение. В технических
руководствах Вам все равно предстоит научиться заглядывать вперед, чтобы понять интере-
12

сующее Вас место. Чтение книги как последовательного файла — слишком часто наименее
эффективный метод ее изучения.
618 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

<?xml version=’1.0’ encoding=’windows-1251’ ?>


<automat name="Test">
<action><![CDATA[char symbol; int cnt;]]>
</action>
<ref>St1</ref>
<state name="St1">
<if>
<condition><![CDATA[’a’<=symbol && symbol <= ’z’]]>
</condition>
<action><![CDATA[printf ("%c", symbol); cnt = 1;]]>
</action>
<ref>St2</ref>
</if>
<eif>

<action><![CDATA[/* Так как нужно печатать только слова,


<condition> <![CDATA[ symbol != ’\n’]]> </condition>

действия не заполняются */ ]]>


</action>
<ref> St1 </ref>
</eif>
<eif>
<condition><![CDATA[ failure]]>

<action><![CDATA[/* Переход не требует чтения,


</condition>

symbol == ’\n’ не нужно читать */ ]]>


</action>
<ref>Exit</ref>
</eif>
</state>
<state name="St2">
<if>
<condition><![CDATA[ ’a’<=symbol && symbol <= ’z’]]>
</condition>
<action><![CDATA[printf ("%c", symbol); cnt++;]]>
</action>
<ref>St2</ref>
</if>
<eif>
10.5. ПРОГРАММНЫЕ ПРЕДСТАВЛЕНИЯ ГРАФА СОСТОЯНИЙ 619

<condition><![CDATA[ symbol != ’\n’]]>


</condition>
<action><![CDATA[printf (" - %i\n", cnt);]]>
</action>
<ref>St1</ref>
</eif>
<eif>
<condition><![CDATA[ failure]]>
</condition>
<action><![CDATA[printf (" - %i\n", cnt);]]>
</action>
<ref>Exit</ref>
</eif>
</state>
</automat>
В этом описании нашли свое отражение следующие свойства нашего конеч-
ного автомата (за основу взята таблица конечного автомата из § 10.2.3):
• Конечный автомат (тег <automat>) включает в себя теги <state> — пе-
речень всех состояний в любом порядке а также теги <action> — дей-
ствие и <ref> — ссылка на исходное состояние13 .
• Каждый <state> содержит атрибут name, чтобы на него можно было
ссылаться, и набор условий: один тег <if> и любое количество (> 0)
тегов <eif> (означающих “else if”). Эти два тега также можно было бы
заменить одним универсальным, тем более что структура их потомков
не различается, но опять же этого не сделано по соображениям облег-
чения форматирования.
• Каждый <if> или <eif> включает в себя три тэга: <condition> — соб-
ственно условие и уже описанные теги <action> и <ref> — действие
при выполненном условии и ссылка на следующее состояние.
• Теги <action> и <condition> содержат специальный тег <![CDATA[. . . ]]>
для включения строк на языке C++.
Можно было бы оформить эти тэги с помощью особого состояния Init, но при этом мы
бы потеряли универсальность тэга <state>, т. к. состояние Init, например, не нуждается в
13

чтении символа, не содержит условий и должно быть всегда первым. А, как будет понятно
в последствии, универсальность составных частей модели сильно упрощает работу с ней.
620 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

В описание не включено состояние Exit. Это сделано (а точнее не сделано)


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

Программа 10.5.2

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl">
<xsl:template match="/">
<DIV STYLE="font-family:Courier; font-size:12pt">
<xsl:for-each select="automat">
<TABLE border="1" width="75%">
<TR>
<TD colspan="3"><xsl:value-of select="action" /></TD>
<TD width="10%"><xsl:value-of select="ref" /></TD>
</TR>
<xsl:for-each select="state">
<TR>
<td rowspan="3" width="10%" valign="top">
<xsl:value-of select="@name" /></td>
<td><xsl:value-of select="if/condition" /></td>
<td><xsl:value-of select="if/action" /></td>
<td><xsl:value-of select="if/ref" /></td>
</TR>
<xsl:for-each select="eif">
<TR>
<TD><xsl:value-of select="condition" /></TD>
<TD><xsl:value-of select="action" /></TD>
<TD><xsl:value-of select="ref" /></TD>
</TR>
</xsl:for-each>
</xsl:for-each>
</TABLE>
10.5. ПРОГРАММНЫЕ ПРЕДСТАВЛЕНИЯ ГРАФА СОСТОЯНИЙ 621

</xsl:for-each>
</DIV>
</xsl:template>
</xsl:stylesheet>

Следующая визуализация — это автоматическое преобразование XML осно-


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

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl">
<xsl:template match="/">
<blockquote>
<pre STYLE="font-family:Courier; font-size:12pt;" >
<xsl:for-each select = "automat">//C code for automat
"<xsl:value-of select="@name" />"
<![CDATA[
#include <stdio.h>
#define failure true

void main( void )


{]]>
<xsl:value-of select = "action"/>
goto <xsl:value-of select = "ref"/>;<br/>
<xsl:for-each select="state">
<xsl:value-of select="@name" />: symbol = getchar ();
<blockquote>if (<xsl:value-of select="if/condition" /> )
<blockquote>{ <xsl:value-of select="if/action" />
goto <xsl:value-of select="if/ref" />; }
</blockquote>
<xsl:for-each select="eif">
else if (<xsl:value-of select="condition" /> )
622 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

<blockquote>{ <xsl:value-of select="action" />


goto <xsl:value-of select="ref" />; }
</blockquote>
</xsl:for-each>
</blockquote>
</xsl:for-each>
Exit: return;<br/>}
</xsl:for-each>
</pre>
</blockquote>
</xsl:template>
</xsl:stylesheet>

Как легко видеть, разница этой и предыдущей визуализаций только в том, что
там, где в таблице вставляются графические элементы, которые изображают
рамки, образующие саму таблицу, здесь находятся части синтаксиса языка
С++/C# и отступы для лучшей читаемости получаемой программы.
Итак, благодаря использованию формата представления данных XML, мы
получили возможность автоматически создавать программы для данной за-
дачи путем простой замены стилевых таблиц!
Отметим теперь еще несколько интересных возможностей, которые мы
бы могли использовать.
Хотя данная технология позволяет легко создавать С++/С# программы,
основой для них остается язык XML и чтобы составить новый автомат, про-
граммист должен как минимум знать синтаксис этого языка. Следующим
естественным шагом будет исключение этого звена: требуется скрыть вну-
треннее представление данных от конечного пользователя, оставив только
интуитивно понятное представление в виде таблицы. Но для этого в первую
очередь необходимо выбрать:

• преобразование таблица => XML представление и

• средство для удобного редактирования таблиц.

Естественным будет редактировать таблицы там же, где мы их уже научились


генерировать — в окне браузера. Доступ к редактированию этих таблиц мо-
жет предоставить упомянутый не стр. 786 DOM (стандарт, реализованный в
браузерах Internet Explorer 5.0 и Netscape Navigator 6.0). Изменения, доба-
вления и другие редактирующие действия определяются довольно просто.
10.5. ПРОГРАММНЫЕ ПРЕДСТАВЛЕНИЯ ГРАФА СОСТОЯНИЙ 623

Например, на языке Java script добавление новой ячейки в таблицу можно


описать следующим образом:
var oRow;
var oCell;

oRow = oTable.insertRow();
oCell = oRow.insertCell();
oCell.innerHTML = "This cell is <b>new</b>."
Точно так же можно создавать таблицы, строки и ячейки в них. Реализа-
ция же самого интерфейса (кнопочек, средств выделения, полей ввода) зави-
сит только от вашей фантазии.
Тот же DOM, точно таким же образом, может работать с XML, реплицируя
все действия конечного пользователя с таблицей в XML представление для
последующей записи последнего в виде готового файла со структурой нового
конечного автомата.
Еще одним шагом в развитии проекта, использующим язык XML может
быть формализация используемого представления: ведь как только опреде-
лены все теги представления, правила их вложения и способы задания, тем
самым получился новый язык (по аналогии с современными языками, по-
строенными таким же образом, мы можем назвать его “automatML”). Пока
теги и элементы XML используются исключительно ради удобства для ваше-
го собственного проекта (как если бы вы использовали CSS на своей домаш-
ней страничке), то не имеет никакого значения, что вы даете этим элементам
и тегам имена, смысл которых отличается от стандартного и известен только
вам. Если же, с другой стороны, вы хотите предоставлять данные внешнему
миру и получать информацию от других людей, то это обстоятельство при-
обретает огромное значение. Элементы и атрибуты должны употребляться
вами точно так же, как и всеми остальными людьми, или, по крайней мере,
вы должны документировать то, что делаете.
Для этого придется использовать определения типов документов (Document
Type Definition, DTD). Хранимые в начале файла XML или внешним образом в
виде файла *.DTD, эти определения описывают информационную структуру
документа. DTD перечисляют возможные имена элементов, определяют име-
ющиеся атрибуты для каждого типа элементов и описывают сочетаемость
одних элементов с другими.
Каждая строка в определении типа документа может содержать деклара-
цию типа элемента, именовать элемент и определять тип данных, которые
624 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

элемент может содержать. Она имеет следующий вид:

<!ELEMENT имя_элемента (тип_данных)>

Например, декларация

<!ELEMENT action (#PCDATA)>

определяет элемент с именем action, содержащий символьные данные (т. е.


текст). Декларация

<!ELEMENT automat (state_1, state_2, state_3)>

определяет элемент с именем special_report, содержащий подэлементы state_1,


state _2 и state_3 в указанном порядке, например:

<state_1>XML: время пришло</state_1>


<automat>

<state_2>XML превосходит самое себя</state_2>


<state_3>Управление сетями и системами с помощью XML</state_3>
</automat>

После определения элементов DTD могут также определять атрибуты с


помощью команды !ATTLIST. Она указывает элемент, именует связанный с
ним атрибут и затем описывает его допустимые значения. !ATTLIST позволя-
ет управлять атрибутами и многими другими способами: задавать значения
по умолчанию, подавлять пробелы и т. д. DTD могут также содержать декла-
рации !ENTITY, где определяются ссылки на объекты, а также декларации
!NOTATION, указывающие, что делать с двоичными файлами не в формате

Серьезное и несколько удивительное ограничение DTD состоит в том, что


XML.

они не допускают типизации данных, т. е. ограничивают данные конкретным


форматом (таким, как дата, целое число или число с плавающей точкой). Как
вы, вероятно, уже заметили, DTD используют иной синтаксис, нежели XML,
и не очень-то интуитивно понятны. По названным причинам DTD будут, ви-
димо, заменены более мощными и простыми в использовании схемами XML,
работа над которыми ведется в настоящее время.
Возможно, вам приходилось слышать определения ‘правильно составлен-
ный’ (well-formed) и ‘действительный’ (valid) применительно к документам
10.6. ПЕРЕХОД ОТ ДАННЫХ К КОНЕЧНОМУ АВТОМАТУ 625

XML. Документ является правильно составленным, если для каждого откры-


вающего тега имеется соответствующий закрывающий тег, а накладываю-
щиеся теги отсутствуют. (Таким образом, большая часть документов HTML
составлена неправильно.) Документ является действительным, если он со-
держит DTD и соответствует его правилам.

§ 10.6. ПЕРЕХОД ОТ ДАННЫХ К КОНЕЧНОМУ АВТОМАТУ

Таблицы переходов и состояний являются методом программирования не


только для задач, сводящихся к конечным автоматам. При обсуждении син-
таксических таблиц и XML/XSL подхода к задаче стандартизованного пред-
ставления таблиц переходов были указаны возможности применения мето-
дики оперирования со структурными представлениями данных и программ
для более широкого класса алгоритмов.
Однако мы пока не решали задачи, когда какое-либо представление ал-
горитма зависит от входных данных. Эта задача расклассифицирована для
автоматов как задача динамического порождения автомата (см. § 10.5, пункт
3 на стр. 613). Конечно же, под таким углом зрения можно рассматривать
трансляцию: текстовый файл на входном языке есть часть данных, фиксиру-
ющая план обработки другой части данных, которая предъявляется для ре-
шения конкретной задачи. Задача специализации универсальной программы
(см. § 3.9.3) также может рассматриваться как уточнение общего плана, ис-
ходя из частичного знания обрабатываемых данных. Упомянутые случаи ха-
рактеризуются тем, что представление алгоритма, зависящее от части вход-
ных данных, строится из заранее определенных заготовок. Например, для
трансляции такими заготовками являются алгоритмы выполнения абстракт-
но-синтаксического представления программы.
В данном разделе показан иной метод построения алгоритма, зависящего
от входных данных. Его идея не в компиляции некоего объектного кода из за-
готовок, комбинируемых по принципу домино, а прямом составлении такого
представления алгоритма, которое допускает непосредственную интерпрета-
цию. Естественный путь демонстрации метода — взять за основу известный
класс алгоритмов, конкретный представитель которого выбирается, исходя
из знания о входных данных.
Будем решать задачу, которая для каждого конкретного случая решается
с помощью конечного автомата специального вида (как и всегда, выбор кон-
кретного представления сильно влияет на сложность и другие характеристи-
626 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

ки программы, и автоматическое применение ранее использованных пред-


ставлений в других задачах не рекомендуется).
Пусть требуется подсчитать, сколько раз каждое из вводимых слов встре-
чается в некотором большом файле (теперь слово — это любая последова-
тельность символов). α1 , α2 , . . . , αn — вводимые слова; αi = a1 a2 . . . aki —
слово. Напечатать:
Число вхождений α1 = <Число 1>,
Число вхождений α2 = <Число 2>,

Число вхождений αn = <Число n>


...,

где <Число k> — полное число вхождений слова αk в файл с учетом возмож-
ного перекрытия слов (например, в строке “*МАМАМА{}” два вхождения
слова “МАМА”).
Для заданных заранее слов легко построить граф, каждая вершина кото-
рого представляет символы внутри слов. Его вершины помечены символом.
Из такой вершины исходят две дуги: первая указывает на вершину, к которой
следует переходить, когда очередной читаемый символ совпадает с пометкой
вершины, а вторая — на вершину, которая должна стать преемником данной
в случае несовпадения. Легко видеть, что это одна из форм представления ко-
нечного автомата, каждое состояние которого кодирует множество всех вер-
шин, связанных дугами второго вида, а состояния-преемники определяются
дугами первого вида исходного графа. Чтобы этот автомат работал, т.е. решал
поставленную за-дачу нужно снабдить его действиями, которые сводятся к
увеличению счетчиков, соответ-ствующих найденным словам, а также опре-
делить начальное и конечное состояния. Мы не будем заниматься переделкой
исходного графа, поскольку такая его форма удобнее для интерпретации.
Если дуги первого вида изображать стрелками, исходящими в горизон-
тальном на-правлении, дуги второго вида - вертикальными стрелками, а дей-
ствия со счетчиками - соответствующими пометками при дугах, то, напри-
мер, для множества слов
1) МАМА,

2) МАШИНА,

3) ШИНА,

4) МАТ,

5) НА
10.6. ПЕРЕХОД ОТ ДАННЫХ К КОНЕЧНОМУ АВТОМАТУ 627

может быть построен граф, показанный на рис. 10.6.


На языке С++/C# структура, которая представляет граф, почти такой, как
только что описанный, может быть изображена следующим образом:

// поле признака текущего значения в union:


struct {
bool Tag;
union { char Symb; // <- Tag = true

// имя объединения здесь не нужно


int Num; // <- Tag = false

// индекс перехода по совпадению


};

// индекс перехода по несовпадению


int yes;
int no;
} Table[];

Особенность данной интерпретации таблицы в том, что она соответству-


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

1) МАМА, 2) МАШИНА, 3) ШИНА, 4) МАТ, 5) НА.

М
0. ’\n’ 111 1

А
1. 2 15

М
2. 3 0

А
3. 4 6
4. 5 0

Ш
5. <1> 3 -

И
6. 7 14

Н
7. 8 0

А
8. 9 0
9. 10 0
10. <2> 11 —
628 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

‘\n’

М А М А
1

Ш И Н А
2
3
5

Т
4

Ш И Н А
3
5
Н А
5

Рис. 10.6. Пример конечного автомата для распознавания вхождений слов


10.6. ПЕРЕХОД ОТ ДАННЫХ К КОНЕЧНОМУ АВТОМАТУ 629

11. <3> 12 —

Т
12. <5> 1 —
13. 14 0

Ш
14. <4> 1 —

И
15. 16 19

Н
16. 17 0

А
17. 18 0

Н
18. 11 0

А
19. 20 0
20. 12 0
21 ∀ 0 —

Программа интерпретации графа достаточно проста. Для данной задачи


нет смысла использовать транслирующий вариант реализации оперирования
с таблицей (операторы Current_Reaction(); и Final_Reaction(); использованы
для обозначения действий со счетчиками, например тех, которые приведены
в комментариях):
Программа 10.6.1

s = getchar ();
i = 1;
for (;;) {
if (Table[i].Tag) {

i = Table[i].yes; // следующая строка


if ( Table[i].Symb == s ) {

if ( s != ’\n’)
s = getchar ();
else return;
}

i = Table[i].no; // следующая строка


else

}
else {

i = Table[i].yes; // следующая строка


Current_Reaction(); // M[Table[i].Num]++

Final_Reaction(); // Распечатка M
}
630 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

В данном интерпретаторе структура данных не содержит описаний действий —


они только идентифицируются значениями соответствующих полей табли-
цы, а потому нет противоречий двойной трактовки данных как информаци-
онных и операционных единиц, т. е. нет тех проблем, которые были отме-
чены выше для автомата, использованного для подсчета длин слов. Важно,
что здесь достигается универсальность, независимость от таблицы для всех
вариантов ввода слов.
Ничуть не сложнее решение, когда вместо таблицы-массива используется
списочная структура. По существу ничто, кроме доступа к данным, который
можно строго локализовать в соответствующих процедурах, не изменится.
Выполните это решение самостоятельно. В то же время, решение разработ-
ки текстового представления таблицы с числовой разметкой было бы во всех
отношениях опрометчивым. Оно и сложнее, и не дает никаких преимуществ
даже в тех случаях, когда процесс построения автомата отделен во времени
от его использования. По тем же причинам вариант с XML не имеет ника-
ких преимуществ. Другое дело, если речь пойдет об оперировании со спис-
ком слов (а не с таблицей!). Этот список целесообразно уметь редактировать,
запуская программу построения графа с помощью соответствующего обра-
ботчика списка слов, и затем вызывая интерпретатор для работы с файлом.
Целесообразность такого подхода нужно оценить на этапе анализа жизнен-
ного цикла конструируемой программной системы.
Для обоих удовлетворительных решений требуется разработка алгорит-
ма построения автомата по заданному набору слов. Если используется та-
блица-массив, то результатом такого построения должен быть заполненный
массив. При списочной организации таблицы нужно составить соответству-
ющий список. Для решения задачи могут быть построены различные автома-
ты, но эффективность дальнейшего их использования будет различна. Сле-
довательно, можно ставить задачу оптимизации: выбор такого автомата из
множества автоматов, который справляется с подсчетом вхождений за наи-
более короткое время.
Построение графа автомата, достаточного для решения задачи, но не обя-
зательно оптимального, можно реализовать, используя следующее рекуррент-
ное описание алгоритма:

1. Если множество слов пустое, то граф задается структурой:


10.6. ПЕРЕХОД ОТ ДАННЫХ К КОНЕЧНОМУ АВТОМАТУ 631

‘\n’

Вершина, содержащая ‘\n’, объявляется выходной для графа в целом


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

2. Пусть граф G определяет автомат, распознающий некоторое множество


слов {α1 , α2 , . . . , αk }, и пусть есть слово αk+1 , которое нужно добавить
к этому множеству. Добавление слова достигается с помощьюи следу-
ющих шагов:

(a) по слову αk+1 = a1 . . . am строится список вида

a1 ... am E
k+1

E
который рассматривается как заготовка для пополнения графа G;
(b) для каждого слова α из {α1 , α2 , . . . , αk } ищутся такие β, γ 6= ε, δ
и ξ, что α = βγδ, αk+1 = γξ и (δ = xδ 0 &ξ = yξ 0 ⇒ x 6= y). В
графе G есть фрагменты, отвечающие за распознавание γ. Следо-
вательно, надо склеить заготовку с каждым из таких фрагментов,
т. е. вставить вертикальную дугу от вершины x или ее вертикаль-
ных преемников x1 , . . . , xi к остатку заготовки:
632 ГЛАВА 10. МЕТОДЫ ПРОГРАММИРОВАНИЯ ОТ СОСТОЯНИЙ

x ... x ...

... ⇒ ...

Xi ... Xi ...

E
y ... am E
k+1

(c) если αk+1 есть собственная часть какого-либо слова, то склейка


заготовки с графом сводится к добавлению пометки k + 1 у соот-
ветствующей горизонтальной дуги;
(d) повторять (b,) пока можно найти соответствующие β, γ, δ и ε.

3. Последовательно выполнить процесс, описанный в п. 2, для всех слов


из набора.

Доведите этот алгоритм до программной реализации самостоятельно.


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

***
10.6. ПЕРЕХОД ОТ ДАННЫХ К КОНЕЧНОМУ АВТОМАТУ 633

Только что решенная задача, разумеется, является модельной. В реаль-


ной практике подобные по постановке задачи приходится решать в основном
в частных случаях (например, вводится требование игнорирования пересе-
чения слов, вместо подсчета числа вхождений может потребоваться другая
обработка, в частности, для использующей программы). Подобные допол-
нительные условия существенно влияют на выбор подхода, но применение
метода динамически порождаемого автомата — это хорошее решение с мно-
гих точек зрения: высокая эффективность, наглядность, автономность.
Логически подобные задачи возникают в случае предварительного пла-
нирования действий по сложной структуре данных, и они, как правило, еще
сложнее, хотя часто подход к их решению упрощается тем, что требуется
найти приемлемый, а не оптимальный, план действий. Тут такой прием ди-
намического порождения и последующей интерпретации еще более ценен.
Анализ предложенного решения, которое исходит из двухэтапной схемы
(построение автомата и его применение), показывает, что естественный ме-
тод реализации первого этапа — рекурсивный алгоритм, который на концеп-
туальном уровне следует отнести к стилю функционального программирова-
ния. Возможно, что и в качестве языка его реализации вам покажется наибо-
лее подходящим, к примеру, LISP. В то же время адекватный стиль реализа-
ции второго этапа — программирование от состояний. Таким образом, разде-
ление стилей требует в идеале применения двуязычной системы программи-
рования. К сожалению, чисто технические трудности сопряжения двух язы-
ков (упомянем только одну из них: согласование представлений данных) ча-
сто приводят к тому, что предпочитают моделирование стиля. И это порою
прагматически обоснованное решение, особенно если систему не придется
развивать дальше. Но если ее придется развивать, лучше один раз преодо-
леть технические трудности14 , зато затем работать концептуально продуман-
но. Хорошим примером здесь является система Autocad, языком для преобра-
зования планов в которой служит расширение языка LISP.

И занести соответствующие приемы в свою базу технологических решений для исполь-


зования в других подобных ситуациях.
14
Глава 11

Методы, основанные на рекурсии

Использование рекурсии являются характерным для нескольких стилей


программирования. Прежде всего, надо отметить функциональное и струк-
турное программирование, а затем нужно упомянуть многие разновидности
объектно-ориентированного. Это можно объяснить, вспомнив наблюдение,
что при структурном программировании и действия, и условия локальны.
Такая локальность приводит к достаточно гибким средствам оперирования
с контекстами, причем как раз к тем, которые даны в современных функци-
ональных языках в качестве базовых возможностей комбинирования функ-
ций.
Языковые средства задания рекурсии в этих стилях различаются, прежде
всего, в следующих отношениях. Для функционального программирования
рекурсия — атомарное действие, реализация которого остается за рамками
модели вычислений. В структурном программировании средства задания ре-
курсии основываются на оперировании со стековой структурой контекстов.
Они дают возможность говорить об экземплярах действий, о других элемен-
тах данных и действий абстрактного вычислителя языка, необходиых для
исполнения рекурсивных процедур. На уровне методов разделение стилей
проявляется в том, что при программировании в функциональном стиле по-
ощряется свободное применение рекурсии над значениями высших типов,
что расширяет потенциальные возможности стиля. Но, как уже неоднократ-
но говорилось, реализация значений высших типов в настоящее время без-
надежно плоха. Она всегда проецирует (причем ошибочно во многих тонких
случаях) программу на операционные вычисления. Даже если нет ошибок,
то программы, написанные в функциональном стиле, могут проигрывать в
эффективности на три-четыре порядка. По этой причине в изложении мето-

634
11.1. МЕХАНИЗМЫ РЕКУРСИИ 635

дов, основанных на рекурсии, мы преимущественно будем придерживаться


стиля структурного программирования. Это позволит давать более прямые
и реалистичные по отношению к существующим реализациям оценки алго-
ритмов.

§ 11.1. МЕХАНИЗМЫ РЕКУРСИИ

При обсуждении подпрограмм уже говорилось о рекурсивных процеду-


рах. Была указана связь рекурсивных алгоритмов с рекуррентными соотно-
шениями между данными. По существу это основа обширного класса ре-
курсивных методов, которые можно охарактеризовать как проецирование ре-
куррентности на программу. Но рекуррентное соотношение является лишь
предпосылкой к тому, чтобы строить рекурсивную программу. В частности,
рекурсивная программа проигрывает итеративной схеме по эффективности,
когда конкретная задача фактически не требует дополнительной памяти, во-
влекаемой в вычисления при рекурсии. Именно здесь, где целесообразно ди-
намически выделять и освобождать память, но можно сделать это без явного
обращения к механизмам распределения памяти, проходят границы разум-
ного применения рекурсивных методов1 .
Подводный камень рекурсии, также рассмотренный ранее, — повторный
счет. Мы увидели это на примере прямолинейной реализации рекуррентно-
го соотношения. В реальной практике подобное встречается и в более заву-
алированной форме, что, собственно говоря, и приводит к отрицательному
отношению к рекурсии среди программистов, привыкших к операционным
моделям вычислений. Если же обратиться к функциональному программи-
рованию, то, во-первых, среди его приемов есть достаточно полезные сред-
ства преодоления повторения вычислений, а во-вторых, автоматический ана-
лиз программ этого стиля, направленный на выявление повторений, гораздо
легче и продуктивнее, чем для императивных программ. Тем не менее, опас-
ность повторного счета остается всегда.
Методы, основанные на рекурсии, укладываются в следующую универ-
Заманчиво выглядят применения автоматической трансформации рекурсивной програм-
мы в итеративную, когда дополнительная память фактически нужна лишь для организации
1

рекурсивных вызовов. Однако соответствующие алгоритмы довольно трудны и далеко не


всегда распознают ситуацию, а потому такой путь нельзя рекомендовать как универсаль-
ный. К тому же, если программист составляет рекурсивный алгоритм по той причине, что
он просто не проанализировал реальную потребность в ресурсах для решаемой задачи, то
это свидетельствует о его низкой квалификации. И очень часто такой анализ прямо ведет
разработчика к ручной трансформации решения.
636 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

сальную схему.
Определение 11.1.1. Пусть Σ — набор данных, обрабатываемых при вы-
полнении некоторого действия ∆(Σ). Σ рассматривается как обстановка вы-
числений действия ∆ (см. п. 7.3.5 и § 8.3), и, таким образом, она может включать
как входные данные, так и результаты вычислений: ∆In ∪ ∆Out (∆In ∩ ∆Out = ∅
не обязательно выполнено). Если даны наборы данных Σ1 , . . . , Σn , действия
∆1 , . . . , ∆n , и схема программ Ω над ∆1 (Σ1 ), . . . , ∆n (Σn ), с входными данны-
ми ∆In и результатами ∆Out , такая, что:
• выполнение Ω приводит к выполнению ∆(Σ) в целом;
• каждое из ∆i является либо атомарным, либо в свою очередь описано
через декомпозицию;
• среди ∆1 , . . . , ∆n или среди реализующих их схем представлено один
или более раз ∆;
то ∆ называется рекурсивным, а Ω называется рекурсивной схемой программ
для ∆(Σ). Ω называется также декомпозицией ∆(Σ), а ∆1 (Σ1 ), . . . , ∆n (Σn ) —
базисом этой декомпозиции.
Конец определения 11.1.1.
Применение этого определения требует конкретизации большинства затра-
гиваемых им понятий. Во-первых, схема Ω здесь не обязательно схема про-
грамм в классическом смысле. Она может включать и совместные, и недетер-
минированные, и параллельные действия. Во-вторых, отнюдь не все элемен-
ты обстановки могут представляться реальными программными объектами,
они могут (и часто должны быть) призраками.
Рассмотрим пример. Если пополнить входные данные программы всеми
их возможными комбинациями при помощи имеющихся функций (постро-
ить т. н. сколемовский универс), то можно считать все Σi подмножествами
Σ. Но такое разбиение данных, хотя полезно для анализа программы, в боль-
шинстве случаев остается лишь концептуальным, и нет нужды строить его
явно: это не только нецелесообразно, но зачастую и просто невозможно. Бо-
лее того, если явное построение осуществимо, а не остается призраком, то
именно это свидетельствует о том, что, по-видимому, в данном случае более
разумно итеративное решение, которое не нуждается дополнительной памя-
ти рекурсивного вычисления.
В то же время, необходимо научиться выстраивать последовательность
данных (наборов данных), которая будет динамически предъявляться ∆1 , . . . ,
11.1. МЕХАНИЗМЫ РЕКУРСИИ 637

∆n при каждом обращении к рекурсивно задаваемому действию. С разбие-


нием действий чуть проще: это, собственного говоря, и есть декомпозиция
алгоритма. Основные задачи здесь сводятся к следующему. Надо понять, что
именно будет предохранять от бесконечного вычисления рекурсивных схем
и от повторного счета.
Определение 11.1.2. Пусть Ω рекурсивная схема для ∆(Σ) с базисом ∆1 (Σ11 ),
. . . , ∆n (Σ1n ), пусть Ex — исполнение данной схемы. Пусть j1 — номер компо-
нента базиса, для которого ∆j1 (Σ1j1 ) присутствет в исполнении и приводит к
исполнению ∆(Σ2 ). Тогда можно построить рекурсивную схему следующего
уровня, совпадающую с Ω по структуре, но имеющую базис
∆1 (Σ21 ), . . . , ∆n (Σ2n ).
Таким путем может быть выстроена последовательность активизаций ∆, на-
зываемая путем рекурсии:
∆(Σ) = ∆(Σ1 ), ∆j1 (Σ1j1 ), ∆(Σ2 ) . . . , ∆(Σk ), ∆jk (Σkjk ), ∆(Σk+1 ), . . . . (11.1)
Если эта последовательность конечна, т. е. существует k такое, что в испол-
нении ∆(Σk ) уже нет компонент, исполняющих ∆, то номер j, 1 6 j 6 k,
называется уровнем рекурсии, а k — глубиной рекурсии для исполнения Ex.
Рекурсивная схема называется конечной, если для всех данных, с которы-
ми выполняется ∆, гарантируется конечность глубины рекурсии. Рекурсив-
ная схема называется избыточной, если существует набор данных, с кото-
рыми выполняется ∆, для которого в последовательности (11.1) встречают-
ся повторяющиеся элементы. Рекурсивная схема называется слабо избыточ-
ной, если существует набор данных, с которыми выполняется ∆, для которого
на двух путях рекурсии (11.1) встречаются повторяющиеся элементы.
Конечная и не избыточная рекурсивная схема называется слабо коррект-
ной. Конечная, не избыточная и не слабо избыточная рекурсивная схема на-
зывается корректной.
Конец определения 11.1.2.
Нужно всегда знать, чем в конкретном случае становятся контексты компонет
рекурсивного вызова, а также обосновывать, что рекурсивная схема является
корректной. Следующие примеры поясняют данное определение.
Пример 11.1.3. Разбиение данных и действий.
Процедура рекурсивного вычисления факториала (см. программу 8.7.1 на
стр. 464) приводит к следующим контекстам данных и разбиениям действий:
638 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

∆1 = {N | (N < 2)} =⇒ result = 1; Σ11 = {1, result} ;


∆2 = {N | (N > 2)} =⇒ result = Fact (N-1); Σ21 = {N − 1, result};
∆3 = =⇒ result = result(∆2 ) ∗ N ; Σ31 = {result(∆2 ) ∗ N };
Для дальнейших уровней рекурсии выражения остаются совершенно анало-
гичными выражениям для ∆3 , Σ31 .
Разбиение аргумента N остается призраком. Оно представляет базу для
соответствующих действий, второе из которых задает рекурсию, а третье –
синтезирует результат. Условие из программы 8.7.1 и зависимость ∆N +1 от
∆N гарантируют сильную корректность схемы. Путь рекурсии всего один.
Конец примера 11.1.3.
Анализ разбиений данных и контекстов действий является мощным инстру-
ментом для сложных рекурсивных программ, но он требует во всех нетриви-
альных случаях дополнения аппаратом т. н. сигнализирующих функций.
На практике непосредственная проверка конечности глубины рекурсии
может оказаться затруднительной. Поэтому, согласно парадоксу изобрета-
теля, нужно вводить идеальные конструкции, гарантирующие конечность.
Первое, что здесь приходит на ум — ввести некоторое частичное упорядоче-
ние  на множестве контекстов Σ, такое, что на любом пути рекурсии

Σ0  Σ1j1  · · ·  Σkjk  · · ·

Это отношение лучше всего задать при помощи функции-призрака, отобра-


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

ϕ(Σkjk )  ϕ(Σk+1
j(k+1) ).

Когда ϕ становится достаточно малой (например, доходит до нуля), рекурсия


заканчивается. На самом деле ϕ может принимать в качестве аргумента лишь
входные части контекстов.
Но даже это не всегда гарантирует конечности. Пусть, например, значе-
ниями ϕ являются рациональные числа и на пути рекурсии она выдает убы-
вающую последовательность
1 1
1, , . . . , , . . .
2 k
11.1. МЕХАНИЗМЫ РЕКУРСИИ 639

Сами видите, что такая рекурсия бесконечна. К счастью, в логике накоплен


багаж фундированных чумов, в которых любая убывающая последователь-
ность конечна. Самым часто применяемым из них является множество орди-
налов (см., например, [63]). Для построения сигнализирующей функции для
любой практической программы достаточно ординалов до ε0 . Другим полез-
ным примером фундированного чума является лексикографически упорядо-
ченная последовательность n-ок натуральных чисел.
Пример 11.1.4. Корректность процедуры MCD и избыточность функции F
из § 8.7.
Проанализируем процедуру MCD (программа 8.7.3), предназначенную для
вычисления наибольшего общего делителя двух чисел. Воспользуемся од-
ним из стандартных решений: лексикографическим порядком на парах на-
туральных чисел. Каждая из входных частей In(Σkjk ) множеств Σkjk является
парой чисел, и очевидна монотонность последовательности In(Σkjk ). таким
образом, здесь сигнализирующая функция просто выделяет входную часть
Σ. Отсутствие избыточности вычислений обеспечивается структурой зави-
симостей действий процедуры, благодаря которой последовательность (11.1)
не содержит повторений. В функции F (процедура 8.7.2), вычисляющей чи-
сла Фибоначчи, действия в синтезирующей части схемы (сложение двух ре-
зультатов) прямо указывают на повторное появление рекурсивно вызывае-
мых действий на различных путях рекурсии. Это повторение появляется в
связи с тем, что от каждого уровня рекурсии возникают по две независимых
друг от друга последовательности вызовов, а потому внутри каждой из них
нет способа узнать о повторениях вычислений на другой последовательно-
сти. Но каждый из путей рекурсии упорядочен отношением < для аргумен-
тов вызовов функции, и эта процедура слабо корректна.
Конец примера 11.1.4.
При задании рекурсивных алгоритмов важно хорошо представлять, в какой
последовательности выполняются действия

∆1 (Σm m
1 ), . . . , ∆n (Σn ),

относящиеся к разным уровням рекурсии. От этого порядка, в частности, за-


висит накапливаемый результат вычислений, и, если не заботиться о нем,
то выходные данные будут порождаться, но совершенно не соответствую-
щие тому, что требуется. На рис. 11.1 приводится схема, иллюстрирующая
640 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

∆0 = ∆1до рекурсии ∆1рекурсия ∆1после рекурсии

Рис. 11.1. Порядок действий при использовании рекурсии

порядок исполнения действий из разбиений, относящихся к разным уров-


ням рекурсии. Он показан стрелками, соединяющими элементы разбиений
(пунктирные блоки).
Определение 11.1.5. При последовательной организации рекурсивных вы-
числений их порядок задается как один из следующих вариантов рекурсив-
ных схем:
• префиксная схема, когда делается попытка сначала выполнить действие
∆ (т. е. первое исполняемое действие из ∆1 , . . . , ∆n приводит к ∆), а
затем выполняются остальные действия;
• постфиксная схема, когда сначала выполняются все действия из ∆1 ,
. . . , ∆n , не приводящие к ∆, после чего делается попытка выполнить
действие ∆i , которое приводит к ∆;
• инфиксная схема, когда попытке выполнить действие ∆ предшествует
некоторое число действий из ∆1 , . . . , ∆n , а после выполнения ∆ выпол-
няются остальные действия;
• смешанные схемы — комбинации предыдущих случаев.
Конец определения 11.1.5.
11.2. ЗАКРАШИВАНИЕ ЗАМКНУТЫХ ОБЛАСТЕЙ 641

Постфиксная схема обладает свойством, которое на первый взгляд может по-


высить эффективность выполнения рекурсивных программ: за счет того, что
после выполнения действия на максимальной глубине рекурсии, нет дей-
ствий других уровней рекурсии, которые требуется выполнить, можно по-
следовательный возврат по уровням заметить завершением сразу всей цепоч-
ки рекурсивных вызовов. Однако при ближайшем рассмотрении становится
ясным, что в общем случае здесь никакого выигрыша нет. В обоих случаях
корректное завершение требует последовательной ликвидации в обратном
порядке (см. рис. 11.1) процессов, порожденных на каждом уровне рекур-
сии, а это как раз то, что происходит при последовательном возврате. В то же
время явное указание возможности завершения всех уровней рекурсии сразу
может повысить выразительность алгоритма.
При префиксной схеме в принципе возможно бесконечное углубление ре-
курсии, если не гарантируется, что данные, поставляемые для переработки
на каждый новый уровень рекурсии, ведут к некоему пределу, обеспечива-
ющему конечность уровней. Для этого предела должно быть предусмотрено
действие, которое уже не приводит к рекурсии. При использовании инфикс-
ной и постфиксной схем оба эти условия можно выполнить за счет начальных
действий, предшествующих рекурсии.
Рассмотренные выше положения никак не привязаны к моделям вычи-
слений языков. Здесь выделено то, что в полной мере присуще как опера-
ционному стилю структурного программирования, так и неимперативному
функциональному стилю. По существу, речь идет об алгоритмах, т. е. о том,
что нужно иметь ввиду программисту, когда он намерен применять в своей
практике методы, основанные на рекурсии.

§ 11.2. ЗАКРАШИВАНИЕ ЗАМКНУТЫХ ОБЛАСТЕЙ

Первая задача, на примере которой можно показать продуктивность при-


менения методов, основанных на рекурсии относится к области, где они ис-
ключительно широко применяются: машинной графике. При оперировании с
изображениями зачастую возникает потребность закрашивать части экрана,
которые содержательно соответствуют областям плоскости, ограниченным
замкнутыми кривыми. Необходимость различать область как объект пред-
ставления, с которым работает пользователь, и область как часть экрана обу-
словлена тем, что на уровне пользовательского представления существуют
понятия непрерывной кривой, замкнутости, тогда как экранное представле-
642 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

ние — это просто набор точек растра, называемых пикселями, которые раз-
личаются местоположением на экране и цветом.
Экранная область представляет собой связную группу пикселей, т. е. та-
кую, в которой между любыми двумя пикселями можно найти путь, целиком
лежащий в данной области. Соответственно, путь — последовательность
пикселей πi , таких, что πi и πi+1 — соседние. Области выделяются либо об-
щим цветом своих пикселей, и тогда они называются внутренне определен-
ными, либо цветом границы — линии, составленной из одноцветных сосед-
них пикселей, и тогда они называются гранично определенными. Чтобы эти
определения стали корректными, необходимо указать, как понимается тер-
мин “соседние пиксели”. Различаются 4-связное соседство и 8-связное со-
седство. В первом случае соседними считаются такие пиксели, у которых
либо горизонтальная, либо вертикальная координата отличаются на едини-
цу, во втором — такие, у которых обе координаты различаются не более чем
на 1 (допускается диагональное соседство).
В условиях приведенных определений задача закраски области ставит-
ся следующим образом. Дана замкнутая область и известны координаты ка-
кого-либо ее пикселя. Требуется, чтобы все пиксели области и только они
оказались закрашены одним цветом. Дополнительным условием для данной
задачи является то, что не делается никаких предположений относительно
формы закрашиваемой области.
Стратегия закраски внутренне определенной области, которую можно пред-
ложить для решения задачи, заключается в следующем. Для каждого пиксе-
ля, начиная с известного, проверяется его цвет. Если цвет старый, он заменя-
ется на новый и делается попытка запустить этот алгоритм рекурсивно для
каждого из соседей. Для гранично определенных областей выбор этих же
действий осуществляется по результату проверки несовпадения цвета пик-
селя с граничным цветом, дополненному отказом от перехода к соседу, если
очередной пиксель оказался уже перекрашенным. Понятно, что запуск алго-
ритма для соседей нужно осуществлять четыре раза в случае 4-связного и
восемь раз для 8-связного соседства.
Ниже приводится процедура закраски внутренне определенной области
при 4-связном соседстве. Остальные варианты предлагается запрограмми-
ровать читателю самостоятельно в качестве упражнения. Чтобы не привязы-
ваться к какой-либо графической библиотеке, в представленном алгоритме
вместо конкретных функции выявления и процедуры задания цвета пикселя
11.2. ЗАКРАШИВАНИЕ ЗАМКНУТЫХ ОБЛАСТЕЙ 643

указываются условные2 имена: ЧитатьЦветПикселя и ПисатьЦветПикселя.


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

{ координаты внутренней точки}


{ Pascal }

{ цвет области до закраски}


procedure FloodFill4(x,y,

{ требуемый цвет области}


OldColor,
NewColor
: Integer );

if ЧитатьЦветПикселя ( x, y ) = OldColor
begin

ПисатьЦветПикселя ( x, y, NewColor );
then begin

{ изменение цвета текущего пикселя }

{переход к верхнему соседу }


FloodFill4 ( x, y - 1, OldColor, NewColor );

{переход к нижнему соседу }


FloodFill4 ( x, y + 1, OldColor, NewColor );

{ переход к левому соседу }


FloodFill4 ( x - 1, y, OldColor, NewColor );

{переход к правому соседу }


FloodFill4 ( x + 1, y, OldColor, NewColor );

end
end;

/* C */

//координаты внутренней точки


void FloodFiLL4 (

//цвет области до закраски


int x, y,

//требуемый цвет области


OldColor,
NewColor )
{

ПисатьЦветПикселя (x,y,NewColor);
if (ЧитатьЦветПикселя (x,y) == OldColor) {

//переход к верхнему соседу


FloodFiLL4 ( x, y-1, OldColor, NewColor);

2
Чтобы подчеркнуть их условность, они записаны на русском языке.
644 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

//переход к нижнему соседу


FloodFiLL4 ( x, y+1, OldColor, NewColor);

//переход к левому соседу


FloodFiLL4 ( x-1, y, OldColor, NewColor);

//переход к правому соседу


FloodFiLL4 ( x+1, y, OldColor, NewColor);

}
}

Процедура FloodFill4 достаточно проста и будет часто (но не всегда, см.


упр. 1 ) работать правильно. Однако, хорошо ли она работает? Ответ на этот
вопрос весьма поучителен. При вызове ее для областей, содержащих значи-
тельное количество точек, расположенных по горизонталям и вертикалям,
FloodFill4 будет вызывать себя рекурсивно столько раз, сколько точек лежит
правее, левее, выше и ниже от текущей. К примеру, если правее текущей вну-
тренней точки в области находится 500 точек, то глубина рекурсии, появляю-
щейся только за счет первого внутреннего вызова процедуры, окажется рав-
ной 500, а значит одновременно будет существовать соответствующее число
экземпляров процедуры (из-за других рекурсивных вызовов это число ока-
жется еще больше).
Если удастся сократить рекурсию, то возможности применения процеду-
ры возрастут за счет снижения требований к памяти. Такое сокращение воз-
можно. Достаточно заметить, например, что для одного из направлений мож-
но заменить два рекурсивных вызова (вверх и вниз или влево и вправо) на два
цикла заполнения новым цветом соответствующей полосы области. При та-
кой замене нужно еще позаботиться о переходах к соседям по другому напра-
влению. Это можно сделать разными путями, но, вообще говоря, для продол-
жения процесса без запоминания в стеке или рекурсии точек-соседей полосы
(что, как уже говорилось, одно и то же) здесь не обойтись, что видно, в част-
ности, из рисунка 11.2а. Если первой внутренней точкой будет указана точка
А, направление циклов горизонтальное, а после заполнения полосы в каче-
стве соседей для перехода к следующей полосе выбираются только верхние и
нижние соседи точки А, то верхние треугольники области, отделенные гори-
зонтальными штриховыми линиями, окажутся не закрашенными. Напротив,
для областей другой формы такой путь вполне правомерен (см. рис. 11.2б), но
это уже нарушение условия задачи. Читателю предлагается самостоятельно
выяснить вопрос о том, когда можно, а когда нельзя ограничиваться выбором
соседей для продолжения процесса только теми точками, которые окружают
11.3. ПЕРЕБОРНЫЕ АЛГОРИТМЫ И РЕКУРСИЯ 645

первоначальную текущую. Будут полезны при этом и программные экспери-


менты.
незакрашиваемое

А А

а) область не закрасится б) область закрасится


Рис. 11.2. Иллюстрация решения задачи о закраске области

При решении данной задачи мы смогли убедиться в том, что то или иное
выстраивание разбиений данных приводит к различным алгоритмам. Так, ко-
гда для выбора рекурсивного вызова мы ограничивались лишь соседями, это
приводило к неоправданно большой глубине рекурсии, а когда мы перешли
к рассмотрению направлений, появилась опасность некорректного решения.
Сложность разбиения действий для выбранного разбиения данных различна.
Для простого соседства ничего, кроме явного указания нужных параметров и
совместного (а не последовательного, как приходится делать, используя тра-
диционные языки!) выполнения четырех рекурсивных действий, не требова-
лось. Пожалуй, это самый простой случай рекурсии. Следующий, чуть более
сложный, класс случаев был рассмотрен, когда мы сопоставляли рекурсив-
ную и итеративную схемы (см. п. 8.7.1). Здесь вся сложность прямо связано
со сложностью и с избыточностью рекуррентного соотношения.
Принципиально более сложными являются рекурсивные алгоритмы, ко-
торые используются для перебора и генерации вариантов. Методы организа-
ции таких программ обсуждаются в следующем параграфе.

§ 11.3. ПЕРЕБОРНЫЕ АЛГОРИТМЫ И РЕКУРСИЯ

В практике программирования зачастую приходится решать задачи, в ко-


торых необходимо организовать перебор вариантов. Простейший случай пе-
646 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

ребора — это цикл, каждая итерация которого выделяет вариант данных для
конкретной обработки. Вложенные циклы позволяют выделять все пары, трой-
ки и тому подобные группы данных. Так, для массива A [1..N] в цикле
for i := 1 to N do
for j := 1 to N do
for k := 1 to N do . . .

индексы i, j, и k могут быть использованы, чтобы задать вычисления для всех


троек (A [i], A [j], A [k]), причем каждая позиция в тройке пробегает все эле-
менты массива A. Для перебора элементов без их повторения целесообразно
использовать цикл
for i := 1 to N - 2 do
for j := i + 1 to N - 1 do
for k := j + 1 to N do . . .

Однако циклическая схема далеко не всегда применима. Например, для


генерации последовательностей кортежей 1 6 ij 6 N ,
(A [i1]),
(A [i1], A [i2]),
...
(A [i1], A [i2], . . . , A [im])

не получится воспользоваться вложенными циклами, поскольку здесь глу-


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

11.3.1. Перебор/генерация вариантов с возвратами

Для решения переборных задач, подобных только что представленной,


можно воспользоваться методом перебора/генерации вариантов с возврата-
ми. Суть его состоит в следующем. Определяется множество возможных, но
не обязательно окончательных, вариантов, для которого можно задать пере-
ходы от одного варианта к другим. Это множество просматривается с отсеи-
ванием неудовлетворительных вариантов. При нахождении неудачного вари-
анта происходит возврат к предыдущему варианту и попытка перейти от него
к следующему. Если все возможные следующие варианты для данного вари-
анта оказываются неудовлетворительными, то он отвергается и происходит
11.3. ПЕРЕБОРНЫЕ АЛГОРИТМЫ И РЕКУРСИЯ 647

возврат к тому варианту, из которого он получен. Таким образом обеспечи-


вается рекурсивный просмотр всех вариантов.
Все задачи, для которых оправдано практическое применение метода, мож-
но разделить на два вида, в зависимости от того, подготовлены или нет про-
сматриваемые варианты до выполнения алгоритма, реализующего метод. В
первом случае метод становится переборным, а во втором — генерационным.
Однако для применения метода это не очень существенно, поскольку в лю-
бом случае можно говорить о подпрограмме-поставщике просматриваемых
вариантов, осуществляющей доступ либо к готовым, либо к генерируемым
вариантам. Именно это обстоятельство отражено в названии метода
Выделяются четыре ситуации использования метода перебора/генерации
вариантов с возвратами:
1. требуемое решение является результатом вычислений, связанных с од-
ним из вариантов, для нахождения которого применяется перебор;
2. требуемое решение есть совокупность результатов, получаемых как в
предыдущем случае, для всех не отвергаемых при переборе вариантов;
3. требуемое решение накапливается в процессе перехода от начального
варианта к следующим — каждый элемент последовательности не от-
вергаемых вариантов вносит свой вклад в результат, а при отказе от
варианта нужно восстанавливать предыдущее состояние решения;
4. требуемое решение есть совокупность результатов, получаемых как в
предыдущем случае, для всех возможных последовательностей не от-
вергаемых вариантов.
Теоретически все эти ситуации можно свести к последней с помощью
надлежащей конкретизации понятий варианта, решения и упорядоченности,
но для практических целей (как обычно) полезнее разграничивать случаи
применения метода, а не сводить их к единой схеме. Более важно задать хо-
рошие алгоритмы построения походящего множества возможных вариантов
и вычисления порядка перебора его элементов. Так, в первой ситуации для
упрощения алгоритмов возможно дублирование вариантов, а во второй оно
не допускается, иногда полезно рассматривать более широкое множество ва-
риантов, чем это определяется постановкой задачи. Важно помнить, что и
тогда, когда решение не зависит от порядка перебора вариантов, конкретный
порядок может существенно влиять на время получения результата3 .
3
Для тех, кто изучал достаточно продвинутый курс логики. Семантические (аналитиче-
648 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

Метод перебора/генерации вариантов не исключает случаи, когда полу-


чить решение не удается, но тогда полный просмотр множества возможных
вариантов гарантирует, что решение данной задачи не существует.
Для пояснения данного метода на рис. 11.3 представлена схема работы

C1 C1
1
E 2 F C2 E 12 F
… …
B C2 B
D D
3 3

… …
C3 C3
A A

… …

… …
I II

— множество возможных — нерассмотренные варианты

Рис. 11.3. Схема выполнения перебора вариантов в методе перебо-


ра/генерации возможных вариантов с возвратами

переборного алгоритма. Ниже приводятся пояснения к схеме. В состоянии I


последовательность вариантов

A, B, C2 , D

является текущей последовательностью рассматриваемых вариантов. При этом


оказалась отвергнутой последовательность

A, B, C1
ские) таблицы для классической логики высказываний являются недетерминированным ал-
горитмом проверки ывысказываний, приводящим к результату при любой последовательно-
сти действий. Но Вы могли сами убедиться на практике, насколько сильно зависят размер и
сложность структуры строения таблицы от избранного порядка действий.
11.3. ПЕРЕБОРНЫЕ АЛГОРИТМЫ И РЕКУРСИЯ 649

(т. е. выбор варианта C1 был неудачным). Очередным следующим для B явля-


ется вариант C2 , который принимается и приводит к варианту D в качестве
текущего варианта в состоянии I. Следующим для D является вариант E, к
которому происходит переход при смене состояния.
В состоянии II последовательность

A, B, C2 , D, E

— текущая последовательность, а

A, B, C2 , E, F

— одна из возможных следующих (числовые пометки на стрелках обознача-


ют порядок рассмотрения следующих вариантов). Вариант C1 в рассмотре-
нии не участвует, т. к. он уже был пройден ранее.
Если на каком-то шаге вычислений вариант F оказывается отвергнутым,
то происходит возврат в состояние II (эти действия на схеме не показаны).
Если в этом состоянии все следующие для E варианты отвергаются, то отвер-
гается и он. Происходит возврат к текущему варианту D в состоянии I (серая
стрелка на рис. 11.3). Для D определен единственный следующий вариант,
оказавшийся отвергнутым, поэтому D также отвергается и происходит воз-
врат к C2 . Этот вариант также отвергается, и текущим снова становится B, а
следующим — C3 .
Из приведенной схемы видно, что для реализации алгоритмов по мето-
ду перебора/генерации возможных вариантов хорошо подходит использова-
ние рекурсии. В качестве иллюстрации этого утверждения и для демонстра-
ции некоторых сложных моментов, связанных с обсуждаемым методом, ниже
рассматривается способ построения множества индексов для всех кортежей
элементов некоторого массива A[1..N]

(A[i1 ], A[i2 ], . . . , A[ik ])), 1 6 ik 6 N.

Представленные фрагменты программ работают в контексте следующих опи-


саний:
const N = 4; { значение N выбрано для определенности }

type TVariants = array [1..N] of Integer;

var Variants : TVariants;


650 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

procedure Handle (var V : TVariants; K : Integer );


var i: Integer;
s: string;
begin
s := ”;
for i := 1 to K do
s := s + Str ( V [i] ) + ’ ’;
writeln ( s );
end;

Процедура Handle дает возможность вывода построенного варианта, ко-


торый представляется массивом (V) и переменной (K), V используется для
передачи кортежа индексов, а K — для хранения его длины.
Идея алгоритма в том, что состояние процесса перебора вариантов запо-
минается в параметрах рекурсивной процедуры (Select), а переход от вари-
анта к варианту — это просто увеличение на единицу значения компоненты
кортежа или его длины. Инициация процесса осуществляется следующими
операторами:
Variants [1] := 1;
Select ( Variants, 1 )

Первая версия процедуры Select является некорректной.


Программа 11.3.1

procedure Select ( V : TVariants; K : Integer );


begin
Handle( V, K );
if V [ K ] < N
then begin V [ K ] := V [ K ] + 1;
Select ( V, K );
end
else if K < N
then begin K := K + 1;
V [ K ] := 1;
Select ( V, K ); {*}
end
end;
11.3. ПЕРЕБОРНЫЕ АЛГОРИТМЫ И РЕКУРСИЯ 651

Она не учитывает, что после увеличения длины кортежа и просмотра всех


расширенных кортежей нужно довести до конца процесс просмотра для кор-
тежа меньшей длины (см. строку {*}). В результате выполнения процедуры
будут напечатаны не все варианты:

1
2
3
4
41
42
43
44
441
...

Правильная версия, представленная ниже, избавлена от ошибки с помо-


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

Программа 11.3.2

procedure Select ( V : TVariants; K : Integer );


begin
Handle( V, K ); if K < N
then begin V [ K + 1 ] := 1;
Select ( V, K + 1 );
end;
if V [ K ] < N
then begin V [ K ] := V [ K ] + 1;
Select ( V, K );
end;
end;

Необходимо заметить, что для порядка перебора вариантов, выбранного в


данной версии процедуры Select, передача параметра V как значения — рас-
точительное решение. Оно приводит к избыточному дублированию масси-
вов, тогда как в данном случае этого легко избежать: достаточно увидеть,
652 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

что при рекурсивных вызовах можно передавать только изменения вариан-


тов и восстанавливать их после возвратов. Это довольно характерный прием
для обсуждаемого метода. Однако в общем случае без запоминания всей ин-
формации о варианте не обойтись. Поэтому в тексте процедуры Select было
решено сохранить полное запоминание, а соответствующее преобразование
предоставить читателю.
Способы сокращения запоминаемых сведений о вариантах следует рас-
сматривать как оптимизацию алгоритма. При оптимизации решения обсу-
ждаемой задачи для запоминания вариантов достаточно сохранять лишь дли-
ны кортежей (т. е. оставить только второй параметр процедуры). Выбор по-
рядка перебора кортежей позволяет не заботиться о передаче изменений ва-
риантов и о восстановлении. Понятно, что от порядка перебора вариантов
зависит и порядок вывода порождаемых кортежей. Для приводимой проце-
дуры рост индексов по мере вывода происходит справа налево:
1
11
111
1111
1112
1113
1114
112
1121
1122
1123
1124
113
1131
1132
...
Представленное решение демонстрирует подход, который можно считать
типичным для стиля структурного программирования. Он характеризуется
тем, что разбиение данных и действий исходит из понятия обстановки вычи-
слений, а решение об обращении к рекурсии принимается на основе анализа
желаемых преобразований данных обстановки. В качестве альтернативы это-
му подходу можно указать на разбиение данных и действий, основанные на
анализе результатов в целом, которые должны быть получены при решении.
11.3. ПЕРЕБОРНЫЕ АЛГОРИТМЫ И РЕКУРСИЯ 653

Такой подход более функционален по сути, чем операционное манипулиро-


вание элементами обстановки вычислений. На нашей задаче он может быть
продемонстрирован следующим образом.
Множество кортежей, которое должно быть построено, может быть упо-
рядочено так, как это схематически показано на рис. 11.4. Схема явно выделя-
ет два направления упорядоченности: горизонтальное, соответствующее на-
ращиванию размеров кортежей, и вертикальное, отражающее порядок меж-
ду группами кортежей, которые имеют совпадающее начало и различаются
значением ie-того элемента.
Идея нового решения заключается в том, чтобы явно использовать эти на-
правления в алгоритме. В соответствии с этой идеей рекурсивная процедура
SelectF имеет два параметра-аргумента, отвечающие за горизонтальное (Ie)
и вертикальное (k) направления, Ie, k ∈ {1, . . . , N }. Здесь уже учтено, что
передача процедуре в качестве параметра такого элемента обстановки как
массив V, не требуется. Процесс порождения нового кортежа из предыдуще-
го есть присваивание значения k элементу массива V[Ie]. Назначение следую-
щих кортежей, соответствующих текущему началу и различающихся оконча-
ниями — вызов SelectF( Ie + 1, 1 ), а смена текущего Ie-символа — SelectF( Ie,
k + 1 ). В результате этих рассуждений получается программа 11.3.3, которая
по существу от предыдущей отличается только тем, как задается Ie-символ.
Программа 11.3.3

procedure SelectF( Ie, k : Integer);


begin
V [Ie] := k;
Handle ( V, Ie );
if Ie < N then SelectF( Ie + 1, 1 );
if k < N then SelectF( Ie, k + 1 );
end;

Стоит обратить внимание на то, что порядок рекурсивных вызовов существе-


нен. Так, при N = 2 в указанном порядке строится последовательность кор-
тежей
1
1 1
1 2
2
654 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

Номера элементов в кортежах


1 … ie-1 ie ie+1 … N

Начало

Окончания

ie-ый элемент

Рис. 11.4. Схема упорядочивания кортежей


11.3. ПЕРЕБОРНЫЕ АЛГОРИТМЫ И РЕКУРСИЯ 655

2 1
2 2

При обратном порядке некоторые кортежи пропадают, другие — повто-


ряются:

1
2
2 1
2 2
2 1
2 2

Причина тому понятна: направления упорядоченности не независимы!


Из приведенной программы видно, что второй рекурсивный вызов можно
преобразовать в цикл. И тогда тело процедуры превращается в:

for k := k to N do
begin
V [Ie] := k;
Handle ( V, Ie );

{ if k < N then SelectF( Ie, k + 1 ); — Этот оператор исчезает!}


if Ie < N then SelectF( Ie + 1, 1 );

end;

К сожалению, небольшое повышение эффективности, которое достига-


ется при такой модификации, снижает наглядность программы: вместе со
вторым рекурсивным вызовом исчезает явно показанное в прежнем тексте
представление о принятом упорядочивании. Это следствие плохой совме-
стимости рекурсий и циклов! Таким образом, данную модификацию мож-
но рекомендавать лишь в тех случаях, когда требуется особая забота об эф-
фективности. Программа 11.3.3 легко может быть преобразована так, чтобы
в результирующих кортежах не появлялись повторяющиеся элементы. Для
этого в нее можно встроить фильтрацию: проверку повторения элемента, ко-
торая будет экранировать присваивание значения k элементу массива V[Ie] и
первый рекурсивный вызов. В Pascal’е это достигается с помощью исполь-
зования множества задействованных индексов:

В результате получается программа 11.3.4.


var Ma: set of 1..N;
656 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

Программа 11.3.4
procedure SelectF1( Ie, k : Integer);
begin
if not (k in Ma) then
begin
Ma := Ma + [k];
V [Ie] := k;
Handle ( V, Ie );
if Ie < N then SelectF( Ie + 1, 1 );
Ma := Ma - [k];
end
if k < N then SelectF( Ie, k + 1 );
end;
Проверка, добавление и удаление элемента множества — быстрые операции
при представлении малых множеств (см. п. 9.3.5), принятом в языке Pascal.
Поэтому получается практически чистый выигрыш в эффективности, и во
многих случаях такое прямое преобразование можно рекомендавать. Если
это не так, то требуется более точно (или порою более грубо, но более про-
сто; необходимо соблюдение баланса между сложностью проверки и слож-
ностью повторных вызовов) определить порядок кортежей, не имеющих по-
вторений. Использование множеств может быть рекомендовано для решения
еще одной переборной задачи, подобной только что рассмотренной: для по-
строения множеств неповторяющихся индексов (упражнение 3).
Все приведенные решения (а также другие решения, оставленные для
самостоятельного рассмотрения) демонстрируют ситуацию, в которой кон-
тексты рекурсий содержат существенную долю вычисляемых результатов.
Именно здесь обычно хорошо работает метод перебора/генерации. В сущно-
сти он представляет собой неявное задание отношения полного порядка на
данных, который согласован с естественным частичным порядком. То, что
такое доопределение чаще всего не является однозначным, понятно. Резуль-
таты решения, а также его эффективность обычно существенно зависят от
того, как именно произведено доопределение порядка.
Представленные программы корректны с точки зрения конечности вычи-
слений. Однако их анализ показывает, что все они не являются сильно кор-
ректными, и, соотвественно, вызывают повторный счет. Это хорошо видно из
схемы на рис. 11.4: окончания кортежей для разных значений ie-того элемен-
та оказываются одинаковыми. Для данной задачи повторный счет не очень
11.3. ПЕРЕБОРНЫЕ АЛГОРИТМЫ И РЕКУРСИЯ 657

велик, но в других случаях он может оказаться значительным. В связи с этим


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

Начало Общие
1 окончания

ie-ый элемент

Рис. 11.5. Схема упорядочивания с общими окончаниями кортежей

между составляющими разбиения данных для обсуждаемой задачи.


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

type PL =^TL;
TL = record inf : Integer;
down, right : PL;
end;

где inf — поле для размещения индекса, а down и right — поля ссылок на
следующий элемент в вертикальном и горизонтальном направлениях. Пере-
менная, которая будет указывать на список в целом, описывается как:

В новой программе у рекурсивной процедуры SelectF2 появляется допол-


var Sta : PL;

нительный параметр co, который показывает номер в горизонтальном напра-


влении (ранее он был бы излишним из-за его совпадения с индексом Ie). В
658 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

алгоритме процедуры вместо присваивания k элементу массива генерирует-


ся новый элемент списка. Первый рекурсивный вызов аналогичен такому же
вызову в прежней программе. Если мы переделаем и второй рекурсивный вы-
зов в подобной манере, то получится решение, отличие которого от SelectF
будет лишь в том, что следы вычисления кортежей сохраняться в списке.
Для исключения повторного счета вместо этого надо встроить генерацию се-
рии элементов вертикального направления с общей ссылкой на Окончание.
В этом построении неизбежно теряется одно качество прежнего решения:
теперь нельзя совместить обработку (вывод) с формированием списка. Это
вполне понятно, поскольку дополнительный просмотр только что построен-
ных элементов вертикального направления с нужными для обработки про-
смотрами по горизонтальному направлению лишает всех преимуществ ре-
шение о слиянии Окончаний. Следующая программа реализует процедуру
SelectF2, обладающую указанными свойствами.

Программа 11.3.5

procedure SelectF2 ( var Ie : PL; co, k : Integer );


var Cr, Nx : PL;
begin
new (Ie);
Cr := Ie;
with Ie^do
begin inf := k;
down := nil;
right:= nil;
end;
if co < N then SelectF2 ( Cr^.left, co + 1, 1 );
while k < N do
begin k := k + 1;
new (Nx);
Cr^.down := Nx;
Nx^.inf := k;
Nx^.right:= Cr^.right;
Nx^.down := nil;
Cr := Nx;
end;
end;
11.3. ПЕРЕБОРНЫЕ АЛГОРИТМЫ И РЕКУРСИЯ 659

Вызов процедуры SelectF2(Sta,1,1) для фиксированного N приводит к по-


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

Sta

inf down right


1 1 … 1

2 2 … 2

… … … …

N N … N

Рис. 11.6. Граф решений, представленный списком

ся в вершине, на которую указывает Sta. Построение кортежа сводится к


выписыванию последовательности пометок вершин графа (поля inf) при их
посещении согласно следующему правилу: из всех вершин вертикального
направления перемещения пометка берется только у последней из них. По-
строение всех кортежей может рассматриваться как самостоятельная задача
отслеживания нужных маршрутов без повторений. Возможно ее решение ре-
курсивным методом. При разработке соответствующей программы (это пред-
лагается выполнить самостоятельно) целесообразно не ограничиваться гра-
фами, которые получаются при выполнении процедуры SelectF2. Решая за-
дачу для общего случая, мы не только расширяем сферу ее применения (в
частности, должно получиться так, что она будет работать и тогда, когда
склейка окончаний не предусматривается, можно предусмотреть фильтра-
цию решений и т. д.), но и получаем универсальный подход для оперирования
660 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

с графами.
Структура получившегося графа решений вполне регулярна, и можно го-
ворить о построении ее иными методами, нежели те, что были применены
выше. Разумеется, это можно (и нужно!) было выяснить на этапе анализа
задачи, который устанавливает принимаемый метод решения. Но здесь хо-
телось бы подчеркнуть другое. Если сравнить различные решения задачи с
точки зрения того, как вовлекаются данные в вычисления, то становится яс-
но, что структура графа отражает именно эти потоки данных, т. е. фиксирует
в себе все допустимые с точки зрения задействованных алгоритмов порядки4
доступа к данным.
Каждый из путей из начальной вершины графа соответствует одному из
искомых решений. Ацикличность построенного графа позволяет при постро-
ении решений не заботиться о контроле длин проходимых путей. Если отка-
заться от использования этого свойства, то можно еще раз произвести склей-
ку графа по вертикальному направлению (см. рис. 11.7а). Поиск решений для
нового графа может осуществляться той же программой, которая применима
для графов обоих прежних видов (без учета и с учетом совпадений оконча-
ний), если только она контролирует длины проходимых путей. Это принци-
пиально, т. к. новый граф перестает быть ациклическим.
В списочном представлении нового графа оказалось, что все ссылки на-
право (поле right) имеют одно и то же значение. Зачем же тогда хранить их
в каждом элементе? Очевидный ответ на этот вопрос приводит к тому, что
требуемую списочную структуру целесообразно преобразовать в простой по-
следовательный список (см. рис. 11.7б). Прежняя программа для его обработ-
ки не годится, но из нее легко (даже механически!) можно построить новую
программу, соответствующую измененной структуре данных.
Содержимое элементов этого списка для решаемой задачи соответствует
следованию чисел отрезка натурального ряда, записанного в поле Inf. Поэто-
му сам такой список становится избыточным: вместо его просмотра более
эффективно реализовать непосредственное оперирования с этим отрезком.
А это возврат к тому, что было реализовано в первом или втором решении,
которые вовсе не используют граф. Впрочем, теперь они оказались упрятан-
ными в программе обработки (Handle).
Круг замкнулся. Приведенными построениями мы показали, что для ре-
В данном случае это слово практически является термином: поток данных задает частич-
ный порядок на них, фиксирующий разрешение доступа к данным: чтодбы данное a стало
4

доступным, нужно обработать все предшествующие ему.


11.4. ЛАБИРИНТ 661

Sta Sta’

1 1

2 2

… …

N N

Рис. 11.7. Два вида графа решений: (а) — список со склеенными вертикалями
и (б) — список-последовательность.

шаемой задачи переход от рекурсивной программы к рекурсивным данным


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

§ 11.4. ЛАБИРИНТ

В настоящем параграфе приводится иллюстрация метода перебора/генерации


вариантов с возвратами, когда требуется найти не все, а одно из возможных
решений. В качестве области применения метода обсуждается разработка ал-
горитмов блуждания по лабиринту, т. е. нахождения путей в лабиринте из
заданной точки. В учебном плане задача блуждания по лабиринту удобна в
нескольких отношениях. Во-первых, она является весьма характерной пере-
662 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

борной задачей, требующей просмотра вариантов с возвратами. Во-вторых, в


ней легко усмотреть связь с задачей закраски области (§ 11.2). Попытка при-
менить уже наработанный подход к решению оказывается поучительной. В-
третьих, на примере данной задачи можно проследить один важный техно-
логический подход к решению программистских задач, связанный с разбие-
нием задачи на подзадачи.
11.4.1. Блуждание по лабиринту и закраска области

Задача закраски области может быть достаточно просто переформулиро-


вана для случая блуждания по лабиринту, когда требуется найти все точки
области, достижимые из заданной точки. В такой постановке новая задача
допускает решение, ничем не отличающееся от решения задачи о закраске.
В самом деле, возможность перехода из одной точки лабиринта в другую пол-
ностью соответствует понятию соседства точек, и как следствие, закраску со-
седней точки можно трактовать как переход в нее, а осуществление закраски
всей области — как отметку всех последовательно достижимых точек. Од-
нако если слегка изменить постановку задачи блуждания, потребовав опре-
делить не существование пути, а сам путь из одной точки в другую, то об-
наружится, что алгоритм закраски области потребует корректировки. Кстати
сказать, новая постановка более естественна для задачи блуждания.
Почему для нахождения пути из одной точки в другую подход, предста-
вленный в предыдущем разделе, не годится? Дело в том, что он не фиксирует
последовательности перемещений по точкам, более того, для закраски прин-
ципиально, что одна и та же точка посещается не один раз, а столько, сколько
нужно для закраски всех соседствующих с ней точек. При решении новой же
задачи нужно научиться отвергать (исключать из рассмотрения) точки, пере-
ходы из которых ведут в тупики (не только зацикливание). В прежней задаче
отвергались лишь граничные точки, что сохраняется и для задачи блужда-
ния, и те точки, соседи которых лежат на границе области или уже закраше-
ны. Иными словами, определить, нужна ли конкретная точка для продолже-
ния процесса (для рекурсивного вызова процедуры), можно было локально.
Теперь же, составляя последовательность точек (последовательности, если
требуется найти разные пути), представляющей искомый путь (пути), отвер-
гать точки нужно после исследования, ведет ли из нее путь в точку назначе-
ния. Весьма поучительно попробовать попытаться воспользоваться прежним
алгоритмом для решения данной задачи непосредственно и увидеть его де-
фекты.
Есть еще одна особенность новой задачи, важная уже на уровне уточ-
11.4. ЛАБИРИНТ 663

нения постановки: при блуждании по лабиринту для каждой точки всегда


можно выделить множество очевидно достижимых из нее точек. В него, к
примеру, попадают все соседние неграничные точки, весь луч из данной точ-
ки до пересечения его с границей (интуитивное представление видимости).
Поэтому можно ввести понятие множества точек, очевидно достижимых из
данной. Очевидно достижимые точки определять можно по-разному, но при
выполнении одного условия: для поиска пути в лабиринте в качестве началь-
ной (конечной) точки выбор какой-либо из них в множестве очевидно дости-
жимых точек безразличен.
Иными словами, для блуждания в лабиринте все множества взаимно оче-
видно достижимых точек группируются в классы эквивалентности, и вместо
точечного пространства, аналогичного области закраски, в задачах о лаби-
ринте нужно оперировать с набором классов эквивалентности. Для опреде-
ленности будем называть такие классы комнатами, искусственно устанавли-
вая границы комнат как окончания очевидно существующего перехода меж-
ду точками. Эти границы либо совпадают с фактическими границами лаби-
ринтного пространства, которые в дальнейшем называются стенами, либо
добавлены для разграничения комнат, связанных возможным переходом, —
двери. В результате в качестве начала и конца искомого пути указываются не
точки, а комнаты, а сам путь рассматривается как последовательность ком-
нат.
Таким образом, задача о лабиринте становится дискретной. В ней можно
выделить в ней два независимых аспекта:
• графическое отображение лабиринта и

• нахождение пути в нем.

Следует заметить, что и при решении задачи раскраски фактически был


осуществлен переход к дискретной постановке: непрерывность границ обла-
сти рассматривалась как наличие среди соседей каждой граничной точки
других граничных точек. В свою очередь, именно это позволило разрабо-
тать универсальный алгоритм, не зависящий от (аналитического) вида гра-
ниц области закраски (правда, у этого алгоритма есть недостаток: он очень
чувствителен к непрерывности отображения границы области на экране, т. е.
неустойчив к пропуску граничных пикселей). В задаче раскраски не было не-
обходимости выделять аспект графического отображения, она формулирует-
ся для заданной, т. е. уже изображенной области. Таким образом, дискретное
представление области закраски остается реализационным.
664 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

Для блуждания по лабиринту ситуация иная. Задача поиска пути может


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

11.4.2. Абстрактное и конкретное представления данных

Прежде всего, разберемся с тем, что означает абстрактное представление


данных в программировании.
Предельно абстрактным представлением данных (пока мы не имеем дело
со значениями высших типов) можно считать алгебраическую систему. Она
состоит из совокупности непустых множеств (носителей системы), которые
соответствуют базовым типам данных рассматриваемой структуры, совокуп-
ности функций и предикатов над этими носителями, часть из которых станет
методами создаваемых программных объектов5 , а часть так и останется при-
зраками.
Соответственно, данные считаются изоморфными, если изоморфны со-
ответствующие им алгебраические системы.
Но это предельно абстрактное математическое представление данных не-
достаточно для программирования. Подобно тому, как было введено понятие
абстрактного синтаксиса программы, можно выделить и абстрактное про-
граммное представление, фиксирующее те программные сущности, которые
обязаны быть представлены для реализации нужных нам алгоритмов над рас-
сматриваемой структурой данных. Некоторым (весьма полезным, но недо-
статочным!) приближением к абстрактному представлению данных в про-
граммировании служат абстрактные классы в ООП (см. стр. 717).
В абстрактном представлении мы фиксируем лишь необходимые для ре-
ализуемого алгоритма особенности структур данных и методов. В частно-
сти, можно практически полностью игнорировать вопросы представления
информации при вводе и выводе, если данные вопросы не являются критиче-
скими для алгоритма. Но даже если эти вопросы оказались для нас критиче-
Здесь слово метод полностью согласуется с тем значением, которое ему в идеале желают
придать в объектно-ориентированном программировании, но никак не привязано к тому,
5

является ли конкретная реализация объектной.


11.4. ЛАБИРИНТ 665

скими, то в абстрактном представлении можно и нужно отвлечься от конкрет-


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

• абстрактное преджставление приспособлено для построения с помо-


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

По существу, выделение абстрактного и конкретного представлений явля-


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

При разработке абстрактного представления поставьте на пер-


вое место естественность и органичность представления данных.
666 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

Простота приложится, дополнительный труд многократно оку-


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

11.4.3. Абстрактное представление лабиринта

Конкретизируя данное в предыдущем разделе определение изоморфно-


сти абсторактных представлений, получаем следующее определение для ла-
биринтов.
Определение 11.4.1. Два лабиринта называются изоморфными, если меж-
ду множествами их комнат и дверей можно установить взаимно-однозначное
соответствие, для которого выполняется следующее условие: если дверь со-
единяет две комнаты одного лабиринта, то соответствующие комнаты друго-
го лабиринта соединяются соответственной дверью.
Конец определения 11.4.1.
Это определение позволяет говорить о лабиринтах безотносительно их
изображений, как о системах комнат с переходами. Такой лабиринт может
изображаться совершенно по-разному в зависимости от того, например, ка-
кие цели преследуются при решении задачи блуждания.
Для иллюстрации на рис. 11.8 предлагаются два изоморфных лабиринта,
зрительно совсем не похожих друг на друга.
Подобие показанных лабиринтов становится наглядным, если отразить
на их изображениях номера комнат (см. рис. 11.8б). Чтобы подчеркнуть услов-
ность изображения комнат, стенок и дверей на рис. 11.8б пунктиром показа-
ны разделительные линии между комнатами.
11.4. ЛАБИРИНТ 667

а) Лабиринты без разметки

31 30
23 22
1 2 3 4 5 6 7 8
15 14
32 29
24 7 6 21
16 13
8 5
1 4
9 12
2 3
17 20
25 28
10 11

18 19
26 27

б) Лабиринты с разметкой

Рис. 11.8. Два изоморфных лабиринта

Задача распознавания подобия лабиринтов интересна, но она не рассма-


тривается в данной книге. Здесь важнее заметить, что для всех подобных ла-
биринтов нахождение пути между комнатами достигается с помощью одного
и того же алгоритма, тогда как построение их графического представления
может существенно различаться.
Что нужно для такой унификации решения задачи блуждания?
Абстрактное представление лабиринта — это программистское отраже-
ние математического понятия, а конкретное представление — это изобра-
жение абстрактного лабиринта. Поскольку пользователь программы поиска
пути имеет дело с конкретным представлением, а алгоритм решения форму-
668 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

лируется для абстрактного представления, необходимо иметь возможность


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

2. комнаты идентифицируются парой индексов массива Rooms;

3. из комнаты можно перейти только в одну из четырех соседних, индексы


которых в массиве отличаются на единицу, а число дверей комнаты —
не более четырех (следствие предыдущих положений);

4. все комнаты лабиринта стандартного одинакового размера, максималь-


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

5. в качестве стенок рассматриваются запреты на переходы к соседним


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

6. для унификации вычисления возможности перейти влево (противопо-


ложное Rt направление Lf) и вверх (противоположное Dn направление
Up), массив Rooms дополняется рядом и колонкой фиктивных комнат
11.4. ЛАБИРИНТ 669

с нулевым соответствующим индексом. Этим комнатам принудительно


задаются, соответственно, атрибуты запрета на переход Dn для ряда и
запрета на переход Rt для колонки, соответственно. Их нельзя указы-
вать в качестве начальной или конечной комнат для блуждания;

7. начальная и конечная комнаты задаются как пары индексов, соответ-


ственно, bX, bY и eX, eY;

8. искомый путь в лабиринте представляется массивом Way, который со-


держит пары индексов, идентифицирующих комнаты, и переменной
Lway — длиной пути;

9. для отражения хода вычислительного процесса поиска пути в лабирин-


те каждая комната снабжается числовым атрибутом Inf, который коди-
рует состояние комнаты. То, как определять состояния, непосредствен-
но связано с алгоритмом, работающим с абстрактным представлени-
ем. Например, ненулевые значения этого атрибута могут указывать на
номер комнаты в последовательности искомого пути, что нацеливает
использование атрибута Inf на изображение лабиринта с пометками у
комнат, выделяющими искомый путь.
В соответствии с данными положениями, абстрактное представление лаби-
ринта, принимаемое для решения поставленной задачи, на реализуется на
языке Turbo Pascal описаниями, приводимыми ниже. Для простоты и в со-
ответствии с тем, что размеры массивов на этом языке должны быть фикси-
рованы до выполнения программы, вводятся константы, характеризующие
максимально возможные числа комнат в лабиринте по горизонтали — MaxX
и вертикали — MaxY; фактические размеры лабиринта задаются переменны-
ми mX и mY. Дополнительные описания вспомогательного характера приво-
дятся без пояснений.
Программа 11.4.1

const MaxX = 20;


MaxY = 20;
MaxXY = MaxX * MaxY;
type Troom = record Rt, Dn : Boolean;
Inf : Integer;
end;
Tdir = ( Lf, Rt, Up, Dn );
670 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

{ направления возможных переходов из комнаты }


TmaxX = 0 .. MaxX;
TmaxY = 0 .. MaxY;
TRooms = array [ TMaxX, TMaxY ] of Troom;
TRoomIndex = record
Col : TmaxX;

{ представление комнат }
Row : TmaxY;

{ представление начальной комнаты }


end; var Rooms : TRooms;
bX : TMaxX;

{ представление конечной комнаты }


bY : TMaxY;
eX : TMaxX;

{ фактические размеры лабиринта }


eY : TMaxY;
mX : TMaxX;
mY : TMaxY;

{ представление пути в лабиринте }


Way : array [ 1 .. MaxXY ] of TroomIndex;

Lway : 0 .. MaxXY; { длина текущего пути в лабиринте }

{ функция проверки отсутствия запрета на переход из комнаты}


function isWay ( X : TMaxX; Y : TMaxY; dir : TDir ): Boolean;

{ X, Y по направлению dir }
begin
case dir of
Lf : isWay := Rooms [ X - 1, Y ].Rt;
Rt : isWay := Rooms [ X, Y ].Rt;
Up : isWay := Rooms [ X, Y - 1 ].Dn;
Dn : isWay := Rooms [ X, Y ].Dn;
end
end;

В этом описании специального комментария требуют фрагменты, связанные


с понятием направления возможных переходов: имена первых двух полей
типа Troom (Rt, Dn), значения перечислимого типа Tdir (Lf, Rt, Up, Dn), а также
функция isWay.
Само понятие направления можно рассматривать с формальной точки зре-
ния. Оно фиксирует максимально возможное число абстрактных дверей, упо-
рядочивает их и стандартизирует их обозначения. Ничто не препятствует
определению и другого числа дверей, и другого упорядочивания. С другой
стороны, неявно имеется ввиду, что абстрактное представление построено
11.4. ЛАБИРИНТ 671

с учетом его преобразования в требуемые конкретные представления. Здесь


рассматривается частный случай абстрактного представления прямоуголь-
ного лабиринта, в котором комнаты разбивают прямоугольник на квадрат-
ные клетки, имеющие левую, правую, верхнюю и нижнюю стороны, что и
отражено в обозначениях.
Что касается функции isWay, то ее назначение — нивелировать различия
проверки возможности перемещения по разным направлениям. В рамках ис-
ходной постановки задачи открытость двери не зависит от направления, в
котором она проходится. Из этого следует, что было бы ошибкой указывать
статус двери дважды, т. е. как различные атрибуты двух соединяемых дверью
комнат. Приведенное описание исходит из соглашения, о том, что указывает-
ся статус двери комнаты по направлениям вправо и вниз, а статус двух других
дверей вычисляется. Именно это и делает функция isWay. Для упрощения ал-
горитма этой функции, а также для последующей унификации вводятся ряд
и столбец фиктивных запертых комнат с нулевыми координатами, т. е. типы
TmaxX и TmaxY описываются как отрезки целых, начинающиеся с нуля (а не
с единицы). В результате пограничные (из первой строки и первого столбца)
комнаты перестают быть исключительными.
Данное абстрактное представление позволяет решать задачу блуждания
в условиях плоского лабиринта (все комнаты и переходы между ними рас-
полагаются в одной плоскости) с дверями, ориентированными по сторонам
света6 и допускающими переходы в обе стороны. Возможно модифициро-
вать представление для лабиринтов, которые имеют динамически изменяе-
мое состояние дверей (открыта или закрыта в зависимости от маршрута, чи-
сла пройденных комнат и других условий), но здесь это не делается.
Для работы с данным абстрактным представлением необходимо преду-
смотреть алгоритмы очистки (ликвидации стенок и пометок комнат) — ClearRooms
и инициализации лабиринта (расстановка стенок) — InitRooms. В данном ре-
шении предлагается случайное размещение комнат: стенка выставляется с
вероятностью, равной константе p. Переменные mX и mY передаются про-
цедурам ClearRooms и InitRooms через общий контекст. Их значения долж-
ны быть заданы до вызова этих процедур. Отрисовка и стирание лабиринта,
т. е. построение и уничтожение его конкретного представления, намеренно
отделены от задания и ликвидации абстрактного представления, что мотиви-
Любой способ глобальной ориентации в лабиринте называется в теоретических работах
компасом. Так что ориентация по сторонам света — термин дискретной математики и не
6

имеет никакого отношения к фактической ориентации комнат.


672 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

руется возможностью определения различных конкретных представлений. В


реальной программе эти действия вполне могут быть совмещены с очисткой
и инициализацией.

Программа 11.4.2

const p = 0.5;

procedure ClearRooms;
var Col, Row : Integer;
begin
for Col := 0 to mX do
for Row := 0 to mY do
begin Rooms [ Col, Row ].Rt := True;
Rooms [ Col, Row ].Dn := True;
Rooms [ Col, Row ].Inf := 0;
end;
bX := 0; eX := 0;
bY := 0; eY := 0;
end;

procedure InitRooms;
var r : Real;
Col, Row : Integer;
begin
ClearRooms;
for Row := 1 to mY do
begin for Col := 1 to mX do
begin r := Random;
Rooms [ Col, Row ].Rt := r > part;
r := Random;
Rooms [ Col, Row ].Dn := r > part;
end;
Rooms [ 0, Row ].Rt := False;
Rooms [ 0, Row ].Dn := True;
Rooms [ mX, Row ].Rt := False;
end;
for Col := 1 to mX do
11.4. ЛАБИРИНТ 673

begin Rooms [ Col, mY ].Dn := False;


Rooms [ Col, 0 ].Dn := False;
Rooms [ Col, 0 ].Rt := True;
end;
end;

Случайная расстановка стенок лабиринта является, пожалуй, единственным


методом, который в состоянии непосредственно работать с абстрактным пред-
ставлением. Другие возможные методы так или иначе связаны с конкретны-
ми представлениями. На абстрактном уровне для их поддержки требуется
лишь обеспечить внешнюю возможность задания значений атрибутов Rt и Dn
комнат. Если есть необходимость закрыть прямой доступ к массиву Rooms,
то взамен можно предоставить процедуры, подобные следующей:
procedure CloseRtRoom ( Col : TMaxX; Row : TmaxY );
begin
Rooms [ Col, Row ].Rt := False;
end;

В качестве конкретного представления лабиринта можно предложить раз-


личные визуализации абстрактного представления. Два примера такого рода
представлены на рис. 11.8. Кроме изображения лабиринта, конкретное пред-
ставление должно снабжаться средствами отражения начальной, конечной
комнат и пути, а также включать в себя интерфейсы активизации вычисле-
ний.
Заметно, что лабиринты, порождаемые процедурой InitRooms, при кон-
кретном представлении не очень “красивы”. Причина тому — слишком рав-
номерное, не учитывающее взаимного расположения стенок распределение
дверей. Это недостаток выбранного абстрактного представления, а точнее —
алгоритма его инициализации. Исправление его — еще одна задача для са-
мостоятельного решения.

11.4.4. Поиск пути в лабиринте

В настоящем разделе обсуждается построение алгоритма поиска пути в


лабиринте методом перебора/генерации вариантов с возвратами. В данном
случае рассматривается вариант задачи перебора, описанный в п. 3 на стр. 647,
когда решение накапливается в виде последовательности комнат при пере-
ходе от комнаты к комнате. Если такая последовательность ведет к конечной
674 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

комнате, то она и есть результат, т. е. искомый путь, в противном случае она


отвергается. Как уже отмечалось, все ситуации можно свести друг к другу. В
качестве иллюстрации ниже будет показано такое сведение.
Как и в задаче о раскраске, множество возможных вариантов строится на
базе отношения соседства: комната считается соседней к данной, если в нее
ведет дверь. Порядок рассмотрения соседних комнат может быть любой. На-
правленность процесса поиска пути может быть задана разбиением его на
части в соответствии со следующим соотношением. Если вычисление неко-
торой процедуры Find с параметрами начX, начY, конX и конY приводит к
нахождению пути из комнаты начX, начY в комнату конX, конY, то два после-
довательных ее вызова

Find ( bX, bY, X, Y ); Find ( X, Y, eX, eY ); (11.2)


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

function FindWay1 (bX: TMaxX; bY: TMaxY; eX: TMaxX; eY: TMaxY): Boolean;
begin
if ( bX = eX ) and ( bY = eY )
then FindWay1:= True
else if isWay ( bX, bY, Lf )
then FindWay1:= FindWay ( bX - 1, bY, eX, eY )
else if isWay ( bX, bY, Rt )
then FindWay1:= FindWay ( bX + 1, bY, eX, eY )
else if isWay ( bX, bY, Up )
then FindWay1:= FindWay ( bX, bY - 1, eX, eY )
else if isWay ( bX, bY, Dn )
then FindWay1:= FindWay ( bX, bY + 1, eX, eY )
else FindWay1:= False
end;

не годится, но совсем не потому, что здесь нарушена схема (11.2). По су-


ществу схема сохраняется, просто первый вызов Find(bX, bY, X, Y) в целях
уменьшения избыточного перебора заменяется фактической генерацией сле-
дующего возможного варианта.
11.4. ЛАБИРИНТ 675

Это решение не подходит совсем по другой причине: FindWay1 не рабо-


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

Рис. 11.9. Мнимый лабиринт (Н - начальная, К - конечная комнаты)

жить число непроверенных комнат, поскольку, как и при закраске, возврат в


уже пройденные комнаты бессмысленен. Поэтому нужен запрет на перехо-
ды в комнаты, которые уже пройдены (в случае закраски он был реализован
с помощью проверки цвета пикселя). Его реализация, естественно, зависит
от абстрактного представления лабиринта.
Для предложенного выше абстрактного представления лабиринта вполне
достаточно помечать в массиве Rooms комнаты, которые уже пройдены (ис-
пользуется атрибут Inf), а путь запоминать в массиве Way, корректируя его
при возвратах. В качестве множества возможных вариантов здесь выступа-
ют все допустимые значения этого массива (т. е. такие последовательности
пар индексов, которые являются реальными путями) вместе со значениями
676 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

переменной Lway (длина пути). Переходы от одного варианта к другому ре-


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

• 0 — комната еще не посещалась;

• число из диапазона 1..mX*mY (фактическое число комнат) — номер ша-


га текущего пути, на котором осуществляется вход в комнату (или вы-
ход из нее, если это будет удобнее для задания алгоритма);

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

Длина пути в новом абстрактном представлении лабиринта, т. е. переменная


Lway, сохраняется.
Кодирование состояний комнат нового абстрактного представления удоб-
но, например, для визуального отражения пути сразу в ходе его построения.
Тем не менее, абсолютное утверждение, что решение задачи без массива Way
лучше, чем с использованием его, было бы необоснованным. В частности,
если необходимо распечатать результирующую последовательность шагов,
то решение задачи усложнится. Как станет понятно из дальнейшего, в ряде
случаев новое абстрактное представление потребует больше памяти по срав-
нению с алгоритмом, предложенным для самостоятельной работы.
Для нового абстрактного представления в качестве множества возмож-
ных вариантов выступают все допустимые значения массива Rooms (допу-
стимость значения определяется, как такая кодировка состояний, которая обес-
печивает возможность переходов между комнатами). Отношение порядка меж-
ду такими значениями задается следующим образом. Выбирается комната с
максимальным значением атрибута Inf, не превосходящим MXY. Пусть это
значение равно t. В качестве следующих возможных вариантов рассматри-
ваются все такие значения массива Rooms, которые отличаются от текущего
варианта тем, что у одной из соседних комнат, связанных с текущей комна-
той открытой дверью, значение атрибута Inf равно t+1. Иными словами, сле-
дующие варианты — это все те, которые могут быть получены из текущего
11.4. ЛАБИРИНТ 677

варианта при правильном переходе (без прохода через закрытую дверь и без
повторов) в новую комнату. Если таких комнат несколько, то все следующие
комнаты могут быть упорядочены произвольно. Для определенности далее
выбирается такой порядок перебора: сначала проверяется возможность идти
налево, затем направо, вверх и, наконец, вниз. Если следующих вариантов
нет, то текущий вариант тупиковый, и нужно вернуться в предыдущую ком-
нату, чтобы попытаться выйти из нее в другую следующую комнату.
Для старого абстрактного представления каждый возможный вариант —
это значение построенной последовательности пар индексов (массив Way
и переменная Lway), начальным значением которой является одноэлемент-
ная последовательность с индексами, указывающими на начальную комнату.
Следующие варианты строятся из текущего после проверки, в какие сосед-
ние комнаты можно пройти, в сочетании с упорядочиванием выбора очеред-
ной следующей комнаты, если их несколько (аналогично предыдущему). Во-
прос зацикливания решается за счет выставления ненулевых значений атри-
бута Inf элементов массива Rooms для комнат, которые уже посещались.
Нужно заметить, что стратегия преодоления зацикливания в принципе
может быть различна и, в частности, не обязательно связывать ее с помет-
ками комнат. Необходимо лишь позаботиться о том, чтобы каждый выстраи-
ваемый путь не пересекался. Для этого достаточно проверять тем или иным
способом, что в очередном варианте комната, намеченная для перехода, уже
представлена в текущем пути (в старом абстрактном представлении это легко
сделать, проверяя массив Way). Однако при таком решении окажется, что не-
которые из ком-нат будут посещаться неоднократно, поскольку информация
о том, что все пути из данной комнаты тупиковые, не запоминается. Именно
поэтому предпочтительнее оставлять пометки (ненулевые значения атрибу-
та Inf элементов массива Rooms) для комнат, которые уже исследованы. Это
решение верно для статических лабиринтов, т. е. таких, у которых состояние
открытия и закрытия дверей не меняется. В противном случае отмечать ту-
пиковые комнаты бесполезно (впрочем, если удастся ввести понятие полно-
стью исследованной комнаты, разумеется, зависящее от правил открывания
и закрывания дверей, то пометки таких комнат могут оказаться целесообраз-
ными).
Прямое решение задачи блуждания связывается с тем, что при переходе
от варианта к варианту текущее состояние программы запоминается полно-
стью. Все, что изменяется в ходе работы с вариантом, восстанавливается при
отказе от него. Иными словами, предлагается запоминать текущие значения
переменных абстрактного представления. Проще всего это делать через па-
678 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

раметры, которые передаются рекурсивной процедуре, реализующей поиск.


Однако некоторые из переменных состояния программы не меняют своих
значений, а потому их запоминать не требуется. При выборе первого из рас-
смотренных выше абстрактных представлений требуется запоминать Rooms,
Way, Lway. Кроме того, использование идеи разбиения, представленное вы-
ше (см. функцию FindWay1), предполагает запоминание начальной комнаты
пути. Для этого в FindWay1 предусмотрены параметры bX и bY. Что каса-
ется конечной комнаты, то уместно заметить, что как в FindWay1, так и в
программе, которая строится ниже, запоминание ее координат избыточно:
при переходе от варианта к варианту eX и eY остаются неизменными. Тем
не менее, для повышения гибкости программы решение о передаче этих ко-
ординат в качестве параметров оправдано. Оно позволяет не думать о том,
как осуществляется переход от варианта к варианту. Ничто не заставляет ре-
ализовывать схему (11.2), как в FindWay1. Возможны и переходы в обратном
направлении, т. е. от eX, eY к bX, bY, или даже навстречу друг другу, в обоих
направлениях, когда выбор прямого или обратного хода делается на основе
сравнения количеств доступных соседних комнат. За счет этого может замет-
но сокращаться число рассматриваемых вариантов (правда, появится необ-
ходимость восстановления пути при обратном ходе, но это не очень дорого
и достаточно просто).
При выборе абстрактного представления без Way запоминаются Rooms,
bX, bY и Lway, а также eX и eY.
Что должно делаться в ходе обработки текущего варианта пути? В пря-
молинейном решении задачи блуждания требуемые действия складываются
из следующих шагов.
1. Следует проверить, не ли достигнута ли цель поиска, т. е. (bX = eX)
and (bY = eY). В этом случае ничего дополнительного с текущим ва-
риантом делать не требуется, надо просто передать факт достижения
цели на уровень обработки предыдущего варианта (возможность реа-
лизации тех или иных декоративных эффектов здесь не обсуждается,
т. к. она рассматривается как часть задачи трансформации абстрактно-
го представления в конкретное).
2. Если цель поиска не достигнута, следует проверить для каждого на-
правления, достижима ли цель из соседней комнаты, связанной с теку-
щей открытой дверью. Эта проверка распадается на выяснение возмож-
ности перехода по выбранному направлению, проверку достижимости
цели из выбранной соседней комнаты и дальнейшие действия.
11.4. ЛАБИРИНТ 679

(a) Первая часть проверки достижимости в соответствии со сделан-


ными выше соглашениями задается условием:
isWay (bX, bY, <направление>)

(Rooms[<X координата следующей комнаты>,


and

<Y координата следующей комнаты >].inf = 0)


X и Y координаты следующей комнаты определяются как моди-
фикация текущих координат bX и bY, соответствующая направле-
нию.
(b) Вторая часть проверки достижимости цели требует перехода к об-
работке следующего варианта. Для этого нужно предварительно
пометить текущую комнату как исследуемую, что удобно сделать,
задав атрибуту inf значение очередного шага:
Rooms[<X координата следующей комнаты>,
<Y координата следующей комнаты>].inf:= Lway + 1;
подготовить новое состояние программы:
< X координата следующей комнаты >,
< Y координата следующей комнаты >,
eX, eY,
Lway + 1,
Rooms
и, запомнив текущее состояние, активизировать процесс обработ-
ки следующего варианта, который в данном случае реализуется
как рекурсивный вызов:
FindWay (< X координата следующей комнаты >,
< Y координата следующей комнаты > ],
Lway + 1, Rooms);
(c) Дальнейшие действия выполняются после окончания обработки
следующего варианта. Они связаны с анализом результата этой
обработки: если обработанный вариант принят, то и текущий ва-
риант принимается. В противном случае делается попытка прове-
рить следующий переход в соседнюю комнату, т. е. повторяются-
предыдущие шаги для очередного направления, а если направле-
680 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

ния исчерпаны, текущий вариант объявляется тупиковым и отвер-


гается.

3. Последнее, что нужно сделать в связи с выходом из обработки текущего


варианта, — подготовить обработку после возврата. В алгоритме, кото-
рый сейчас обсуждается, такая подготовка сводится к восстановлению
состояния памяти (обеспечивается возвратом из рекурсивного вызова)
и передаче сведений о результате завершаемой обработки: достигает
данный вариант цели или нет. Проще всего результат передавать по-
средством оформления алгоритма в виде функции, вырабатывающей
логические значения true и false в зависимости от достижения цели.

Представленный подход реализован функцией FindWay2, которая описана


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

Программа 11.4.4

function FindWay2 ( bX : TMaxX; bY : TMaxY; eX : TMaxX; eY : TMaxY;


Lway : Integer; Rooms : TRooms ) : Boolean;
var oX : TMaxX;
oY : TMaxY;
Rslt : Boolean;
begin
if ( bX = eX ) and ( bY = eY )
then FindWay2 := true
else begin Rslt := false;
if isWay ( bX, bY, Lf ) and ( Rooms [ bX - 1, bY ] . inf = 0 )
then begin Rooms [ bX - 1, bY ] . inf := Lway + 1;
Rslt := FindWay2(bX-1,bY,eX,eY,Lway+1,Rooms);
FindWay2 := Rslt
end;
if Rslt then exit;
if isWay ( bX, bY, Rt ) and ( Rooms [ bX + 1, bY ] . inf = 0 )
then begin Rooms [ bX + 1, bY ] . inf := Lway + 1;
11.4. ЛАБИРИНТ 681

Rslt := FindWay2(bX+1,bY,eX,eY,Lway+1,Rooms);
FindWay2 := Rslt
end;
if Rslt then exit;
if isWay ( bX, bY, Up ) and ( Rooms [ bX, bY - 1 ] . inf = 0 )
then begin Rooms [ bX, bY - 1 ] . inf := Lway + 1;
Rslt:= FindWay2(bX,bY-1,eX,eY,Lway+1,Rooms);
FindWay2 := Rslt
end;
if Rslt then exit;
if isWay ( bX, bY, Dn ) and ( Rooms [ bX, bY + 1 ] . inf = 0 )
then begin Rooms [ bX, bY + 1 ] . inf := Lway+ 1;
Rslt := FindWay2(bX,bY+1,eX,eY,Lway+1,Rooms);
FindWay2 := Rslt
end
end;
end;

Стоит обратить внимание на то, за счет чего в FindWay2 исключается заци-


кливание процесса. Это следствие двух обстоятельств. Во-первых, в каждом
новом комплекте переменных весь пройденный к данному моменту путь по-
мечен номером шага (см. присваивание и рекурсивный вызов в п. 2b, кото-
рые распространяют ненулевые значения атрибута inf посещаемой комнаты
по глубине рекурсии). Во-вторых, принятый способ перехода от варианта к
варианту гарантирует, что всякий раз выбирается новая соседняя комната, и
пути из нее исследуются до конца, а при возврате только истинное значение
результата берется в расчет для определения того, произошло ли достиже-
ние цели. Однако это наблюдение показывает и существенный недостаток
алгоритма: при построении пути некоторая комната может быть отвергнута,
но эта информация нигде не запоминается, и, таким образом, если комната
будет выбрана повторно, то повторится процесс выявления тупика (см. об-
суждение кодирования состояний комнат при выборе второго абстрактного
представления). Подобные повторения наслаиваются, и в результате эффек-
тивность программы падает.
Запоминание всех переменных состояния программы при генерации ва-
риантов при использовании второго абстрактного представления лабирин-
та делает решение не только неэффективным, но и неприемлемым. В са-
мом деле, единственный эффект вычисления FindWay2 — это значение true
682 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

или false в зависимости от того, есть или нет путь между начальной и ко-
нечной комнатами. Здесь восстановление пути как последовательности ин-
дексов вершин потребовало бы на самом внутреннем рекурсивном вызове
осуществить обход массива Rooms, что явно избыточно. При использовании
абстрактного представления с массивом Way построенный путь запоминал-
ся бы, но и в этом случае понимаемая буквально установка на запоминание
всех переменных состояния программы привела бы к аналогичному резуль-
тату: возможность работы с найденным путем была бы ограничена самым
внутренним рекурсивным вызовом. Это мешает разграничению оперирова-
ния с абстрактным и конкретным представлениями.
Таким образом, желательно выделить в запоминаемом состоянии то, что
целесообразно вынести на уровень общего контекста, доступного каждому
рекурсивному вызову. Решению этой задачи способствует следующее наблю-
дение. Нет нужды передавать от вызова к вызову массив Rooms. Вместо этого
можно выставлять пометку исследуемой и уже исследованной комнаты не-
посредственно в Rooms общего контекста. При выяснении того факта, что из
комнаты нельзя выйти к цели ее вместо значения номера шага текущего пути
ее атрибуту Inf следует присвоить признак исследованности:

Rooms [ bX, bY ] . Inf := MXY;

Это решает все проблемы абстрактного представления без массива Way. Тем
не менее, следует проанализировать и другие возможности минимизации объ-
ема запоминаемого состояния. Уже отмечалось, что включение переменных
eX и eY в запоминаемое состояние сделано для повышения алгоритмической
гибкости программы. Что касается Lway, то эту переменную можно также не
передавать между рекурсивными вызовами, но тогда переход от шага к ша-
гу нужно смоделировать: увеличивать Lway на единицу перед каждым вы-
зовом и уменьшать ее после вызова. Считать ли такую модификацию целе-
сообразной, зависит от глубины рекурсии. Если исследуемые лабиринты не
очень велики, то накладными расходами (требование к памяти), связанны-
ми с запоминанием, можно пренебречь, поскольку дополнительные вычи-
сления, пусть и весьма незначительные (как в данном случае) — это допол-
нительное время выполнения программы.
Окончательный алгоритм поиска пути в лабиринте представлен следую-
щей рекурсивной функцией.

Программа 11.4.5
11.4. ЛАБИРИНТ 683

function FindWay ( bX : TMaxX; bY : TMaxY; eX : TMaxX; eY : TMaxY;


Lway : Integer ) : Boolean;
var Rslt : Boolean;
begin
if ( bX = eX ) and ( bY = eY )
then FindWay := True
else
begin Rslt := False;
if isWay ( bX, bY, Lf ) and ( Rooms [ bX - 1, bY ] . inf = 0 )
then begin Rooms [ bX - 1, bY ] . inf := Lway + 1;
Rslt := FindWay(bX-1,bY,eX,eY,Lway+1,Rooms);
FindWay := Rslt
end;
if Rslt then exit;
if isWay ( bX, bY, Rt ) and ( Rooms [ bX + 1, bY ] . inf = 0 )
then begin Rooms [ bX + 1, bY ] . inf := Lway + 1;
Rslt := FindWay(bX+1,bY,eX,eY,Lway+1,Rooms);
FindWay := Rslt
end;
if Rslt then exit;
if isWay ( bX, bY, Up ) and ( Rooms [ bX, bY - 1 ] . inf = 0 )
then begin Rooms [ bX, bY - 1 ] . inf := Lway + 1;
Rslt:= FindWay(bX,bY-1,eX,eY,Lway+1,Rooms)
FindWay := Rslt
end;
if Rslt then exit;
if isWay ( bX, bY, Dn ) and ( Rooms [ bX, bY + 1 ] . inf = 0 )
then begin Rooms [ bX, bY + 1 ] . inf := Lway+ 1;
Rslt := FindWay(bX,bY+1,eX,eY,Lway+1,Rooms)
FindWay := Rslt
end
end;
if not Rslt then Rooms [ bX, bY ] . Inf := MXY;
end;
684 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

§ 11.5. РЕКУРСИЯ ПРИ ОБРАБОТКЕ СИМВОЛЬНОЙ ИНФОРМАЦИИ

Следующая процедура демонстрирует рекурсивное решение задачи обра-


щения символьной последовательности: требуется напечатать все вводимые
символы в обратном порядке.
Программа 11.5.1

procedure REV;
var S : Char;
begin
if not Eof
then begin
Read (S); { * }
REV; { ** }
Write(S); { *** }
end
end;

Процедура вычисляется следующим образом. Если файл ввода не пустой, то


сначала в локальной переменной S запоминается очередной входной сим-
вол (строка { * }), далее порождается новый экземпляр процедуры (строка
{ ** }, в которой указан рекурсивный вызов процедуры REV), а затем, т. е.
после завершения рекурсивного вызова REV, распечатывается запомненный
символ (строка { *** }). Таким образом, алгоритм порождает одновременно
существующие экземпляры REV ровно в том количестве, какое необходимо
(равном числу символов файла ввода), и в той последовательности, которая
обеспечивает нужный порядок вывода (в соответствии с правилом «первым
порожден — последним завершен»).
Последний пример представлен без использования рекуррентного соот-
ношения. При желании, вводя соответствующую нотацию, такое соотноше-
ние легко составить. Его можно считать определением обращения строки.
Пусть V — алфавит входных символов, ε — обозначение пустой строки (со-
стоящей из нуля символов), A — переменная для строк. Любая непустая
строка xy . . . z (x, y, z ∈ V ) может быть записана в виде xA, где A = y . . . z.
При этих соглашениях функция R обращения произвольной строки A опре-
деляется следующим образом:

если A = ε;
(
R(A) = ε,
R(xA) = R(A)x, если A 6= ε.
11.5. РЕКУРСИЯ ПРИ ОБРАБОТКЕ СИМВОЛЬНОЙ ИНФОРМАЦИИ 685

В рамках приведенных соглашений представленное рекурсивное определе-


ние в точности соответствует рекуррентному соотношению: проверка усло-
вия выявляет пустую строку, с которой ничего не надо делать, оператор Read
(S) отрезает первый символ и запоминает его, REV обращает остаток строки
(и, разумеется, печатает полученную строку), а Write (S) дописывает запо-
мненный символ.
Если для решения данной задачи попытаться составить итеративную про-
грамму, то, по-видимому, придется предусмотреть запоминание читаемых
входных символов в специальной области памяти (массив, список и др.), ко-
торую программист будет вынужден отводить явно. Эта ситуация возникает,
поскольку поставленная задача является рекурсивной по существу (см. со-
поставление рекурсивной и итеративной схем).
Пример 11.5.1. Задача, для которой рекурсия позволяет описать алгоритм
решения достаточно просто.
Пусть обрабатываемая последовательность символов (текст) содержит не-
которое количество парных символов, называемых открывающими и закры-
вающими скобками. Текст считается сбалансированным по скобкам в следу-
ющих случаях:

1) текст не содержит скобочных символов (в том числе, пустой текст);

2) если тексты T1 и T2 сбалансированы, то сбалансирован текст, составлен-


ный из T1, за которым помещается T2;

3) если текст T сбалансирован, то сбалансирован текст, образованный из T


путем обрамления его парными открывающей и закрывающей скобками;

4) других сбалансированных текстов нет.

Заметим, что это определение есть просто словесно записанное рекуррент-


ное соотношение. Если “(” и “)”, “[” и “]”, “<” и “>” — пары соответствующих
открывающих и закрывающих скобок, то сбалансированы тексты
“()”,
“asd(ill0)[q]”,
“(ро+i<l><[]>(123))”,

а тексты
“([<()>])([])”,

“((”,
686 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

“([)]”,
“asd((ill0)[q]”,
“(ро+i<l><[]>(123)”,

не сбалансированы.
“)(”

Требуется написать программу, проверяющую сбалансированность по скоб-


кам вводимой последовательности символов.
Решение этой задачи, когда имеется только одна пара соответствующих
скобок, для определенности “(” и “)”, очень просто:
Программа 11.5.2
count := 0;
while not Eof and ( count >= 0 ) do
begin
Read ( S );
if S = ’(’
then count := count + 1
else if S = ’)’
then count := count - 1
end;
if count = 0
then Write (’OK’)
else Write (’ERR’);

Здесь count — счетчик скобок, который увеличивается, когда во входном по-


токе встречается открывающая скобка, и уменьшается, когда встречается за-
крывающая скобка. Цикл просмотра входного потока прекращается при об-
наружении лишней закрывающей скобки (count в этом случае оказывается
отрицательным) или по исчерпанию вводимых символов. Дополнительная
проверка того, что при исчерпании ввода число открывающих и закрыва-
ющих скобок совпадает, т. е. count = 0, завершает алгоритм. (Прежде чем
перейти к дальнейшему изучению материала обучаемым предлагается само-
стоятельно преобразовать этот алгоритм в рекурсивную подпрограмму.)
По-видимому, это самый лучший алгоритм, когда имеется единственная
пара скобок. Однако непосредственно перенести его на случаи с двумя и бо-
лее парами скобок не удается. Причина тому — невозможность без использо-
вания дополнительной памяти отследить правильность чередования скобок,
11.5. РЕКУРСИЯ ПРИ ОБРАБОТКЕ СИМВОЛЬНОЙ ИНФОРМАЦИИ 687

к примеру, “[(])”. Каждая парная открывающая скобка требует, чтобы провер-


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

a) отвечают за обработку содержимого скобок, и

b) подчиняются стековой дисциплине.

Иными словами, для решения задачи можно воспользоваться рекурсивной


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

Требуемое для обработки значение процедура Scan передает через пере-


Scan.

менную S. Еще одна глобальная переменная, используемая в программе, —


P, предназначенная для индикации выявления дисбаланса. В глобальном кон-
тексте программы представлено также описание констант:
const Etxt = ’*’; { для определенности }
bs1 = ’(’; es1 = ’)’;
bs2 = ’[’; es2 = ’]’;
bs3 = ’<’; es3 = ’>’;
688 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

смысл которых очевиден. Вполне удовлетворительный вариант подпрограм-


мы, проверяющей фрагмент текста, может быть задан функцией Balans:

function Balans ( Q : Char ) : Boolean;


begin
Scan;
case S of
Etxt : Balans := TRUE;
bs1, bs2, bs3 : Balans := Balans ( S );

{ это лучше, чем}


es1 : Balans := Q = bs1;

{if Q = bs1 then Balans := TRUE}


{else Balans := FALSE }
es2 : Balans := Q = bs2;
es3 : Balans := Q = bs3;
end
end; {Balans}
Однако в этой подпрограмме имеется некоторая избыточность действий,
связанная с проверкой Q = bsI, (I = 1, 2, 3): до этого уже проверялось, что
значение S равно bsI (второй вариант оператора case). За счет того, что через
параметр функции можно передавать код не открывающей, а закрывающей
скобки, можно избавиться от этой избыточности:

function Balans ( Q : Char ) : Boolean;


begin
Scan;
case S of
Etxt : Balans := true;
bs1 : Balans := Balans ( es1 );
bs2 : Balans := Balans ( es2 );
bs3 : Balans := Balans ( es3 );
es1, es2, es3 : Balans := S = Q;
end
end; {Balans}
Окончательно программа, предлагаемая в качестве решения задачи, вы-
глядит следующим образом:
11.5. РЕКУРСИЯ ПРИ ОБРАБОТКЕ СИМВОЛЬНОЙ ИНФОРМАЦИИ 689

Программа 11.5.3

const Etxt = ’*’; { для определенности }


program BalansTest;

bs1 = ’(’; es1 = ’)’;


bs2 = ’[’; es2 = ’]’;
bs3 = ’<’; es3 = ’>’;
var S : char;
P : Boolean;
procedure Scan;
begin
if Eof
then S := Etxt
else repeat
Read ( S );
until Eof or
( S in [ bs1, bs2, bs3, es1, es2, es3 ] );
if Eof then S := Etxt
end; {Scan}

function Balans ( Q : char ) : Boolean;


begin
Scan;
case S of
Etxt : Balans := TRUE;
bs1 : Balans := Balans ( es1 );
bs2 : Balans := Balans ( es2 );
bs3 : Balans := Balans ( es3 );
es1, es2, es3 : Balans := S = Q;
end
end; {Balans}
begin
P := true;
while not Eof and P do P := Balans ( S );
if P then Write (’OK’)
else Write (’ERR’);
end.
Уместно обратить внимание на то, что в данной программе не потребовалось
690 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

использовать какие бы то ни было счетчики (аналогичные Count из предыду-


щего алгоритма). Их роль, очевидно, играют рекурсивные вызовы функции
Balans, точнее, соответствующие экземпляры функции для каждой открыва-
ющей скобки.
Для того, чтобы увидеть, насколько трудозатраты, красота и модифици-
руемость решения зависят от правильно выбранного базиса (в частности,
от стиля), сравните полученное решение с программой 13.1.2, написанной
в сентенциальном стиле.
Конец примера 11.5.1.
Рекурсия в обработке символьной информации занимает очень важное
место. Достаточно сказать, что задача синтаксического анализа, решаемая
при разработке любого транслятора, является рекурсивной по существу. Де-
ло в том, что, как мы уже знаем, синтаксис языков обычно определяется при
помощи так называемых контекстно-свободных грамматик, языки которых
требуют для своего распознавания использовать автоматы с магазинной па-
мятью (т. е. со стеком, в котором запоминаются состояния автомата, другая
информация, управляющая вычислительным процессом). В дальнейшем мы
этой задаче уделим особое внимание (см. п. 11.6 и следующий за ним).

§ 11.6. РЕКУРСИЯ В ТРАНСЛИРУЮЩИХ ПРОГРАММАХ

11.6.1. Синтаксический распознаватель простых выражений

Рекурсивная по своей природе структура программных текстов в их кон-


кретно-синтаксическом и абстрактно-синтаксическом представлениях наво-
дит на мысль о том, что при трансляции могут продуктивно применяться
рекурсивные методы. И действительно, многие алгоритмы, используемые в
реальных трансляторах, являются рекурсивными. В данном параграфе рас-
сматриваются некоторые из них, которые помогут выделить явно вырази-
тельный и эффективный метод рекурсивного спуска. В последующих далее
главах мы еще не раз будем обращаться к алгоритмам трансляции, чтобы
выделить другие методы и сопоставить их с рекурсивным спуском. Базовой
иллюстрацией для дальнейшего рассмотрения методов служат упрощенные
арифметические выражения, для которых строятся следующие программы:
a) синтаксический анализатор: проверяет тот факт, что вводимая последо-
вательность символов принадлежит языку арифметических выражений с
точки зрения его конкретного синтаксиса;
11.6. РЕКУРСИЯ В ТРАНСЛИРУЮЩИХ ПРОГРАММАХ 691

b) конструктор абстрактного синтаксиса: переводит конкретно-синтакси-


ческое представление строки в форму дерева, которое можно интерпрети-
ровать как ее абстрактно-синтаксическое представление;
c) интерпретатор дерева абстрактного синтаксиса: вычисляет выраже-
ние.
Упрощенные выражения описываются следующей контекстно-свободной грам-
матикой:
<E> ::= <M> | <M> “+” <E> | <M> “-” <E> (1)
<M> ::= <T> | <T> “*” <M> | <T> “/” <M> (2)
<T> ::= <I> | “(” <E> “)” (3)
<I> ::= <L> | <L> <I> (4)
<L> ::= “a” | “b” | “c” | . . . | “z” (5)

Эта грамматика записана в нотации из § 2.1, но для краткости мы дали


однобуквенные обозначения нетерминальным символам: <E> — выражение
(определяемое понятие); <M> — простое выражение, <T> — терм, <I> — имя
переменной, <L> — буква. Символы, из которых строится выражение — бу-
квы латинского алфавита (правило 5, в котором для краткости варианты по-
рождения недостающих букв заменены многоточием), знаки сложения и вы-
читания “+” и “-” (правило 1), знаки умножения и деления “*” и “/” (правило 2),
скобки “(” и “)” (правило 3). Правило 4 служит для задания способа формиро-
вания имени из букв (имя может состоять из любого положительного числа
букв). В дальнейшем, употребляя терминальные и нетерминальные симво-
лы, мы будем опускать кавычки и угловые скобки, поскольку это нигде не
вызывает двусмысленности.
Понятно, что грамматика задает рекуррентное соотношение, определя-
ющее структуру простых выражений. Это соотношение играет ту же роль,
что и словесное описание структуры строк в задаче про баланс скобок: оно
будет использовано для непосредственного построения алгоритма анализа.
Возможности использования данной грамматики для порождения выраже-
ний иллюстрируется примерами, которые приведены в таблице 11.1. В левой
колонке таблицы записаны выражения, а в правой — их вывод-порождение
в виде цепочки строк, выводимых одна из другой путем применения правил
и их вариантов. Единичное, так называемое непосредственное порождение
обозначается с помощью “⇒”, которое для пояснения обычно индексирует-
ся парой чисел: номер правила и номер варианта (через точку). Если такой
692 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

индекс для понимания не представляет интереса, то он опускается. В некото-


рых примерах ряд непосредственных промежуточных порождений, не прин-
ципиальных для понимания, опускается. Эта ситуация отмечается символом
“*” после “⇒”.

11.6.2. Метод рекурсивного спуска

Алгоритм синтаксического анализа, который используется в предлагае-


мой программе для распознавания принадлежности строк к языку, задавае-
мому грамматикой, основывается на методе рекурсивного спуска. Суть мето-
да состоит в моделировании вывода анализируемой строки путем выдвиже-
ния и проверки гипотез о применении в ходе вывода тех или иных правил. Ка-
ждому правилу грамматики сопоставляется (рекурсивная) подпрограмма —
процедура или функция, которая способна распознавать свойство выводи-
мости части входной строки из данного правила. Эти подпрограммы орга-
низуют синхронное перемещение по правым частям правил грамматики и
входному потоку символов. В ходе такого перемещения возможно либо срав-
нение входного символа с символом из правила, либо проверка гипотезы о
выводимости очередной части строки из некоторого (нового, не являющегося
текущим) правила. В первом случае, когда сравнение успешно, происходит
продвижению по входной строке слева направо, а когда оно не успешно —
отвержение текущей гипотезы и возврат к подпрограмме правила, которая
вызвала исполняемую в данный момент подпрограмму.
Для произвольной грамматики метод рекурсивного спуска требует, во-
обще говоря, возвратов анализа к уже прочитанной части строки. Но, если
грамматика удовлетворяет некоторым условиям, то возможен безвозвратный
анализ. Приведенная грамматика не удовлетворяет этим условиям.
Мы специально выбрали такое представление грамматики, которое тре-
бует преобразования, приспосабливающего ее к безвозвратной схеме анали-
за, чтобы продемонстрировать достаточно типичный метод. Ради этого мы
даже пожертвовали соответствием конкретного синтаксиса правилам вычи-
слений выражений. К слову сказать, представленное ниже преобразование
вернет новой грамматике это полезное свойство.7
При модификации грамматики варианты правил переписываются в ви-
де так называемых регулярных выражений, которые образуются из исход-
Обсуждение вопросов, связанных с преобразованием сложно структурированных данных
см. в главе, посвященной сентенциальным методам.
7
11.6. РЕКУРСИЯ В ТРАНСЛИРУЮЩИХ ПРОГРАММАХ 693

Выражение Вывод-порождение выражения


a E ⇒1.1 M ⇒2.1 T ⇒3.1 I ⇒4.1 L ⇒5.1 a
b E ⇒1.1 M ⇒2.1 T ⇒3.1 I ⇒4.1 L ⇒5.2 b
a+b E ⇒1.2 M + E ⇒2.1 T + E ⇒3.1 I + E ⇒4.1 L + E ⇒5.1 a + E
⇒1.1 a + M ⇒2.1 a + T ⇒3.1 a + I ⇒4.1 a + L ⇒5.2 a + b
ac * ( a - b ) E ⇒1.1 M ⇒2.2 T * M ⇒3.1 I * M ⇒4.2 LI * M ⇒5.1 aI * M
⇒5.3 ac * M ⇒2.1 ac * T ⇒3.2 ac * (E) ⇒1.3 ac * (M-E) ⇒∗
ac * (a - E) ⇒ ac * (a - M) ⇒∗ ac * (a - b)
( a - b ) * E ⇒1.1 M ⇒2.2 T * M ⇒3.2 ( E ) * M ⇒∗ ( a - b ) * M ⇒∗ ( a
(c+d) - b) * ( c + d )

{В предыдущих примерах для очередного порождения


ac * bd E ⇒1.1 M ⇒2.2 T * M

использовалось самое левое из понятий строки; сей-


час демонстрируется возможность продолжать вывод из
любого понятия, имеющегося в строке (в данном слу-
чае это M, второе понятие, из которого получено T).
Поскольку грамматика выражения контекстно-свобод-
ная, результат вывода не зависит от выбора правил.}
⇒3.1 T * I ⇒4.2 Т * LI ⇒5.2 T * bI ⇒∗ ac * bd

Таблица 11.1. Примеры вывода выражений


694 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

ной грамматики путем объединения вариантов. Эта модификация приводит


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

• внутри фигурных скобок возможно использование символа “|” со ста-


рым смыслом (он указывает на возможность вариантов выбора поро-
ждения);

• если непосредственно после фигурных скобок употреблен символ “+”


или “*”, то это означает, что возможно повторение обрамленной фи-
гурными скобками части строки. Символ “*” указывает на необязатель-
ность обрамленной фигурными скобками части — повторение нуль и
более раз, а символ “+” на то, что эта часть в порождении должна по-
явиться по крайней мере один раз.

Данная нотация регулярных выражений более традиционна для теоретиче-


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

E ::= M { + E | - E }* (1)
M ::= T { * M | / M }* (2)
T ::= I|(E) (3)
I ::= { a | b | c | . . . | z }+ (4)

Первое правило состоит в том, что E (выражение) есть M (простое выра-


жение), за которым, быть может, следуют “+ E” или “- E”, повторенные нуль
и более раз. Аналогично, второе правило читается: M есть T (терм), за ко-
торым, быть может, следуют “* E” или “/ E”, повторенные нуль и более раз.
Определение терма (правило 3) не изменилось. Четвертое и пятое правило
объединены. Теперь I (имя переменной) есть буква, повторенная один и бо-
лее раз.
Докажите эквивалентность двух определений простых выражений.
11.6. РЕКУРСИЯ В ТРАНСЛИРУЮЩИХ ПРОГРАММАХ 695

В соответствии с новой грамматикой процесс распознавания того, явля-


ется ли входная строка выражением, задается с помощью комплекта логиче-
ских функций: E, M, T и I. Они выясняют, распознается ли некоторая часть
строки как выражение (E), простое выражение (M), терм (T) и имя перемен-
ной (I), и тогда вырабатывают значение true, а в противном случае — false.
Для наглядности программы этих функций поименованы также, как прави-
ла грамматики. Они составляются систематическим переписыванием пра-
вил, при котором понятия, появляющиеся в правиле, превращаются в вызовы
соответствующих им функций, а обрамленные фигурными скобками части
оформляются как циклы.
Обработка символов, из которых строится выражение (букв, знаков “+”,
“-”, “*”, “/”, скобок), задается как сравнение этих символов с значением ли-
теры из входного потока. Это значение поставляется специальной процеду-
рой Scan, которая записывает его в глобальную переменную S. В предста-
вленной ниже программе Scan осуществляет чтение литеры из файла вво-
да и печатает пробел под очередным читаемым символом (чтобы в случае
ошибки можно было бы отметить этот символ в вводимой строке). Как пра-
вило, подобные действия сопровождаются другими дополнительными вычи-
слениями лексического характера, например, распечаткой читаемой последо-
вательности символов, удалением незначащих пробелов и др. Тем самым в
программе синтаксического анализа выделяется часть, отвечающая за прове-
дение лексических вычислений, главная цель которых — выработка лексем
(в предлагаемой программе это значения переменной S), передаваемых для
выполнения собственно синтаксических вычислений без избыточной инфор-
мации. Обучаемому предлагается модифицировать программу с тем, чтобы
указанные выше дополнительные лексические вычисления осуществлялись.
Переменная S и процедура Scan включены в комплект подпрограмм син-
таксического распознавания.
Последнее предварительное замечания касается использования в програм-
ме конструкции #13 языка Turbo Pascal. Она обозначает символьное значе-
ние, которое нельзя представить графически, в данном случае это символ пе-
рехода на новую строку, который позволяет узнавать, действительно ли вся
входная последовательность прочитана.

Программа 11.6.1

program PARSER;
var S : Char;
696 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

function E : Boolean; forward;


function M : Boolean; forward;
function T : Boolean; forward;
function I : Boolean; forward;

procedure Scan;
begin
Read ( S ); Write (’ ’);
end; Scan
function E : Boolean;
var f : Boolean;
begin
if M
then begin
f := true;
while f and ((S = ’+’) or (S = ’-’)) do
begin
Scan; f := E
end;
E := f;
end
else E := false
end; {E}
function M : Boolean;
var f : Boolean;
begin
if T
then begin
f := true;
while f and ((S = ’*’) or (S = ’/’)) do
begin
Scan; f := M
end;
M := f;
end
else M := FALSE
end; {M}
function T : Boolean;
11.6. РЕКУРСИЯ В ТРАНСЛИРУЮЩИХ ПРОГРАММАХ 697

begin
if I
then T := true
else if (S = ’(’)
then begin
Scan;
if E
then if S = ’)’
then begin Scan; T := true
end
else T := false
end
else T:= false
end; {T}
function I : Boolean;
var f : Boolean;
begin
f := S in [’a’..’z’];
while S in [’a’..’z’] do Scan;
I := f
end; {I}

begin { Начало программы PARSER }


Scan;
if E and ( S = #13 )
then begin Writeln; Writeln (’OK’) end
else begin Writeln (’^’); Writeln (’ERR’) end
end.

Предлагаемая программа демонстрирует опосредованную рекурсивность под-


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

procedure P;
698 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

begin
. . . Q; . . .
end;
procedure Q;
begin
. . . P; . . .
end;

В таких случаях говорят о взаимной рекурсивности подпрограмм.


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

function E : Boolean; forward;


В программе распознавателя обязательно предописание только для функ-
ции E, т. к., расположив остальные подпрограммы в соответствующем по-
рядке, можно добиться выполнения отмеченного принципа. Дополнитель-
ные предописания здесь служат иной цели: они дают возможность такого
порядка подпрограмм, который лучше соответствует восприятию (в той по-
следовательности, в которой заданы правила грамматики). Еще одна роль
предописаний связана с методологическим принципом разделения уровней
определения программных единиц и их использования: при употреблении
подпрограмм, имеющих предописания легче абстрагироваться от того, как
они реализуют требуемые действия, сосредоточивая внимание на том, что
они должны делать.
Приведенная программа — иллюстративный пример распознавателя. В
практических случаях, например, в трансляторах, синтаксический анализ обыч-
но устроен существенно сложнее. Намек на такое усложнение был предста-
влен, когда говорилось о лексических вычислениях (процедура Scan). Сле-
дует добавить, что целью синтаксического анализа, как правило, является не
только распознавание принадлежности строки языку, но и выполнение дру-
гих, более содержательных действий.8
Одно из таких действий — выдача осмысленного сообщения об ошибке в случае не-
правильного выражения, другое — составление по ходу анализа таблиц идентификаторов,
8
11.6. РЕКУРСИЯ В ТРАНСЛИРУЮЩИХ ПРОГРАММАХ 699

11.6.3. Обратная польская запись выражений: понятие, алгоритмы вы-


числения и построения

В качестве еще одной иллюстрации рекурсивной обработки строк при


трансляции предлагается программа, строящая по строке-выражению так на-
зываемую обратную польскую запись (ПОЛИЗ) данного выражения. ПОЛИЗ —
это такая запись выражения, при которой каждое бинарное подвыражение
<левый операнд> <знак операции> <правый операнд>

заменяется на
<левый операнд> <правый операнд> <знак операции>.

Например, выражение a+b*(c/e-d) представляется как abce/d-*+. Для упро-


щенной грамматики выражений
E → M { + E |- E }* (1)
M → T { * M |/ M }* (2)
T → I |( E ) (3)
I → { a |b |c |. . . |z }+ (4)
грамматику ПОЛИЗ можно определить, просто преобразовав правые части
правил ( 1 ), ( 2 ) и ( 3 ):
M { + E |- E }* =⇒ M { E + |E - }* ( 1’ )
T { * M |/ M }* =⇒ T { M * |M / }* ( 2’ )
I |( E ) =⇒ I |E . ( 3’ )
Часть строки, распознаваемая как имя переменной (правило (4)) перено-
сится в ПОЛИЗ неизменной.
Нетрудно заметить, что ПОЛИЗ есть просто другое представление струк-
туры выражения по сравнению с исходным его видом. Как мы скоро уви-
дим, оно великолепно согласуется с вычислением-интерпретацией, причем
именно так, как того требует определение семантики абстрактным вычисли-
телем, заданным на структуре синтаксического домино. Таким образом, ПО-
ЛИЗ вполне можно рассматривать в качестве одной из форм задания абстракт-
ного синтаксиса (при этом, разумеется, надо еще задавать атрибуты элемен-
тов структуры, например, подобно тому, как мы это делали с синтаксическим
домино).
блоков, операторов и вообще накопление вспомогательной информации для последующей
трансляции и отладки программы.
700 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

Употребление обратной польской записи выражений в качестве внутрен-


него представления в транслирующих программах позволяет избежать ско-
бок и приоритетов операций. Но, пожалуй, более важно то, что ПОЛИЗ хо-
рошо согласуется с выполнением вычислений на стеке. Соответствующий
алгоритм можно представить следующей схемой:

Открыть пустой стек;


Установить чтение ПОЛИЗ на начало;
Прочитать два символа из ПОЛИЗ, сохраняя их в стеке;
{ в стеке находятся два операнда, прочитанные из ПОЛИЗ,
очередной символ из ПОЛИЗ — операнд или знак операции }
пока не все символы ПОЛИЗ прочитаны цикл
начало
Прочитать символ из ПОЛИЗ; если прочитанный символ — операнд
то Поместить прочитанный символ в стек
иначе { прочитанный символ — знак операции }
начало
Вычислить бинарное выражение с операндами из стека
и с операцией, определяемой прочитанным символом;
Поместить результат вычисления в стек;
{ два верхних элемента стека заменяются
на результат вычисления операции }
Прочитать символ из ПОЛИЗ
конец
конец

Существенным условием работоспособности данного алгоритма являет-


ся предположение о корректности польской записи. Это предположение впол-
не оправдано, поскольку в практически важных случаях ПОЛИЗ строится по
обычной записи выражения, а в ходе такого построения проверка корректно-
сти возможна и весьма целесообразна. Для учебных целей может потребо-
ваться программа, которая не связана с обсуждаемым ограничением. Обуча-
емым предлагается записать приведенный алгоритм на Pascal либо C/C++, а
также модифицировать его с учетом возможной некорректности польской за-
писи. Следует сравнить два варианта программы. Полезно также попытаться
сделать алгоритм более эффективным за счет других его улучшений. Кроме
того, предлагается трансформировать данную итеративную схему в рекур-
11.6. РЕКУРСИЯ В ТРАНСЛИРУЮЩИХ ПРОГРАММАХ 701

сивную. Какие структуры данных при этой трансформации окажутся избы-


точными и почему?
В таблице 11.2 представлены примеры выражений в обычной записи и их
перевод в ПОЛИЗ, а также последовательности состояний вычислений вы-
ражения по его польской записи, соответствующие приведенному алгорит-
му. Состояния вычислений показаны в виде двух частей: содержимое стека и
непрочитанного остатка ПОЛИЗ. Промежуточные результаты вычислений в
стеке обозначаются как R и R1. Несущественные для понимания состояния
заменены многоточием.
Предлагаемая ниже программа перевода обычной записи выражения в
ПОЛИЗ составляется путем простого дополнения программы распознавате-
ля необходимыми структурами данных и действиями.
Дополнительные структуры данных — это глобальная переменная стро-
кового типа R, предназначенная для накопления ПОЛИЗ выражения, и ло-
кальные переменные op символьного типа в функциях E и M, в которых со-
храняется знак соответствующей операции до тех пор, пока он не станет нуж-
ным для переноса в R (т. е. до завершения обработки второго операнда ка-
ждого из бинарных подвыражений).
Дополнительные действия — это наращивание R, запоминание знаков
операций в op. Уместно отметить, что такое запоминание есть неявная орга-
низация стека, которая осуществляется посредством рекурсивных вызовов.
Кроме него к дополнительным действиям также относится результирующий
вывод ПОЛИЗ вводимого выражения, который задается, если выражение ока-
зывается корректным.
С учетом сделанных замечаний программа POLIS, приводимая ниже, не
нуждается в дополнительных пояснениях.

Программа 11.6.2

program POLIS;
var S : Char;
R : String;
function E : Boolean; forward;
function M : Boolean; forward;
function T : Boolean; forward;
function I : Boolean; forward;

procedure Scan;
702 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

Выражение ПОЛИЗ Состояния вычислений:


стек остаток ПОЛИЗ
a a a
a+b ab + a b+
ab +
R
ac * ( a - b ) ac a b - * . . . ac a b -*
ac R *
R
(a-b)*(c+d) ab-cd-* ... a b -cd+*
R cd+*
R R1 *
R
a+(b+(c+(d+e)*f)*g)*h abcde+f*+g*+h*+ ... a b c d e +f*+g*+h*+
abcR f*+g*+h*+
abcRf *+g*+h*+
abcR +g*+h*+
... a b R g *+h*+
... a R h *+
aR +
R
(((a+b)*c+d)*e+f)*g+h ab+c*d+ ... R c* d +e * f + g *h+
e*f+g*h+ Rc * d + e * f + g *h+
R d+e*f+g*h+
... R f +g*h+
... R g *h+
R h+
R
a-(b-(c-(d -(e-f)))) abcdef----- ... a b c d e f - - - - -
abcdR ----
abcR ---
abR --
aR -
R

Таблица 11.2. Обычная и обратная польская записи выражений, вычисления


по обратной польская записи.
11.6. РЕКУРСИЯ В ТРАНСЛИРУЮЩИХ ПРОГРАММАХ 703

begin
Read ( S ); Write (’ ’);
end; {Scan}

function E : Boolean;
var f : Boolean;
op: char;
begin
if M
then begin
f := TRUE;
while f and ((S = ’+’) or (S = ’-’)) do
begin
op := S;
Scan; f := E;
R := R + op + ’ ’
end;
E := f;
end
else E := FALSE
end; {E}

function M : Boolean;
var f : Boolean;
op: char;
begin
if T
then begin
f := TRUE;
while f and ((S = ’*’) or (S = ’/’)) do
begin
op := S;
Scan; f := M;
R := R + op + ’ ’
end;
M := f;
end
else M := FALSE
704 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

end; {M}

function T : Boolean;
begin
if I
then T := TRUE
else if (S = ’(’)
then begin
Scan;
if E
then if S = ’)’
then begin Scan; T := TRUE
end
else T := FALSE
end
else T:= FALSE
end; {T}

function I : Boolean;
var f : Boolean;
begin
f := S in [’a’..’z’];
while S in [’a’..’z’] do
begin
R := R + S;
Scan;
end;
if f
then R := R + ’ ’;
I := f
end; {I}

begin { Начало программы POLIS }


Writeln( ’Введите выражение’ );
R := ”;
Scan;
if E and ( S = #13 )
then
11.6. РЕКУРСИЯ В ТРАНСЛИРУЮЩИХ ПРОГРАММАХ 705

begin Writeln;
Writeln (’OK’);
R := R + ’|’;
end
else
begin Writeln (’^’);
Writeln (’ERR’)
end;
Writeln (R);
Readln;
end.

Программы PARSER и POLIS являются чисто демонстрационными и не пре-


тендуют на то, чтобы использоваться в практических целях. Так, их оче-
видный недостаток — работа с одной строкой ввода, невозможность полу-
чать входную информацию из файла. Обучаемым предлагается исправить
его, обращая внимание на то, что части программ, которые должны быть
модернизированы, никак не касаются собственно синтаксического анализа
и генерации ПОЛИЗ (изменятся лишь лексические вычисления). Это хоро-
шее качество с точки зрения модуляризации. Напротив, встраивание генера-
ции потребовало бы правки всего текста программы PARSER, т. е. модульное
разделение двух процессов синтаксического анализа и генерации не достиг-
нуто. Вообще говоря, такое разделение требует гораздо больше средств, чем
те, которые предлагаются в обычных системах программирования. При раз-
работке реальных транслирующих систем решение этой задачи обыкновен-
но осуществляется за счет использования специализированных систем, спе-
циально приспособленных к адекватному описанию всех процессов, связан-
ных с трансляцией. Такие системы получили название систем построения
трансляторов.
Реализация систем программирования для алгоритмических языков —
лишь одна из сфер приложения рекурсивного спуска. Всегда, когда можно
предложить рекурсивное описание структуры и согласованное с ней зада-
ние обработки, появляются возможности для рекурсивного спуска. Совсем
не обязательно такое описание связывать с грамматической формой. Требу-
ется лишь монотонность разбиения данных
{Σ0 , Σ1j1 , . . . , Σkjk , . . . }
по некоторому фундированному отношению , которое отслеживается при
706 ГЛАВА 11. МЕТОДЫ, ОСНОВАННЫЕ НА РЕКУРСИИ

рекурсивном вызове соответствующих этому разбиению действий. Для грам-


матических форм это свойство гарантируется конечностью перерабатывае-
мой строки и, как следствие, вариантов ее разбора.
Особый вопрос, связанный с реализацией рекурсивного спуска — обес-
печение вычислений без возвратов. Для трансляционного применения это ка-
чество принципиально, но если речь идет о данных, которые можно просма-
тривать неоднократно, безвозвратность требовать не обязательно. Однако и
в таком случае следует стремиться минимизировать число просмотров, пре-
жде всего за счет пересмотра структуры определений понятий. Как правило,
такой пересмотр позволяет улучшить эту структуру и в других отношениях,
но порою ценой усложнения определений и ухудшения их понятности.
Задания для самопроверки
1. Что будет с программой FloodFill4 в следующем ‘граничном’ случае,
если в области есть пиксели, лежащие на границе экрана? Исправьте
этот недочет и другие, замеченные Вами.
2. Составьте программу для закрашивания гранично определенных 8-связ-
ных областей.
3. Разработать программу построения множеств (а не последовательно-
стей!) неповторяющихся индексов.
4. Разработать конкретное представление лабиринта, включающее визуа-
лизацию пути и возможности активизации и приостановки вычислений
и слежения за их ходом.
Наводящие указания. Перед тем, как пытаться построить какое-либо
содержательное отображение, стоит затратить усилия на построение
конкретного представления в виде прямоугольной таблицы, изображен-
ной на рис. 11.8. Если отрисовку такого представления выполнить по-
клеточно, т. е. предусмотреть процедуру рисования отдельной клетки,
то построение других конкретных представлений может быть выпол-
нено с помощью локальной модификации этой процедуры.
5. Разработайте алгоритм генерации достаточно красивых и интересных
лабиринтов. Некоторые возможные идеи. Размещать а одновременно
пары вертикальных и горизонтальных стенок. Случайным образом вы-
брать комнату, а затем вертикальное или горизонтальное направление.
Использовать технику фрактальной графики.
11.6. РЕКУРСИЯ В ТРАНСЛИРУЮЩИХ ПРОГРАММАХ 707

Сравните Ваш алгоритм с алгоритмом товарища, прежде всего, по эсте-


тическим критериям получающихся лабиринтов9 .

6. Реализовать алгоритм поиска пути в лабиринте с пометками уже прой-


денных комнат и с использованием массива Way.

7. Какие последовательности помечает программа 8.7.4?

8. Программа 8.7.4 составлена с учетом удобного для чтения с экрана вы-


вода информации. Объясните, в каком месте программы эта ‘пользуха’
реализована и каким образом?

9. Написать нерекурсивную программу, делающую то же, что 8.7.4. Срав-


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

10. Программа 8.7.4 содержит неиспользованную процедуру Inpt. Зачем


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

Здесь Вы сталкиваетесь с частым в практике случаем, когда точная оценка качества ре-
шения бессмысленна, оно будет оцениваться по неформальным критериям, в соответствии
9

с вкусами заказчика. Тем не менее и в этом случае не нужно пренебрегать накопленным


опытом, и часто полезна консультация с профессиональным дизайнером.
Глава 12

Объектно-ориентированный
подход

В объектно-ориентированном программировании собственно объектный


стиль занимает не более 40%. В остальных случаях это — формальное пред-
ставление в виде объектных конструкций систем понятий, принадлежащих
другим стилям. В настоящее время ООП может рассматриваться как общая
высокоуровневая надстройка над структурным программированием и про-
граммированием от состояний.
Графовые модели ООП, таблицы переходов и синтаксических таблицы
внешне похожи, что порой создает путаницу. Так что объектно-ориентиро-
ванный стиль естественно разбивать на три направления: рекурсия в форме
ООП, состояния в форме ООП, собственно объекты и роли. В данной главе
мы рассмотрим ООП, ориентируясь на главные области применения.

§ 12.1. ОБЪЕКТЫ

В данном параграфе рассматривается объектная ориентированность в язы-


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

708
12.1. ОБЪЕКТЫ 709

ные в состав объекта, называются его методами.


Как всегда бывает в достаточно сложных и взаимосогласованных систе-
мах1 , изменения (в данном случае расширения) в базовом понятии влекут
необходимость адекватной надстройки на следующих уровнях.
12.1.1. Объекты как структуры данных и права доступа

Поскольку включенные в состав описания объекта методы в некотором


аспекте определяют смысл данных, имеющихся в данном объекте, то в по-
давляющем большинстве случаев необходимо запретить прямое обращение
к внутренним данным объекта. Но такое запрещение не может быть абсо-
лютным. Скажем, реализация методов самого объекта должна иметь воз-
можность прямого обращения к данным. Совокупность совместно разраба-
тываемых объектов может иметь возможности прямого обращения к дан-
ным друг друга (такая возможность абсолютно безвредна лишь в том случае,
когда объекты настолько взаимосвязаны, что создаются и модифицируются
одной и той же командой в рамках одних и тех же задач, но она слишком при-
влекательна, чтобы и в других случаях не пойти на некоторый риск). Более
того, некоторые из методов могут создаваться лишь для внутреннего исполь-
зования, отражая конкретные детали реализации, и поэтому должны быть
экранированы от внешнего мира. В связи с этим возникает необходимость
специфицировать права доступа к компонентам объекта.
Общепринятый во всех развитых практических объектно-ориентирован-
ных системах программирования ‘джентльменский набор’ прав доступа сле-
дующий.
public Специфицирует открытые для внешнего использования данные и ме-
тоды.

private Специфицирует данные и методы, закрытые повсюду, кроме реали-


заций методов описываемого объекта.

protected Специфицирует защищенные данные и методы, доступные дан-


ному объекту и его наследникам.

В C++/C# эти спецификации могут применяться также к полям записей и


объединений.
Первая характеристика, безусловно, подходит системам программирования, а вторая тоже
с некоторой натяжкой может быть применена к ним.
1
710 ГЛАВА 12. ООП

В конкретных системах ООП (C#, C++, Java, Object Pascal) имеются и


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

Внимание!
В C++ и C# значения и методы записи, никак не специфициро-
ванные, считаются по умолчанию открытыми, но и в этих языках,
и в Java, и в Object Pascal значения и методы класса по умолча-
нию считаются закрытыми!2

Видно, что спецификации доступа тесно связаны с теми концепциями огра-


ничения доступа (см., напр., § 8.6.2), которые естественно возникли при раз-
витии модульности. Поэтому модульность и объектная ориентированность
идут в нынешних системах рука об руку. Особенно четко это выявилось в
языке Java, где открытый класс (public class) практически отождествляется
с содержащим его JAVA-файлом.
Рассмортим пример описания класса:

Программа 12.1.1

{ Object Pascal }
TBits = class(TObject)
private
FSize: Integer;
FBits: Pointer;
procedure Error;
procedure SetSize(Value: Integer);
procedure SetBit(Index: Integer; Value: Boolean);
function GetBit(Index: Integer): Boolean;
public
constructor Create; override;
destructor Destroy; override;
function OpenBit: Integer;
property Bits[Index: Integer]: Boolean read GetBit write SetBit; default;
property Size: Integer read FSize write SetSize;
end;

В одном из стандартных пакетов Visual C++ версии 6 имеется комментарий: «Данный тип
описан как структура, а не как класс, чтобы не писать многочисленные public.»
2
12.1. ОБЪЕКТЫ 711

...
implementation
...
constructor Tbits.Create;
begin
inherited Create;
Fsize:=0;
Fbits:=nil;
end
...

Уже на этом примере видно несколько особенностей объектных типов.


С точностью до спецификаторов private и public (а также других описа-
телей и соглашений, регламентирующих доступ к составляющим объектов)
они описываются в духе обычных записей (структур), но с весьма существен-
ным дополнением: описание объектов обязательно включает объявление их
методов, определяющих возможное поведение объектов. Более того, в язы-
ках C# /C++ записи, объединения и классы даже с формальной точки зрения
объединены под синтаксическим понятием объекта, и почти все специфика-
торы, допустимые для объектов, переносятся на записи и объединения.
Как правило, описания методов выносятся из описания собственно клас-
са, и, более того, даже помещаются в другой раздел модуля. Для них исполь-
зуются специфицированные имена (смотри пример выше) с указанием имени
класса, метод которого описывается. Например:

procedure TBits.Error;
begin
raise EBitsError.CreateRes(SBitsIndexError);
end;

Развивая принцип замены физического доступа логическим, некоторые


поля объектов могут специфицироваться как property, при этом операции
взятия значения и присваивания данному полю специфицируются как мето-
ды данного объекта (обычно закрытые). Таким образом, использование в вы-
ражении имени TheBits.Bits[i], где TheBits — конкретный объект типа Tbits,
является неявным вызовом функции TheBits.Getbit(i). Соответственно, при-
сваивание
712 ГЛАВА 12. ООП

TheBits.Bits[i]:=true;

интерпретируется как вызов


TheBits.Setbit(i,true);

Cреди методов выделяются два особых подкласса: конструкторы и де-


структоры. В расширениях Pascal конструкторы описываются как constructor,
а деструкторы как destructor. Даже вызов конструктора несколько отличает-
ся по синтаксису от вызова обычного методов. В частности, в Object Pascal
вызов конструктора записывается со спецификатором класса создаваемого
объекта (поскольку в момент создания самого объекта еще нет), например,
Tbits.Create;

Это решение достаточно логично, но в других объектно-ориентированных


языках часто принимается в некотором смысле противоположное соглаше-
ние: процесс создания объекта исполняется в следующнм порядке.
a. выделение нужной для объекта памяти (вычисление ее размеров осуще-
ствляется по сведениям, которые готовятся статически),
b. образование указательного значения и передача его в точку программы, в
которой принято решение о создании объекта, и
c. вызов конструктора, который завершает создание объекта.
Таким образом, вопрос о том, когда именно появляется объект, в разных си-
стемах решается по-разному. В частности, на его решение влияет далеко не
однозначное соглашения о том, как появляются части объекта, созданные в
нем самом и унаследованные им от ‘родителей’: создается ли объект весь
сразу или по частям (сначала ‘родительские’ части, а затем — собственные.
Ответ на указанный вопрос влечет за собой алгоритмические следствия. Так,
в Java и в C++ приняты прямо противоположные точки зрения, из-за чего
получается, что в одном случае виртуальные методы, которые вызываются в
теле конструктора, нужно выбирать из родительского класса, а в другом — из
класса-наследника3 . Как всегда, данная неоднозначность — следствие теоре-
тической недоработки концепций. В последующих пунктах мы постараемся
Авторам приходилось слышать весьма эмоциональные комментарии тех, кто сталкивался
с этой проблемой в ходе переноса программ с одного из этих языков на другой, причем
3

противоположно понимаемые конструкции изображаются в них совершенно одинаково.


12.1. ОБЪЕКТЫ 713

указать на некоторые ориентиры, способные облегчить понимание подобных


вопросов.
В C++ и Java конструкторы носят просто имя класса, но, поскольку в С++
и Java может быть несколько процедур с одним и тем же именем, но разны-
ми параметрами (полиморфизм процедур), конструкторов тоже может быть
несколько.
Деструкторы в С++ носят имя класса с отрицанием (например, Tbits). В
языке Java их просто нет, поскольку действия по уничтожению возлагаются
на систему сборки мусора.
Система объектно-ориентированного программирования по умолчанию
предлагает простейшие конструкторы и деструкторы. В частности, а параме-
трами их являются все открытые поля данного класса в порядке их появления
в описании. В Object Pascal конструктор, создаваемый по умолчанию, носит
имя Create, а деструктор — Destroy, и параметров они не имеют.
При вызове метода класса необходимо указать конкретный объект данно-
го класса, которому принадлежит метод. Это связано, в частности, с тем, что
объект может являться не чистым объектом данного класса, а его наследни-
ком, а наследник может заменить некоторые методы (как у нас сделано для
конструктора Create). При описании нового метода на старый метод можно
сослаться как на inherited, (super в языке Java) и явно выписать лишь новые
действия. Но это уже переход к принципиально новым возможностям объект-
ного программирования: динамической подстановке методов в момент вы-
полнения, которые рассматриваются в следующем пункте. Именно эта воз-
можность делает объектно-ориентированное программирование следующим
шагом по сравнению с модульностью (в первую очередь для традиционных
языков; для, скажем, сентенциальных и функциональных языков такое рас-
ширение является непринципиальным).
В языках С++ и Java некоторые поля класса могут быть объявлены как
static. Статические поля создаются при входе в программу один раз для всех
будущих экземпляров объектов данного класса. Поэтому, в частности, в та-
ком поле удобно хранить глобальные характеристики системы объектов дан-
ного класса, например, число активных в данный момент объектов. В Object
Pascal этого описателя нет, так как он, во-первых, грубо противоречит духу
языка Pascal, а во-вторых, просто избыточен. Поскольку модульность и объ-
ектность не слиты в этом языке воедино, в модуле, где описывается класс,
можно объявить переменную, которая будет глобальной для всех экземпля-
ров класса. Эта переменная будет невидима извне, если ее поместить в сек-
цию implementation и может быть проинициализирована в секции initialization
714 ГЛАВА 12. ООП

модуля.

12.1.2. Наследование и полиморфизм

Практически все описания классов в нынешних программах имеют вид,


подобный

{Object Pascal}

Тело описания класса


CRoleOfActor = class(CRole)

end;
/* C++ */
(12.1)
{ Тело описания класса };
class CRoleOfActor:: public CRole

/* Java */

{ Тело описания класса };


class CRoleOfActor extends CRole

Это означает, что новый класс CRoleOfActor является наследником ранее опре-
деленного класса CActor, а он его предками. Отношение наследования тран-
зитивно, так что CRoleOfActor является также наследником любого из пред-
ков CActor4 . Наследник включает все данные и все методы предков. Наслед-
ник базируется именно на классах-предках, а не на конкретных объектах
этих классов, так что при создании наследника ссылаться на ранее создан-
ные объекты классов-предков, как правило, не приходится. Далее, скобки в
определении класса являются не ограничителем параметров, а скорее опре-
делением отношения порядка на множестве классов. Математический смысл
В Turbo Pascal и Object Pascal не имеет предков лишь один, предопределенный в системе,
класс TObject. Если предки класса не указаны, то он считается прямым наследнком TObject,
4

так что описания

Hermit = сlass

Hermit = сlass(TObject)

эквивалентны.
12.1. ОБЪЕКТЫ 715

(12.1) можно выразить формулой:


CRoleOfActor  CActor (12.2)
Класс-наследник может переопределить любое имя из класса-предка (точ-
но так же, как модуль может переопределить любое имя из используемого мо-
дуля, а внутренний блок — имя из объемлющего). Если нет дополнительных
спецификаций, то новое имя просто экранирует старое (и, таким образом, в
памяти компьютера они оба сосуществуют).
Любой наследник может быть использован там, где правила синтаксиса
требуют его предка. Так что объектно-ориентированное программирование
вводит новое приведение данных: приведение наследника к типу предка. Оно,
в частности, производится при присваивании
TheActor := TheRoleOfActor; (12.3)
где TheActor — конкретный объект класса CActor.
Внимание!
В большинстве книг по ООП используется квазинаучная и
вводящая в заблуждение терминология, когда наследник объявля-
ется ‘подтипом’ или ‘конкретизацией’ предка. С точки зрения ал-
гебраических систем наследник является обогащением сигнату-
ры предка и его надмоделью, а вовсе не конкретизацией. А если
учесть, что смысл функций при этом не обязан сохраняться, то от-
ношения между ними еще слабее и практически полностью про-
тивоположны тем, на которые намекает общепринятая термино-
логия.
Если переопределенный в наследнике метод не имел дополнительной спе-
цификации virtual, то он экранирован новым методом, но при приведении
наследника к типу предка старый метод всплывает наружу и используется в
вызовах. Такой метод называется статическим. Обработка статических ме-
тодов трансляторами и вызов их в ходе исполнения программы не отличают-
ся принципиально от работы с обычными процедурами. Здесь по-прежнему
осуществляется статическое связвание имени с именуемой им сущностью.
Если же он имеет эту спецификацию, то он называется виртуальным ме-
тодом и может быть вытеснен новым методом в случае, если новый метод
задан со спецификацией override. Таким образом, при присваивании значе-
ния переменной объектного типа происходят неявные и согласованные при-
сваивания новых значений некоторым виртуальным методам предков.
716 ГЛАВА 12. ООП

При вызове виртуального метода сначала вычисляется связь между име-


нем и методом для данного конкретного экземпляра объекта (как говорят,
сущность и имя связаны динамически). При вычислении связи используется
информация о типе конкретного экземпляра, и, таким образом, информация
о типах объектных значений частично сохраняется в программе. В объект-
но-ориентированных языках есть способы работы с такой информацией, со-
ответствующие переменные имеют типом (имена) классов, конкретный тип
значения вычисляют функции, подобные ClassOf.
Сам вызов представляется косвенной ссылкой, которая по типу значения
находит адрес традиционного вызова нужного в данный момент метода. Ко-
нечно же, абстрактно-синтаксическое представление вызова метода при ди-
намическом связывании становится другим (упражнение 1). Механизм дина-
мического связывания был создан и достаточно хорошо реализован еще на
заре объектно-ориентированного программирования в языке Simula 67.
Единственной возможностью проимитировать динамическое связывание
в языке со статическими связями имен и значений является буквальное по-
вторение в нескольких классах всех нужных процедур.
В связи с возможностью замены методов целесообразно иметь возмож-
ность указания, что данный объект либо метод не подлежит переписыванию
и экранированию у наследников, что он является конечным: выполняет завер-
шенную функцию, которую можно считать системной для данной библио-
теки классов. Такая спецификация final имеется из рассматриваемых нами
лишь в языке Java.
Пример 12.1.1. При моделировании программирования от состояний в рам-
ках ООП (статический автомат) чаще всего целесообразно описать все со-
стояния как классы, являющиеся наследником одного и того же класса, на-
пример, CState. В этом классе целесообразно описать общие данные всех
состояний, а также метод, применяемый для исполнения действия, сопоста-
вленного состоянию, и для перехода к новому состоянию. Первый из этих ме-
тодов должен заменяться в каждом конкретном состоянии, а второй должен
быть финальным (если не формально, то фактически). Получается примерно
следующее описание.
CState={abstract} class(TObject)
NextState: CState;
ExecResult: Condition;
CommonData: Data;
procedure Execute; {abstract}
12.1. ОБЪЕКТЫ 717

function CState.Transfer: CState; {final}


begin
...
result NextState
end;
end;
Заметим, что процедура Execute не должна иметь реализацию. Она долж-
на быть переписана в наследниках этого класса. Насчет Transfer ситуация
противоположная: она должна быть описана при реализации класса CState
либо унаследована им от его предков.
После такого описания тело цикла моделирования конечного автомата
имеет примерно следующий вид
while true do begin
State.Execute;
Next:=State.Transfer; if Next<> nil then State:=Next else Leave;
end;
Заметим, что поскольку состояние у нас именуется переменной типа CState,
возможное переписывание функции Transfer ни на что не влияет: будет вы-
зван ее исходный вариант. Но, особенно если тип CState скрыт глубоко вну-
три библиотеки моделирования, программист порою может быть весьма уди-
влен, почему это переписанный им метод не работает? Каждый из авторов не
раз наблюдал эту ситуацию и на своей, и на чужой практике. Так что явная
спецификация final, которая выдаст ошибку еще на этапе компиляции, явля-
ется полезной.
Конец примера 12.1.1.
Рассмотренный пример приводит к важному понятию абстрактного клас-
са. Это класс, некоторые из методов которого должны быть переписаны в
его наследниках, перед тем, как вызываться. У нас таким методом являет-
ся Execute. Во всех современных системах ООП предусмотрено понятие аб-
страктного класса и спецификация методов как абстрактных.
Рассмотрим соотношение между абстрактными классами и абстрактны-
ми представлениями данных, описанными в п. 11.4.2. При абстрактном пред-
ставлении некоторые методы задаются достаточно грубо. По сути дела они
должны быть переписаны в ходе реализации. Но они не являются просто пу-
стыми затычками, они могут быть использованы для отладки программы и
718 ГЛАВА 12. ООП

часто могут оставаться на ходу вплоть до самых последних этапов разработ-


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

заменить на
typedef Concept_Realization = Abstract_Realization;

typedef Concept_Realization = Concrete_Realization;

Заметим, что сделать конкретную реализацию потомком абстрактной ни-


чем не облегчает задачу.
Тот факт, что в языках C++ и Java происходит привязка некоторых функ-
ций к заранее фиксированным именам, превращает в необходимость поли-
морфизм функций, создававшийся в С++ скорее всего без всякой задней мы-
сли, просто для облегчения неаккуратного программирования или для фор-
мального инкорпорирования в язык того, что неплохо зарекомендовало себя
в других местах, например, в Simula 67. В частности, конструкторы имеют
имя класса, но конструкторов часто нужно много для разных нужд. Точно так
же и с деструкторами. По аналогии и для достижения общей согласованно-
сти интерфейсов5 элементы полиморфизма проникли в Object Pascal, но там
они не являются чем-то необходимым.

12.1.3. Множественное наследование и интерфейсы

Глядя на приведенный в (12.1) заголовок класса, сразу возникает соблазн


5
Все равно достичь ее не удалось, так что попытки в этом направлении были бесплодными!
12.1. ОБЪЕКТЫ 719

определить новый класс приблизительно следующим образом:

/* C++ */

{ Тело описания класса }


class CRoleOfActor:: CActor,CRole,CState,CAnimation (12.4)

Это означает, что новый класс CRoleOfActor является наследником сразу че-
тырех ранее определенных классов: CActor, CRole, CState, CAnimation, а все
они — его предками. Математический смысл (12.4) можно выразить следу-
ющей системой формул:
CRoleOfActor  CActor
CRoleOfActor  CRole
(12.5)
CRoleOfActor  CState
CRoleOfActor  CAnimation
Так что возможны все присваивания вида
TheActor := TheRoleOfActor;
TheRole := TheRoleOfActor;
(12.6)
TheState := TheRoleOfActor;
TheAnimation := TheRoleOfActor;

где TheState — конкретный объект класса CState и т. п. Наследник включает


все данные и все методы предков. Никакой попытки согласования данных и
методов разных предков в нынешнем ООП не делается. Они просто свали-
ваются в одну кучу, а если возникают конфликты имен, программист должен
специфицировать, к какому из предков относится имя, например:
TheMainLover::CRole::face или TheMainLover::CActor::face

Есть один момент, который представляет ценность в исключительно пло-


хо (даже по меркам С++) продуманном механизме множественного насле-
дования. Часто нам целесообразно установить, что у нескольких типов, на
которые опирается потомок, есть общая часть. Именно возможность устано-
вления этой согласованной общей части была бы ценной в множественном
наследовании, но, как известно в теории абстрактных типов данных, задача
согласования частей исключительно трудна и с теоретической, и с практиче-
ской точки зрения. В С++ имеется обходной путь, который позволяет задать
эту возможность.
720 ГЛАВА 12. ООП

Спецификатор virtual в языке C++ может быть применен также к предку


в описании непосредственного потомка. Он имеет смысл лишь тогда, когда
несколько потомков данного класса в дальнейшем будут иметь общего на-
следника. Это единственный способ объявить в C++, что некоторые части
предков отождествляются у наследника, а именно, все наследники данного
класса и других классов, также объявивших этот класс виртуальным, будут
пользоваться общим экземпляром виртуального класса. Рассмотрим пример.

class CActor:: CPerson, virtual CFace {. . . };


class CAnimation: CView, virtual CFace {. . . };

Здесь объект, представляющий лицо актера, один и тот же и внутри CPerson,


и внутри CView. Если бы хоть в одном случае virtual было бы опущено, лица
были бы разные.
В связи с несогласованностью и конфликтами смыслов при наследова-
нии, наследование одним потомком сразу от нескольких предков никуда, кро-
ме C++, не проникло, и правила хорошего тона требуют, чтобы везде, где воз-
можно, у класса был бы один непосредственный предок. Но объективная по-
требность наследования операций сразу нескольких предшественников есть,
значит, нужно разобраться в том, что на самом деле нужно наследовать.
Конечно же, наследовать все данные всех предков — лобовое, и, как все-
гда бывает, самое тупое решение, складывающее в кучу все недостатки и тем
самым почти уничтожающее достоинства. На самом деле нам нужно иметь
возможность обращаться к новому классу, как к другим, ранее определен-
ным. Поэтому во многих системах middleware и в языке Java было введе-
но понятие интерфейса, которое сразу же было воспринято языком Object
Pascal. Нам важно иметь возможность обращаться к данному объекту как к
нескольким другим, и интерфейс фиксирует методы (а в языке Java и данные,
так что понятия интерфейса Java и Object Pascal не полностью совместимы),
которые должны быть предоставлены классом для обращения в соответствии
с данным интерфейсом. Пример интерфейса в языке Object Pascal.

type

IMalloc = interface(IInterface)
[’{00000002-0000-0000-C000-000000000046}’]
function Alloc(Size: Integer): Pointer; stdcall;
function Realloc(P: Pointer; Size: Integer): Pointer; stdcall;
procedure Free(P: Pointer); stdcall;
12.1. ОБЪЕКТЫ 721

function GetSize(P: Pointer): Integer; stdcall;


function DidAlloc(P: Pointer): Integer; stdcall;
procedure HeapMinimize; stdcall;
end;
Первая, самая загадочно выглядящая, строка в описании интерфейса за-
дает его идентификацию для систем middleware и может быть опущена. Сле-
дующие строки задают методы (в Java также и константы) интерфейса. Все
они внутри интерфейса абстрактные, они должны быть реализованы в объ-
ектах, допускающих доступ по данному интерфейсу.
Поэтому конструкции, аналогичные (12.4), в Object Pascal и Java записы-
ваются следующим образом:
{Object Pascal}
type

CRoleOfActor = class(CRole,IActor,IState,IAnimation)
...
end;

/* Java*/
class CRoleOfActor extends CRole implements IActor,IState,IAnimation;
{. . . };
Заметим, что возникают те же самые проблемы с двусмысленностью, что
и при множественном наследовании в C++. Способы их разрешения в Java и
в Object Pascal умилительно расходятся. В Java сигнала ошибки не выдает-
ся, Вы можете пользоваться несовместимыми интерфейсами, но если Ваша
программа будет иметь глупость обратиться к неоднозначно понимаемому
методу или константе, произойдет ошибка. В Object Pascal Вы можете про-
сто назвать по-другому метод, примененный для реализации объявленного в
интерфейсе:
function IActor.Visualize = DrawFace;
procedure IAnimation.Visualize = PlayAnimation;
Решение, включающее интерфейсы, не дает возможности отождествить
подобъекты интерфейсов, но это и не нужно: ведь теперь не предки предо-
ставляют свои объекты и методы потомку, а, наоборот, все интерфейсы ссы-
лаются на один и тот же реализующий их объект, который и поддерживает
единство данных.
722 ГЛАВА 12. ООП

В спецификациях поведения объекта, которые по сути своей определяют


его как единицу декомпозиции, может встретиться, к примеру, следующее со-
всем непроцедурное утверждение. Объект не должен у себя реализовывать
требуемое от него действие (т. е. в его классе и в классах его прародителей
не будет подходящего метода), т. к. есть другой объект, умеющий это делать,
которому первый объект делегирует полномочия выполнять требуемое дей-
ствие. Реализация так специфицированного поведения может быть выполне-
на путем задания атрибута-ссылки на другой объект и косвенного обращения
к его методу. Т. е. в операционном аспекте обычная процедурность сохра-
няется, правда, дополненная ссылочными связями. Делегирование — один
из примеров того, как в объектном стиле подменяется традиционная про-
цедурная декомпозиция новыми понятиями. Подобные способы объектной
декомпозиции, которые образуют набор технологических шаблонов (паттер-
нов) проектирования, изучаются специально в рамках курсов объектно-ори-
ентированного проектирования. С точки зрения обсуждения языковых аспек-
тов стоит отметить, что специальные конструкции для подобных шаблонов в
языках программирования не предлагаются. Сегодня они рассматриваются
лишь как типовые приемы программирования и задаются стандартными для
операционных языков средствами. Тем не менее, как было показано, шабло-
ны проектирования существенно отходят от традиционной процедурности
как основы декомпозиции программ. Иными словами, расширяется понятие
процедуры, и это расширение неизбежно должно оказать влияние и на базис.

§ 12.2. ОБЪЕКТНАЯ МОДУЛЬНОСТЬ

В данном разделе рассматривается, как средства объектно-ориентирован-


ного программирования сочетаются с модуляризацией программ, и в каком
направлении идет их развитие. Для этой цели лучше всего подходит линия
развития TURBO и Delphi (Object Pascal) языка Pascal. Изложение иллюстри-
руется на модификации демонстрационного примера из § 8.6.3.
Первый объектно-ориентированный диалект Pascal’я в линии TURBO —
TURBO Pascal версии 5.5. Он предлагает программисту лишь минимально
необходимые средства поддержки данного стиля. Как и в последующих вер-
сиях языка, задача объектной модуляризации решается на основе модульно-
сти версии TURBO Pascal’я 5.0, рассмотренной в предыдущем разделе. Но
новые средства дают и новые возможности. В частности, появляется под-
держка рассмотрения представления (в примере — массив Container; пере-
12.2. ОБЪЕКТНАЯ МОДУЛЬНОСТЬ 723

менные IB и IE, связанные с алгоритмом кольцевого буфера, мы пока будем


считать частью реализации) отдельно от реализации. Теперь можно вовсе не
знать, является Container массивом или чем-то иным, — методы работы с
этим объектом, описанные в отдельном модуле, обеспечивают реализацию
всем необходимым. Более того, мы можем рассматривать такие Container’ы,
которые в состоянии принимать на хранение объекты разных типов, даже
не зная того, каким способом это достигается. Тем самым решается пробле-
ма реализации очереди с содержимым разных типов.6 Сама очередь стано-
вится объектом-наследником от Container’а. Как следствие, методы работы
с этим объектом оказываются и методами объектов очередь. Для реализации
очереди, как кольцевого буфера нужны переменные IB, IE, BFull и BEmpty,
рассмотрение которых совместно с Container’ом выделяет очередь как са-
мостоятельную структуру данных. В новой ситуации естественно задавать
MaxSize не константой, а атрибутом конкретного экземпляра очереди. Кро-
ме того необходимо решение вопроса об элементе, который предъявляется в
ошибочной ситуации, когда делается попытка чтения из пустой очереди, т. е.
о значении NotEl. Оно должно быть не целочисленной константой, а каким-
то выделенным значением одного из типов содержимого Container. Без све-
дений о конкретных очередях узнать их выделенные элементы невозможно,
значит, определение NotEl, как и MaxSize, нужно возложить на инициализа-
цию очереди. В результате представление очереди определено более точно:
это Container, IB, IE, BFull, BEmpty, MaxSize и NotEl.
Для независимого от реализации понимания очереди в объектном стиле
нужно декларировать методы для работы с объектами конструируемого типа.
Это те же процедуры PutElQueue, GetElQueue и StateQueue, которые были
представлены в первой реализации, а также новая процедура (со старым име-
нем) InitQueue, дополнительные действия которой — установка для MaxSize
значения, передаваемого через параметр, генерация представляющего оче-
редь объекта типа Container и определение NotEl. После окончания работы с
экземпляром очереди этот экземпляр должен быть уничтожен, соответствен-
но, появляется еще метод-деструктор DoneQueue.
Еще одно проектное решение, которое нужно принять, касается проце-
Словосочетание «разные типы» понимается здесь как возможность одновременного на-
личия в очереди объектов различных классов. Если мы захотим иметь общую реализацию
6

для очередей «разных типов» с содержимым одного типа, то для этого придется воспользо-
ваться более развитыми объектно-ориентированными средствами, чем необходимо для по-
яснения модуляризации.
724 ГЛАВА 12. ООП

дуры IncI. Это служебная процедура, осуществляющая “закольцовывание”


буфера с помощью манипулирования индексом. Если оставить ее в прежнем
виде, то она становится методом конструируемого типа объектов, т. к. ее ал-
горитм зависит от атрибута объекта MaxSize. В то же время, предоставлять
IncI на уровне использования не нужно. В TURBO Pascal’е версии 5.5 средств
разграничения предоставляемых и служебных методов не предусмотрено, а
значит, придется специально сообщать пользователю модуля об этих особен-
ностях реализации. Более того, в этом языке нет средств оградить пользова-
теля модуля от прямого обращения к атрибутам объектов. В качестве аль-
тернативы в данном конкретном случае уместно указать на возможность вы-
несения процедуры IncI на уровень общих средств модуля, представленных
в разделе реализации, а значит, доступных только в данном модуле. Зави-
симость этой процедуры от контекста сводится к использованию атрибута
MaxSize экземпляра очереди, который можно передавать через дополнитель-
ный параметр, и тогда процедура может быть общей для всех экземпляров.
В дальнейшем мы реализовываем первый вариант работы с IncI.
С учетом сказанного следующий фрагмент модуля AllQueue_5_5 нужда-
ется только в одном пояснении. Мы определяем два типа для описания объек-
тов-очередей: Tqueue и Pqueue. Первый из них задает структуру всех экзем-
пляров очереди, а второй — тип указателей на нее. Смысл такого (практи-
чески стандартного в ООП) построения в том, что с объектными значени-
ями намного удобнее работать, когда отведение памяти для них не регули-
руется стековым механизмом. Пока можно считать, что принятое решение
диктуется особенностями реализации объектного типа Container в модуле
ContainerUnit, который подключается к модулю AllQueue_5_5 в разделе uses.
Программа 12.2.1

unit AllQueue_5_5;

{ модуль, предоставляющий средства }


interface

{ хранения объектов разной природы }


uses ContainerUnit;

type States = ( Full, Empty, Normal );


Pqueue = ^Tqueue;
Tqueue = object ( Container );
MaxSize,
IB,
IE : Integer;
BFull,
12.2. ОБЪЕКТНАЯ МОДУЛЬНОСТЬ 725

BEmpty : Boolean;
NotEl : PElemContainer;
constructor InitQueue ( InputMaxSize : Integer);
procedure PutElQueue ( X : PElemContainer );
function GetElQueue : PElemContainer;
function StateQueue : States;
procedure IncI ( var I : Integer );
destructor DoneQueue;
end;

Модуль AllQueue_5_5 предлагает описание типа Tqueue с использованием


понятий конкретного представления. В частности, если бы появилась по-
требность в реализации очередей не с помощью кольцевого буфера, а на осно-
ве списочной структуры, то простой замены модуля могло бы оказаться не-
достаточно, поскольку есть опасность, что в использующей программе име-
ются прямые обращения, например, к IB, IE и другим полям, не имеющим
смысла для нового представления. И хотя это дурной стиль, но нельзя га-
рантировать, что все использования AllQueue_5_5 будут подчиняться никак
не поддержанным соглашениям. Надежность программирования снижается.
Подобные причины мотивируют появление в новых версиях языка TURBO
Pascal дополнительных средств защиты.
Мы обстоятельно обсудили недостаточность средств TURBO Pascal’я вер-
сии 5.5 для скрытия того, что не предоставляется при использовании модуля
(в этой связи сравните разделы interface модулей AllQueue_5_5 и IntQueue
из пункта 8.6.1). Поэтому приведем пример на более развитом языке: TURBO
Pascal 7.0. В новом варианте для транслятора дается указание, какие из средств
следует предъявлять на уровне использования модуля (секции описаний public
в объявлении типа), а какие скрывать от пользователя (секции описаний private).
Обратите внимание на эти спецификаторы — их названия были приведены
в комментариях в самой первой программы, реализующей очереди на стан-
дартном языке Pascal.
Программа 12.2.2

unit AllQueue_7_0;

{ модуль, предоставляющий средства }


interface

{ хранения объектов разной природы }


uses ContainerUnit;

type States = ( Full, Empty, Normal );


726 ГЛАВА 12. ООП

Pqueue = ^Tqueue;
Tqueue = object ( Container );
private
MaxSize,
IB,
IE : Integer;
BFull,
BEmpty : Boolean;
NotEl : PElemContainer;
public
constructor InitQueue ( InputMaxSize : Integer);
procedure PutElQueue ( X : PElemContainer );
function GetElQueue : PElemContainer;
function StateQueue : States;
private
procedure IncI ( var I : Integer );
public
destructor DoneQueue;
end;
...

В тексте представлены по две секции public и private. Расположение их отно-


сительно друг друга и их количество языком не регламентируется. В TURBO
Pascal 7.0 есть и другие спецификаторы управления доступом, повышающие
надежность программ.
Язык Object Pascal системы программирования Delphi с точки зрения мо-
дуляризации мало чем отличается от TURBO Pascal 7.0: появились новые
секции описаний с более тонким разграничением возможностей доступа, а
также специальные средства управления доступом (так называемые поля свой-
ства, задаваемые описателем property) и все. Некоторые из новых средств
системно зависимы, что отрицательно сказывается на качестве языка (см. об-
суждение системы Delphi на стр. 85).
В Object Pascal’е принято прагматическое соглашение о том, что все объ-
ектные типы — теперь это называется классами, что соответствует терми-
нологии, ставшей общепринятой, — могут порождать только динамические
объекты, размещение которых на стеке не разрешается. Это соглашение, от-
ражающее реальную потребность, поскольку в програмах оперировали в по-
давляющем большинстве случаев с указателями на объекты, избавляет от не-
12.2. ОБЪЕКТНАЯ МОДУЛЬНОСТЬ 727

обходимости описаний для классов типов указателей, подобных

совмещая два описания в одном:


type Pqueue = ^Tqueue;

Соответственно, исчезает необходимость изображать переменные этих ти-


type Tqueue = class ( Container );

пов, используя “^” для извлечения объекта по указателю. Таким образом, наш
пример изменится лишь в строках, описывающих класс объектов (по-старо-
му, объектный тип).
В последних примерах мы приводили описание только раздела интерфей-
са, поскольку реализация практически повторяет то же, что было написано в
программе 8.6.1, с учетом синтаксических модификаций, требуемых описа-
нием класса.
В языке Java возникает противоположная языкам линии Pascal задача.
Модуль является классом. Класс может быть создан и уничтожен. А если нам
этого не нужно, нужно лишь воспользоваться понятиями из данного класса?
Для этой цели в Java предлагается следующее решение. Описать лишь один
конструктор без параметров и объявить его private. Тем самым он не сможет
быть использован снаружи модуля, где описывается класс, и класс превра-
щается в модуль.

Вопросы для самопроверки

1. Постройте абстрактно-синтаксическое представление вызова динами-


ческого метода. Внимание! Возможны разные схемы реализации дина-
мического связывания, поэтому сравните Ваше решение с двумя (прак-
тически идентичными для обычного программиста, и поэтому концеп-
туально избыточными) реализациями динамического связывания в Object
Pascal, специфицируемыми как virtual и dynamic.

2. Как по-Вашему, полезно ли чем-нибудь наличие в Object Pascal двух


спецификаций virtual и dynamic? Если полезно, то в каких случаях?
Глава 13

Сентенциальные методы

Сентенциальное программирование возникает тогда, когда данные име-


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

Действия глобальны, условия глобальны. (13.1)

В этом случае необходимы мощные средства распознавания глобальных усло-


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

пожалуй, является критическим для сентенциального программирования. В


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

728
13.1. КОНКРЕТИЗАЦИЯ 729

получает никакой полезной для себя информации для проведения выбранно-


го действия.
Следует заметить, что еще одна особенность, общая для трех стилей: сен-
тенциального, от приоритетов и событийно-ориентированного —
Отделение проверки условий от выполнения действий (13.3)
представляется общей характеристикой технологических решений для сти-
лей, соответствующих инварианту (13.1).
В настоящее время глобальная проверка условий осуществляется в систе-
мах сентенциального программирования путем обработки метавыражений.
Метавыражение отличичается от выражения тем, что в нем встречаются пе-
ременные. При проверке выясняется возможности отождествить некоторые
метавыражения друг с другом или с данными. При отождествлении попут-
но вычисляется подстановка, т. е. функция, сопоставляющая переменным
их значения. Подстановка дает необходимую для выполнения шага работы
программы информацию. Такой способ работы согласуется с формулиров-
кой глобальных условий в логике и в теории алгоритмов, но данное решение
нельзя считать a priori исчерпывающим. В дальнейшем могут появиться дру-
гие формы глобальной проверки данных.

§ 13.1. КОНКРЕТИЗАЦИЯ

Под конкретизацией понимается такая глобальная проверка, при которой


переменные встречаются лишь в метавыражении и поиск их значений про-
исходит путем подбора без рекурсии. Идея конкретизации была явно сфор-
мулирована и получила последовательное выражение в языке Рефал.
Язык Рефал был создан В. Ф. Турчиным для программирования символь-
ных преобразований. Исходный толчок он получил от идеи алгорифмов Мар-
кова (см. стр. 849), но она была полностью пересмотрена в ходе работы по
созданию языка. Работа была проведена на исключительно высоком идей-
ном и математическом уровне, но вопросам дизайна практически не было
уделено внимания.
Язык определен через три компоненты: структуру данных, Рефал-маши-
ну, обрабатывающую эти данные, и собственно конкретный синтаксис языка.
13.1.1. Структура данных

Прежде всего, рассмотрим структуру данных, обрабатываемых Рефалом.


Она состоит из основного поля данных (поля памяти), в котором перед ка-
ждым шагом исполнения выделяется активная часть (поле зрения), и стеков
730 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

глобальных данных (закопанные данные). Данные в поле зрения и закопан-


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

1. Атомы являются выражениями. В конкретном синтаксисе нынешнего


Рефала атомы делятся на

(a) Символы (байты);


(b) Составные символы (имена, определенные в программе);
(c) Целые числа без знака, меньшие 232 ;
(d) Действительные числа.

2. Любая последовательность выражений является выражением.

3. Выражение E, заключенное в структурные скобки, является выраже-


нием.

4. Если E — выражение, A — описанный в программе атом, AE, заключен-


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

В конкретном синтаксисе Рефала функциональные скобки обозначаются <


>, структурные — ( ).
Объектное выражение — выражение, не содержащее функциональных
скобок. Минимальное функциональное выражение — выражение, имеющее
вид <E >, где E — объектное выражение. Поле памяти Рефал-машины в лю-
бой момент, за исключением момента остановки программы и выдачи ре-
зультата, представляет из себя выражение, в котором есть функциональные
скобки. Поле зрения (активная часть) поля памяти — первое из встречаю-
щихся в поле памяти минимальных функциональных выражений.
Конец определения 13.1.1.
Детерминатив функциональных скобках интерпретируется как имя функции,
обрабатывающей содержимое скобок. Поэтому в ходе вычислений не могут
образовываться выражения вида <<E1 >E2 >. В подавляющем большинстве
13.1. КОНКРЕТИЗАЦИЯ 731

случаев первый символ в функциональной скобке должен быть именем, но


некоторые обычные символы, например, +, также могут использоваться в ка-
честве детерминативов.
Пример 13.1.2. Рассмотрим пример памяти Рефал-машины в ходе вычи-
слений.
Поле памяти:
’aaxzACDE’ <Sort <Perm ’G’1.5E5> <Perm 115 ’F’> <Perm 112 -2.0E-5>><Sort

Поле зрения выделено в поле памяти жирным шрифтом. Заметим, что в по-
AllRight Sort Perm ’QRTS’>’XZ<(’

ле памяти можно выделить данные, обработка которых уже завершена и ко-


торые не изменятся до конца исполнения программы (те, которые находят-
ся вне программных скобок; в нашем случае ’aaxzACDE’ и ’XZ<(’). Далее, у
символов, представляющих скобки, есть ‘обычные’ двойники, не обязатель-
но имеющие парные и не влияющие на структурирование выражения. Далее,
если первый атом Perm стоит в позиции функции, то последний из атомов
Perm стоит в позиции данных, так что имена функций могут формироваться
динамически. Пробелы, если они не находятся внутри символьных констант,
игнорируются, за исключением тех, которые отделяют один символ от дру-
гого. И, наконец, еще одна тонкость. Если 123 — это один символ, ’123’ —
три символа, -123.0 — опять один символ, то -123 — два символа: символ ’-’,
после которого стоит число.
Кроме рассмотренного поля памяти, в ходе исполнения может появиться
несколько стеков закопанных выражений, например:
Stack1 ’=’ 15 Stack2 ’=’ <Perm B A>
21 <Perm C2 C1>
-45 <Perm ’X’ 20>
60
Второй стек показывает, что в стеке могут храниться произвольные вы-
ражения, в частности, в нем могут накапливаться отложенные вызовы функ-
ций.1
Конец примера 13.1.2.
В принципе несколько стеков — избыточная конструкция. Но, поскольку здесь стеки рас-
сматриваются как общие области памяти, лучше в каждом модуле иметь свой стек. К сожа-
1

лению, связи между стеками и модулями в явном виде в Рефал-программах нет.


732 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

Пусть есть строка < AB(CD)(E)F>. Она практически во всех Рефал-систе-


мах представляется двунаправленным списком, изображенным на рис. 13.1.
Этот формат представления стал общераспространенным для Рефал-систем,

< >

A B ( ) ( ) F

C D E

Рис. 13.1. Структура данных Рефала

начиная с реализации, описанной в [69]. Однако он не является обязатель-


ным.2 Преобразование выражений в формат для обработки проделывается
один раз, при их вводе, либо даже заранее, при трансляции программы.
Рассмотрим конкретный синтаксис выражений. Идентификатор — любая
последовательность цифр и букв, начинающаяся с большой буквы. Иденти-
фикатор является символьным литералом и представляет составной символ.
Символы, заключенные в одинарные ’ ’ или двойные " " кавычки, являют-
ся символьными литералами, представляют сами себя. Дважды повторенная
кавычка представляет себя. Если вне кавычек встречается символ, не являю-
щийся скобкой, частью идентификатора или числа, это трактуется как ошиб-
ка.
Определение 13.1.3. Метавыражение Рефала может содержать перемен-
ные, чье обозначение включает тип и символ переменной, записываемые в
конкретном синтаксисе как тип.атом. Переменные не могут быть детермина-
тивами. В стандартном Рефале имеются переменные трех типов: символь-
При реализации языка Рефал В. Ф. Турчин задумался, как избежать слишком частых
обращений к каким-то суррогатам возможностей традиционных языков. Поэтому он решил
2

заранее описать алгоритм преобразования выражений и подобрать для него подходящую


структуру данных. Описанный им алгоритм оказался хорошо работающим для целого клас-
са случаев, включая те, которые заранее не предусматривались — характеристический при-
знак хорошего решения. В качестве того, что происходит, если это хорошо не продумать на
ранних, см. параграф, относящийся к языку PROLOG.
Более того, описанные Турчиным алгоритм и структуры данных являются хорошей базой
для точного определения семантики на базе абстрактного синтаксиса языка Рефал.
13.1. КОНКРЕТИЗАЦИЯ 733

ные (s.First), термовые (t.Inner) и общие (e.Last). Метавыражение называется


образцом, если оно не содержит функциональных скобок.
Конец определения 13.1.3.
Значением символьной переменной служит атом, термовой — неделимое объ-
ектное выражение (терм, в терминах Рефала), то есть символ или выражение
в скобках, общей — произвольное (может быть, пустое) объектное выраже-
ние. Идентификаторы переменных вводить не понадобилось, поскольку ка-
ждый идентификатор является атомом.
Рефал развивался как язык символьных преобразований в самом широ-
ком смысле этого слова, и поэтому в него была заложена возможность обра-
батывать собственные программы. Он предоставляет стандартные функции
метакодирования, взаимно-однозначно и взаимно-обратно преобразующие
произвольное метавыражение в объектное и наоборот. Таким образом, по-
является возможность создавать программы в ходе выполнения других про-
грамм и затем выполнять их «на лету». Такая возможность, просто гибель-
ная для программ в большинстве систем и стилей программирования, в дан-
ном случае не приводит ни к каким отрицательным последствиям из-за уни-
кального концептуального единства и концептуальной продуманности язы-
ка.3 Тем самым мы естественно перешли от модели данных к модели вычи-
слений.

13.1.2. Модель вычислений и Рефал-программа

Основными двумя шагами при Рефал-вычислениях являются конкрети-


зация переменных в образце в соответствии с областью зрения и подстанов-
ка полученных значений в другое метавыражение. В языке рассматривается
лишь частный случай конкретизации.
Конкретизацией образца M e в объектное выражение E называется такая
подстановка значений вместо переменных M e, что после применения дан-
ной подстановки M e совпадет с E.
Заметим, что одно и то же метавыражение может иметь много конкрети-
заций в одно и то же объектное выражение. Например, рассмотрим метавы-
ражение
e.Begin s.Middle e.End (13.4)
3
К этой концептуальной конфетке еще бы красивую обертку!
734 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

и объектное выражение

AhAhAh ’OhOhOh’ (Ugu’,’Udgu) ’(((’ Basta’!’ (13.5)

Имеется 11 вариантов конкретизации (13.4) в (13.5) (проверьте!).


Если у метавыражения Me есть много вариантов конкретизации в E, то
они упорядочиваются по предпочтительности в следующем порядке.
Пусть Env1 и Env2 — два варианта конкретизации Me в P. Рассмотрим все
вхождения переменных в Me. Если Env1 и Env2 не совпадают, они приписы-
вают некоторым переменным различные значения. Найдем в P самое первое
слева вхождение переменной которому Env1 и Env2 приписывают разные
значения и сравним длину этих значений. Та из конкретизаций, в которой
значение, приписываемое данному вхождению переменной, короче, предше-
ствует другой и имеет приоритет перед ней.
Например, сопоставим объектное выражение (A1 A2 A3)(B1 B2) с образ-
цом e.1 (e.X s.A e.Y) e.2. В результате получится следующее множество ва-
риантов сопоставления:
{e.1 = , eX = , sA = A1, eY = A2 A3, e.2 = (B1 B2) }
{e.1 = , eX = A1, sA = A2, eY = A3, e.2 = (B1 B2) }
{e.1 = , eX = A1 A2, sA = A3, eY = , e.2 = (B1 B2) }
{e.1 = (A1 A2 A3), eX = , sA = B1, eY = B2, e.2 = }
{e.1 = (A1 A2 A3), eX = B1, sA = B2, eY = , e.2 = }

где варианты сопоставления перечислены в соответствии с их приоритетами,


т. е. самый первый вариант находится на первом месте и т. д. Описанный
способ упорядочения вариантов сопоставления называется сопоставлением
слева направо.
Тот алгоритм конкретизации, который используется в Рефале, называ-
ется проецированием и согласован с введенным нами отношением порядка.
Опишем его (описание взято из учебника Турчина [90]). Обратите внима-
ние, как в данном случае общая и не всегда эффективно реализуемая опе-
рация ‘проецируется’ на свою частную реализацию, одновременно повыша-
ющую эффективность, сохраняющую общность и навязывающую методику
программирования.
Алгоритм для сопоставления объектного выражения E с образцом P
в РЕФАЛ-5.
Вхождения атомов, скобок и переменных будут называться элементами
выражений. Пропуски между элементами будут называться узлами. Сопо-
ставление E : P определяется как процесс отображения, или проектирова-
13.1. КОНКРЕТИЗАЦИЯ 735

ния, элементов и узлов образца P на элементы и узлы объектного выраже-


ния E. Графическое представление успешного сопоставления приведено на
рис. 13.2. Здесь узлы представлены знаками o.

Рис. 13.2. Сопоставление E : P является отображением P на E. Здесь объ-


ектным выражением E является ’A’((2’B’))’B’, а образцом P является ’A’(e.1
t.2)s.3.

Следующие требования являются инвариантом алгоритма сопоставления


и их выполнение обеспечивается на каждой его стадии:

Общие требования к отображению P на E


(сопоставлению E : P )

1. Если узел N 2 расположен в P правее узла N 1, то проекция N 2 в E


может либо совпадать с проекцией N 1, либо располагаться справа от
нее (линии проектирования не могут пересекаться).

2. Скобки и атомы должны совпадать со своими проекциями.

3. Проекции переменных должны удовлетворять синтаксическим требо-


ваниям их значений; т. е., быть в соответствии с типом переменной ато-
мами, термами или произвольными выражениями. Различные вхожде-
ния одной переменной должны иметь одинаковые проекции.

Предполагается, что в начале сопоставления граничные узлы P отобра-


жаются в граничные узлы E. Процесс отображения описывается при помощи
736 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

следующих шести правил. На каждом шаге отображения правила 1–4 опре-


деляют следующий элемент, подлежащий отображению; таким образом, ка-
ждый элемент из P получает при отображении уникальный номер.

Правила отображения

1. После того, как отображена скобка, следующей подлежит отображе-


нию парная ей скобка.

2. Если в результате предыдущих шагов оба конца вхождения некоторой


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

3. Вхождение переменной, которая уже получила значение, является по-


вторным. Скобки, атомы, символьные и термовые переменные и по-
вторные вхождения переменных для выражений в P являются жест-
кими элементами. Если один из концов жесткого элемента отображен,
проекция второго конца определена однозначно. Если Правила 1 и 2
неприменимы, и имеется несколько жестких элементов с одним спро-
ектированным концом, то из них выбирается самый левый. Если воз-
можно отобразить этот элемент, не вступая в противоречие с общими
требованиями 1–3, приведенными выше, тогда он отображается, и про-
цесс продолжается дальше. В противном случае объявляется тупиковая
ситуация.

4. Если Правила 1–3 неприменимы и имеются несколько переменных для


выражений с отображенным левым концом, то выбирается самая левая
из них. Она называется открытой e-переменной. Первоначально она
получает пустое значение, т. е. ее правый конец проектируется на тот
же узел, что и левый. Другие значения могут присваиваться открытым
переменным через удлинение (см. Правило 6).

5. Если все элементы P отображены, это значит, что процесс сопоставле-


ния успешно завершен.
13.1. КОНКРЕТИЗАЦИЯ 737

6. В тупиковой ситуации процесс возвращается назад к последней откры-


той e-переменной (т. е. к той, что имеет максимальный номер проек-
ции), и ее значение удлиняется; т. е., проекция ее правого конца в E
продвигается на один терм вправо. После этого процесс возобновляет-
ся. Если переменную нельзя удлинить (из-за Общих требований 1–3),
удлиняется предшествующая открытая переменная, и т. д. Если не име-
ется подлежащих удлинению открытых переменных, процесс сопоста-
вления не удался.

На рис. 13.2 сопоставление производится следующим образом. Вначале име-


ется два жестких элемента с одним отображенным концом: ’A’ и s.3. В соот-
ветствии с Правилом 3, отображается ’A’ и этот элемент получает при ото-
бражении номер 1. Номера 2 и 3 будут назначены левой и правой скобкам,
согласно Правилам 3 и 1. Внутри скобок начинается перемещение справа
налево, так как t.2 является жестким элементом, который может быть ото-
бражен, в то время как значение e.1 еще не может быть определено. На сле-
дующем шаге обнаруживается, что e.1 является закрытой переменной, чью
проекцию не требуется обозревать для того, чтобы присвоить ей значение;
что бы ни было между двумя узлами, это годится для присвоения (на самом
деле, значение e.1 оказывается пустым). Отображение s.3 завершает сопо-
ставление. Расположение отображающих номеров над элементами образца
дает наглядное представление описанного алгоритма:

1 2 5 4 3 6
’A’ ( e.1 t.2 ) s.3

Конец алгоритма
Этот сложный алгоритм упрятан для программиста в простые программ-
ные конструкции. Программа на Рефале состоит из последовательности опре-
делений функций. Относительное расположение членов этой последователь-
ности никакой роли не играет, и функции можно группировать из логических
или технологических соображений. Каждое описание функции в базисном
Рефале имеет вид
Имя функции {Последовательность сопоставлений}
Каждое сопоставление имеет вид
Образец = Метавыражение;
Пример 13.1.4. Рассмотрим пример Рефал-программы.
738 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

Программа 13.1.1 Программа вычисления предиката предшествования од-


ного символа другому в заданном алфавите

*1. Отношение рефлексивно


Pre_alph {

*2. Если буквы различны, проверить, входит ли


s.1 s.1 = T;

* первая из них в алфавит до второй


s.1 s.2 = <Before s.1 s.2 In <Alphabet>>; }

Before {
s.1 s.2 In e.A s.1 e.B s.2 e.C = T;
e.Z = F; }

Alphabet {
= ’abcdefghijklmnopqrstuvwxyz’; }

Строки, начинающиеся с *, служат комментариями. Последняя из функций


Alphabet введена для технологичности, чтобы определение алфавита было в
одном месте и его легко было изменять. Так что функция с пустым образцом
может пониматься как константное выражение. In является атомом-раздели-
телем, заведомо не встречающимся в алфавите.
Последнее из правил сопоставления в Before применимо всегда. Это га-
рантирует, что предикат никогда не заканчивается неудачей.
Конец примера 13.1.4.
Если взаимное расположение функций никакой роли не играет, то внутри
функции расположение сопоставлений важно. Сначала применяется первое
из сопоставлений, при неудаче переходят ко второму и так далее до послед-
него.
Заметим, что в языке Рефал отсутствие сопоставления, конкретизацией
которого является текущая область зрения, означает аварийный останов про-
граммы с выдачей текущего поля памяти и закопанных данных. Пожалуй, это
достаточно технологичное решение, поскольку для тех, кто сам желает обра-
батывать ошибки, всегда имеется возможность поместить последней строкой
в определение функции правило вида
e.A=Обработка ошибки;
Имеются также встроенные функции, в частности, функции работы с чи-
слами <’+’ s.Number1 s.Number2> и подобные ей.
13.1. КОНКРЕТИЗАЦИЯ 739

Рассмотрим связь между языком и программным окружением.


Для запуска Рефал-программы необходимо инициализировать поле па-
мяти. Принято, что в начале выполнения программы поле памяти имеет вид

<Go >

Таким образом, вначале применяется функция Go к пустому полю зрения.


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

$ENTRY Go { =<Prog <Open ’r’ 1 ’data.txt’><Get 1> >}

13.1.3. Дополнительные возможности

В нашей программе 13.1.4 алфавит определен статически. Но из смысла


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

<Br e.N ’=’ e.Е >

рассматривает свой первый аргумент, который должен быть строкой симво-


лов, не включающей ’=’, как имя стека, и помещает свой второй аргумент на
вершину этого стека. Если стек был пуст, то он создается. Соответственно,
функция
<Dg e.N>
выкапывает верхушку стека. Если стек пуст, то ошибки нет, просто выдает-
ся пустое выражение. Несколько других функций дополняют возможности
работы с глобальной информацией. Cp копирует верхушку стека без ее уда-
ления, Rp замещает верхушку стека на свой аргумент, DgAll выкапывает сразу
весь стек4 .
4
(И.Н. Скопин) Милая штучка! Сразу видно, что она для тех случаев, когда запутываешься!
740 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

Ввод-вывод организован в Рефале достаточно аскетически. Имеется функ-


ция открытия канала ввода, которая открывает файл либо для ввода, либо для
вывода (в этом случае первым аргументом служит ’r’) и присваивает ему но-
мер. Одна строка символов из файла читается с помощью функции Get, за-
меняющей свой вызов на прочитанную строку, одна строка пишется в файл
путем функций
<Put s.Channel e.Expression>
либо
<Putout s.Channel e.Expression>
Вторая функция стирает свое поле зрения, а первая оставляет в качестве ре-
зультата напечатанное выражение.
Следует заметить, что эти функции читают и пишут именно последова-
тельности символов. При их использовании программист должен сам пре-
образовать последовательности цифр в числа, а скобки-символы в структур-
ные скобки. Более того, при выводе часть информации теряется: невозможно
различить последовательность букв и идентификатор, последовательность
цифр и число, структурные скобки и скобки-символы. Поэтому имеется еще
одна совокупность функций, автоматизирующих преобразование. Каждая из
этих функций обрабатывает сбалансированное по скобкам выражение. При
вводе это выражение заканчивается пустой строкой.

<Input s.Channel> или <Input e.File-name>

<Xxout s.Channel e.Expr> или <Xxout (e.File-name)e.Expr>


<Xxin e.File-name>

Первая из функций предназначена для ввода подготовленных вручную фай-


лов, вторая и третья — для обмена с диском промежуточной информацией.
Только что перечисленные функции вместе с функцией Go требуют объ-
яснения инструментов модульности в Рефале. Рефал-модуль — просто Ре-
фал-программа, не обязательно включающая Go. Функции, предоставляе-
мые в пользование другим модулям, описываются как входы, описанные спе-
цификатором $ENTRY. В свою очередь. использующий модуль должен опи-
сать внешние функции:
$EXTRN F1,F2,F3;
Вызов программы, состоящей из нескольких модулей, производится опера-
тором примерно следующего вида:
refgo prog1+functions+reflib
13.1. КОНКРЕТИЗАЦИЯ 741

Модуль основной программы должен идти первым. Никаких средств включить


требование вызова модуля в текст другого модуля нет, модули сопрягаются
внешним образом. При конфликтах имен берется определение функции из
первого в порядке подключения модуля.
В частности, только что описанные расширенные функции ввода-вывода
определяются в стандартном модуле reflib.
Важнейшими средствами современного Рефала является работа с мета-
выражениями. Базовое ее средство — встроенная функция Mu, которая за-
ключает свой аргумент в функциональные скобки и тем самым дает возмож-
ность вычислить динамически построенное выражение. По словам Турчина,
Mu работает так, как работало бы определение

Mu { s.F e.X = <s.F e.X> },

если бы оно было синтаксически допустимо.


В частности, через Mu работает стандартный модуль Рефала e (Evaluation),
дающий возможность вычислить динамически введенное выражение. Он об-
рабатывает это выражение через функцию Upd, которая должна быть доба-
влена к модулю, желающему осуществлять динамическое вычисление выра-
жений. Например, если добавить описание

$ENTRY Upd {e.X = <Mu e.X>;}

то командная строка refgo e+prog1 приведет к требованию написать выраже-


ние. Это выражение будет сделано полем памяти программы prog1 и вычи-
слено, а результат выведен. Например, написав для программы 13.1.4

мы получим в качестве результата


<Alphabet>

Естественно возникает вопрос об обработке внутри языка произвольных,


’abcdefghijklmnopqrstuvwxyz’

а не только объектных, выражений. Для этого имеются стандартные функ-


ции Up и Dn. Первая из них превращает объектное выражение в выражение
произвольного вида, вторая кодирует свою область зрения (ей, по семантике
языка, может быть лишь объектное выражение) в форме, годящейся для об-
щих выражений и даже метавыражений. В стандартном комплекте Рефала
есть даже модуль прогонки, который позволяет подать на вход программе ме-
тавыражение и вычислить его настолько, насколько это возможно без знания
значений переменных.
742 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

При решении сложных задач на Рефале естественно возникла задача пред-


ставления мультииерархической структуры. Для частного случая двух неза-
висимых иерархий, одна из которых считается главной, а вторая начнет рабо-
тать после исчерпания первой, в Рефале разработано мультискобочное пред-
ставление выражений. Оно поддерживается библиотечными функциями ко-
дирования и декодирования выражений и программ, переводящих мультис-
кобочное выражение в его стандартный код и наоборот. Для частного случая,
когда вторичная иерархия — наши обычные скобки, а первичная иерархия —
ссылка глубоко внутрь скобочного выражения, неявно задающая одноуров-
невую скобочную структуру, независимую от стандартной, алгоритм кодиро-
вания исключительно прост. Выражение разбито на две несбалансированных
по скобкам части: левую и правую. В обеих частях непарные скобки заменя-
ем на пары )(. Открывающую скобку высшего уровня представляем как ((,
место, куда ведет ссылка — как ))(, закрывающую скобку высшего уровня
как )).
И в заключение рассмотрим достаточно сложный алгоритм на Рефале,
иллюстрирующий многие приемы программирования. Пусть у нас дано вы-
ражение с различными парными скобками (в конкретном случае мы исполь-
зуем пары ’()[]{}<>’, но программа составлена так, чтобы эти пары можно бы-
ло заменить в любой момент). Для эффективной работы на Рефале это вы-
ражение нужно закодировать, используя структурные скобки. Кодом пары
скобок Левая Правая будут скобки (Левая и Правая). Ниже дан алгоритм
кодировки.
При записи данного алгоритма используется еще одна возможность Ре-
фала-5. После образца через запятую может идти произвольное выражение,
включающее свободные переменные образца, а затем другой образец. Во вто-
ром образце мы отождествляем вспомогательное выражение, не изменяя зна-
чений переменных, унаследованных из предыдущего образца. Если после
второго образца идут фигурные скобки, то мы задаем безымянную функцию
внутри функции. Если же их нет, то это отождествление рассматривается как
дополнительное условие успешности первого отождествления. Эта иерархия
вложенности может продолжаться на несколько уровней, причем перемен-
ные внешних уровней на внутренних уровнях остаются связанными, уже не
изменяя значений.
Иерархически вложенные функции и условия в принципе не нужны для
Рефала, но их использование позволяет сократить и, главное, лучше струк-
турировать текст программы.
Программа 13.1.2 Мультискобочное выражение: Рефал
13.1. КОНКРЕТИЗАЦИЯ 743

$ENTRY Go{=<Init>;}

* Инициализация поля зрения и констант


$EXTRN Xxout;

Init{=<Open ’r’ 1 ’Input.txt’><Trans <Acquire(<Get 1>)»;}


Acquire {

* Конец ввода - пустая строка


e.Got ()= e.Got;

e.Got (e.New)=<Acquire e.Got e.New (<Get 1>)>;


}
Brackets {=(’()’)(’[]’)(’’)(’<>’);}
Trans {
e.A =<Result <Pairing () e.A > >;
}

* В первой скобке содержится последовательность всех незакрытых


Pairing {

* скобок вместе с сегментами данных, подлежащими помещению


* в данную пару скобок;
* каждый сегмент данных также заключен в скобки
(e.Unclosed (e.LastUn)(s.Lbrack e.Middle)) s.Rbrack e.Last,

Встретилась правая скобка, парная последней незакрытой;


<Brackets>: e.A(s.Lbrack s.Rbrack ) e.B =

выбрасываем отработанный сегмент из поля зрения


*
*
<Pairing (e.Unclosed (e.LastUn (s.Lbrack e.Middle s.Rbrack))) e.Last>;
((s.Lbrack e.Middle)) s.Rbrack e.Last,

Парная незакрытой, находящейся внутри другой незакрытой


<Brackets>: e.A(s.Lbrack s.Rbrack ) e.B =
*
(s.Lbrack e.Middle s.Rbrack) <Pairing () e.Last>;
(e.Unclosed (e.LastUn)(s.Lbrack e.Middle)) s.Rbrack e.Last,

Непарные скобки
<Brackets>: e.A(s.Lbrack1 s.Rbrack ) e.B =
*
<Prout "Brackets Mismatch!"> Error;
(e.Unclosed ) s.Lbrack1 e.Last,

Еще одна открывающая скобка; создаем новую группу данных


<Brackets>: e.A(s.Lbrack1 s.Rbrack ) e.B =
*
<Pairing (e.Unclosed (s.Lbrack1)) e.Last>;
() s.Lbrack e.Last,

Первая открывающая скобка


<Brackets>: e.A(s.Lbrack s.Rbrack ) e.B =
*
744 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

<Pairing ((s.Lbrack)) e.Last>;


() s.Rbrack e.Last,

Первая скобка - закрывающая


<Brackets>: e.A(s.Lbrack s.Rbrack ) e.B =
*

Нейтральный символ вне скобок


<Prout "Extra right bracket"> Error;
*

Выражение и скобки исчерпаны - успех


() s.Other e.Last = s.Other <Pairing () e.Last>;
*

Выражение исчерпано, а скобки - нет


() =;
*

Нейтральный символ в очередной скобке


(e.Unclosed (e.Lastun))=<Prout "Not all brackets are closed» Error;
*
(e.Unclosed (e.Lastun))s.Other e.Last=
<Pairing (e.Unclosed(e.Lastun s.Other)) e.Last>;
}

* Если была ошибка, выйти


Result {

* Иначе вывести результат для дальнейшего использования


e.A Error=;

e.A =<Xxout (’output.rdt’) e.A>;


}

Чтобы нагляднее увидеть влияние стиля на программные решения, сравните


эту программку с развитием программы в традиционном стиле, приведен-
ным в § 11.5. Данная программа намного выразительнее, короче. легче мо-
дифицируема и не менее эффективна, чем программа 11.5.3.

13.1.4. Развитие языка и его диалекты

Созданный в теоретической работе Турчина язык сразу же поменял свою


конкретно-синтаксическую форму для удобства представления и работы. Язык
Рефал-2 длительное время был практическим стандартом Рефал-систем. В
нем появились ввод-вывод и стеки закопанных данных.
В языке Рефал-4 было сделано две попытки. Одной из них была попыт-
ка достаточно механически совместить Рефал с нарождавшимися объектно-
ориентированными средствами, которая быстро завела в тупик и была оста-
влена. Другой было определение метаопераций, которое доказало свою жиз-
неспособность. В языке Рефал-5 [90], который сейчас является фактическим
13.1. КОНКРЕТИЗАЦИЯ 745

стандартом, объекты были отброшены, зато последовательно была проведе-


на как стандартная надстройка над языком идея метакодирования. В нем по-
лучили свое окончательное оформление вложенные процедуры и дополни-
тельные условия.
Из других существующих версий языка стоит отметить Рефал-6 и Ре-
фал+ [28], которые развивают одну и ту же линию. В реализации Рефал+
отошли от представления, принятого в [69], с тем, чтобы воспользоваться
современными алгоритмами сборки мусора. Вместо стеков закопанных зна-
чений в этих языках предлагаются объекты, которые имеют лишь одно зна-
чение. В частности, такие объекты используются для описания графического
ввода и вывода, что полностью игнорируется в стандартном Рефале. Далее,
имеется возможность объявить функцию откатной, и пытаться при невоз-
можности отождествлений обработать неудачу. Но автор этих языков про-
игнорировал концептуальную несовместимость неудач с общей структурой
управления в языке Рефал. Из находок авторов Рефал+, помимо новой струк-
туры данных, стоит отметить использованную нами концепцию упорядоче-
ния возможных отождествлений и возможность до некоторой степени упра-
влять этим упорядочением (правда, в языке предусмотрен лишь переход от
прямого порядка к его обращению, но уже это дает в некоторых случаях боль-
шой выигрыш в выразительности).
Наиболее заметным недостатком новых версий языка Рефал5 явилось от-
сутствие различения абстрактного и конкретного синтаксиса. Вместо того,
чтобы сделать четверть шага по напрапвлению к лучшему оформлению про-
граммы, можно было бы просто отказаться от фиксации оформления в опре-
делении языка и дать возможность определять синтаксические расширения
и представления самим разработчикам.
Стоит заметить, что нынешний язык Рефал находится в мягком концеп-
туальном противоречии со столь блестяще реализованной в нем же идеей ди-
намического вычисления программ. Решение исполнять ту функциональную
скобку, внутри которой нет других таких скобок, было оправдано и логично
при создании Рефала, а теперь оно уже мешает эффективно использовать
аппарат метапреобразований. Аналогично, необходимо заметить, что Рефал
созрел для настоящего представления мультииерархической структуры, что
позволит языку выйти на новые классы приложений. Таким образом, в бли-
жайшее время можно ожидать появления нового сентенциального языка, ре-
Разделяемым подавляющим большинством систем программирования, но в данном слу-
чае ощущаемым наиболее остро.
5
746 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

ализующего идею конкретизации. Громадной бедой будет, если под красивой


оберткой кто-нибудь подсунет в эту область концептуально непродуманное
‘прагматическое’ решение. Выигрыши от прагматизма будут минимальны-
ми, временными и локальными, а потери — длительными, на порядок пре-
восходящими аналогичные потери для традиционных языков, и глобальны-
ми.
Тем более можно прогнозировать такое развитие данной ветви сентенци-
ального программирования, поскольку Рефал великолепно подходит для &-
параллелизма (см. § A.7). Подстановки значений переменных в результиру-
ющее выражение можно осуществлять независимо руг от друга. Более того,
в принципе можно было бы даже в этом варианте Рефала вычислять различ-
ные функциональные скобки в параллель, помешать этому могут лишь зна-
чения, передаваемые через аппарат закапывания-выкапывания. Но это стан-
дартная проблема синхронизации для параллельных вычислений, с которой
можно справляться достаточно разработанными средствами, тем более что
концептуально аппарат для соответствующей разметки программы еще в хо-
де ее составления в языке подготовлен.
Из литературы по языку Рефал можно рекомендовать учебные пособия
[90, 5, 8], первое из которых имеется в общедоступном русском переводе, в
частности, на сайте .
Стоит заметить, что сентенциальное программирование, и особенно его
разновидность, базирующаяся на модели отождествления-замены, велико-
лепно сочетается с идеей ассоциативной памяти (см. стр. 21), и здесь воз-
можен прорыв одновременно на аппаратном и программном уровнях.

§ 13.2. УНИФИКАЦИЯ

13.2.1. Общие концепции

Язык логического программирования PROLOG представляет собой дру-


гую модель сентенциального программирования, несовместимую6 с моде-
лью Рефала.
Британско-канадская группа, создававшая PROLOG, рассмотрев ортого-
нальный Рефалу подход7 , замаскировала его сущность примитивным ме-
Это — глубочайший теоретический результат и одно из редких прямых предупреждений,
сделанных теорией практике. Но даже прямые предупреждения, как правило, игнорируются
6

практиками, многие из которых не понимают самого понятия алгоритмической неразреши-


мости.
7
Конечно же, они не имели никаких сведений о Рефале и работали совершенно неза-
13.2. УНИФИКАЦИЯ 747

тодологически-теоретическим анализом и неадекватным названием. В этом


проявилась коренная разница русской и англо-американской8 школ науки:
В. Ф. Турчин проделал адекватный анализ, не устаревший за 30 лет, но он
не стремился к общепонятности и слишком большой популяризации (может
быть, потому, что ему не нужно было выбивать гранты). Создатели PROLOG
с самого начала в значительной мере использовали теорию и методологию
как заклинания либо молитвы, не имеющие отношения к сути дела и произ-
носимые для его освящения. Тем самым теоретическая база сразу же оказа-
лась неадекватной, что и привело к быстрому расползанию системы и потере
концептуального единства. Хуже другое. Это помешало осознанию реальных
достижений подхода, основанного на унификации, поскольку они часто про-
тиворечили мифам и саморекламе.
PROLOG вдохновлялся ограничением классического исчисления преди-
катов, предложенным Хорном. Как известно (см., например, книгу [63] и
Приложения), в классической логике в любой достаточно богатой теории
появляются чистые теоремы существования, когда из доказательства невоз-
можно выделить построение требуемого объекта. Если ограничиться конъ-
юнкциями импликаций вида

∀x1 , . . . , xn ∃y1 , . . . , yk . . . (A1 & · · · &An ⇒ B) (13.6)

где каждая составляющая является элементарной формулой, и само доказан-


ное утверждение имеет такой же вид, то получить построение из доказатель-
ства возможно, поскольку у нас не остается даже косвенных способов сфор-
мулировать и нетривиально использовать что-либо похожее на A∨¬A. Такие
формулы называются хорновыми.
Далее, от лишних кванторов можно избавиться при помощи сколемиза-
ции, когда кванторы существования заменяются на функцию, аргументами
которой являются все переменные, связанные ранее стоящими кванторами
висимо, просто потребность сентенциального программирования назрела, и счастьем для
информатики явилось неосведомленность двух групп инициаторов о работах друг друга,
поскольку иначе одна из двух ортогональных концепций осталась бы скорее всего неразви-
той, как подтверждает история других стилей: тот, кто первым добился успеха, захватывает
экологическую нишу и видоизменяет условия в ней таким образом, чтобы ничто другое не
могло в ней выжить (причем при этом часто нет никакой злой воли людей, просто работают
законы развития науки и вообще эволюционирующих систем).
Анекдот из жизни. Программа проверки правописания предложила заменить данное сло-
во на ‘нагло-американской’.
8
748 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

всеобщности. Таким образом, мы приходим к простейшему виду хорновых


формул (хорновы импликации):

∀x1 , . . . , xn (A1 & · · · &An ⇒ B) (13.7)

Применив к последней формуле преобразование, выполненное лишь в клас-


сической логике, мы приходим к хорновым дизъюнктам:

∀x1 , . . . , xn (¬A1 ∨ · · · ∨ ¬An ∨ B) (13.8)

Именно от последней формы представления хорновых утверждений получи-


ла имя основная структура данных языка PROLOG. Но тот факт, что импли-
кация преобразована в дизъюнкцию, на самом деле нигде в этом языке не
используется, он послужил лишь для установления взаимосвязей с алгорит-
мом, который в тот момент был последним криком моды в автоматическом
доказательстве теорем (и действительно громадным концептуальным про-
движением), который и до сих пор остается одним из нескольких наиболее
часто применяемых и единственным, известным широкой публике: с мето-
дом резолюций [79]. Этот метод поставил три идеи (унификация, стандарти-
зация цели, стандартизация порядка вывода), красивой программистской ре-
ализацией которых явился исходный PROLOG. Но находки языка PROLOG
не исчерпываются реализацией идей, взятых из теории. Они внесли суще-
ственный вклад в теорию и методологию программирования9 .
Одной из сильнейших сторон метода резолюций явился алгоритм унифи-
кации выражений. Два выражения называются унифицируемыми, если они
могут быть приведены к одному и тому же виду подстановкой значений вме-
сто свободных переменных. В методе резолюций было показано, что для вы-
ражений первого порядка имеется эффективный алгоритм унификации, на-
ходящий для двух выражений унифицирующую подстановку либо обосно-
вывающий, что такой подстановки нет.
Пример 13.2.1. Две последовательности выражений

f (x, ϕ(x, χ(y), a)) g(z, x) h(ϑ(u))


(13.9)
f (ω(b), ϕ(y, z, w)) u h(q)
Одновременно данное направление нанесло и существенный вред этой теории и методо-
логии, который был бы еще глубже, если бы у PROLOG с самого начала не было бы друга-
9

соперника Рефала.
13.2. УНИФИКАЦИЯ 749

где a, b — константы, а латинские буквы из конца алфавита — переменные,


унифицируются в

f (ω(b), ϕ(ω(b), χ(ω(b)), a)) g(χ(ω(b)), ω(b)) h(ϑ(g(χ(ω(b)), ω(b))))

подстановкой
(13.10)

x ← ω(b), y ← ω(b), u ← g(χ(ω(b)), ω(b)),


(13.11)
w ← a, z ← χ(ω(b)), q ← ϑ(g(χ(ω(b)), ω(b)))

А в следующей формуле

g(x) g(z) f (x, a)


(13.12)
h(y) g(ϕ(z)) f (b, x)

никакие два соответственных выражения унифицированы быть не могут.


Конец примера 13.2.1.
Уже в приведенном примере видно, что унификация — столь же глобаль-
ная операция, как и конкретизация в Рефале. Но направления унификации
и конкретизации ортогональны.

• Если конкретизация имеет дело с выражениями, подвыражения в кото-


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

• Если конкретизация не заглядывает внутрь иерархии глубже, чем это


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

• Если при конкретизации переменные локальны, и их значения не влия-


ют на интерпретацию остальной области памяти, то при конкретизации
переменные глобальны, полученная в результате конкретизации под-
становка производится сразу во всем поле зрения, а не только в его ак-
тивной части.
750 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

• Если при конкретизации в ходе установления значений переменных


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

Заметим, что логический алгоритм унификации обладает свойством ча-


стичного исполнения: если унифицировать две подструктуры, то затем мож-
но продолжить унификацию остальных подструктур после исполнения уни-
фицирующей подстановки, и результат унификации не изменится. Так что
последовательность выражений может унифицироваться одно за другим10 .
Второй находкой, перенесенной авторами языка PROLOG из специализи-
рованных программ в языки программирования, явилась система обработки
неудач. Успешно произведенная унификация (точно так же, как успешно про-
изведенное отождествление в языке Рефал) является разрешением выпол-
нить некоторое действие. После этого действия поле зрения изменяется, и
ищется следующая унификация. Если на некотором шаге унификация невоз-
можна, произошла неудача, и мы возвращаемся к тому последнему моменту,
когда было возможно унифицировать поле зрения несколькими способами.
Мы теперь запрещаем предыдущую успешную модификацию и пытаемся
испробовать другие возможности. Такие возвраты могут происходить один
за другим, при этом, как правило, значения переменных, измененные вви-
ду подстановок, восстанавливаются. Этот метод управления принципиально
отличается от управления в языке Рефал. В PROLOG неудача глобальна, но
исправима, а в Рефале локальна, но фатальна.
И, более того, можно было бы унифицировать произвольно выбранные внутренние соот-
ветственные подвыражения в произвольном порядке, лишь бы объемлющие унифицирова-
10

лись после подчиненных. Авторам неизвестно ни одно использование этого естественного


и многообещающего обобщения алгоритма унификации.
В конкретной первой реализации языка PROLOG, основные особенности которой ста-
ли в дальнейшем фактическим стандартом, создатели допустили ошибку в понимании и,
соответственно, в реализации алгоритма унификации, которая, в частности, разрушает это
свойство, и порою может привести к излишней конкретизации, когда можно было бы найти
более общую конкретизирующую подстановку. Эта ошибка несущественна, она не влияет
на подавляющее большинство программ, но иногда она приводит к появлению бесконеч-
ных термов, и некоторые реализаторы языка PROLOG с гордостью пишут, что они умеют
печатать и показывать на экране даже такие термы.
Желающие в качестве упражнения выловить ошибку самостоятельно, сравните алгорит-
мы унификации, описанные в книге [79] и в [41].
13.2. УНИФИКАЦИЯ 751

И, наконец, последняя находка метода резолюций, перенесенная в про-


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

13.2.2. Поле зрения, поле памяти и PROLOG-программа

Рассмотрим структуру данных, обрабатываемых языком PROLOG. Все


данные языка PROLOG являются термами. Термы построены из атомов (ко-
торыми могут быть переменные и константы, в свою очередь делящиеся на
имена и числа) при помощи функциональных символов, которые также явля-
ются атомами (а именно, именами), и называются функторами. Среди функ-
торов выделяются детерминативы. Детерминативы в реализации делятся на
предикаты и встроенные функции (функции). Функции обычно используют-
ся внутри выражений, а предикаты являются основными единицами упра-
вления и обычно используются вне скобок, как основной функциональный
символ выражения. Детерминативы должны быть описаны в программе, а
остальные функторы рассматриваются просто как структурные единицы и
могут оставаться неописанными.
В конкретном синтаксисе переменные языка — имена, состоящие из букв
и начинающиеся с большой буквы либо с символа подчеркивания _. Пере-
менная _ называется анонимной переменной и считается различной во всех
своих вхождениях.
Константы языка PROLOG11 в конкретном синтаксисе — имена (иденти-
фикаторы, начинающиеся с маленькой буквы либо совокупность нескольких
11
Внутри языка они называются атомами
752 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

специальных символов типа <, =. $), символы и числа (символы отождествля-


ются с целыми числами, являющимися их кодами). Произвольная последо-
вательность символов может быть сделана единой константой, например:
’C:\”SICS Prolog”\program.pl’

В конкретном синтаксисе имена функторов являются константами, не явля-


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

Для некоторых предопределенных в системе функций и предикатов имеются


дополнительные ограничения на аргументы12 . В частности, можно указать,
что аргументом может служить имя файла, атом, переменная и т. п.
Поле зрения, содержащее непосредственно обрабатываемые программой
данные, называется также целью, и состоит из последовательности термов,
разделенных либо запятыми (в этом случае они понимаются как “последо-
вательно достигаемые цели”), либо символами |, в этом случае цели “альтер-
нативны”. Наиболее важным и классическим случаем является случай по-
следовательно достигаемых целей, через который, в частности, определяется
семантика и альтернативных целей13 .
Далее, поле данных имеет скрытую (при использовании стандартных воз-
можностей языка) часть, в которой прослеживается история выполнения про-
граммы с тем, чтобы в случае необходимости произвести обработку неудачи.
И, наконец, в поле данных помещается сама PROLOG-программа, кото-
рая естественно структурируется на две части, часто перемешанные в тексте
самой программы, но обычно разделяемые при использовании внешней па-
мяти: база данных и база знаний. Более общий вид имеет база знаний.
База знаний состоит из предложений (клауз). Каждое предложение имеет
вид, подобный
В части реализаций эти ограничения могут распространяться и на функции, определен-
ные программистом.
12

Мы сознательно отказались от сопоставления двух видов целей в PROLOG с конъюнкци-


ей и дизъюнкцией, поскольку их семантика принципиально отличается от семантики этих
13

логических связок.
13.2. УНИФИКАЦИЯ 753

grandfather(X,Z) :- parent(X,Y), father(Y,Z).

Таким образом, предложение состоит из головного выражения и его раскры-


тия, состоящего из нескольких выражений, соединенных как последователь-
но достигаемые цели.
Как правило, предложения, относящиеся к одному и тому же предикату,
группируются вместе:

parent(X,Y) :- mother(X,Y).
parent(X,Y) :- father(X,Y).

Порядок предложений существенен.


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

father(ivan,vasilij):-true.

Здесь мы встретились с одной из двух стандартных целей. true обозначает


очевидную удачу, а fail — очевидную неудачу.
Во-вторых, специально для фактов имеется скоропись:

В принципе все остальные структуры языка PROLOG выражаются через


mother(maria,sofia).

элементарные. Но некоторые из них прагматически настолько важны, что по-


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

понимается как линейный список и обозначается [a,b,. . . ,z].


.(a,.(b,. . . , .(z,[]). . . ))

Для обозначения присоединения нескольких данных термов к началу списка


имеется стандартная операция

Строки рассматриваются как линейные списки кодов символов и обозна-


[t,u|L].

чаются последовательностью символов, взятой в двойные кавычки:


"Ну, получили то, что искали? Ответьте y или n."
754 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

Заслуживает упоминания механизм введения новых операций в язык PROLOG.


Каждый пользователь может определить свои собственные унарные или би-
нарные операции или переопределить стандартные. Приведем в качестве при-
мера описания некоторых стандартных операторов языка PROLOG.

:- op(1200,xfx,’:-’).
:- op(1200,fx, [’:-’,’?-’]).
:- op(1000,xfy,’,’).
:- op(700, xfx,[=,is,<,=<,==]).
:- op(500, yfx,[+,-]).
:- op(500, fx, [+,-,not]).
:- op(400, yfx,[*,/,div]).

Первый аргумент в этих описаниях — приоритет операции. Он может


быть от 1 до 1500. Второй аргумент — шаблон операции. x обозначает выра-
жение с приоритетом, строго меньшим приоритета операции, y — выражение
с приоритетом, который меньше или равен приоритету операции, f — поло-
жение самого символа операции относительно аргументов. Таком образом,
шаблон yfx для операции - означает, что X-Y-Z понимается как (X-Y)-Z, ша-
блон xfy для запятой означает, что t,u,r понимается как t,(u,r), шаблон xfx для
:- означает невозможность использования нескольких таких операций подряд
без дополнительных скобок. Операции с меньшими приоритетами связыва-
ют сильнее. Один и тот же атом может быть определен и как унарная, и как
бинарная операция.

Внимание!

То, что некоторый функтор определен, как операция, не зна-


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

В частности, бинарная операция is при унификации должна иметь вторым


аргументом выражение, составленное из атомов при помощи функций, и вы-
числяет это выражение. Даже выражение вида 1+2 остается в таком же виде,
пока оно не попадет во второй аргумент is.
13.2. УНИФИКАЦИЯ 755

Пример описаний операций показывает, что даже локальное использо-


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

13.2.3. Управление исполнением программы

Рассмотрим, как исполняется программа на языке PROLOG. В программе


может быть один целевой дизъюнкт, не имеющий головной части. Он начи-
нается с функтора :- или ?-. В программе, транслируемой и исполняемой в
пакетном режиме, обычно используется первый функтор, а при задании цели
с терминала в режиме диалога — второй. Разница между ними проявляется
лишь в режиме диалога. Второй вариант цели позволяет пользователю после
нахождения одного из решений продолжить выполнение программы для по-
иска следующего решения. Первый такой возможности ему не представляет,
программа находит первое решение и останавливается.
Цель состоит из последовательности термов, разделенных запятыми. Эта
последовательность термов является полем зрения программы. Исходная цель
называется запросом. Переменные, входящие в запрос, носят особый статус.
Их значения в ходе последовательных унификаций накапливаются в скрытой
части поля памяти программы и при успешном исполнении выдаются в каче-
стве ответа на запрос. В каждый момент рассматривается первый из термов
данной последовательности. Если его детерминатив не является встроенной
функцией или встроенным оператором с особым определением, то ищется
предложение, голова которого унифицируется с этим термом. При этом пре-
жде всего проверяется наличие нескольких предложений, детерминатив ко-
торых совпадает с детерминативом первого терма. Если таких предложений
несколько, то создается точка возврата, в которой запоминается состояние
программы для отработки возможных неудач. Предложения испытываются,
начиная с первого.
Полученная унифицирующая подстановка применяется ко всем термам
в поле зрения и к хвосту успешно унифицированного предложения. После
этого хвост заменяет унифицированную голову, и выполнение возобновляет-
ся. Исполнение считается успешным, если на некотором шаге цель исчезает.
Исполнение считается неудачным, если в некоторый момент для первого из
756 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

термов не найдется унифицируемого с ним предложения.


Заметим, что переменные предыдущих унификаций отождествляются с
переменными следующих унификаций лишь в том случае, если эти пере-
менные дожили в поле зрения до соответствующей унификации. Если пе-
ременная уже получила постоянное значение либо значение, в котором она
не встречается, то в последующем переменные с тем же именем трактуются
как новые. Таким образом, конкретные имена переменных имеют значение
лишь внутри одного предложения (точно так же, как в Рефале).
Если исполнение оказалось неудачным, то программа возвращается к по-
следней из точек возврата (происходит откат), и испытывается следующее
по порядку предложение с тем же детерминативом. Если таких предложе-
ний больше не осталось, то происходит откат к следующей точке возврата, и
так далее. Если исполнение откатилось до запроса, и больше кандидатов на
унификацию не осталось, программа заканчивается общей неудачей.
Стандартным ответом программы на запрос служит Yes, если программа
закончилась удачно, и No, если она закончилась неудачно. При удаче выво-
дятся значения всех переменных исходного запроса.
Так, например, если программа и ее база данных имеют вид
Программа 13.2.1

greater(X,Y):-greater1(X,Y).
greater(X,Y):-greater1(Z,Y),greater(X,Z).
greater1(X,f(X)).
estimation(X,Y):-greater(X,Y),known(Y).
known(f(f(f(f(a))))).
unknown(a).
unknown(b).

то ответом на запрос

будет
?-unknown(Y),estimation(Y,X).

Y=a
X=(f(f(f(f(a))))
Yes

а при попытке ответа на запрос


?-estimation(b,X).
13.2. УНИФИКАЦИЯ 757

программа зациклится.14
Насколько коварны вроде бы невинные предположения, сделанные в язы-
ке PROLOG, видно из того, что при логически эквивалентной переформули-
ровке одного из предложений программы

программа успешно ответит на второй запрос


estimation(X,Y):-known(Y),greater(X,Y).

Еще более ‘впечатляющий’ пример рассмотрен в упражнении 1.


No

Рассмотренные до сих пор средства языка PROLOG не дают возможности


сформулировать отрицание. Впрочем, отрицание и не может присутствовать
в хорновых формулах, его наличие разрушает все их хорошие свойства, по-
служившие основой для модели вычислений PROLOG. Но практически оно
нужно, и поэтому в языке PROLOG введен его суррогат. Этот суррогат заодно
дает возможность программисту минимально управлять точками возврата.
Если в цели встал на первое место атом ! (называемый предикат отсечения),
то он успешно унифицируется и уничтожает последнюю точку возврата. Он
используется прежде всего для определения отрицания как явного неуспе-
ха подцели. Рассмотрим, как с помощью ! и списков программируется поиск
пути в лабиринте (и даже в произвольном ориентированном графе). Преди-
кат-операция in успешно унифицируется, если первый его аргумент является
элементом списка, который стоит на месте второго аргумента.
Программа 13.2.2 Статический лабиринт

way(X,X,[X]).
way(X,Y,[Y|Z]):-connect(U,Y), nomember(Y,Z),way(X,U,Z).
way(X,Y,[Y|Z]):-connect(U,Y), way(X,U,Z).
nomember(Y,Z):-member(Y,Z),!,fail.
nomember(Y,Z).

connect(begin,1).
connect(1,begin).
connect(1,2).
connect(2,3).

Если Вы проверяете данные примеры на системе SICS Prolog, то некоторые из них могут
вести себя менее гадко, поскольку ее алгоритм управления несколько более интеллектуален.
14

Для получения описанных эффектов требуется переключить систему в режим совместимо-


сти со стандартом.
758 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

connect(3,1).
connect(3,4).
connect(4,end).

В ответ на запрос

программа выдаст
?-way(begin,end,X).

X = [end, 4, 3, 2, 1, begin]
Yes

Можно не определять nomember, а прямо написать предложение

Предикат отсечения можно использовать для того, чтобы превратить PROLOG-


way(X,Y,[Y|Z]):-connect(U,Y),not (member(Y,Z)),way(X,U,Z).

программу в программу с традиционным управлением, оставив из специфи-


ки языка лишь операцию унификации.15 Как правило, при такой трансфор-
мации теряются главные преимущества языка PROLOG, но зато сохраняются
все его недостатки.
Прагматические соглашения о порядке выполнения действий в програм-
ме привели к тому, что, если мы запишем в форме языка PROLOG тривиаль-
ную тавтологию

и этот оператор16 выполнится, то программа зациклится. И по этой причине,


A:-A.

и по причине ошибки в унификации, предложения языка PROLOG, сохранив


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

repeat.
repeat:-repeat.

Если вставить теперь цель repeat в раскрытие другой цели, и позаботить-


ся о том, чтобы последующие подцели в большинстве случаев заканчивались
Один из соавторов на практике наблюдал специалиста, который никак не мог совладать с
PROLOG и отладить свою программу. Когда он узнал о !, он облегченно вздохнул и перепи-
15

сал свою программу таким образом, что она стала изоморфной программе в обычном языке
программирования, зато быстро ее отладил.
Здесь мы не оговорились: в данном случае предложения лучше трактовать как операторы,
поскольку как утверждения их в данном случае рассматривать невозможно.
16
13.2. УНИФИКАЦИЯ 759

неудачей, а после удачи поставить !, то эти подцели будут повторяться вплоть


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

1. Имеются точки разветвления, допускающие переходы без проверки усло-


вий выбора вариантов — точки недетерминированного разветвления;

2. Вычислительный процесс выбирает в такой точке любое из возможных


продолжений;

3. Некоторые из возможных продолжений объявляются тупиковыми, или


тупиками, т. е. такими, которые не приводят вычислительный процесс
к заранее определенной цели. Принципиально, что тупик продолжения
не может определиться в точке разветвления.

4. Успешным вычислением называется такая последовательность выбира-


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

Недетерминированным достижением цели называется существование успеш-


ного вычисления. Иными словами, если какая-то из последовательностей про-
должений приводит к цели, то цель процесса считается достигнутой. Неде-
терминированная модель вычислений может применяться как средство де-
композиции решаемой задачи, когда программист просто откладывает ‘на
потом’ вопрос, как будет организован перебор вариантов. Не всегда это ра-
ботает, но когда работает, это часто удобно (см. п. 10.2.3).
Есть теорема, доказывающая, что в принципе недетерминированный ко-
нечный автомат всегда можно преобразовать в детерминированный. Идея
преобразования — склейка состояний, как показано на рис. 13.3. при этом,
содержательно говоря, мы создаем линейный порядок на множестве альтер-
натив и выбираем альтернативы в строгом соответствии с этим порядком.
Именно так с самого начала поступили в языке PROLOG. Греха и беды в этом
нет (особенно в то время, когда создавался язык: идея совместности и неде-
терминированности как положительного фактора была только что осознана,
760 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

A x Sx
S2 x …
S1 a Sz
ε
… Sx S12 a z
… Sa … Sa
z
c Sz
Sc c
Sc

B
a S2 x
S1
… S2x a S23 x
S1
z … S2x
a S2z
z z
… x S2z
S3 x
… S3x S3c S3x
z
S3c
Некоторые из них совпадают

Рис. 13.3. Преобразование недетерминированного поиска в детерминирован-


ный
13.2. УНИФИКАЦИЯ 761

а, как уже говорилось, те, кто делают что-либо принципиально новое, почти
всегда забывают оглянуться кругом и согласовать свою находку с другими
принципиальными достижениями того же времени, предпочитая локализо-
вать новизну и в остальных пунктах работать как можно более традицион-
но). Грехи и беды начались, когда особенности конкретного упорядочивания
стали беспощадно использоваться в хакерском духе да еще и выставляться
как принципиальные новации.
Теорема про детерминирование конечного автомата дает лишь уверен-
ность в существовании успешного вычисления. Но она не дает никаких хоро-
ших оценок изменения сложности вычислений при детерминировании поис-
ка. Другой негативный аспект: даже если успешное вычисление существует,
это не означает, что легко трансформировать ассоциированные с переходами
действия и что после трансформации они будут хоть сколько-нибудь пони-
маемы. Поэтому часто удобнее обработку описывать как недетерминирован-
ную, поручая решение задачи детерминирования, или, что то же, перебора
вариантов системе программирования. Скорее всего, впервые эта идея полу-
чила языковые формы в языке PLANNER. PLANNER построен чисто прагма-
тически на базе FORTRAN и LISP для решения задач искусственного интел-
лекта. Не привязываясь к особенностям синтаксиса этого языка, детермини-
рование-перебор в нем можно описать следующей схемой:
backtracking: non-deterministic case
<вариант1>;
...
<вариантk>
end-case

В вычислениях вариантов могут присутствовать там, где это необходи-


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

Конечно же, PROLOG, отделенный от PLANNER всего несколькими го-


backtracking: go to M1, ..., Mk

дами, реализовал эту идею несравненно лучше. Здесь сработал еще и тот
эффект, что PLANNER не успел декларировать блестящие успехи17 и осуще-
Насколько они реальны, в подобных случаях уже второй вопрос. Главное: успеть прокри-
чать о своем успехе и загадить экологическую нишу.
17
762 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

ствить захват экологической ниши.

13.2.4. Динамическое пополнение и порождение программы

Поскольку структура программы и структура поля зрения практически


изоморфны, естественно ставить вопрос о динамическом порождении PROLOG-
программ. Кроме этого высокоуровневого соображения, есть и прагматиче-
ское, которое можно извлечь из нашей программы 13.2.2. В нее мы были вы-
нуждены записать и определение лабиринта. Конечно же, можно было бы
прочитать с помощью встроенной функции read определение лабиринта и
записать его в список, но тогда мы почти утратили бы все преимущества язы-
ка PROLOG, не избавившись при этом ни от одного его недостатка. Гораздо
естественнее иметь возможность прочитать базу данных из файла.
Для этой цели в PROLOG был введен встроенный предикат consult(file[.pl]).
Он читает предложения и факты из файла и помещает их в конец програм-
мы, тем самым оставляя в неприкосновенности ранее данные определения
предикатов. С его использованием наша программа может быть переписана
в следующем виде.
Программа 13.2.3 Вводимый лабиринт

way0(X,Y,Z):-consult(labyr),way(X,Y,Z).
way(X,X,[X]).
way(X,Y,[Y|Z]):-connect(U,Y), not member(Y,Z),way(X,U,Z).
way(X,Y,[Y|Z]):-connect(U,Y), way(X,U,Z).

Пример файла labyr.pl:

connect(begin,1).
connect(1,begin).
connect(1,2).
connect(2,3).
connect(3,1).
connect(3,4).
connect(4,end).

Сравним программу 13.2.3 с теми программами, которые были рассмо-


трены при решениии той же задачи при помощи рекурсий в § 11.4. Эта про-
грамма представляет лишь идею решения. Но идея затемнена частностями
языка PROLOG, которые все равно не дают возможность решить задачу столь
13.2. УНИФИКАЦИЯ 763

же эффективно, как это делается на традиционном языке. В этом принципи-


альная разница языков PROLOG и Рефал. Рефал делает символьные пре-
образования столь же эффективно, как и программа на традиционном язы-
ке, и поэтому может быть использован при квалифицированнном разделении
стилей на всех уровнях: и для создания прототипа программы, и для написа-
ния надпрограммы, и для написания подпрограммы. PROLOG приспособлен
для описания идеи решения, но отвратительно работает уже на уровне про-
тотипирования.
Первоначально предикат consult предназначался для ввода фактов, но за-
тем стало ясно, что идею и структуру языка не нарушает и ввод произволь-
ных предложений. Поэтому был определен предикат reconsult(file.pl), кото-
рый помещает введенные предложения и факты в начало программы, тем са-
мым заменяя данные в программе определения предикатов на содержащиеся
во внешней базе знаний.
Для динамического порождения фактов и предложений имеются функ-
ции, разбирающие предложения и синтезирующие их. Предикаты могут быть
объявлены метапредикатами, и тогда некоторые из их аргументов могут быть
предложениями. Из метапредикатов важнее всего два, перечисленных ниже.
Метапредикат assert(P:-P1,. . . ,Pn) помещает свой аргумент в PROLOG-
программу. Имеются несколько его вариантов, помещающие новое предло-
жение или факт в начало или в конец программы. Метапредикат retract(P:-
P1,. . . ,Pn), наоборот, удаляет из программы предложение или факт, унифи-
цируемый с его аргументом.
С их помощью можно, в частности, сымитировать все предложенные на-
ми алгоритмы работы с лабиринтом, но при этом программа полностью поте-
ряет ясность структуры и станет крайне трудной для отладки и модификаций,
а эффективности, сравнимой с традиционным языком, достичь все равно не
удастся.
Многие из реализаций языка PROLOG включают пакет прогонки, позво-
ляющий осуществлять частичное вычисление PROLOG-программы.
Как руководство по программированию на языке PROLOG можно исполь-
зовать, скажем, книгу [12].
Поскольку в 1996 г. принят стандарт языка PROLOG [83], мы использо-
вали лишь те его возможности, которые согласуются со стандартом.
Вариант сентенциального программирования, базирующийся на возвра-
тах и унификации, отлично сочетается с идеей машины потоков данных. По-
этому японцы попытались использовать его в своем проекте ЭВМ 5-го поко-
ления. Провал данного проекта надолго дискредитировал данное направле-
764 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

ние, но к нему придется вернуться в будущем на другой технической и, ви-


димо, на гораздо более концептуально выверенной программистской основе.
Тем более это неизбежно, потому что концепция унификации и возвра-
тов после неудач исключительно хорошо подходит для выражения ∨-парал-
лелизма (см. Приложение). Более того, уже имеются системы (в частности,
Muse), реализующие этот вариант параллелизма в языке PROLOG.
Таким образом, две ветви сентенциального программирования ориенти-
рованы даже на разные классы параллельных вычислений. По аналогии мож-
но сделать вывод о том, что и для других стилей должны были быть другие
реализации, принципиально отличные от общепринятых и имеющие другой
потенциал развития, если бы не сработал закон экологической ниши. Авто-
ры уверены, что, в частности, для структурного программирования варианты
программ, соответствующие параллелизму типа потока данных, но столь же
выразительно и эффективно исполняемые и на последовательных системах,
требуют другого варианта стиля программирования и другой методологии
с самого начала. Алгол 68 сделал первый шаг в этом направлении, а затем
движение полностью прекратилось.
Развитию языка PROLOG мешает прежде всего слишком большая привяз-
ка к конкретной модели вычислений, которая была зафиксирована слишком
рано, в погоне за мнимой эффективностью. Фатально совершенно извращен-
ное понимание его места и роли. Значительную лепту вносит сюда монопо-
лизированный им и полностью извращающий суть дела термин ‘Логическое
прогораммирование’. PROLOG великолепная заготовка для языка моделиро-
вания идей.
Рассмотрим, что ему мешает в реальности стать таким языком. Как мы
уже неоднократно видели, главное препятствие — лишние возможности. В
первую очередь, это эклектичные добавки операционных средств, которые
провоцируют программиста к операционному мышлению и хакерскому сти-
лю. Так, в задаче о лабиринте, было бы естественно не зацикливание ис-
полнения, когда не предусмотрены ограничивающие условия, заставляющие
программу продвигаться вперед, а указание того, что в процессе выполнения
не хватает условий, что возможны такие-то варианты ограничений, делаю-
щие программу корректной.
Эклектичность PROLOG’а обусловлена двумя обстоятельствами. Прежде
всего, это установка на то, что логика Хорна достаточна для специфициро-
вания вычислений, поскольку, как было доказано, она эквивалентна маши-
не Тьюринга. С этим можно было бы согласиться, если бы решения реаль-
ных задач, которые апеллируют к базам данных-фактов и должны решать-
13.3. ЯЗЫКИ РАЗМЕТКИ 765

ся с использованием баз знаний-утверждений, являющихся следствиями из


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

§ 13.3. ЯЗЫКИ РАЗМЕТКИ

Поскольку различные виды сентенциального программирования исполь-


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

Д. Кнут, детально ознакомившись с типографским процессом в ходе под-


готовки к печати своих книг [40], предложил язык типа ассемблера для описа-
ния процесса верстки документа, содержащего сложнейшие математические
формулы. Этот язык TEX18 до сих пор остается базовым языком для большин-
ства систем подготовки текстов, содержащих сложные формулы (математи-
Читается по-русски “тех”, поскольку, по разъяснению Кнута, буквы в названии не латин-
ские, а греческие, от слова τ εχνικoς.
18
766 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

ческих, физических, химических).


Одновременно с ‘ассемблером’ TEX Д. Кнут описал технологию рабо-
ты на нем. Описывать структуру текста нужно не на уровне ассемблера, а
на уровне системы макроопределений над этим ассемблером, образующей
практически новый язык гораздо более высокого уровня (формат). Сам Кнут
предложил в качестве примера такого формата язык plain TEX; и TEX, и plain
TEX описаны в книге [39].
LATEX — формат TEXа, ставший практически монополистом в настоящее
время. Он, в частности, практически вытеснил формат AMS-TEX, который
навязывался в качестве стандарта Американским математическим обществом,
при этом включив в себя самые привлекательные особенности языка AMS-

TEX четко проводит основную идею языков разметки: команды языка раз-
TEX.

метки вставлены в массив данных, а не наоборот, как в языках программи-


рования.19
При работе над текстом TEX постепенно преобразует данные в элемен-
тарные команды и в качестве последнего промежуточного представления да-
ет линейную последовательность команд, каждая из которых специфицирует
расположение на странице элементарного куска текста (например, символа
или графического файла) либо переход к следующей странице текста. На-
пример, лого TEX превращается в следующую последовательность элемен-
тарных команд:
\hbox(6.8831+2.15277)x18.6108
.\tenrm T
.\kern -1.66702
.\hbox(6.83331+0.0)x6.80567, shifted 2.15277
..\tenrm E
.\kern -1.25
.\tenrm X
Cентенциальность облегчает возможность делать активными структурные единицы пере-
рабатываемых данных, и именно это качество используется во всех языках, которые нагру-
19

жают разметку смыслом, в частности, выполнением определенных действий. В этом плане


уместно сравнить сентенциальный и функциональный стили, общность которых в том, что в
обоих случаях данные и действия естественно описывать сходными (и даже изоморфными)
структурами. В LISP на базе этого изоморфизма введен функционал (EVAL Expression), по-
зволяющий проинтерпретировать как программу значение, получившееся в результате вы-
числения Expression. Но попытки прямой активизации данных чужеродны функциональ-
ным языкам.
13.3. ЯЗЫКИ РАЗМЕТКИ 767

Эта линейная последовательность команд интерпретируется в команды си-


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

a) Последовательность символов является обрабатываемым текстом.

b) Обрабатываемый текст, заключенный в пару соответствующих друг дру-


гу скобок, является обрабатываемым текстом. Такая единица называется
группой.

c) Атом, после которого в случае, если определение этого атома требует па-
раметров, идут соответствующие параметры, называется командой. Доба-
вление команды к обрабатываемому тексту дает обрабатываемый текст.

Внутри разных групп один и тот же текст может пониматься по-разному. Да-
же конкретный синтаксис атомов может быть переопределен!
Таким образом, структура текста носит подчеркнуто иерархический ха-
рактер. Иерархия перекрещивается с другими структурами. Прежде всего,
это структура результирующего текста.
В стандартном конкретном синтаксисе группировку задают, в частности,
символы {}. Имеется потенциально бесконечное число других пар скобок,
например, пара команд \begingroup и \endgroup, выполняющих ту же функ-
цию, что и фигурные скобки. Важнейшими и наиболее часто используемыми
скобочными структурами являются окружения. Окружения выделяются па-
рой команд, имеющих в конкретном синтаксисе чаще всего форму, подобную
следующей:
\begin{example} и \end{example}.
Некоторые окружения заданы в самом языке, а другие (как, например, при-
веденное только что) описываются программистом.
Далее, исключительно важными единицами структурирования являются
боксы, которым могут быть приписаны глобальные свойства, а внутри них
все форматирование и обработка осуществляется независимо от окружения.
Эта структура опускается вниз до результирующей последовательности ко-
манд (где уровни вложенности боксов отмечены точками в начале строк).
768 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

Например, текст вставка порожден при помощи


команды

которая превратила внутренний текст в нераздельное целое, объявила для


\makebox[15em][c]{вставка},

внешнего использования его длиной 15em (ширину 15 букв m текущего шриф-


та) и поместила результат обработки текста в центр получившегося проме-
жутка. Текст, помещенный внутрь бокса, уже не может быть, например, раз-
бит переносами, он целиком размещается на одной строчке. С точки зрения
окружающего контекста бокс выглядит как один большой символ.
И, наконец, самый верхний этаж структуры текста — его рубрикация, си-
стема заголовков. Например, данный параграф описан при помощи команды
\subsection{\TeX{} и \LaTeX}.
Хотя формально заголовки не входят в иерархическую структуру групп, все
группы, начатые после заголовка, должны быть завершены до следующего
заголовка.
С локальной точки зрения текст делится на:

a) символы, которые в свою очередь подразделяются, в частности, на:20

i. буквы,
ii. пробелы,
iii. переводы строки,
iv. другие символы;

b) атомы (типичный пример атома — последовательность \TeX, производя-


щая лого);

c) активные символы (неявно вызывающие команды; в частности, в паке-


те Babel, поддерживающем разные языки, все русские буквы объявлены
активными символами; спасибо русским программистам!);

d) символы группировки: начало группы { и конец группы };

e) символ комментария % (после него вся оставшаяся часть строки игнори-


руется;
20
Некоторые неприниципиальные классы символов опущены.
13.3. ЯЗЫКИ РАЗМЕТКИ 769

f) другие символы переключения режимов (например, символ ^ включает


режим верхнего индекса, символ \ переключает лексический анализатор
в режим создания атома).

Из сказанного выше видно, что обработка текста процессором языка раз-


метки TEX включает в себя прежде всего глубокое преобразование текста
на самом языке TEX, постепенно низводя его на самый низший уровень ко-
манд. Поэтому язык TEX снабжен мощной системой обобщенных вычисле-
ний, которая практически не видна его обычным пользователям, поскольку
они ограничиваются системами определений и метаопределений, образую-
щих пакеты и форматы языка TEX.
В современной терминологии, формат языка TEX — глобальная система
определений, задающая практически новый язык разметки, в основном рас-
ширяющий TEX.21 Пакет аналогичен модулю в языках программирования
и пишется для более частных целей. Данный текст подготовлен с помощью
стандартного формата LATEX2ε и пакета ffit, написанного одним из авторов.
Система обобщенных вычислений языка TEX важна еще и потому, что
она впитала в себя опыт мощного средства высокоуровневого программиро-
вания, бурно развивавшееся до 80-х гг. Возврат к этому средству на будущем
витке развития неизбежен, и поэтому забывать его не стоит. Это языки ма-
кроопределений или макрокоманд.
Основной внутренней единицей системы TEX является атом (токен, в
терминологии TEX) — последовательность символов, рассматриваемая как
неделимая. Обычные символы являются токенами. Далее, в обычном кон-
кретном синтаксисе как токен рассматривается последовательность, подоб-
ная \this, начинающаяся со спецсимвола \ и содержащая лишь буквы. И, на-
конец, любой символ дает возможность определить простейший токен, на-
пример, \#.
Каждому токену, не являющемуся символом, должно быть сопоставлено
определение, или (макро)раскрытие, имеющее вид
\def\ИМЯ ПАРАМЕТРЫ {ТЕКСТ ПОДСТАНОВКИ}
Внутри текста подстановки параметры обозначаются выражениями #1, . . . ,
И что исключительно важно, одновременно запрещающий использование некоторых низ-
коуровневых возможностей языка TEX, замещенных более высокоуровневыми средствами
21

формата! В современных пакетах и инструментальных системах уж очень заботятся о со-


вместимости назад, стесняясь запретить низкоуровневые средства, вступающие в глубокое
концептуальное противоречие с предлагаемыми высокоуровневыми. Сообщество TEX с са-
мого начала отказалось от этого фетиша и не жалеет о нем.
770 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

#9. Таким образом, несимвольные атомы (и активные символы тоже) играют


роль имен.
Когда токен, которому сопоставлено определение, встречается в тексте в
‘нормальной’ позиции, следующий за ним текст отождествляется с его па-
раметрами, причем каждый параметр должен быть сбалансирован по сим-
волам группировки (в стандартном конкретном синтаксисе { и }), а в слу-
чае, если он является заключенной в такие скобки группой, охватывающие
группу скобки опускаются (следующая пара уже остается нетронутой) и про-
изводится замена вхождения вызова макрокоманды на текст подстановки, в
котором параметры заменяются их значениями. Например, при раскрытии
токена \twophrases, определенного как

формально-лексически (по критерию того, что предложение заканчивается


\def\twophrases #1. #2. {(#1) (#2)}

точкой, после которой идет пробельный символ) выделяются следующие за


командой два предложения и они заключаются в скобки.
На первый взгляд данная система кажется крайне бедной по своим воз-
можностям, но уже в ее основе заложена рекурсивность и взаимная рекур-
сивность вызовов, и, более того, возможность, которая отсутствует в т. н. вы-
сокоуровневых языках. Внутри подставляемого текста может быть оператор
\def, который произведет в момент вызова макроса новое определение, при-
чем его текст может варьироваться в зависимости от значений параметров
объемлющего определения. Таким образом, новые определения могут поро-
ждаться динамически!
Чтобы отличать параметры внутренних определений от параметров внеш-
него, внутри текста подстановки символ # обозначается ##, и, соответствен-
но, параметр #1 внутреннего определения обозначается как ##1.
Конечно же, имеется система операторов выбора, и здесь стоит остано-
виться на одном красивом решении, которое является изобретением систе-
мы TEX. Стандартным способом создания условных операторов является их
определение оператором \newif, который одновременно вводит булевскую пе-
ременную, использующий ее логический оператор и операторы установки ее
значения.
Пример 13.3.1. Когда при подготовке данного текста возникла необходи-
мость срочно издать его сокращенный вариант для отчета по гранту, в пре-
амбулу текста был вставлен оператор

Тем самым были определены операторы установки новой логической пе-


\newif\iffull
13.3. ЯЗЫКИ РАЗМЕТКИ 771

ременной (не имеющей явного имени) в истину и в ложь, соответственно:


\fulltrue и \fullfalse, а в тексте появилась дополнительная разметка типа
Эти соглашения \iffull для глобальных переменных заменяются на проти-
воположные. Глобальная переменная, определенная во внутреннем блоке,
отождествляется с глобальной переменной, определенной в самом внеш-
нем блоке. \else имеют исключения. \fi
После этого достаточно заменить в начале текста \fulltrue на \fullfalse, чтобы
перейти от полного варианта к сокращенному.
Конец примера 13.3.1.
Конечно же, при программировании возникают переменные. Один вид пере-
менных в TEX мы уже видели: логические. Кроме них, имеются более явно
вводимые переменные для фиксированного множества других типов, в част-
ности:

• Целые, для 64-разрядных целых чисел.

• Переменные для длин, значениями которых служат размеры.

• Переменные для боксов.

Имеются функции вычисления размеров фрагментов текста, обозначаемые


подобно вызовам макрокоманд, операции присваивания и логические опера-
торы, причем для каждого типа они свои.
Заметим, что в некотором смысле любой токен является переменной, по-
скольку определение его в любой момент может быть заменено. Но для то-
кенов имеется и явная операция присваивания:

При этой операции токен \this получает то же определение, которое имеет


\let\this=\that

токен \that в момент исполнения присваивания.


Иерархичность доведена в языке TEX до логического конца. Локальны и
определения, и присваивания. После конца группы, вообще говоря, отменя-
ются все сделанные в ней определения и присваивания. Так что внутри груп-
пы можно изменять значения переменных, не беспокоясь о том, как это ска-
жется на глобальном контексте.
Проблемы конфликта определений в TEX вообще нет. Если дано новое
определение того же токена, то оно просто заменяет старое, и действует по-
следнее из определений, которое встретилось в данной группе или в ее объ-
емлющих. Более того, отсутствие определения для токена является семанти-
772 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

ческой, а не синтаксической ошибкой. Оно проверяется лишь в момент, ко-


гда токен оказывается в позиции имени макроса. Оно может быть проверено
с помощью оператора \ifundefined \this . . . \fi
Конечно же, предоставлены все возможности для (применяя собственное
выражение Д. Кнута) ‘грязных трюков’. В частности, любое определение ли-
бо присваивание может быть сделано глобальным (оно будет воздействовать
на экземпляр токена в самой внешней группе и отменит все более локальные
определения и присваивания), просто путем постановки перед ним \global.
Как и полагается по функциональной семантике, в момент обработки опре-
деления текст подстановки не раскрывается. Но имеется возможность дина-
мически раскрыть определение в тот момент, когда оно встретилось, и произ-
вести все возможные вызовы макросов внутри подставляемого текста зара-
нее. Для этого достаточно воспользоваться оператором \edef вместо \def. Со-
ответственно, есть и возможность при таком действии защитить некоторые
токены от преждевременного раскрытия. Для этого служит команда \noexpand,
которая при раскрытии исчезает, но запрещает раскрывать следующий за ней
токен.
Еще одна спецификация такова, что к грязным трюкам ее не отнесешь,
скорее стоит пожалеть об отсутствии такой возможности в ‘развитых’ язы-
ках. Специфицировав определение как \outer, мы требуем тем самым трак-
товать как ошибку вызов данной команды в операнде другой команды или
в альтернативе условного оператора. Именно такой статус имеют команды
рубрикации.
И, наконец, для людей с хакерскими наклонностями есть целый набор
тонких возможностей изменять порядок раскрытия определений.
• \expandafter сначала оставляет данный токен нераскрытым, раскрыва-
ет следующий за ним, ставит перед получившимся выражением запо-
мненный токен и возвращается к его раскрытию.
• \futurelet раскрывает следующий за присваиванием токен и уже после
этого производит присваивание.
• \afterassignment запоминает данный токен и возвращает его в текст по-
сле того, как будет выполнено какое-нибудь присваивание. Если это
присваивание боксовой переменной, то токен будет помещен в начало
запомненного бокса.
• \aftergroup запоминает данный токен и возвращает его в текст после
того, как завершится текущая группа.
13.3. ЯЗЫКИ РАЗМЕТКИ 773

Теперь несколько слов о наиболее популярном формате LATEX. Эта над-


стройка приблизила структуру TEXовского текста к структуре языков высо-
кого уровня. LATEX формально запретил ряд низкоуровневых команд систе-
мы TEX, ввел новые конструкции высокого уровня (в частности, команды
\label{ИМЯ} и \ref{ИМЯ} для автоматизации перекрестных ссылок). Он, хотя и
не запретил формально команду \def, но ограничил ее применение внутрен-
ними потребностями самой системы LATEX и создаваемых пакетов.
Вместо нее предоставлена система определений более высокого уровня,
в которых обращение к новым командам стандартизовано, и производится в
момент их определения проверка конфликта описаний. Эти команды следу-
ющие.

• \newcommand дает ошибку, если такая команда уже есть в системе.

• \renewcommand наоборот, дает ошибку, если такой команды не было.


Эта команда используется в основном для локального переопределения
параметров.

• \providecommand ошибки не дает. Она проверяет, есть ли такая коман-


да. Если она уже есть, Ваше определение игнорируется, если ее нет,
оно принимается.

Макрогенерация, вообще говоря, включает возможность переопределе-


ния базовых символов, но это мало где реализовано. TEX является приятным
исключением. Роли особых символов, указанные в самом начале, также мо-
гут быть в любой момент переопределены, и эти переопределения также ло-
кальны. Этим, в частности, воспользовалась система LATEX для создания имен
команд, недоступных внутри текста, обрабатываемого системой. Символ @
рассматривался в исходном TEXе как буква. В ядре формата LATEX определе-
ны, в частности, следующие две команды. \makeatletter исполняется внутри
тел подстановки команд и разрешает использование имен команд, содержа-
щих @, \makeatother исполняется перед началом основного форматируемого
текста и закрывает от пользователя такие команды.
В целом TEX сделал большой шаг по пути избавления языка от прокля-
тия конкретного синтаксиса: за счет системы макрогенерации, собравшей
практически все известные хорошие черты продвинутых макропроцессоров,
можно кардинально менять облик конкретного формата языка.
774 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

13.3.2. Языки разметки для Internet

Опыт языка TEX оказал громадное влияние на языки разметки. В духе


форматов TEXа был выдержан SGML — стандарт для создания языков раз-
метки, принятый в 1988 г. SGML, в свою очередь, породил современные язы-
ки разметки для Web-документов.
HTML
Наиболее распространенным из языков разметки Web-страниц является
HTML. Этот язык разметки был создан и рекламировался как одна из конкре-
тизаций SGML. Впервые предложенный в 1974 году Чарльзом Голдфарбом
и в дальнейшем после значительной доработки принятый в качестве офици-
ального стандарта ISO, SGML (Standard Generalized Markup Language) пред-
ставляет собой метаязык — систему для описания других языков. Он по-
казался слишком сложен для большинства браузеров Web, поскольку одна
спецификация языка SGML занимает свыше 500 страниц, а потрудиться кор-
ректно использовать внутренний механизм SGML для выделения несложного
конкретного полностью соответствующего его стандартам диалекта никто не
пожелал, пока возникшие проблемы не стали слишком острыми.
HTML до сих пор используется как фактический стандарт представления
документов для показа их наиболее распространенными браузерами22 . Тер-
мин ‘разметка’ применительно к документу означает обычно все, что не от-
носится к его информационному наполнению.
В данном пункте мы вернемся к задаче о словах (см. п. 10.2.5), где бы-
ла введена достаточная, но только для данной задачи, разметка. В качестве
разметки можно рассматривать все те элементы текста, которые выделяют
его структурные единицы: строки таблицы, условия, действия и переходы.
Разметка в SGML связана с тегированием, т. е. с расстановкой специальных
стандартизованных последовательностей вида
<тег> содержание </тег>

где тег — имя, которое может быть распознано браузером и каким-то образом
трактовано им, <тег> и </тег> — открывающая и закрывающая скобки выде-
ленного содержания — элемента информационного наполнения (им может
быть как обычный текст, так и целая структура, составленная из аналогичных
Стоит заметить, что, хотя HTML чаще всего предоставляется серверами по HTTP, он также
может использоваться на CD-ROM или в локальной сети. Универсальные языки разметки не
22

привязаны к какому-либо конкретному транспорту.


13.3. ЯЗЫКИ РАЗМЕТКИ 775

объектов, вкрапленных в текст). HTML воспринял внешнюю форму разметки


SGML, но использует ее чисто как знаки перехода к тому или иному формату
текста, поэтому в HTML текстах часто встречаются конструкции, подобные

<br> text <bf> text </br> text </bf>,

где не соблюдается требование иерархической вложенности друг в друга эле-


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

Черный нормальный Черный курсив Красный курсив Красный нормальный

Этот вывод естественно рассматривать как наложение на текст двух видов


структур: шрифтовой (нормальный и курсив) и цветовой (черный и крас-
ный). Первая структура дает следующее разбиение данных:
Черный нормальный Красный нормальный
Черный курсив Красный курсив

Вторая структура требует такого разбиения:

Черный нормальный Черный курсив


Красный курсив Красный нормальный

Эти структуры налагаются друг на друга, а потому естественным кажется


такое их представление в HTML:

<FONT color="black">Черный нормальный <i>Черный курсив </FONT>


<FONT color="red">Красный курсив </i>Красный нормальный</FONT>

И действительно, многие браузеры понимают его именно так, как надо.


Но, к примеру, Netscape Navigator (Mozilla) покажет этот материал в искажен-
ном виде:

Черный нормальный Черный курсив Красный курсив Красный нормальный


776 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

Логика этого браузера соответствует соглашению об иерархической вложен-


ности структурных единиц, а потому команда <i> рассматривается как часть
команды <FONT color="black">, завершение которой </FONT> трактуется как
одновременное завершение всех вложенных команд. Соответственно, </i> счи-
тается просто мусором и игнорируется.
Чтобы вывод всегда происходил правильно, нужно в HTML тексте исполь-
зовать только иерархически вложенные структуры, порою искусственно со-
здавая иерархию (вставка тегов <i> и </i>):

<FONT color="black">Черный нормальный


<i>Черный курсив </i></FONT>
<FONT color="red"><i>Красный курсив </i>
(13.13)

Красный нормальный</FONT>
В примере не затронута такая сторона дизайна, как размещение текста на дву-
мерной плоскости экрана. А это еще один вид структурирования материала
для показа, который требуется наложить на другие структуры.
Практическая потребность одновременного оперирования с несколькими
структурами, одни из которых имеют отношение к содержанию материала,
а другие определяют форму его предъявления, в HTML решена с помощью
отказа от строгости языка. Взамен этого было достигнуто некоторое упро-
щение браузеров, которое ныне в связи с резко увеличившимися ресурсами
машин уже не является хоть сколько-нибудь актуальным. Но высокомерно
игнорировать принятое в HTML решение не стоит: оно впервые привлекло
внимание к тому, что обрабатываемые данные могут иметь полииерархи-
ческую структуру. Правда, в явном виде эта структура в HTML выделена не
была.
Более того, HTML рассматривался его создателями как ‘плоский’ язык,
т. е. его не предполагалось использовать для предоставления информации
об иерархии данных. Например, хотя большинство открывающих тегов (та-
кие, как <b> или <h1>) имеют соответствующие закрывающие теги, некото-
рые (например, <p>) их не имеют. Это лишь одна из непоследовательностей

Теоретические аналогии между иерархической и полииерархической струк-


HTML.

турой весьма грубо соответствуют соотношению между деревьями и сетями.


Сеть может быть преобразована в дерево путем дублирования вершин, но
при этом число вершин может вырасти экспоненциально, а заодно возника-
ет проблема поддержания целостности и непротиворечивости информации,
13.3. ЯЗЫКИ РАЗМЕТКИ 777

поскольку вершины, получившиеся в результате дублирования одной и той


же, должны всегда содержать одинаковую информацию.
Если оставить в стороне все аспекты HTML, не относящиеся задаче о сло-
вах, то средства этого языка не имеют преимуществ по сравнению с число-
вой разметкой, представленной выше (см. § 10.2.5). Как в том, так и в другом
случае есть возможность при помощи разметки задать требуемое структу-
рирование. Однако HTML — это стандартное для браузеров представление,
и есть возможность использовать для работы с ним весь арсенал средств,
предлагаемых для этого стандарта.
Главной особенностью разметки HTML является, конечно, возможность
вставки ссылок на внешние документы или на внутренние разделы того же
самого документа. Для решаемой задачи это качество в принципе можно
было бы использовать, чтобы отделить программные части от данных. Но
даст ли это преимущество по сравнению с тем, что позволял сделать тип
T_StructTableLine? Да, если иметь в виду дополнительные возможности, и
нет, если ограничиться утилитарными рамками. Если не иметь ввиду допол-
нительные еще не рассмотренные возможности, связанные с HTML, то это
всего лишь еще одна форма описания структурированных данных.
HTML преуспел не только как адаптируемый язык разметки, но и в ка-
честве промежуточного программного обеспечения (middleware), активизи-
рующего нужные действия браузера. HTML обеспечивает также простейшие
посреднические функции для общения с различными серверами. Но это —
слишком примитивный язык и с точки зрения задания разметочной структу-
ры, и для передачи заданий другим серверам. Его ограниченные возможно-
сти форматирования пытались преодолеть с помощью определения расшире-
ний. Прежде всего, конечно же, вводились специфические расширения для
конкретных браузеров.
В дальнейшем попытались определить механизм описания расширений:
т. н. каскадные таблицы стилей, CSS (Cascading Style Sheet). CSS приме-
няются некоторой частью Web-сообщества, состоящей из достаточно гра-
мотных в информатике людей. Они позволяют изменять форматирование из-
вестных тегов HTML и определять новые теги. В частности, на Web-сервере
Network Magazine таблицы стилей CSS используются и для стандартизации
представления типичных элементов, таких, как <H1>, и для введения новых,
таких, как врезки.
Для повышения качества выполнения посреднических функций пытают-
ся применять промежуточное программное обеспечение (с помощью Java,
ActiveX и т. п.). Тем не менее, все это не устраняет фундаментальные недо-
778 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

статки HTML.
По сути, HTML — это лишь способ представления информации, он опи-
сывает то, как браузер должен скомпоновать текст, графики и другие элемен-
ты изображений на странице. В результате «то, что вы видите, — это все, что
вы получаете» (WYSIWYG)23 . Нет никакого способа описать данные неза-
висимо от отображения этих данных (за исключением чрезвычайно слабой
системы ключевых слов в заголовке страницы Web). В частности, это главная
причина, почему так трудно найти нужную информацию с помощью меха-
низма поиска. Клиент не имеет приемлемых средств извлечения данных со
страницы для дальнейшей работы с ними. На любой конкретной странице
клиент получает только одно представление конкретного множества данных.
Это подтверждает следующая иллюстрация.
Пример 13.3.2. Предположим, что вы просматриваете список аукционов,
упорядоченный по дате открытия торгов. Если вы захотите взглянуть на тот
же список, но отсортированный по дате закрытия торгов, то браузеру придет-
ся посылать новый запрос серверу. В свою очередь, серверу придется заново
отправлять полную страницу HTML со списком аукционов.
Конец примера 13.3.2.
Такого рода манипулирование данными ведет к значительному увеличению
числа обращений к серверам и к другим неприятным последствиям.
На примере задачи о словах и попыток адекватно представить таблицу
автомата с помощью разметки текста видна необходимость стандартной раз-
метки, а также то, что не всякая стандартная разметка годится. Последнее
демонстрирует язык HTML, который не в состоянии дать ничего существен-
но нового по отношению к показу таблицы. Ситуации, когда необходимы до-
полнительные возможности разметки, достаточно типична,
Простым решением для некоторых из перечисленных проблем было бы
введение дополнительных семантических тегов HTML, таких, как <DATE> и
<PRICE>, а в нашем случае <_1>, <_2> и т. д. С их помощью клиент мог бы
определить, каков смысл данных. История, однако, показывает, что введение
Термин WYSIWYG обычно применяется для характеристики качества интерфейса про-
граммных систем. Если система обладает этим качеством, то пользователю для понимания
23

того, что делает программа, нет необходимости заглядывать в исходный текст для выявления
действий, а дизайнер при написании исходного текста немедленно получает образ резуль-
тата своих действий. Пример задачи оперирования с таблицами демонстрирует ситуацию,
когда прямолинейное понимание WYSIWYG противоречит требованиям обработки данных.
13.3. ЯЗЫКИ РАЗМЕТКИ 779

дополнительных тегов для HTML может занять годы; консенсуса по поводу


того, что они должны значить, редко когда удается достичь, если он вообще
возможен. Если же вы решите не дожидаться изменения стандарта, то имейте
в виду, что вы создаете нечто свое и тем самым полностью отказываетесь от
остатков переносимости: одного из главных преимуществ HTML.
Таким образом, история развития и применения HTML является класси-
ческим примером результатов ‘прагматического’ подхода. Подытожим их.

• Хотели создать корректную конкретизацию SGML, но пожалели време-


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

• Хотели создать расширяемую заготовку, а появился стихийный стан-


дарт, зафиксировавший слишком раннюю, недоделанную и внутренне
несогласованную стадию развития языка и его использования.

• Хотели создать механизм для middleware, а появился голем, который


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

• В общем, по словам Черномырдина, «Хотели, как лучше, а. . . ».

XML — расширенный язык описания структуры


Нет ничего удивительного в появлении других стандартных языков, кото-
рые устраняют проявившие себя недостатки HTML. В настоящее время роль
такого языка играет XML.
Чтобы устранить самые зияющие из выявившихся недостатков HTML, в
1996 году члены рабочей группы Консорциума World Wide Web (W3C,
http://www.w3.org) вернулись к рассмотрению стандартного обобщенного язы-
ка разметки — SGML), вульгарным жаргоном которого является HTML24 .
Есть легенда о том, что, поскольку двумя единственными кандидатами на основу языка
разметки для Интернет являлись SGML и TEX, а делали первые его варианты в значительной
24

степени в физическом институте CERN, инициаторы Интернет обратились прежде всего к


Д. Кнуту. Но он заявил, что авторские права им уже переданы Американскому математиче-
скому обществу и поэтому он не может дать добро на новые жаргоны. Инициаторы предпо-
чли, не продолжая переговоров, обратиться к альтернативному источнику и изуродовали его
780 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

Наконец-то пойдя по пути корректной специализации SGML для исполь-


зования с Web, группа W3C предложила XML (eXtensible Markup Language —
расширяемый язык разметки; см., напр. [55]). XML представляет собой кор-
ректно определенный язык, порождаемый как конкретизация SGML, и как
следствие, любой действительный документ XML является действительным
документом SGML. Более того, как и SGML, XML — это метаязык, являю-
щийся подъязыком SGML и определяющий процедуру порождения языков
разметки для специфических целей. Например, язык синхронизированной
интеграции мультимедиа (Synchronized Multimedia Integration Language —
SMIL) базируется на XML.
XML используется для разметки стандартных документов во многом так
же, как HTML. Однако XML превосходит его при работе со структурирован-
ными данными, такими, как результаты запроса, метаинформация об узле
Web или элементы и типы схем.
Документ XML выглядит во многом похожим на HTML, поскольку внеш-
няя форма взята из того же источника, и сохранены даже многие наиболее
широко распространенные теги. Однако, в отличие от HTML, смысл тега за-
висит от регистра, а каждый открывающий тег должен во всех случаях иметь
парный закрывающий тег. Не ограничивая автора каким-либо фиксирован-
ным набором тегов, XML позволяет ему вводить любые имена, представляю-
щиеся полезными. Эта возможность является ключевой для активного мани-
пулирования данными. В качестве примера ниже сравнивается, как список
имен и адресов выглядит на HTML, и как он может быть представлен на XML.
Вот фрагмент HTML:
Программа 13.3.1

<H1>Еditor Сontacts</H1>
<H2>Имя: Иван Дураков</H2>
<P>Должность: старший редактор</P>
<P>Издание: Сибирские сети</P>
<P>Улица и дом: Морской, 13 </P>
<P>Город: Академгородок</P>
<P>Штат: Сибирь</P>

<P>Электронная почта: ivan_durak@uchenych.net</P>


<P>Индекс: 630090</P>

в меру своей испорченности (так что, возможно, с неких высших позиций Кнут был и прав,
отфутболивая их).
13.3. ЯЗЫКИ РАЗМЕТКИ 781

Теги позволяют браузеру размещать данные на экране, но ничего не сооб-


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

Программа 13.3.2

<? xml version = "1.0"?>


<editor_contacts>
<editor>
<first_name>Иван</first_name>

<title>старший редактор</title>
<last_name>Дураков</last_name>

<publication>Сибирские сети</publication>
<address>
<street>Морской, 13</street>
<city>Академгородок</city>
<state>Сибирь</state>
<zip>630090</zip>
</address>
<e_mail>ivan_durak@uchenych.net</e_mail>
</editor>
</editor_contacts>

Обратите внимание, что в данном фрагменте отсутствуют такие элементы


данных, как «Имя», «Должность», «Издание» и т. д., зато безликие <P> и
</P> заменены тегами с содержательными идентификаторами. Естественно,
что для таких тегов где-то должно быть дано определение, причем такое, при-
менение которого позволило бы, например, печатать структурные единицы
нужным образом, в частности, вставляя опущенные элементы данных. Имен-
но это свойство XML позволяет нагружать теги смыслом, который использу-
ется обработчиками для задания тех или иных действий, и поэтому зачастую
использовать его не только как язык разметки текстов, но и как язык разметки
данных.
782 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

Новое качество XML — единственность структуры, задаваемой тегами:


теги не могут накладываться. Однако они могут быть вложены друг в друга.
Следовательно, пример со шрифтовой и цветовой структурами описывается
только вариантом текста, представленным в (13.13).
Некоторые элементы, как <first_name> и <e_mail>, содержат данные, в то
время как другие (<address>) присутствуют только в целях структурирова-
ния. А как быть с альтернативными видами структур? Считается, что их мож-
но моделировать на главной структуре XML текста. Это делается, во-первых,
с помощью приема, который продемонстрирован упомянутым примером, а
во-вторых, за счет атрибутов элементов текста и ссылок на другие объекты.
Атрибутирование аналогично имеющейся в HTML возможности, где, на-
пример, элементу <table> может быть присвоен атрибут align="center". В XML
элемент может иметь один или более связанных с ним атрибутов, причем при
составлении документа их можно вводить столько, сколько потребуется для
конкретной работы. Например:

<publication topic="networking" circulation="controlled">.

Ссылка на другой объект представляет собой строку, начинающуюся с & и за-


канчивающуюся точкой с запятой. Эти ссылки позволяют, в частности, вста-
вить в документ специальные символы, включение которых самих по себе
могло бы сбить с толку программу разбора. Например, чтобы поместить в
документ знак “меньше, чем” (<) вы должны вставить ссылку &lt;, а чтобы
вставить сам амперсенд — ссылку amp;, и т. д. До сих пор все так же, как и
в HTML. Однако ссылки XML на объекты предоставляют гораздо больше воз-
можностей, так как они могут отсылать к определенным автором разделам
текста в том же самом или в другом документе:

<article>
&introduction;
&body;
&sidebar;
&conclusion;
&resources;
</article>

Другими видами разметки XML являются комментарии (они выделяют-


ся точно так же, как в HTML) и инструкции по обработке. Инструкциям по
13.3. ЯЗЫКИ РАЗМЕТКИ 783

обработке предшествует знак вопроса. Они описывают, что именно програм-


ма разбора должна использовать для интерпретации конкретного документа
или его раздела. Например, инструкция

<?xml version = "1.0"?>

сообщает программе разбора XML, что обрабатываемый документ действи-


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

Этот текст, даже если он содержит элементы кода HTML, такие,


<![CDATA[

как <B>жирныйшрифт</B> или <H1>заголовок</H1>,


не подвергается грамматическому разбору. Вместо этого он отображается,
как есть.

Все, что рассмотрено при обсуждении XML до сих пор, не затрагивало два
]]>

важных вопроса:

• как именно должны форматироваться элементы XML и

• как браузеры смогут понимать нестандартные теги (типа <publication>).

Одним из возможных ответов для HTML является использование CSS.


CSS могут служить для форматирования документов XML, но это не очень
удачный выбор. Главное достоинство XML в том, что он представляет фор-
мат документа в виде древовидной структуры. К сожалению, CSS не спо-
собны взаимодействовать с деревом, они могут только форматировать до-
кументы XML “как они есть”. Можно вывести документ на экран в любом
приглянувшемся формате, но нельзя осуществить какое-либо избирательное
представление его данных без применения языка сценариев. Более того, для
использования CSS приходится изучить еще один синтаксис.
Данные ограничения привели к созданию XSL. Это приложение XML со
своей собственной семантикой (фиксированным набором элементов), следо-
вательно, оно может быть использовано для создания таблиц стилей (шабло-
нов документов), понятных любой программе разбора XML.
Таблицы стилей XSL описывают, как документы XML должны преобразо-
вываться в другие форматы, такие, как HTML, RTF и т. п. Но таблицы стилей
784 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

XML — это нечто большее, чем просто преобразователи форматов: они также
предоставляют механизм для манипулирования данными. Например, данные
можно сортировать, производить по ним поиск, удалять или добавлять прямо
из браузера.
Давайте рассмотрим какую-либо простую таблицу стилей, которой мы
могли бы воспользоваться для приложения Editor Contacts.
Программа 13.3.3
<?xml version = "1.0" ?>

<!-декларация, что документ является таблицей стилей и


<xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl">

что он связан с xsl: namespace ->

<!-Применить шаблон ко всему в исходном документе XML ->


<xsl:template match="/">

<HTML>
<BODY>
<H1>Editor Contacts</H1>
<xsl:for-each select="editor_contacts/editor">
<H2>Имя: <xsl:value-of select="first_name">
<xsl:value-of select="last_name"/></H2>
<P>Должность: <xsl:value-of select="title"/></P>

<P>Улица и дом:
<P>Издательство: <xsl:value-of select="publication"/></P>

<xsl:value-of select="address/street"/></P>
<P>Город: <xsl:value-of select="address/city"/></P>
<P>Страна: <xsl:value-of select="address/state"/></P>
<P>Zip: <xsl:value-of select="address/zip"/></P>
<P>E-Mail: <xsl:value-of select="e_mail"/></P>
</xsl:for-each>
</BODY>
</HTML>
</xsl:template>
</xsl:stylesheet>

При сохранении на диск под именем EDITORS.XSL (или любым другим) этот
шаблон будет применен к EDITORS.XML при добавлении в него следующей
строки после первой:
<?xml-stylesheet type="text/xsl" href="editors.xsl" ?>
13.3. ЯЗЫКИ РАЗМЕТКИ 785

В конечном итоге текст на экране браузера будет выглядеть так же, как некий
фрагмент HTML, показывающий данные о редакторе.
Однако XSL может действовать как функция текстового процессора merge-
print. Определенный как часть пространства имен XSL, элемент xsl:for-each
сообщает процессору о том, что он должен циклически обрабатывать все уз-
лы в исходном файле XML. Атрибут xsl:value-of вставляет значение узла XML
в элемент HTML. Таким образом, если придется вернуться к EDITORS.XML и
вставить десятки или сотни контактных адресов, то они без каких-либо изме-
нений будут отображаться в соответствии с таблицей стилей. Благодаря тому,
что информацию о форматировании требуется передать только один раз, XML
и XSL экономят пропускную способность.
Таблицы стилей XSL имитируют функцию merge-print еще и в том, что
они позволяют избирательно опускать поля данных при отображении. Кроме
того, вывод может быть отсортирован по любому конкретному полю данных.
Для сортировки базы данных контактных адресов по фамилии редактора в
прямом алфавитном порядке элемент xsl:for-each следует изменить следую-
щим образом:
<xsl:for-each select="editor_contacts/editor" order-by="last_name">

XSL способен осуществлять условную трансформацию вывода в зависимо-


сти от значений различных элементов или атрибутов. Более того, он позво-
ляет запрашивать данные с использованием множества разнообразных опе-
раторов шаблонов, символов подстановки, фильтров, булевых операторов и
выражений.
XML и XSL никоим образом не предназначены для замены систем рабо-
ты с базами данных, например, SQL25 Однако XSL открывает возможность
разнообразного поиска по данным после их загрузки в браузер.
Из перечня возможностей XSL видно, что XML снабжен выразительными
средствами оперирования с древовидно структурированными текстами, но
что особенно важно, здесь четко прослеживается поддержка принципа раз-
деления распознавания структуры и работы с ней. Мы уже имели возмож-
ность убедиться в преимуществах такого подхода, когда обсуждали стиль
программирования от событий. Эти преимущества существенно возрастают
за счет стандартизации XML. В частности, ведется разработка стандартных
способов оперирования с размеченными текстами. Одним из значительных
Да, наверно, вряд ли найдется много желающих хранить свои базы данных непосред-
ственно в формате XML.
25
786 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

достижений в этой области является так называемая объектная модель до-


кумента (Document Object Model, DOM), версия 1.0 которого была принята в
качестве рекомендации W3C в октябре 1998 года. DOM возникла как специ-
фикация для обеспечения переносимости сценариев JavaScript и программ
на Java между браузерами Web и позднее эволюционировала в API для до-
кументов HTML и XML. Она определяет логическую структуру документов,
способы доступа и манипулирования ими. Программисты могут создавать
документы, управлять их структурой и добавлять, модифицировать или уда-
лять элементы и содержимое.
Таким образом, возникнув в функциональных рамках языков разметки и
достаточно хорошо соответствуя этому назначению, XML и XSL нашли свое
место в качестве инструментария технологии программирования Интернет
приложений. Этот инструментарий стремится быть адекватным задачам про-
межуточного программного обеспечения для сетевых разработок. Однако,
если ограничиться рассмотрением XML/XSL как средства оперирования с дре-
вовидными структурами, нельзя не заметить один существенный недостаток
подхода: деревья и перекрестные ссылки между элементами деревьев зада-
ются в текстовом представлении. Это удобно для унификации предъявления
данных для браузеров, но для эффективного оперирования разумнее было
бы пользоваться каким-либо стандартизованным бинарным представлением,
связывая именно с ним все, что относится к просмотрам, поискам и преобра-
зованиям данных. В таком случае вся специфика языка разметки, предназна-
ченного для Интернет и, возможно, для других нужд, могла бы быть задана
как специализированная стандартная надстройка над бинарным представле-
нием.
Применительно к оперированию с таблицами, которое послужило пово-
дом для обсуждения XML/XSL подхода, нужно именно бинарное представле-
ние. Приведенное в § 10.5.3 решение этой задачи на XML в основном обусло-
влено его стандартностью, а не реальными достоинствами. Оно заодно слу-
жит иллюстрацией того, что взаимосвязь между представлением данных и
стилем программирования для их обработки не является однозначной. Даже
если в большинстве случаев хорошо работает некоторый стиль (в частности,
сентенциальный стиль для языков разметки), конкретная задача может про-
диктовать другой. Но в любом случае нужно выбирать стиль, концептуально
согласующийся с представлением данных.
13.4. ПРИМЕНЕНИЯ СЕНТЕНЦИАЛЬНОГО ПРОГРАММИРОВАНИЯ 787

§ 13.4. ПРИМЕНЕНИЯ СЕНТЕНЦИАЛЬНОГО ПРОГРАММИРОВАНИЯ

13.4.1. Аналитические преобразования

Символическое вычисление — это выполнение действий над специально


представленными обозначениями (в данном случае — множеств и их элемен-
тов), а не над конкретными значениями, которые, быть
√ может, и существуют-
то только в идеальном математическом смысле (π, 2 и др.). Это абстрактно-
синтаксическое представление конкретно-синтаксического представления язы-
ка изображения предикатов. Иногда такой путь даже очень облегчает жизнь:
повышает точность, сокращает время выполнения26 .
Пример 13.4.1. Символическое разложение многочленов (высоких поряд-
ков) на множители с целью получения аналитических решений уравнений.
Это стоит делать везде, где можно. Пусть требуется найти корни уравнения

X 16 − 5X 15 − 9X 14 + 37X 13 + 40X 12 + 72X 11 − 360X 10 − 2X 6


+ 10X 5 + 18X 4 − 74X 3 − 80X 2 − 144X + 720 = 0 (13.14)

Это совершенно неподъемная задача, если пытаться ее решить численными


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

(X − 5)(X 2 − 9)(X 3 − 8)(X 10 − 2) = 0.

Конец примера 13.4.1.


Этот пример показывает необходимость владения методами нечисленной ма-
тематики, в данном случае, методами аналитических вычислений.
Что здесь существенно с точки зрения программиста?
Данные — символьные строки определенной структуры (есть правила
задания выражений). Операции — богатый арсенал преобразований одних
Причем, если это удается (что происходит реже, чем хотелось бы, но чаще, чем может по-
казаться неквалифицированному в математике программисту), то сокращение времени явля-
26

ется принципиальным и радикальным, на порядки превосходящим все, что можно получить


прямой оптимизацией программы. Заодно улучшаются при этом (как всегда происходит при
применении преобразований высших уровней) и все другие показатели качества программы.
Расплатой же является необходимость применения интеллектуальных ресурсов программи-
ста и (или) аналитика.
788 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

выражений в другие. Эти операции целенаправленно применяются програм-


мистом, чтобы получить аналитическое выражение для тех или иных целей
(в примере — для решения уравнения, но это совсем не обязательно!). Обыч-
ные операции пополняются специальными, часто выражающими сущности
более высоких порядков (например, дифференцированием).
Существенный момент систем аналитических преобразований — вну-
треннее представление выражений. Текстовое представление явно не похо-
дит. Немногим лучше и абстрактно-синтаксическое представление, если иметь
ввиду семантику, которая вкладывается в соответствующие операции тради-
ционных языков программирования.
Конечно, реализовывать аналитические вычисления можно самыми раз-
нообразными способами. Можно упрятать реализацию всех нужных преобра-
зований в каком-либо прикладном пакете (как это сделано, например в Maple)
и даже не заметить, что мы имеем дело с сентенциальными по сути своей
задачами. Однако этот путь ведет лишь к таким решениям, которые нужны
«здесь и сейчас»: программист, к примеру, не сможет подсказать программе
символического дифференцирования, что она имеет дело в неким частным
случаем, для которого можно резко сократить перебор вариантов. Когда тре-
буется проводить подобные преобразования систематически, удобнее снаб-
дить перерабатываемые данные дополнительными атрибутами, направляю-
щими вычисления в нужное русло. Иными словами, полезно делать струк-
турные единицы данных активными. А это — привнесение в программу, об-
рабатывающую такие данные, элементов сентенциального стиля, пусть даже
без использования подходящей языковой поддержки. Преимущества, кото-
рые при этом появляются, непосредственно следуют из того, что данный вид
обработки является сентенциальным по своей сути, требующим адекватных
средств реализации.

13.4.2. Сентенциальные методы в традиционных языках

Что касается программирования в сентенциальном стиле на операцион-


ном языке, то прежде всего здесь нужно очертить границы применения. Мы
упоминали условия, при которых применение данного стиля оправдано. Из
них следует, что нет никаких оснований говорить об ‘универсальном’ сен-
тенциальном стиле, пригодном для обработки любых данных. Для специа-
лизированного применения, когда перерабатываемые данные естественным
образом структурированы, правильное программирование в сентенциальном
стиле предполагает отражение в составляемой программы этой структуры.
13.4. ПРИМЕНЕНИЯ СЕНТЕНЦИАЛЬНОГО ПРОГРАММИРОВАНИЯ 789

Здесь есть альтернатива: можно структурировать описание данных или дей-


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

Второй подход — отражение структуры перерабатываемых данных в дей-


Pearl).

ствиях программы. Это один из типовых методов программирования, полу-


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

1. Если в программе 13.2.1 переставить местами факты


unknown(a) и unknown(b),
изменится ли что-нибудь?

2. Что произойдет, если в программе 13.2.1 заменить определение преди-


ката greater на

greater(X,Y):-greater(X,Z),greater(Z,Y).
greater(X,f(X)).
790 ГЛАВА 13. СЕНТЕНЦИАЛЬНЫЕ МЕТОДЫ

3. Совпадают ли выражения (a-b)-c и a-b-c? А выражения a+b+c, (a+b)+c,


a+(b+c)?

4. Проверьте, как работает программа 13.2.3. Она имеет недостаток, вы-


являющийся в случае отсутствия пути. Как, изменив лишь одно пред-
ложение в данной программе, избавиться от недостатка?

5. Придумать несколько вариантов представления полиномов, ориенти-


рованных на сложение, умножение и деление с остатком.
Глава 14

Функциональное
программирование

Этот стиль программирования объясняется на примере языка LISP в его


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

§ 14.1. СТРУКТУРА ДАННЫХ

В Lisp, так же, как в сентенциальных языках, структура данных програм-


мы и поля памяти, обрабатываемого программой, совпадают. После столь-
ких примеров такое совпадение уже не кажется случайностью, видимо, это
одна из удачнейших форм поддержания концептуального единства для высо-
коуровневых систем.
Основной единицей данных для Lisp-системы является список. Списки
Lisp изоморфны тем, которые определены в Приложении (определение A.6.1).
Атомами являются истина T, числа и имена, ложью служит пустой список

Если не применены специальные операции блокирования вычислений,


NIL.

первый аргумент списка интерпретируется как функция, аргументами кото-


рой являются оставшиеся элементы списка. Это позволяет и программу за-
давать списком.
1
И уж точно первый из выживших, поскольку Plankalkül Цузе умер вместе с его машинами.

791
792 ГЛАВА 14. ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ

Основная операция для задания списков (list a b . . . z). Она вычисляет свои
аргументы и собирает их в список. Для этой операции без вычисления аргу-
ментов есть скоропись ’(a b . . . z). Она является частным случаем функции
quote (сокращенно обозначаемой ’), которая запрещает всякие вычисления в
своем аргументе и копирует его в результат так, как он есть.
По традиции элементарные операции разбора списков обозначаются име-
нами, которые начинаются с c, кончаются на r, а в середине иден некоторая
последовательность букв a и b. (car s) выделяет голову (первый член) спис-
ка, (cdr s) — его хвост (подсписок всех членов, начиная со второго). Буквы a
и d применяются, начиная с конца. Рассмотрим маленький пример диалога,
иллюстрирующий эти операции.

[13]>(setq a ’(b c (d e) f g))


(B C (D E) F G)
[14]> (cddr a)
((D E) F G)
[15]> (cddar a)
*** - CDDAR: B is not a list
1. Break [16]> ^Z
[17]> (caaddr a)
D
[18]> (cdaddr a)
(E)

Кроме того, в поле памяти с каждым атомом-именем могут быть связа-


ны атрибуты. Стандартный атрибут — значение атома. Для установки это-
го атрибута есть функция (setq atom value), аналогичная присваиванию. Эта
функция не вычисляет свой первый аргумент, она рассматривает его как имя,
коотрому нужно приписать значение.
Значение может является локальным, если мы изменили значение ато-
ма внутри некоторого блока, то такое ‘присваивание’ действует лишь вну-
три минимальных объемлющих его скобок, и исчезает снаружи блока. Кро-
ме значения, имена могут иметь сколько угодно других атрибутов. Все эти
атрибуты глобальны. Они принадлежат самому имени, а не блоку. Способ
установки значений этих атрибутов несколько искусственный. Имеется еще
одна функция setf, которая вначале вычисляет свой первый аргумент, кото-
рый должен дать ссылку на место, которому можно приписать значение (на-
14.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ 793

пример, на атрибут). Функция получения значения атрибута get, даже если


атрибута еще нет, указывает на его место.

[38]> (setf (get ’b ’weight) ’(125 kg))


(125 KG)
[39]> (get ’b ’weight)
(125 KG)

Таким образом, получаем общую структуру данных языка LISP, изобра-


женную на рис. 14.1.

§ 14.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ

Программа на языке LISP задается как список применений функций, часть


из которых могут вычисляться во время исполнения самой программы. Как
правило, заданные в программе списки интерпретируются как применения
функций и вычисляются, если другое не определяют ранее активированные
функции (вы видели, что функция quote запрещает вычисление своего аргу-
мента, функция setq запрещает вычисление лишь первого из двух аргумен-
тов, а функция setf заставляет вычислить первый аргумент лишь до стадии,
когда получена ссылка на его значение). Любое выражение выдает значение
(это используется, в частности, при диалоговой работе с LISP, на примере
которой иллюстрируются понятия).
Основные управляющие функции соответствуют тому. что необходимо
для вычисления условных термов. Функция

(block name e1 . . . en) (14.1)

вычисляет свои аргументы, начиная со второго, один за другим, тем самым


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

(return-from name value) (14.2)

Этим LISP выгодно отличается от большинства языков.


Далее, блоком считается любое описание функции. Описание функции
производится при помощи функции defun, которая, в свою очередь, опреде-
ляется через примитивы function и lambda. Первый из них задает, что имя,
794 ГЛАВА 14. ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ

Block 1 Block 2

( A ( A ) )

Значение Значение

Тип
A Тип

Function Атрибуты Attr2


Attr1

Val2
Val1

Рис. 14.1. Структура данных языка LISP


14.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ 795

являющееся его аргументом, рассматривается как функция (он часто сокра-


щается в конкретном синтаксисе до #’), второй образует значение функцио-
нального типа. Имя функции является и именем функционального блока.
Пример 14.2.1. Определение факториала, вызов анонимной функции и воз-
можность вычисления произвольного функционального выражения, создан-
ного в программе.
[1]> (defun fact (n) (if (= n 0) 1 (* (fact (- n 1)) n)))
FACT
[2]> (fact 40)
815915283247897734345611269596115894272000000000
[3]> ((lambda (x) (fact (* x x))) 5)
15511210043330985984000000
[55]> (setq g ’(lambda (x) (fact (* x x))))
(LAMBDA (X) (FACT (* X X)))
[56]> (eval (list g 3))
362880
Конец примера 14.2.1.
Нужно заметить, что определение функции с данным именем и значение
имени могут задаваться независимо. Например, мы можем в этом же кон-
тексте задать (setq fact 7), хотя, конечно же, это отвратительный стиль про-
граммирования.
Конечно же, все формальные параметры функций являются локальными
переменными. Никакие изменения их значений не выходят наружу. Но все
другие свойства остаются глобальными! Пример смотри ниже.
[23]> (defun f (x) (progn (setf (get ’x ’weight) ’(25 kg)) (+ x 3)))
F
[24]> (setf (get ’x ’weight) ’(30 kg))
(30 KG)
[25]> (get ’x ’weight)
(30 KG)
[26]> (setq x 5)
5
[27]> (f 3)
6
[28]> x
5
796 ГЛАВА 14. ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ

[29]> (get ’x ’weight)


(25 KG)
В LISP имеется возможность создать анонимный блок со своими локаль-
ными переменными, не объявляя его функцией. Это называется связыванием
переменных и производится функцией let.
Внимание! Значение имени, унаследованного извне, все равно будет внеш-
ним! Смотрите пример ниже.
[32]>(setq a ’(b c d))
(B C D)
[33]>(setq b 5)
5
[34]> (list (let ((b 6)) (eval (car a))) (eval (car a)))
(5 5)
[35]> (list (let ((b 6)) b) (eval (car a)))
(6 5)
[36]> (list (let ((b 6)) (list b a)) (eval (car a)))
((6 (B C D)) 5)
[37]> (list (let ((b 6)) (eval (car (list ’b a)))) (eval (car a)))
(5 5)
Важнейшей особенностью языка LISP и функционального программиро-
вания как стиля являются функционалы с аргументами-функциями. Они, в
частности, делают практически ненужными циклы. Например, рассмотрим
пример возведения всех членов кортежа в квадрат.
[57]> (setq a (list 1 5 7 9 11 13 15 19 22 28))
(1 5 7 9 11 13 15 19 22 28)
[58]> (mapcar (function (lambda (x) (* x x))) a)
(1 25 49 81 121 169 225 361 484 784)
Функционал mapcar применяет свой первый аргумент ко всем членам
второго.
Тем не менее в языке LISP есть конструкции циклов, как дань програм-
мистской традиции. Чужеродность циклов подчеркивается тем. что они все-
гда выдают значение NIL.
И, наконец, приведем пример, любезно предоставленный Л. Городней.
Пример 14.2.2. Посмотрите сами, какую из рассмотренных ранее задач ре-
шает данная программа.
14.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ 797

Программа 14.2.1
;==============================================================

; свертка/развертка системы текстов


;

; текст представлен списком


;((Имя Вариант ...)...)
; первое имя в свертке - обозначение системы текстов

(Имя Лексема (Варианты))


; (Элемент ...)

; ((пример (ма (ш н)
;

(ш а) )
(шн))
;

; ((н ина))
;
)

; реализация свертки: unic, ass-all, swin, gram, bnf


;======================================================

;; список уникальных начал


(defun unic (vac) (remove-duplicates (mapcar ’car vac) ))

;; список всех вариантов продолжения (что может идти за ключом)


(defun ass-all (Key Vac)

(cond
((Null Vac) Nil)
((eq (caar Vac) Key) (cons (cdar Vac)
(ass-all Key (cdr Vac)) ))
(T (ass-all Key (cdr Vac)) )
))

;; очередной шаг свертки или снять скобки при отсутствии вариантов


(defun swin (key varl) (cond

((null (cdr varl))(cons key (car varl)))


(T (list key (gram varl)) )
))

;; левая свертка, если нашлись общие начала


(defun gram (ltext)

( (lambda (lt) (cond


798 ГЛАВА 14. ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ

((eq (length lt)(length ltext)) ltext)


(T (mapcar
#’(lambda (k) (swin k (ass-all k ltext ) ))
lt )
) ) ) (unic ltext)
))

;; приведение к виду БНФ


(defun bnf (main ltext binds) (cons (cons main (gram ltext)) binds))

; реализация развертки: names, words, lexs, d-lex, d-names,


;======================================================

; h-all, all-t, pred, sb-nm, chain, level1, lang

;; определяемыя символы
(defun names (vac) (mapcar ’car vac))

;; используемые символы
(defun words (vac) (cond

((null vac) NIL)


((atom vac) (cons vac NIL ))
(T (union (words(car vac)) (words (cdr vac)))) ))

;; неопределяемые лексемы
(defun lexs (vac) (set-difference (words vac) (names vac)))

;; самоопреление терминалов
(defun d-lex ( llex)

(mapcar #’(lambda (x) (set x x) ) llex) )

;; определение нетерминалов
(defun ( llex)

(mapcar #’(lambda (x) (set (car x )(cdr x )) ) llex) )

;; подстановка голов
(defun h-all (h lt)

(mapcar #’(lambda (a)


(cond
14.2. МОДЕЛЬ ВЫЧИСЛЕНИЙ 799

((atom h) (cons h a))


(T (append h a)) )
) lt) )

;; подстановка хвостов
(defun all-t (lt tl)

(mapcar #’(lambda (d)


(cond
((atom d) (cons d tl))
(T(append d tl))
) ) lt) )

;; присоединение предшественников
(defun pred (bnf tl)

(level1 (mapcar #’(lambda (z) (chain z tl )) bnf) ))

;; постановка определений имен


(defun sb-nm (elm tl)

(cond
((atom (eval elm)) (h-all (eval elm) tl))
(T (chain (eval elm) tl))
))

;; сборка цепочек
(defun chain (chl tl)

(cond
((null chl) tl)
((atom chl) (sb-nm chl tl))

((atom (car chl))


(sb-nm (car chl) (chain (cdr chl) tl) ))

(T (pred (all-t (car chl) (cdr chl)) tl)) ))

;; выравнивание
(defun level1 (ll)

(cond
((null ll)NIL)
800 ГЛАВА 14. ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ

(T (append (car ll) (level1 (cdr ll)) )) ))

;; вывод заданной системы текстов


(defun lang ( frm )

(d-lex (lexs frm))


(d-names frm)
(pred (eval (caar frm)) ’(())
))

Конец примера 14.2.2.

§ 14.3. ОБЪЕКТЫ И LISP

Стандартная надстройка над Common Lisp, имитирующая объектно-ори-


ентированный стиль, это модуль CLOS — Common Lisp Object System. Са-
ма по себе объектность не дает никакого выигрыша по сравнению с языком
LISP, поскольку возможности динамического вычисления функций в LISP
даже шире. Видимо, именно поэтому в CLOS имеются две интересных мо-
дификации, делающие его не совсем похожим на стандартное ООП.
Начнем с понятия структуры данных в языке Common Lisp. Структура
определяется функцией defstruct вида

(defstruct pet name (species ’cat) age weight sex),

Задание структуры автоматически задает функцию-конструктор структуры


make-pet, которая может принимать ключевые аргументы для каждого из по-
лей:
(make-pet :nick ’Viola :age ’(3 years), :sex ’femina)),
и функции доступа для каждого из полей, например, pet-nick, использующу-
юся для получения значения поля или ссылки на него. Если поле не иници-
ализировано ни по умолчанию, ни конструктором, оно получает начальное
значение NIL. Никакой дальнейшей спецификации полей структур нет2 , спис-
ки являются столь общей структурой данных, которая провоцирует игнори-
рование типов данных.
В объекте для каждого поля могут явно указываться функции доступа и
имена ключевых парамеров для инициализации аргументов.
2
Имеется редко используемая возможность указать, какого типа должно быть поле.
14.3. ОБЪЕКТЫ И LISP 801

(defclass pet (animal possession) (


(species :initform ’cat)
(nick :accessor nickof
:inintform ’Pussy
:initarg namepet)
)

Этот класс наследует поля, функции доступа и прочее от классов animal и


possession. Например, поле cost имеется в значении класса, если оно имеется
в одном из этих классов. Поскольку статических типов у полей нет, нет и
конфликтов.
Основная функция наследования в CLOS — определение упорядочения
на классах. С каждым классом связано свое упорядочение. Наследник мень-
ше своих предков, из предков меньшим считается тот, который раньше пе-
речислен в списке наследования при определении класса. CLOS достраива-
ет этот частичный порядок до линейного. Способ пополнения порядка мо-
жет быть в любой момент и без оповещения изменен, и хакерское исполь-
зование особенностей конкретного пополнения считается грубой стилисти-
ческой ошибкой. Если система находит несовместимость в определении по-
рядка, то она выдает ошибку, как в следующем примере:

[6]> (defclass init () ())


#<STANDARD-CLASS INIT>
[7]> (defclass a (init) ())
#<STANDARD-CLASS A>
[8]> (defclass b (init) ())
#<STANDARD-CLASS B>
[9]> (defclass c1 (a b) ())
#<STANDARD-CLASS C1>
[10]> (defclass c2 (b a) ())
#<STANDARD-CLASS C2>
[11]> (defclass contr (c1 c2) ())
*** - DEFCLASS CONTR: inconsistent precedence graph,
cycle (#<STANDARD-CLASS A> #<STANDARD-CLASS B>)

Типы в LISP есть, но они определяются динамически. В частности, если


атому придано как значение действительное число, его тип будет float.
В CLOS могут задаваться методы, отличающиеся от функций тем, что их
аргументы специфицированы, например,
802 ГЛАВА 14. ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ

(setf weightofanimal 3.5)) Как видно из примера, методы не обязательно связа-


(defmethod inspectpet ((x pet) (y float))

ны с классами. Они могут быть связаны с любыми типами. Методы в языке


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

Программа 14.3.1

(defclass thing ()
((weight :initform ’(0 kg)
:accessor weightof
:initarg :weight)))
(defclass animal (thing)
((specie :accessor specieof
:initarg :spec)
(sex :accessor sexof
:initform ’m
:initarg :sex)))
(defclass possession (thing)
((owner :accessor ownerof
:initform ’nnn)
(cost :accessor costof
:initform ’(0 bucks)
:initarg :cost))
)
(defclass person (animal)
((specie :initform ’human)
(name :initarg :thename
:accessor nameof)))
(defclass pet (animal possession)
((nick :initarg :thenick
:accessor nickof)
(specie :initform ’cat)))

(defmethod act :before ((p pet))


(print "Cat mews"))
14.3. ОБЪЕКТЫ И LISP 803

(defmethod act :after ((p pet))


(print "Cat turns"))
(defmethod act :around ((p pet))
(progn (print "You have a cat") (call-next-method)))

(defmethod act ((p animal))


(progn (print "Animal is close to you") (call-next-method)))
(defmethod act :before ((p animal))
(print "You see an animal"))
(defmethod act :after ((p animal))
(print "You send the animal off"))
(defmethod act :around ((p animal))
(progn (print "You don’t like wild animals") (call-next-method)))

(defmethod act ((p possession))


(progn (print "You test your property") (call-next-method)))
(defmethod act :before ((p possession))
(print "You see your property"))
(defmethod act :after ((p possession))
(print "You are pleased by your property"))
(defmethod act :around ((p possession))
(progn (print "You admire your property if it is in good state") (call-next-method)))

(defmethod act ((p thing))


(print "You take the thing"))
(defmethod act :before ((p thing))
(print "You see something"))
(defmethod act :after ((p thing))
(print "You identified this thing"))
(defmethod act :around ((p thing))
(progn (print "You are not interested in strange things") (call-next-method)))

(act (make-instance ’pet :thenick "Viola" :cost ’(25 kop)))

При загрузке этого файла происходит следующее:

[1]> (load ’myclasses)


;; Loading file E:\clisp-2000-03-06\myclasses.lsp ...
804 ГЛАВА 14. ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ

"You have a cat"


"You don’t like wild animals"
"You admire your property if it is in good state"
"You are not interested in strange things"
"Cat mews"
"You see an animal"
"You see your property"
"You see something"
"Cat purrs"
"Animal is close to you"
"You test your property"
"You take the thing"
"You identified this thing"
"You are pleased by your property"
"You send the animal off"
"Cat turns"
;; Loading of file E:\clisp-2000-03-06\myclasses.lsp is finished.
T
Как видите, упорядоченность классов по отношению наследования по-
зволяет выстраивать целые последовательности действий при вызове одного
метода.
Поскольку в CLOS нет ни механизмов скрытия конкретных представле-
ний, ни механизмов замены прямого доступа к данным на функции, ни дру-
гих характерных особенностей ООП, мы видим еще один пример того, как
модным словом прикрывается совершенно другая (и на самом деле не менее
интересная) сущность: начатки планирования действий по структуре типов
данных.
Такое неадекватное теоретизирование, как мы уже не раз убеждались, за-
ставляет выпячивать слабейшие места концепции и мешает увидеть и раз-
вить ее реальные достониства.
Вопросы для самопроверки
1. Из примера 14.2.1 можно извлечь информацию о целых числах в языке
LISP. Сделайте это.

2. Из программы 14.2.1 установите, что является в LISP признаком ком-


ментария.
Глава 15

Моделирование

С самого своего отделения от прачеловека человек строил вычислитель-


ные модели. Лунно-солнечно-венерианские календари, видимо, знали уже в
Гиперборее (или Арктиде: легендарной прародине цивилизации, далеко на
Севере, там, где полгода день и полгода ночь); во всяком случае, они най-
дены на костях мамонтов в Сибири. Так что математическое моделирование
и его вычислительная реализация —один из главных методов познания. Как
следствие, он не может быть и, разумеется, не был обделен вниманием тех,
кто занимается проблематикой информатики и программирования.
Одной из первичных целей использования вычислительных машин бы-
ло и остается моделирование некоторых аспектов реального мира. Область
данных модели обычно называют предметной областью. При таком модели-
ровании вычислительные машины используются как инструмент проециро-
вания модели на оборудование: модель (как правило, математическая) здесь
остается вне комплекса вычислительных средств, а задача программистов
сводится к построению ее интерпретации на данном оборудовании. Другой
вид моделирования — когда модель предметной области включается в вычи-
слительную систему. Это можно делать по-разному, и компьютерные модели
можно подразделить на несколько классов, в зависимости от того, что требу-
ется от моделирования.
Исторически первый классом моделей, появивишийся задолго до вычи-
слительных машин, для которого вычислительные средства являются лишь
инструментом, — вычислительные модели. Древние строили их на костях
мамонтов [47], варвары использовали мегалиты типа Стоунхеджа, европей-
цы XVIII века использовали часовые механизмы, европейцы и американцы
первых двух третей XX века — аналоговые машины. Этот тип моделирова-

805
806 ГЛАВА 15. МОДЕЛИРОВАНИЕ

ния стал одним из первых применений первых компьютеров.


Другая область моделирования, к которой привлекаются вычислительные
машины — непосредственное построение систем объектов (в любом пони-
мании этого слова), которые отображают в вычислительной системе реаль-
ные объекты и их связи, признанные существенными в заданных для моде-
ли аспектах предметной области. В этой области мы сталкиваемся с мно-
жеством стратегий и приемов отображения выделенных аспектов реального
мира.
Часто здесь в качестве цели рассматривается принятие решений на осно-
ве модельных данных, по разным причинам подменяющих данные реально-
сти (невозможность или затруднительность их получения, упрощение для на-
блюдения и др.). Этот вариант получил название имитационного моделиро-
вания. Пожалуй, автором его можно считать Клавдия Птолемея, построивше-
го имитационную модель Солнечной системы в геоцентрических координа-
тах, моделируя движения планет как движения систем сфер. Так что и здесь
история метода началась значительно раньше истории его внедрения в про-
граммирование. Но первые вычислительные машины были слишком прими-
тивны для такого моделирования, хотя уже через полтора десятка лет после
машин Цузе они доросли до него. И сразу же стало ясно, что такие модели
требуют своих программных средств.
Появилось довольно много языков и систем программирования (как пра-
вило, включавших собственный жаргон для описания моделей, и тем самым
являвшихся скорее недоделанными языками), ориентированных на модели-
рование для специальных предметных областей, а также языков, которые
претендовали на возможность всеобщего применения для имитационного
моделирования. Был ряд прагматических разработок, из которых дольше все-
го удерживалась CSSL на базе FORTRAN. Из общего прагматического ря-
да выделялись GPSS (язык, построенный на базе FORTRANЎа для моде-
лирования поведения вычислительных устройств — этакая «самопримени-
мость» моделирования с использованием вычислительного оборудования),
SIMSCRIPT и Simula (языки общего назначения для данной сферы примене-
ния). Последний из упомянутых языков заслуживает особого внимания пре-
жде всего потому, что его разработчикам удалось построить концептуально
проработанную систему, а не просто набор средств, помогающих описывать
модели программами. Осознание продуктивности такого подхода позволило
им осуществить в этой области блестящий для своего времени прорыв Simula
67, впервые создавший концепцию объектного программирования.
Еще один класс систем моделирования, также связываемый с принятием
807

решений, исходит из сбора и хранения реальных данных (опять-таки касаю-


щихся лишь выделенных аспектов), которые отображают сведения об объек-
тах предметной области. Это информационные системы.
Все упомянутые выше варианты моделирования носят прикладной ха-
рактер в том смысле, что назначение соответствующих систем — обслужи-
вание специалистов, интересы которых лежат вне области программирова-
ния и информатики. Вместе с тем моделирование как метод, используемый
в данной отрасли человеческой деятельности, не ограничивается уровнем
приложений. С самого начала развития этой отрасли моделирование имеет
весьма разнообразные цели. Здесь и моделирование вычислительными ма-
шинами математических систем вычислений (к сожалению, далеко не всегда
достаточно точное), и построение абстрактных вычислителей, моделирую-
щих в языках действия конкретных вычислителей (адекватность таких моде-
лей вполне удовлетворительна), и даже моделирование процесса разработки
программной системы CASE-средствами (уровень точности зависит от соот-
ветствия между предметной областью и областью, на которую ориентирован
используемый инструмент). Полный перечень задач такого рода привести не
представляется возможным.
Методов моделирования, направлений и подходов к нему великое множе-
ство. В данной книге мы касаемся лишь некоторых аспектов моделирования,
и лишь с точки зрения особенностей программирования, характерных для
обсуждаемого подхода. Мы не будем подменять систематическое изложение
методов моделирования поверхностным обзором, а вместо этого дадим пред-
ставление лишь о некоторых из них, делая упор на предостережениях, с чем
можно столкнуться в том или ином случае.
В частности, вопросы, скажем, связанные с аналитическими моделями
и задачи статистического моделирования – предмет специального изучения,
и в программистском плане можно счиать, что они сводятся к правильному
выполнению этапа анализа жизненного цикла разрабатываемой системы и к
соблюдению должных регламентов технологии разработки. В то же время,
есть общие положения, которые проявляются в подавляющем большинстве
проектов, цель которых — моделирование. К ним относятся:
a) вычислительные трудности моделирования (точность и корректность ре-
зультатов счета, правильность их интерпретации и т. п.);

b) информационное обеспечение моделирования (задачи создания и исполь-


зования информационных систем);
808 ГЛАВА 15. МОДЕЛИРОВАНИЕ

c) моделирование времени (вопросы, касающиеся того, какую роль играет


понятие времени при разработке моделирующих систем.

§ 15.1. МОДЕЛИ И ВЫЧИСЛЕНИЯ

Любые расчеты, для которых применяется вычислительное оборудова-


ние, можно рассматривать как моделирование вычислений над математиче-
скими объектами вычислениями, которые осуществляются на реальной ма-
шине. И здесь возникает первое препятствие для использования получаемых
данных: законы, которым подчиняется оперирование с математическими
объектами, не действуют на объектах, которые используются на уровне
машинного представления данных.
Недостаточно говорить о точности вычислений, понимая просто исполь-
зование нужного количества знаков «после запятой». Хорошо известно, что
ошибки счета, возникающие из-за округлений, могут накапливаться и приво-
дить к ситуациям, когда они оказывают существенное влияние на значащие
разряды чисел. Но и тогда, когда мы можем считать точными исходные ар-
гументы операций, участвующих в вычислениях, не исключаются ситуации,
при которых результат окажется не таким, как он ожидается. Наглядный при-
мер — нарушение ассоциативности сложения: легко подобрать четыре такие
числа A, B, C и D, для которых, а точнее, для машинных представлений ко-
торых неверно, что
(A + B) + (C + D) = A + (B + C) + D
(придумайте сами такие числа для вашего компьютера).
Гарантированные вычисления (вычисления на компьютере, обеспечива-
ющие доказательную правильность некоотрых свойств результата) — сложд-
нейший аспект алгоритмики, игнорируемый в стандатных курсах для студен-
тов. Существуют тонкие, но частные, теоретические проработки и постро-
енные на них методики, с помощью которых можно бороться в ошибками
счета. Безусловно, их надо знать и использовать на практике. Но надо четко
представлять себе, что все они не могут применяться за пределами, которые
определены при их разработке.
Хуже всего то, что запись программы на языке программирования про-
воцирует пользователя не замечать ошибок вычислений. Автоматический же
контроль их появления даже в случаях, когда он возможен, резко снижает эф-
фективность счета. В результате у пользователя создается впечатление, что
15.1. МОДЕЛИ И ВЫЧИСЛЕНИЯ 809

«все правильно» и можно интерпретировать результаты счета как реальные


величины.
Мы уже разбирали, что предлагается в языках программирования для
поддержки преодоления ошибок за счет контроля разрядности (например,
средства типизации арифметических значений в Ada — см. п. 9.2.5). Однако
даже эти весьма скромные и не отвечающие реальной потребности средства,
достаточно легко применимые к поименованным (пусть даже косвенно) объ-
ектам, не применимы к промежуточным результатам вычислений, которые
не видны в исходной программе и которые неотделимы от платформы вычи-
слений.
Мы не собираемся подменять систематическое изложение разделов вы-
числительной математики, изучающих численные методы, заведомо непол-
ными выдержками и рецептами. Наша цель состоит в одном: предупредить
читателя и посоветовать ему для получения необходимых знаний обращаться
к первоисточникам.
Пример 15.1.1. Давно известно (см. напр. [24]), что вычисление собствен-
ных значений — безнадежно некорректная задача, и что на самом деле в
практических целях нужно вычислять инвариантные подпространства ли-
нейных операторов. В CERN FORTRAN накоплена громадная библиотека чи-
сленных методов, которая является одной из причин живучести такого релик-
та, как FORTRAN. О качестве проработки этих методов красноречиво гово-
рит факт наличия в этой библиотеке целого раздела, посвященного вычисле-
нию собственных значений матриц. Так что концептуальные ошибки никогда
не ходят в одиночку.
Конец примера 15.1.1.
То, о чем шла речь выше — объективные факторы. Но есть еще и субъек-
тивные факторы, также препятствующие адекватности выполнения и ис-
толкования расчетов. Они проявляются из-за того, что программирование
выполняют конкретные люди, которым свойственно ошибаться. Эти факто-
ры можно классифицировать следующим образом:

a) ошибки в исходных допущениях — разработчики неправильно понимают


условия, в которых должно применяться проектируемое программное из-
делие;

b) алгоритмические ошибки — используются некорректные или неприме-


нимые в конкретных условиях алгоритмы;
810 ГЛАВА 15. МОДЕЛИРОВАНИЕ

c) ошибки проектирования — декомпозиция проекта проведена без учета


требований корректности расчетов (не предусмотрен контроль результа-
тов, не продумана тестовая база, не запланирован специальный контроль
согласования межмодульных взаимодействий и др.);
d) индивидуальные ошибки программирования — неправильно запрограм-
мированные верные алгоритмы, нарушение регламентов разработки и про-
ектной дисциплины;
e) ошибки в применении программного изделия и/или в интерпретации ре-
зультатов счета — результаты трактуются не в соответствии с исходны-
ми предположениями и допущениями.
В качестве общей рекомендации для того, чтобы преодолевать субъектив-
ные препятствия, можно указать следующее: заранее всесторонне продумай-
те концепции развиваемого проекта, уделите внимание организационным во-
просам, регламентирующим технологию разработки, учитывайте опыт соб-
ственных и чужих ошибок, дабы не повторять их. Не стоит игнорировать
общие предписания и рекомендации, которые сопутствуют вашей разработ-
ке, и, в частности, осознанно выбирайте жизненный цикл развития проекта.
Среди общих рекомендаций особое место занимают вопросы стилей, кото-
рым вы будете следовать при выполнении программистских работ. Надо чет-
ко осознавать, что только адекватность стиля решаемой задаче может, если
не гарантировать качество решения, то хотя бы способствовать успеху.

§ 15.2. МОДЕЛИРОВАНИЕ ВРЕМЕНИ

Время — одно из центральных мпонятий при имитационном моделирова-


нии. Понятие времени в модели, если оно не соответствует реалиям предмет-
ной области чаще всего просто губит модель, делает работу с ней неадекват-
ной. Поэтому нужно рассмотреть разные подходы к моделированию време-
ни. Конкретная модель времени выбирается исходя из потребностей, которые
диктуются прикладной областью и тем, как используются модели. Поэтому
разделим задачи моделирования на два класса в соответствии с принимаемой
точкой зрения на модельное время:
• наблюдение — соответствует имитационному моделированию, в кото-
ром модельное время, а точнее, связанные с ним события только от-
слеживается, а все эндогенные (внешние) воздействия осуществляют-
ся как коррекции траекторий развития имитационной системы. В этот
15.3. ИНФОРМАЦИОННЫЕ СИСТЕМЫ С ВРЕМЕНЕМ 811

класс попадают также системы прямого мониторинга реальных про-


цессов, когда внешняя коррекция траекторий не допускается;

• реальное развитие — соответствует информационным системам с вре-


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

В первом случае имеется столько моделей развития имитируемой системы,


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

§ 15.3. ИНФОРМАЦИОННЫЕ СИСТЕМЫ С ВРЕМЕНЕМ

Учет времени — одно из критических мест в современных информаци-


онных системах. Именно здесь в них возникают связи с моделировнаием.
Обсудим возникшую ситуацию.
В настоящее время наиболее попоулярным видом информационных си-
стем являются реляционные базы данных. Моделирование в данной обла-
сти возникает, когда разрабатываются средства ведения баз данных. Как мы
имели возможность убедиться, почти полная монополия реляционных СУБД
обусловлена не столько неоспоримыми их преимуществами, сколько успеш-
ным захватом экологической ниши. Те же причины, по которым применение
таких систем для хранения объектных сред является неадекватным, относят-
ся и к задачам, связанным с необходимостью информационных систем со
временем.
Наиболее очевидное, и, как обычно, одно из наиболее неудачных реше-
ний в данном вопросе — реализовать в реляционных таблицах специальный
812 ГЛАВА 15. МОДЕЛИРОВАНИЕ

атрибут, отвечающий за отслеживание временных изменений хранимых дан-


ных, и рассматривать его как обычный атрибут хранимых данных. Этот путь
ведет к чрезмерному росту числа таблиц и их объемов, а использование вре-
мени в запросах все-таки принципиально отличается от того, как оперируют
с остальными атрибутами, так что и унификации подхода не получается. На-
пример, в свое время был предложен диалект языка SQL, в предложениях
которого можно было указывать этот специальный временной атрибут, явно
нигде его не определяя. Но этот диалект так и остался диалектом, не получив
распространения.
Немногим лучше объектные и объектно-реляционные СУБД с временем
(например, POSTGREATE). Здесь, правда, за счет сетевой модели предста-
вления данных избыточность резко снижается. Но особый статус времени
никак не задействован, со всеми вытекающими отсюда последствиями.
Если проанализировать как часто используют СУБД с временем в при-
кладных информационных системах, то выяснится, что реального распро-
странения они не получили. Обычно, когда требуется оперирование с време-
нем, оно реализуется на уровне приложений, т. е. без поддержки средствами
СУБД. Причина тому глубже, чем просто неэффективная реализация (если
бы актуальность такой поддержки была осознана, нужные реализационные
механизмы, конечно же, сумели бы придумать, как это было сделано для са-
мих реляционных баз). Здесь явно просматривается отсутствие концепту-
альной проработки понятия времени в системах, которые мы охарактеризо-
вали свойством реального развития.
Рассмотрим одну из важнейших побудительных причин применения СУБД:
поддержка целостности данных. Без этого качества, предоставленного для
всех пользователей в унифицированном виде, особенно при коллективном
использовании, никакая СУБД не может претендовать на полезность. Имен-
но поэтому понятие целостности и средства ее поддержки нашли свое от-
ражение и в теоретических исследованиях, и в проектах. Именно поэтому к
настоящему времени удалось достичь общепризнанных соглашений о том,
какую поддержку целостности нужно возложить на СУБД, а какую ее часть
целесообразно оставить на стороне приложений. Одним из наиболее значи-
тельных достижений этих соглашений следует рассматривать формирование
транзакционного механизма, который в части, касающейся целостности, га-
рантирует что ни при каких обстоятельствах согласованность данных и их
связей не нарушается.
Целостность в реляционных СУБД — это, прежде всего, обеспечение в
информационной системе того, что в ней не появляются зависшие ссылки,
15.3. ИНФОРМАЦИОННЫЕ СИСТЕМЫ С ВРЕМЕНЕМ 813

т. е. нельзя просто так удалить элемент данных, на который ссылаются какие-


либо другие элементы. Также нельзя произвольно манипулировать полями
(добавлять и удалять их) без соответствующего контроля, который влечет за
собой возможность рассогласования связей. Если же проанализировать, что
может означать рассогласование связей при использовании СУБД с време-
нем (именно это и не было сделано для упомянутых выше разработок), то
сразу же обнаруживается, что в соответствующих им информационных си-
стемах необходимо отслеживать не только атрибутные связи, но и зависимо-
сти от времени. В самом деле, события, происходящие в предметной обла-
сти и отраженные в соответствующих хранимых объектах системы, влияют
на другие объекты, причем даже на те их атрибуты, которые появятся или
изменятся в будущем. Это требует того, чтобы будущие сведения предста-
влялись динамическими ограничениями на домены соответствующих атри-
бутов, чтобы при занесении в базу данных сведений об объектах происходила
корректировка хранимых данных, причем направленная как в будущее, так и
в прошлое. Необходимо также корректировать и запросы, которые зависят от
таких актах модификации данных. Словом, понятие временной целостности,
без которого СУБД, обеспечивающие информационные системы с временем,
немыслимы, нуждается в серьезной концептуальной проработке, отсутствие
которой, на наш взгляд, объясняет сегодняшнюю непопулярность таких си-
стем.
В данном случае мы наблюдаем исключительно редкий в современной
иформатике феномен: пользователи информационных систем с временем от-
вергают концептуально слабо проработанные системы. Видимо, это объяс-
няется тем, что пользователи систем рассматривают их не как программные
системы, а как базу реальных данных, и поэтому подходят к ним с точки
зрения здравого смысла и концептуальной ясности. Они просто не являются
программистами.
Характеристической особенностью современного программиста являет-
ся умение работать в исключительно плохо организованной обстановке сред-
ствами, которые по сути своей либо не подходят для решаемой задачи, либо
плохо согласуются друг с другом. Он должен уметь приспосабливать содер-
жание задачи к извращенным формам, которые ему предлагают доступные
средства. Именно поэтому задача концептуальной целостности практически
никогда не ставится, а задача нахождения и устранения либо обхода концеп-
туальных противоречий просто замалчивается.
Пока профессиональной доблестью будет считаться умение извлекать глан-
ды через любое отверстие в системе, приципиальных продвижений в про-
814 ГЛАВА 15. МОДЕЛИРОВАНИЕ

граммировании ждать трудно, и оно будет все больше инструментом, по вы-


ражению Г. Вейценбаума, компьютерной контрреволюции.

§ 15.4. МОДЕЛИРОВАНИЕ И ИНФОРМАЦИОННЫЕ СИСТЕМЫ

Вопросы, которые возникают при решении задач создания и использова-


ния информационных систем, могут рассматриваться в трех планах.
1. Соотношение между построением конкретных моделей и их информа-
ционным обеспечением.
2. Информационные системы рассматриваются как отрасль моделирова-
ния, применяемого для принятия решений.
3. Моделирование организации сбора и хранения сведений (в управляю-
щих системах, в системах реального времени и т. д.).
Три эти аспекта смыкаются в соотношениях между понятиями информаци-
онной системы, базы данных и системы управления базами данных (СУБД).
В наиболее общем виде информационная система — система, которая обес-
печивает пользователя информацией, извлекаемой из хранимых данных. Если
выделить часть действий, производимых безотносительно смысла данных,
то получится следующий список:
a) поступление данных;
b) хранение данных;
c) модификация данных;
d) получение, или извлечение, данных.
Такие действия называют манипулированием данными. Эта часть действий,
связанных с информационной системой, обеспечивается базой данных. Един-
ственное, что база данных в состоянии знать — некоторые формальные кри-
терии корректности данных. Она может (и должна!) проверять эти критерии,
когда данные поступают на хранение и когда они модифицируются.
Пример 15.4.1. В качестве одного из примеров критериев естественно рас-
смотреть доменный анализ. Модифицируемое или вновь вводимое данное
проверяется на принадлежность некоторому множеству, называемому доме-
ном допустимых значений. Домен может зависеть от других данных. Напри-
мер, зарплата заместителя может быть от 60 до 80% зарплаты начальника.
Конец примера 15.4.1.
15.4. МОДЕЛИРОВАНИЕ И ИНФОРМАЦИОННЫЕ СИСТЕМЫ 815

Есть общие критерии, выполняющиеся независимо от того, о какой базе дан-


ных идет речь. В частности, если определено, что некоторые элементы дан-
ных связаны между собой (например, имеют ссылки друг на друга), то об-
щий критерий корректности сохранения связей означает проверку этого при
удалении, а иногда и при модификации объектов. В хорошо устроенной базе
данных нарушение такого критерия приводит к активизации реакции, целью
которой является корректировка связей.
Обеспечение всех общих критериев при манипулировании данными ба-
зы называется целостностью базы данных, а действия, направленные на то,
чтобы при всех (локальных) нарушениях целостности она немедленно (до
последующих пользовательских обращений к базе) восстанавливалась, —
поддержанием целостности.
Теперь перейдем к аспектам, связанным с содержанием.
Во многих случаях из базы данных требуется извлекать не только те све-
дения, которые были переданы ей на хранение. Простейший случай — аг-
регирование данных, т. е. получение интегральных характеристик для не-
скольких хранимых элементов (например, суммирование значения какого-
либо атрибута по всем элементам, его содержащим). В более сложных слу-
чаях, когда требуется извлекать результат вычисления некоторой функции от
хранимых элементов. Иногда некоторый элемент данных, получаемый в ре-
зультате таких вычислений, нужно представлять для пользователя так, как
будто он хранится в базе, тогда говорят о вычисляемых данных, или о вир-
туальных данных. Можно хранить вычисляемые данные, и развитая систе-
ма сама может решать, какие из них экономнее вычислять при запросах, а
какие хранить. В последнем случае среди критериев целостности должны
быть представлены такие, которые указывают на необходимость обновления
вычисляемых данных.
Если база в качестве данных хранит только факты, т. е. такие данные,
которые, будучи помещены, могут меняться лишь по раз и навсегда фик-
сированным правилам, то такая база называется фактографической. Если в
базе данных предусмотрены возможности продуцирования данных с помо-
щью правил, которые хранятся как факты, то такая база превращается в базу
знаний. Граница между базами данных и знаний довольно условна, обычно
в литературе четкого их определения не дается, и это повод для появления
разного рода спекуляций.
Конструирование информационных систем не сводится к построению баз
данных. Для одной и той же предметной области в зависимости от назначе-
ния системы почти всегда будут нужны разные базы данных. И только в ред-
816 ГЛАВА 15. МОДЕЛИРОВАНИЕ

ких случаях удается стандартизованными средствами оперирования с базами


обеспечить все необходимое в конкретной информационной системе.
Суть информационных систем — решение задач, для которых они нуж-
ны. Это предполагает обстоятельное исследование предметной области, ре-
альных, а не декларируемых потребностей и других особенностей, которые
должны изучаться в любом программном проекте (см. п. 4.4.1). Специфи-
ка разработки информационных систем такова, что на этапе анализа в ходе
определения требований и особенно при выборе ближайшей задачи начина-
ющегося проекта необходимо сначала провести такие исследования и уже на
их основе приступать к проектированию базы данных. Как часто это условие
оказывается не выполненным! И так же часто именно это становится причи-
ной многих неудач: переписывание кода, разработка новой структуры базы
данных (особенно болезненное, когда переделываемая база уже использует-
ся и требуется сохранение накопленных данных), замена интерфейса — эти и
другие дополнительные задачи приходится решать, если аналитический этап
проекта выполнен недоброкачественно.
Пример 15.4.2. В задаче моделирования школы были предусмотрены опе-
рации, вязанные с ученическими завтраками, но не был выделен абстракт-
ный класс завтракающих. Когда выяснилось, что нужно учитывать еще учи-
телей и других работников, которые тоже не прочь позавтракать, пришлось
реализацию запросов переписывать заново, а базу данных целиком переструк-
турировать.
Конец примера 15.4.2.
На этапах анализа и конструирования проявляются и различия трех аспектов
информационных систем, о которых было сказано выше.

15.4.1. Информационное обеспечение моделирования

Если система строится для информационного обеспечения моделирова-


ния, то именно она оказывается тем связующим звеном между реалиями пред-
метной области, выделяемыми моделью, средствами управления модельным
поведением и аналитическими инструментами, с помощью которых пользо-
ватель изучает получаемые результаты счета. Это обстоятельство предопре-
деляет разработку следующих компонентов в проекте моделирования:
a) подсистема ввода исходных данных для модели. Анализ показывает, что
не всегда можно использовать предоставляемые внешние данные непо-
средственно. Их предварительная подготовка (о которой, как показывает
15.4. МОДЕЛИРОВАНИЕ И ИНФОРМАЦИОННЫЕ СИСТЕМЫ 817

практика, чаще всего забывают!) обусловлена как необходимостью согла-


сования форматов внешних данных с данными информационной систе-
мы, так и контролем ошибок ввода. На возможности подсистемы влияет
и то, как часто будет осуществляться процедура ввода, и то, нужно или
нет редактировать входные данные;

b) подсистема модели. Часть системы, инкапсулирующая модельные алго-


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

c) подсистема инструментария для работы с моделью. Определяет, как поль-


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

d) база данных информационной поддержки моделирования. Активное хра-


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

15.4.2. Информационные системы для моделей принятия решений

Специфика информационных систем, моделирующих реальные процес-


сы для поддержки принятия решений, состоит в следующем.
Во-первых, они должны ориентироваться на работу с реальными дан-
ными, и, как следствие, с одной стороны, нужна специальная забота о со-
ответствии хранимой информации сведениям, получаемым из предметной
области (поддержка контроля ошибок, организация справочников и других
818 ГЛАВА 15. МОДЕЛИРОВАНИЕ

средств обеспечения удобного и корректного ввода), а с другой — они долж-


ны адекватно отражать объекты, их связи и процессы прикладной область с
тем, чтобы принимаемые решения были бы максимально точны.
Во-вторых, нужна информационная точность моделирования, в частно-
сти:
a) достоверность. Проводимые расчеты следует адаптировать к наблюде-
ниям, должны соблюдаться внешние и модельные ограничения, необхо-
дима постоянная контрольная обратная связь со средой выполнения ре-
альных процессов;
b) гарантируемый уровень точности. Нужно, чтобы пользователь ясно себе
представлял границы применимости информации. Знание точности рас-
четов позволяет повышать и точность управления. В частности, необходи-
мо заботиться о доверительных интервалах, указании пределов, в которых
расчеты остаются достоверными;
c) неизбыточность. Предъявляемая информация должна быть очищена от
незначащих данных, интегрирована для данный условий управления;
d) своевременность. Предъявление информации должно происходить в точ-
но установленные сроки, когда она нужна. Обычно достаточно просто
установить критический временной интервал полезности информации (см.
рис. 15.1), который начинается несколько позднее ее возникновения (с мо-
мента, когда сведения реально можно использовать) и простирается до то-
го момента, когда информация становится бесполезной. Максимум полез-
ности достигается в начале этого интервала, какое-то время полезность
сохраняется, а затем резко падает. За критическим интервалом информа-
ция для принятия решений чаще всего бесполезна1 .
Сравнивая использование информационных систем для моделирования ре-
альных процессов и как инструмента принятия решений, легко заметить, что
здесь сужается применимость многовариантных расчетов, поскольку о ре-
альном сравнении вариантов можно говорить только для самых квалифици-
рованных руководителей2 . Зато повышается роль оперативности получения
результатов. Возрастает нагрузка на интерфейс, т. к. нужны разнообразные
наглядные средства предъявления как исходных, так и расчетных данных.
1
Она может быть использована лишь в качестве поучительного примера.
Но зато в этом случае многовариантность необходима, поскольку такой человек никогда
не захочет следовать однозначной подсказке.
2
15.5. СИСТЕМЫ С ДИСКРЕТНЫМИ СОБЫТИЯМИ 819

Ценность
информации для
управления Интервал максимальной
полезности информации

Время
Момент Критический интервал
возникновения полезности информации
информации

Рис. 15.1. Полезность информации для пользователя

Информационные системы как самостоятельные, не связанными с моде-


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

§ 15.5. СИСТЕМЫ С ДИСКРЕТНЫМИ СОБЫТИЯМИ

Концепция времени является одной из важнейших во многих областях.


Более того, в известном американском аформзме Time is money абсолютная
ценность низводится до уровня условной. Время мы не можем получить ни
от кого, не можем сохранить, можем лишь тратить.
Поэтому при рассмотрении компьютерного моделирования поведения объ-
ектов целесообразно выделить два аспекта моделей:
820 ГЛАВА 15. МОДЕЛИРОВАНИЕ

• образы действующих объектов и

• моделирование времени.

Если время не моделируется в прямом смысле этого слова, а берется из реаль-


ного мира, говорят о системах реального времени. Если же время является
частью виртуального мира системы, то говорят об условном, или модельном,
времени.
Рассмотрим тот случай, когда время может быть представлено как линей-
но упорядоченная совокупность шагов, а все процессы, протекающие между
шагами, можно считать неделимыми. Тогда на временной оси у нас есть лишь
дискретная совокупность точек, в которых нужно поддерживать целостность
состояния модели и каждого из процессов. Если вдобавок к этому время явля-
ется модельным, то говорят о системе с дискретными событиями. Эта си-
стуация хорошо подходит как для применения, так и для моделирования как
конвейерного, так и ∨-параллелизма (см. Приложение А).
Вернемся к уже несколько раз встречавшейся задаче нахождения пути,
которую мы использовали для анализа переборных и рекурсивных методов
программирования. В данном случае модифицируем ее следующим образом.
Пусть нам нужно найти кратчайший путь в нагруженном ориентированном
графе, где каждая дуга оснащена числом. интерпретируемым как ее длина.
Это традиционно интерпретируется как определение кратчайшего пути меж-
ду городами A и B, связанными сетью однонаправленных дорог.
Покажем, что решение задачи можно построить как систему взаимодей-
ствующих процессов с дискретным временем. Это является хорошим мето-
дом структурирования задачи и отделения деталей от существенных черт:
сначала мы строисм абстрактное представление. а затем конкретизируем его,
например, укладывая по сути своей параллельный алгоритм в рамки после-
довательной программы3 . Идея этого подхода восходит к У.-И. Далу и К. Хо-
ору, которые использовали данную задачу для демонстрации возможностей
системы с дискретными событиями, предоставляемой языками SIMULA 60 и
SIMULA 67, совпадавшими по модели времени и структуре управления про-
цессами.
Базовой концепцией в данной задаче естественно является vee-паралле-
лизм. Определение кратчайшего расстояния можно представить как соревно-
Поскольку здесь мы переходим от сравнительно идеальных к низкоуровневым поняти-
ям. такая задача ‘распоследовательностнивания’ является методологически и практически
3

правильно поставленной, в отличие от обратной задачи распараллеливания.


15.5. СИСТЕМЫ С ДИСКРЕТНЫМИ СОБЫТИЯМИ 821

вание действующих агентов4 , «разбредающихся» по разным дорогам, в ско-


рости достижения цели. Сразу же разграничиваются два класса алгоритмов:
прямые, когда общий процесс начинается с A, и обратные, для которых стар-
товым городом назначается B. Для показа принципиальных моментов доста-
точно рассмотреть по одному прямому и обратному алгоритму.
Прямой алгоритм разбредающихся агентов можно описать как поведение
каждого агента по следующей схеме, в качестве параметра которой задается
местонахождение агента:
1. Если агент стоит в городе, то

(a) Если местонахождение агента есть B, то цель достигнута. В каче-


стве результата выдается пройденный путь.
(b) Агент проверяет, является ли город запретным. Если это так, агент
ликвидируется (понятно, что при этом информация по системе в
целом не теряется — другие агенты продолжают действовать).
(c) Город, в котором стоит агент, объявляется запретным.
(d) Порождается столько наследников агента, сколько дорог исходит
из текущего местонахождения данного агента. При этом в каче-
стве локальных данных новых агентов задается пройденный путь,
запомненный родительским агентом от A до текущего местонахо-
ждения (не принципиально, уничтожается ли родительский агент,
или он становится одним из экземпляров наследников). Если нет
дорог из текущего местонахождения, то агент ликвидируется —
он зашел в тупик.

2. Переход к следующему моменту времени — для каждого агента осу-


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

3. Осуществляется переход к пункту 1.


Видно, что алгоритм завершает работу, когда найден путь из A в B, либо
когда все агенты оказываются ликвидированы.
В данном случае для подчеркивания специфики экземпляры объектов лучше называть
так.
4
822 ГЛАВА 15. МОДЕЛИРОВАНИЕ

Как и оычно, более жесткие условия на окончательную программу влекут


за собой во многих отношениях более мягкие требования к прототипу (нужн
расширить возможности его перестройки в разные варианты окончательной
программы), но при этом желательно максимально возможное повышение
уровня понятий прототипа. Если в конце концов программа будет реализо-
ывана на фон Неймановской системе, не нужно обращать внимание на то,
сколько агентов могут действовать одновременно. В частности, в случае га-
рантированного наличия пути из A в B можно не проверять запретность либо
ослабить ее до случая, когда предок данного агента посещал данный город, и,
следовательно, он оказался на запомненном пути. В этом случае не потребу-
ется запоминание глобальной, а точнее общей для всех агентов информации
о графе дорог и городов.
Приведенный алгоритм нуждается в запоминании сведений о пройден-
ном пути в локальных данных агентов. Это означает большую избыточность
хранения: локальные данные каждого из агентов, которые будут ликвидиро-
ваны, хранятся напрасно. А если их забывать, то искомый путь не может быть
получен, получаются лишь некоторые его характеристики. Этого недостатка
лишено решение с помощью обратного блуждания, т. е. от B к A. Для него
все работает точно так же за исключением следующего:
a) локальная информация об агентах и их путях не запоминается;
b) в качестве пометки о посещении указывается, из какого города агент при-
шел в данный город (это можно называть пометкой-рекомендацией).
В результате, когда какой-либо из агентов достигнет A, последовательность,
начинающаяся с A и выстраивающаяся по пометкам-рекомендациям, дает ис-
комый путь (именно этот обратный алгоритм излагается Далом и Хоором).
Рис. 15.2 иллюстрирует работу всех трех алгоритмов: прямого (a), прямо-
го с пометками (b) и обратного с пометками-рекомендациями (c). Надписи на
дугах-дорогах обозначают агентов, верхние индексы на них — порядок по-
рождения агентов. В косых скобках записаны локально хранимые сведения о
посещаемых городах. Зачеркнутые надписи указывают на уничтожение аген-
та в связи с использованием сведений о посещениях городов.
Как прямой, так и обратный алгоритмы работоспособны, если обеспечить
одновременную работу сразу всех процессов агентов. Если модель вычисле-
ний отражает возможность многопроцессорной обработки, причем с потен-
циально неограниченным5 числом процессоров, то это условие выполнено
5
По крайней мере с заведомо большим. чем нужно для данной задачи.
15.5. СИСТЕМЫ С ДИСКРЕТНЫМИ СОБЫТИЯМИ 823

A11/A, t1/ A114/A, t1, t2/ t6

t2
A1/A/ t1 A113/A, t1, t2/
A A111 /A, t1, t2/
A2/A/ B
A112/A, t1, t2/
A21/A, t3/ t A211/A, t3, t4/ A211/A, t3, t4, t5/
t6 t3 4 t5

a) прямой алгоритм с локальным запоминанием пути

A11/A, t1/ A114/A, t1, t2/ t6

A1/A/ t2
A t1 A /A, t1, t2/
111 A113/A, t1, t2/
A2/A/ A112/A, t1, t2/ B

t6 t3
A21/A, t3/ t A211/A, t3, t4/ t5 A2111 /A, t3, t4, t5/
4

b) прямой алгоритм с пометками


A21 t6

A212 t2/B
A/t3 t1/t2 A2
A 211

A1211 A11 B

t6 A1212 A121 A12 A1


t3/t4 t4/t5 t5/B

Рис. 15.2. Логика алгоритмов поиска пути


824 ГЛАВА 15. МОДЕЛИРОВАНИЕ

автоматически.
Если параллелизм возможен, но процессов недостаточно (например, если
Вы решите воспользоваться для реализации этих алгоритмов Prolog-систе-
мой Muse), то мы находимся в самой трудной ситуации, требующей творче-
ских решений, комбинации научного и инженерного подхода.6
Для языка, отвечающего фон Неймановской модели вычислений, парал-
лелизм очевидно невозможен, а значит, решение подсказывается само собой:
необходима имитация параллелизма. Одним из подходов к такой имитации
является, к примеру, хорошо известный метод волны, который исходит из
строго упорядоченного последовательного перебора вариантов, соответству-
ющих агентам-процессам. Но данный метод есть полное устранение агентов,
поскольку все их параллельные действия заменяются выстраиванием кон-
кретной последовательности их выполнения, не требующей синхронизации,
а потому агенты нужны лишь в качестве призраков. Метод выстраивания про-
цессов в линейную последовательность — одно из самых широко применя-
емых средств преодоления сложности реализации взаимодействий. По сути
дела все детерминированные переборные алгоритмы (с разным успехом) за-
нимаются именно таким выстраиванием.
Но с общих позиций линейное выстраивание — вырожденный метод. Бо-
лее того, он концептуально опасен. если призраки-агенты полностью удаля-
ются и вовсю эксплуатируются конкретные свойства конкретного линейного
порядка7 . Поэтому интересен классический подход языка SIMULA 67. Он в
явном виде провозглашает имитацию параллелизма и, следовательно, оста-
вляет для человека возможность мыслить в категориях действующих аген-
тов, что является и более гибким, и более естественным способом выражения
многих алгоритмов. Систему с дискретными событиями можно рассматри-
вать в качестве общего метода преодоления противоречия между принципи-
альным параллелизмом и реальной последовательностью реализации, име-
ющим то преимущество, что концепции остаются нетронутыми и тем самым
качество и гибкость программ резко повышаются.
Классическая реализация систем с дискретными событиями — это общая
Когда математику приносят задачу о расчете устойчивости стола с четырьмя ножками, он
быстро выдает результаты для стола с одной и с бесконечным числом ножек, а затем долго
6

пытается точно решить конкретную задачу.


Линеаризация естественного частичного порядка и явное использование свойств полу-
чившегося линейного — концептуальный тупик (сравни PROLOG, угодивший в данную кон-
7

цептуальную яму, и CLOS, который избегает ее принципиальной недокументированностью


и антистандартностью конкретного алгоритма линеаризации).
15.5. СИСТЕМЫ С ДИСКРЕТНЫМИ СОБЫТИЯМИ 825

среда поддержки процессов. Каждый процесс может находиться в одном из


четырех состояний, которые определяются с использованием так называе-
мого управляющего списка: строго упорядоченной очереди процессов, на-
значаемых на выполнение. В хорошей реализации классической модели этот
список скрыт.
Для каждого процесса определяется структура данных, достаточная для
полного запоминания его состояния в момент, когда дискретный шаг закон-
чен8 . Запомненное состояние называется точкой возобновления.
Состояние называется
a) активным, когда (реально) выполняется программа процесса;
b) приостановленным, когда выполнение программы процесса прервано, но
запомнена точка возобновления и процесс находится в управляющем спис-
ке;
c) пассивным, когда процесс не выполняется и не находится в управляющем
списке, но точка возобновления активности запомнена;
d) завершенным, когда выполнение его программы прервано, и точка возоб-
новления активности не запомнена.
Конструкция управляющего списка имитирует время. Первый процесс все-
гда активный. Это единственный активный процесс. Если он прерывает свое
выполнение, то следующим активным становится следующий за ним при-
остановленный процесс. Процесс может быть вставлен в управляющий спи-
сок (перед каким-либо процессом в списке или после него, через определен-
ное время) или удален из него. Процесс также может быть назначен на опре-
деленное время, и это означает, что он вставляется перед тем процессом, вре-
мя выполнения которого — минимальное время, превосходящее назначае-
мое. Возможно случайное (псевдослучайное) действие по вставке процесса
в то или иное место управляющего списка.
Можно считать, что с помощью управляющего списка процессам задают-
ся относительные приоритеты.
Постулируется, что все оперирование с управляющим списком и с состо-
яниями активности процессов является следствием событий, дискретно про-
исходящих в системе. Пока какое-либо из событий не произошло, состояние
процесса и его положение в управляющем списке не могут изменяться.
Вспомните, что в системах с дискретными событиями целостность данных должна га-
рантироваться лишь в момент между шагами процесса.
8
826 ГЛАВА 15. МОДЕЛИРОВАНИЕ

Видно, что это именно имитационная модель, создающую структуру, для


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

a) создание локальных структур данных агентов-процессов;

b) размещение агентов-процессов в управляющем списке;

c) перемещение каждого активного агента-процесса по управляющему спис-


ку на величину времени его задержки.

Согласно постулату о событиях, выполнение любого процесса-агента не мо-


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

1. Ждать время, необходимое для перемещения из местоположения в дан-


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

2. Если данный город уже посещался (в городе имеется пометка-рекомен-


дация), то ликвидировать себя, т. е. завершить процесс;

3. Сделать пометку-рекомендацию в данном городе, используя местопо-


ложение, из которого пришел агент;

4. Если данный город есть A, то построить путь, используя пометки ре-


комендации, и завершить процесс;
15.5. СИСТЕМЫ С ДИСКРЕТНЫМИ СОБЫТИЯМИ 827

5. Для каждой дороги, ведущей в данный город, породить процесс но-


вого агента, в качестве параметра которого задается данный город. На-
значить активизацию нового процесса непосредственно после прекра-
щения активности родительского процесса;
6. Завершить процесс.

b c d e
a b A1
A1 A11 A
2

A2 A1 A2 A12 A A11 A12 A21


21

τ(t5,B) (A
τ(t2×)
,t5)
τ(t2,B) τ(t4,t5)
τ(t2,t1)
a) модельное время
a b
A1 A →A
2 1
A1 → A2 A1 A11 A12 → A2 → A11
↓ ↓ ↓
A2 A11 A12 → A2

A12 → A2
c
A2 → A11 → A12 A2 A21 → A11 → A12

A21 → A11 → A12
d e
A11 → A12 → A21 A12 → A21 …

b) трансформация состояний управляющего сп иска

Рис. 15.3. Управляющий список

На рис. 15.3 изображено начало последовательности состояний управля-


ющего списка, которая получается при выполнении алгоритма с данными,
представленными на рис. 15.2с. В верхней части рисунка представлена мо-
дель времени: на горизонтальной оси изображены моменты, когда состояние
828 ГЛАВА 15. МОДЕЛИРОВАНИЕ

агентов меняется (τ — функция времени перемещения между городами; a, b,


c и d — моменты, отмеченные для дальнейшего пояснения). В нижней части
рисунка показана последовательность изменений управляющего списка (его
состояния выделены прямоугольными блоками, в которых для наглядности
вертикальные стрелки обозначают связи процессов, назначенных на одно и
то же время, а горизонтальные стрелки — упорядоченность процессов по
модельному времени).
Разбредание агентов начинается с порождения процессов A1 и A2, что со-
ответствует двум дорогам, ведущим в город B. Это момент модельного вре-
мени, омеченный как a. Моменту b соответствуют четыре состояния, сменя-
ющие друг друга, c соответствует три, а d — одно состояние. Из сопоставле-
ния рисунков 15.3a и 15.3b видно, что никакого специального моделирования
времени, а тем более задержек не требуется: время изменяется «мгновенно»,
когда все события, назначенные к более ранним срокам отработали и в голо-
ве управляющего списка появляется событие, назначенное на более поздний
срок. Именно поэтому данная система носит название системы с дискретны-
ми событиями. В принципе здесь не требуется даже атрибут времени —
достаточно отношения порядка между событиями.
Очевидно, что даже бесконечное зацикливание не помешает выполнению
алгоритма, если только вместимость управляющего списка будет достаточ-
ной.
Необходимо подчеркнуть ряд важных моментов:

1. реального параллелизма в системе с дискретными событиями нет, но


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

2. изначально в каждый модельный момент набор одновременно действу-


ющих агентов упорядочен частично, он становится упорядоченным пол-
ностью за счет соответствующей расстановки в управляющем списке;

3. управляющий список — общая структура данных для агентов-процес-


сов, но ее нельзя считать глобальной, так как явного доступа управля-
ющий список не имеет.

Пример 15.5.1. Рассмотрим следующую классическую задачу.


Чтобы воспитанный по европейским стандартам человек мог есть спа-
гетти, ему нужно две вилки: в левой и правой руке.
15.6. UML-МОДЕЛИРОВАНИЕ И RUP 829

Есть пять философов, погруженных в свои размышления и прогуливаю-


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

Внимание!

Если попытаться реализовать систему с дискретными собы-


тиями с использованием реального распараллеливания, то все не-
приятности синхронизации появляются снова.

Впервые система с дискретными событиями возникла как библиотечная


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

§ 15.6. UML-МОДЕЛИРОВАНИЕ И RUP

RUP (Rational Unified Process) — одна из основных технологий инду-


стриального программирования. Поскольку она ориентирована на быстрое
удовлетворение достаточно примитивных запросов заказчиков, для которых
внешнее оформление интерфейса по крайней мере не менее важно, чем про-
граммное наполнение, она ориентирована на интерактивное выявление тре-
830 ГЛАВА 15. МОДЕЛИРОВАНИЕ

бований и их уточнение. Здесь автоматная версия ООП оказывается как раз


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

Не пытайтесь применять эту технологию для задач, содержа-


щих концептуальные проблемы на уровне структуры проекта.

Вопросы для самопроверки

1. Оценить ошибку вычисления собственных значений двух матриц при


использовании стандартной программы:
   
1 1
   
 и 
 1   1 
 1 1 
   
 ...   ... 
1 ε 1
Глава 16

Подведение итогов

Давайте восклицать, давайте восхищаться!


(Б. Окуджава)
Где сказал впервые «Нет»?
На Большом Каретном
(В. Высоцкий)
Программирование является уникальной областью человеческой деятель-
ности, не зря вызвавшей к жизни такое внутренне противоречивое даже в
своем обозначении явление, как виртуальная реальность.
До сих пор человек создавал квазиискусственные объекты, когда свойства
исходных естественных маериалов значили по крайней мере не меньше, чем
замысел конструктора. Чисто искусственные объекты, созданные разумом
человека и подчиняющиеся лишь законам логики, встречались разве лишь
в математике.
Программирование впервые вывело искусственные объекты в общее поль-
зование. И сразу же выяснилось, что человек склонен воображать себя Де-
миургом1 , не обладая основным свойством Творца: всесторонней оценкой
плана перед его реализацией. Внешне деятельность по созданию даже не
очень плохих программ походит на вульгаризированное описание Творения
в Книге Бытия: сначала реализовать самые общие понятия, затем конкрети-
зировать все необхожимые составные части, затем создать венец творения
(основные действующие объекты), увидеть, что это хорошо, почить от тру-
дов своих и наутро узнать, что Ева сожрала яблоко и все пошло наперекосяк.
Одна лишь первоуровневая логика не может застраховать от концептуальных
противоречий.
1
Творцом с маленькой буквы.

831
832 ГЛАВА 16. ПОДВЕДЕНИЕ ИТОГОВ

Поэтому в программировании особенно возрастает роль общего образо-


вания, прежде всего, логического и философского. Лозунг Оруэлла “Невежество —
сила” при внедрении в практику высокого уровня приводит к решениям, за
которые очень долго приходится расплачиваться.
Далее, сопротивление материала не ушло в прошлое и в программиро-
вании. При создании программ на машинных языках исходные примитивы
были столь просты, что они не навязывали своего стиля и, как правило, не
имели концептуальных противоречий внутри себя. Технических трудностей
было масса, но зато человек, преодолев их, мог следовать чистой логике сво-
его замысла.
Сейчас мы вынуждены следовать уже заранее извращенной логике, навя-
зываемой операционными системами, языками и используемыми пакетами.
Избавиться от этого нельзя, поскольку иначе не предолеешь барьер сложно-
сти. Другой вопрос, как минимизоровать вредное влияние чужой логики на
свое творение? Для этого нужно прежде всего подняться в измерения, недо-
ступные достаточно примитивной чужой логике. Словом, нужно владеть как
минимум уровнем метода.
Конечно, не все могут подняться на этот уровень, но помочь подняться на
него тем, кто имеет потенциал — одна из основных задач этой книги.
Далее, сейчас популярен лозунг пользоваться ‘позитивным мышлением’.
Если разобраться в этом понятии, то позитивностью здесь и не пахнет. Это
систематическое закрывание глаз на трудности и наигранный оптимизм. Силь-
ной стороной ведущих русских специалистов было и остается негативное
мышление2 Его основным принципом является ясное осознание ограничен-
ности и недостатков, а затем превращение вреда в пользу. Нужно понимать,
что негативное мышление помогает корректно поставить задачу и найти не-
приивальное высокоуровневое решение, а не вялый концептуально пороч-
ный компромисс.
Систематизируем самые важные методологические моменты, коотрые раз-
бросаны в тексте книги.
1. Нет универсальных систем. Каждая система специализирована, хоро-
шая система спецуиализирована явно. То, что рекламируется как уни-
версальная система — специализхированная система, пытающаяся пе-
рерасти те рамки, где ее разумно использовать.

2. Виртуозное владение одним инструментом заводит человека в концеп-


2
Этот термин здесь применен как нарочитая оппозиция позитивному.
833

туальный и жизненный тупик, поскольку через несколько лет этот ин-


струмент выйдет из пользования.

3. Умение чесать левой ногой за правым ухом является показателем низ-


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

4. За каждой нетривиальной программной конструкцией стоят призраки,


лишь их явное осознание позволяет сделать эту конструкцию гибкой и
переносимой.

5. Понятия высокого уровня не могут быть измыгшлены человеком про-


извольно, они жизнеспособны лишь тогда, когда они являются конкре-
тизациями высших Идей.

6. Понятия и конструкции высокого уровня способны сократить решение


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

7. Лишние возможности являются страшнейшим врагом концептуально


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

8. Средства разного уровня и разной направленности нужно как можно


жестче разделять внутри программной системы. Их нельзя смешивать
в одном и том же тексте.

9. Нетривиальная система должна пользоваться несколькими разными под-


ходами для разных частей.

10. Уровни и стили разных частей системы могут быть независимы, вполне
возможно, когда интегрирующая программа написана с применением
низкоуровневых средств, а интегрируемые модули высокоуровневые3 .
Тем более это возможно в нынешней обстановке, когда средства поддержки взаимодей-
ствия модулей, написанных с помощью разного инструментария, отстают от развития про-
3

граммных инструментов.
834 ГЛАВА 16. ПОДВЕДЕНИЕ ИТОГОВ

11. То, что предается анафеме большинством, не обязательно окажется худ-


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

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

13. Разные стили программирования не сводимы друг к другу и к конкрет-


ному языку. Один и тот же стиль может быть реализован разными сред-
ствами.

14. Если Вам предстоит несколько раз работать над сходными задачами ли-
бо даже длительное время заниматься одной и той же, не жалейте сил
на создание собственного инструментария и выработку собственного
регламента работы.

15. Ваши призраки могут быть реальными понятиями для пользователя


(заказчка) программы, а Ваши реальные понятия — не более чем при-
зраками для него. Учитывайте эту разницу точек зрения!

16. Читайте книги, которые выпущены 20 и более лет назад, если они при
первом просмотре показались Вам интересными. Они дадут Вам в жиз-
ни гораздо больше, чем какое-либо «Программирование в системе .Net
версии 2.00 для профессионалов», которое через два года надо будет
выбросить и забыть. Не думайте, что Ваши предки были глупее Вас!
Опыт показывает, что скорее наоборот.

17. То, что было отброшено, слишком часто содержит здравые идеи, не вы-
державшие в тот конкретный момент конкуренции. Их возрождение
в новых условиях может быть прекрасным решением концептуально
трудных проблем, вставших перед Вами.
835

18. Помните закон экологической ниши! Не пытайтесь продвигать свое


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

19. Помните, что все люди ошибаются4 , и исправлять ошибку — не все-


гда лучшее. Порою надо, осознав ее, попытаться использовать и ее как
достоинство (хотя бы в данных конкретных условиях). Более того, не-
тривиальное решение нетривиальной задачи возможно, лишь если по
пути Вы пройдете через ошибки. Учитесь превращать ошибки в реше-
ния!

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

21. Ищите обходные пути при каждом намеке на концептуальную труд-


ность. Если Вы его найдете, труды окупятся сторицей. Лобовое реше-
ние — худший из возможных паллиативов, и если Вам пришлось вре-
менно его принять, а работа над системой еще будет продолжэаться,
помечайте данный модуль как прототип.

22. Помните, что дурно исполненная своя карма лучше хорошо исполнен-
ной чужой. Лучше быть полноценным русским, чем недоделанным аме-
риканцем. Не стесняйтесь своих особенностей мышления и работы, и
не выставляйте их как принцип. В любом обществе будьте свободным
от него, если у Вас достаточно сил, чтобы это реализовать5 , и сохра-
няйте в нем свое лицо, если Вы выбрали интеграцию.

23. Самоограничение чаще всего приводит к красивым решениям и к боль-


шим достижениям. Вседозволенность и примитивно понимаемая сво-
бода губительна. Смотрите, насколько стихи (самоограничение пшу-
щего) лучше и компактнее выражают мысль и чувства, чем проза (яко-
бы свободное выражение).
4
В том числе и авторы данной книги, и Вы.
Еще раз напоминаем, что негативизм — одна из форм конформизма, соответственно, асо-
циальное поведение не является признаком внутренней свободы.
5
836 ГЛАВА 16. ПОДВЕДЕНИЕ ИТОГОВ

24. Если Вам не удается сказать что-либо стихами, говорите прозой. Учи-
тесь отказываться от ограничений в нужный момент, но при этом не
забюывайте ставить себе другие!

25. Ничто, кроме души, не является абсолютной ценностью. Любая абсо-


лютизированная ценность превращается в идола. Поклонение идолам
губительно. Поэтому остерегайтесь! Особенно остерегайтесь абсолю-
тизации своих мнений и своих красивых находок!

Успехов Вам!
Приложение A

Математические модели

Здесь мы можем лишь дать обзор основных понятий и основных резуль-


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

§ A.1. НЕСКОЛЬКО ТЕРМИНОВ

Порядок(частичный порядок) — транзитивное и иррефлексивное отно-


шение. Порядок обычно обозначается значками, подобными ≺. Два элемен-
та несравнимы, если неверно ни a ≺ b, ни b ≺ a. Множество с заданным на
нем порядком называется чум.
Предпорядок — транзитивное и рефлексивное отношение , обозначает-
ся подобно 4. Два элемента с точки зрения предпорядка эквивалентны, или
неразличимы, если a 4 b и b 4 a.
Порядок линейный, если любые два неравных элемента сравнимы. Пред-
порядок строгий, если неразличимые элементы равны.
Порядок полон, (или множество является полной решеткой), если в лю-
бом подмножестве есть верхняя и нижняя грани.
Порядок называется полным1 , если любое непустое подмножество имеет
наименьший элемент. Множество с линейным полным порядком называет-
ся вполне упорядоченным. Стандартным вполне упорядоченным множеством
Пустячное отличие в словах, а какое отличие в смысле! Будьте внимательны, читая мате-
матические тексты!
1

837
838 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

является множество ординалов.


Начальный отрезок ординалов имеет следующую структуру.

0, 1, . . . , n, . . . ω, ω + 1, . . . , ω + n, . . . 2 · ω.

Далее:
2 · ω, 2 · ω + 1, . . . , 2 · ω + n, . . . 3 · ω.
Обобщая по всем натуральным числам, получаем следующий ряд:

0, 1, . . . , k, . . . ω, ω + 1, . . . , ω + k, . . . 2 · ω, . . . n · ω . . . .

Вслед за всеми этими ординалами вида n · ω + k стоит ω 2 . А за ω 2 выстраи-


вается ряд, изоморфный предыдущему:

ω 2 , ω 2 + 1, . . . , ω 2 + k, . . . ω 2 + ω, ω 2 + ω + 1, . . . , ω 2 + ω + k, . . .
ω 2 + 2 · ω, . . . ω 2 + n · ω . . . 2 · ω 2 .

За всеми k · ω 2 + l · ω + n стоит ω 3 .
Опять-таки продолжая по всем натуральным числам, получаем всевоз-
можные ординалы вида

kn · ω n + kn−1 · ω n−1 + · · · + k1 ω + k0 .

За ними идет ординал, обозначаемый ω ω . А за ним опять-таки цепочка его


“сумм” со всеми предыдущими ординалами:

ω ω + kn · ω n + kn−1 · ω n−1 + · · · + k1 ω + k0 .

Аналогично, мы приходим к 2 · ω ω , и далее к n · ω ω . Законы сложения степе-


ней сохраняются, и дальше идет ω ω+1 . Таким образом приходим к ординалам
вида
lm · ω ω+m + lm−1 · ω ω+(m−1) + · · · + l1 ω ω +
kn · ω n + kn−1 · ω n−1 + · · · + k1 · ω + k0 .
А за ними идет ординал, который естественно обозначить ω 2·ω . Продолжая
данный процесс, мы приходим к ординалам вида

n раз.
···ω
ωω

А за всеми этими башнями стоит ординал, который Г. Кантор назвал ε0 и


определил как наименьшее решение уравнения ω α = α.
A.2. ВЫЧИСЛИТЕЛЬНЫЕ ИНТЕРПРЕТАЦИИ 839

Порядок фундирован, если любое линейно упорядоченное подмножество


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

§ A.2. ВЫЧИСЛИТЕЛЬНЫЕ ИНТЕРПРЕТАЦИИ

Для рассмотрения вычислительных интерпретаций программы абстраги-


руются до схем программ. Схема отличается от программы тем, что все кон-
кретные понятия заменяются на абстрактные, например, вместо операции
сложения x + y пишется двухместная функция f (x, y). Это позволяет сосре-
доточиться на общих аспектах того, что такое исполнение, что такое удача
или неудача исполнения, что необходимо знать для того, чтобы исполнить
программу. Заодно это позволяет исследовать общие свойства программ, не
зависящие от примененных конкретных понятий, и общие их преобразова-
ния, что является самым ценным вкладом любой теории в практику.
Прежде всего, мы видим, что каждая схема содержит некоторый набор
переменных, функций и предикатов, которые в ней встречаются. В соответ-
ствии с этим определим
Определение A.2.1. Словарь (сигнатура) σ — тройка конечных множеств
hX, F, Pi, где X — непустое множество, элементы которого называются пере-
менными, F — множество функций (функциональных символов), P — мно-
жество предикатов (предикатных символов). Каждой функции и каждому
предикату сопоставлено натуральное число n > 0, называемое их арностью.
Если нужно явно указать арность функции, мы записываем f n , аналогично
для предиката.
Термы задаются следующим индуктивным определением.
a) Переменная — терм.
b) Если f n — функция, t1 , . . . , tn — термы, то f (t1 , . . . , tn ) — терм.
Элементарная формула — выражение вида P (t1 , . . . , tn ), где P n — предикат,
ti — термы.
Конец определения A.2.1.
Определение A.2.2. Схема программ словаря σ — оснащенный ориентиро-
ванный граф с вершинами пяти типов.
840 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

1) Входная вершина помечена списком переменных, называемых входными


данными схемы программ. Входная вершина в схеме одна. В нее не входит
дуг.

2) Выходная вершина помечена списком переменных, называемых результа-


тами схемы программ. Выходная вершина в схеме одна. Из нее не выхо-
дит дуг.

3) Функциональные вершины помечены присваиваниями вида

x ← t,

где x — переменная, t — терм.

4) Предикаты (называемые еще проверки, распознаватели или разветвле-


ния2 ) помечены элементарной формулой. Из предиката выходит две дуги,
помеченные символами + и −.

5) В соединения входит несколько дуг. Соединения ничем не помечены.

6) Если количество входящих или выходящих дуг для вершины некоторого


типа не указано, то оно равно 1.

Конец определения A.2.2.


Пример схемы программ можно посмотреть на рис. 3.3.
Определение A.2.3. Интерпретационная модель сигнатуры σ — тройка M =
hS, F, Pi, где

• S — непустое множество, называемое универсом. Мы считаем, что оно


обязательно содержит элемент ⊥, интерпретируемый как ошибка.

• F — отображение, сопоставляющее каждому функциональному сим-


волу f n ∈ F настоящую функцию F bf c ∈ (S n → S); функция должна
удовлетворять условию корректности относительно ошибки: она рав-
на ⊥ тогда и только тогда, когда хотя бы один из ее аргументов равен
⊥.
Последние два термина чаще употребляются в связи с недетерминированными схемами
программ.
2
A.2. ВЫЧИСЛИТЕЛЬНЫЕ ИНТЕРПРЕТАЦИИ 841

• P — отображение, сопоставляющее каждому предикатному символу


pn ∈ P отображение P bpc ∈ (S n → {true, false, ⊥}), удовлетворяю-
щее условию корректности относительно ошибки.

Конец определения A.2.3.


Чтобы определить исполнение программы, необходимо помимо интерпрета-
ционной модели задать оценку входных переменных z: функцию, сопоста-
вляющую каждой входной переменной значение из S.
Определение A.2.4. Состоянием памяти схемы программ называется оцен-
ка переменных данной схемы программ. Состояние схемы программ — пара
из вершины и состояния памяти.
Исполнение E схемы программ — последовательность (конечная или бес-
конечная) пар hai , zi i, где ai — вершины схемы, последовательность ai обра-
зует путь в схеме, и пары удовлетворяют следующим условиям:

1. a0 — начальная вершина схемы;

2. Если ai — присваивание xi ← ti , то состояние памяти zi+1 отличается


от zi лишь тем, что значение zi+1 (xi ) равно значению ti в состоянии zi ,
при этом значение ti должно быть отлично от ⊥.

3. Если ai — не присваивание, то zi+1 = zi .

4. Если ai — предикат P (t1 , . . . , tn ), то ai соединена с ai+1 дугой, помечен-


ной символом +, если P (zi (t1 ), . . . , zi (tn )) истинна, и −, если P (zi (t1 ), . . . , zi (tn ))
ложна.

Исполнение называется успешным, если его последняя вершина — выходная


вершина.
Исполнение при данной оценке y входных переменных — исполнение,
начальное состояние памяти которого z0 определяется условием:

z0 (x) = y(x) x входная;


(

z0 (x) = ⊥ иначе.

Конец определения A.2.4.


842 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

Преобразование оценок переменных, примененное в п. 2, имеет важное зна-


чение в семантике, называется заменой значения переменной и обозначается

y = z ∗ hx, si или y = z [x | s] .

В нашем случае
zi+1 = zi ∗ hxi , si i ,
где si = zi (ti ). Часто такое преобразование применяется сразу к конечному
числу переменных и обозначается

y = z ∗ hhx1 , . . . xn i , hs1 , . . . , sn ii или y = z [x1 , . . . xn | s1 , . . . , sn ] .

Согласно приведенному определению, схема программ в каждой интерпре-


тации задает частичную функцию Θ из оценок входных переменных в оцен-
ки выходных переменных. Θ(z) = y, если существует успешное исполнение
схемы при оценке z, и оценка выходных переменных в последней вершине
есть y.
Рассматривая исполнения программ в различных интерпретациях, можно
установить критерии эквивалентности программ, а, опираясь на эти крите-
рии, можно развить системы эквивалентных преобразований программ.
Определение A.2.5. Две схемы программ называются функционально экви-
валентными, если определяемые ими функции совпадают во всех интерпре-
тациях.
Пусть дано исполнение E схемы программ. Историей исполнения про-
граммы называется последовательность, строящаяся по следующему алго-
ритму.

1. (Инициализация) Начальной вершине сопоставляется в качестве теку-


щей оценки оценка входных переменных ими самими;
repeat

2. Следующая вершина, если она не является присваиванием или преди-


катом, истории не изменяет;

3. Если следующая вершина является присваиванием x ← t, то текущая


оценка x заменяется на t, в которое вместо вместо переменных подста-
влены их текущие оценки; полученная оценка становится очередным
членом истории исполнения;
A.2. ВЫЧИСЛИТЕЛЬНЫЕ ИНТЕРПРЕТАЦИИ 843

4. Если текущая вершина является предикатом, то в его аргументы-термы


подставляются текущие оценки всех переменных, и полученное выра-
жение становится очередным членом истории; текущая оценка не из-
меняется;
until конечная вершина;

5. (Завершение) Конечной вершине сопоставляется ограничение текущей


оценки на множество выходных переменных.

Две схемы программ называются логико-термально эквивалентными, если


их истории исполнения совпадают в любой интерпретации при любой оцен-
ке входных переменных.
Конец определения A.2.5.
Функциональная эквивалентность схем программ неразрешима, а логико-
термальная разрешима. Имеется полная система преобразований схем про-
грамм, сохраняющих логико-термальную эквивалентность.
Имеются три стандартных блока схем программ, рассматриваемых как
структурные блоки: последовательное соединение, разветвление и цикл Кну-
та (см. рис. A.1). Схема программ, которую можно получить из присваиваний
конечным числом применений стандартных блоков к уже построенным ра-
нее таким образом блокам, и затем добавлением входа и выхода, называется
структурированной схемой программ.
К каждой схеме программ можно добавить конечное число булевых пе-
ременных таким образом, что после этого она функционально эквивалентно
преобразуется в структурированную схему программ (теорема Бема-Джако-
пини). Без добавления дополнительных переменных цикл Бема-Джакопини
(см. рис. 3.4) и многие другие программы в структурную форму не преобра-
зуются.
Более детальное изложение рассмотренных в данном параграфе вопросов
можно найти в книге [44] или [45].
Еще один важный вопрос — можно ли избавиться от дополнительных
переменных, ослабив понятие структурности, введя понятие структурного
завершителя цикла. Очевидно, что цикл Бема-Джакопини структурируется
введением завершителя цикла, но этот завершитель, как показано на рис. A.2,
действует на два уровня структурной иерарахии вверх: мы выходим из услов-
ного предложения, вложенного в цикл, а затем уже из цикла.
844 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

A A

B - P

+
B

A + P - B

Рис. A.1. Три структурных блока схем программ

§ A.3. МОДЕЛИ ЯНОВА

Важным частным классом схем программ и соответствующих им вычи-


слительных моделей являются схемы Янова.
Определение A.3.1. Схема Янова — схема программ, в которой есть лишь
одна переменная, все функции и предикаты одноместные, все присваивания
имеют вид x ← f (x), причем функция f для каждого присваивания уникаль-
на.
Модель Янова — вычислительная модель, в которой все предикаты и функ-
ции одноместные, снабженная дополнительным значением s ∈ S, называе-
мым текущим состоянием мира. В модели Янова S называется множеством
(или универсом) полных состояний вычислительной системы (или прибора,
или мира; конкретный содержательный термин берется в зависимости от за-
дачи);
A.4. АВТОМАТЫ И МАШИНЫ ТЬЮРИНГА 845

F1

F2 Q F3

Break

F4 P

Break

Рис. A.2. Структурирование цикла Бема-Джакопини

Конец определения A.3.1.


В схеме Янова присваивание обычно сокращается просто до f , и единствен-
ный параметр у предиката также опускается.
Для схем Янова разрешимо понятие функциональной эквивалентности
и имеется полная система функционально эквивалентных преобразований
этих схем. Однако перевести схему Янова в структурированную форму так,
чтобы она осталась схемой Янова, невозможно.
Более того, это не может быть преодолено добавлением структурных пе-
реходов на любое ограниченное число этажей иерархии вверх. Но после до-
бавления произвольных структурных переходов схемы Янова поддаются струк-
турированию (см. рис. A.2).

§ A.4. АВТОМАТЫ И МАШИНЫ ТЬЮРИНГА

Начнем с предельно абстрактного определения.


Определение A.4.1. Автомат A — отображение I × S → S. Множество I
называется множеством входных символов, множество S — множеством со-
846 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

стояний автомата. Автомат называется конечным, если множества входных


символов и состояний конечны. В множестве состояний выделяется некото-
рое состояние s0 , называемое начальным состоянием автомата.
Конец определения A.4.1.
Если данное определение рассмотреть более операционально, то получаются
следующие трактовки:
a) Автомат — устройство, реагирующее на входные воздействия и меняю-
щее свое состояние в зависимости от предыдущего состояния и очеред-
ного входного воздействия.
b) Автомат — функция, преобразующая последовательности входных сим-
волов в последовательности состояний. Функция преобразования после-
довательностей ΦA определяется рекурсивно:
(a) Если Λ — пустая последовательность, то ΦA (Λ) = [s0 ].
(b) Если s — последний член ΦA (X), то
ΦA (X ∗ [a]) = ΦA (X) ∗ [A(a, s)].

(c) Если X — бесконечная последовательность, то


ΦA (X) = lim ΦA ([X0 , . . . , Xn ]).
n→∞

c) Автомат — устройство, читающее очередной символ из “последователь-


ного файла” входных символов и выдающее очередной символ в “после-
довательный файл” состояний. Решение автомата может зависеть от по-
следнего выданного символа.
Имеется еще одно, более ‘механическое’, представление конечного автомата.
Автомат —машина с центральным процессором, имеющим конечное число
состояний, одной входной и одной выходной лентами. И входная, и выходная
лента движутся каждый ход налево на один символ. Команды процессора
имеют вид
s : [a1 b1 s1 , . . . , an bn sn ],
где si — состояния, ai — символы входного алфавита, bi — символы выход-
ного алфавита. Это определение отличается от предыдущего тем, что автомат
может перекодировать пары
(текущее состояние, входной символ)
A.4. АВТОМАТЫ И МАШИНЫ ТЬЮРИНГА 847

в выходные символы. Наглядное представление работы такого автомата дано


на рис. A.3.

| | ... |a|...| |...

| | ... |b|...| |...


Рис. A.3. Автомат

В этой модели символ, помещаемый на выходную ленту, может интерпре-


тироваться как действие, выполняемое системой, формализацией которой
служит конечный автомат. Соответственно, символ на входной ленте может
пониматься как результат распознавания некоторых характеристик модели-
руемой системы некоторой системой предикатов.
Имеется одна вариация в определении конечного автомата, которая фор-
мально приводит к эквивалентному понятию, а на практике к другому ви-
ду программных представлений автоматных структур. До сих пор рассмо-
тренные формализмы допускали варьирование совершаемого автоматом дей-
ствия в зависимости от того, какой символ стоит на входной ленте (автоматы
Мура). В этом смысле действие является атрибутом не состояния, а перехода.
В программах часто действие выполняется до распознавания характеристик
и определения последующего перехода (автоматы Мили).
Множество называется автоматно разрешимым или регулярным3 , если
оно представимо как множество таких последовательностей, для которых ко-
Последний термин часто встречается в литературе, мы им пользоваться не будем, по-
скольку вводить для каждого типа разрешимости свое слово — пережиток тех времен, когда
3

общей теорией вычислимости еще не занимались.


848 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

нечный автомат A переходит в одно и то же состояние. Автоматная разреши-


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

| | ... |a|...| |...

| | ... |b|...| |...


Рис. A.4. Машина Тьюринга с оракулом

§ A.5. АЛГОРИТМЫ НАД СТРУКТУРАМИ

Для большего подчеркивания похожести моделей и одновременно решающих различий


между ними, мы изобразили на рисунке машину Тьюринга с оракулом: основное поня-
4

тие, используемое в тех случаях, когда изучается вычислимость относительно уже заданной
функции (записанной на входной ленте).
A.6. РЕКУРСИИ 849

Если рассматривать в качестве аргументов алгоритмов более сложные


структуры данных, то возникают другие понятия, в принципе сводимые к ма-
шинам Тьюринга, но представляющие самостоятельный интерес. Некоторые
из них уже послужили идейной основой интересных концепций программи-
рования, а другие еще ждут своего использования.
Прежде всего, рассмотрим понятие алгорифмов Маркова. Данными для
алгорифмов Маркова являются слова в произвольном конечном алфавите A.
Множество слов обозначим A∗ . Элементарные действия — продукции вида:

a → b a → ·b

где a, b ∈ A∗ . Такая продукция означает замену подслова a на подслово b.


Управляющая структура алгорифма Маркова в корне отлична от управля-
ющей структуры машины Тьюринга. Команды не имеют меток. Каждый раз
ищется первая из применимых команд и она применяется в первом возмож-
ном месте. Таким образом, неприменимо понятие состояния, в которой нахо-
дится исполняемая программа. Все определяется лишь состоянием памяти.
Если исполненная команда содержала точку либо ни одна из команд непри-
менима, исполнение завершается.
Рассмотрим пример. Алгорифм


 ba → ab

ca → ac
(A.1)

 cb → bc

→·

сортирует буквы в слове в алфавитном порядке. Последняя команда → · при-


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

§ A.6. РЕКУРСИИ

Еще одно определение алгоритма, принадлежащее Дж. Маккарти (автору,


в частности, языка LISP), лучше всего приспособлено для импортирования
850 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

типов данных, и, самое главное, концентрируется на важнейшем для про-


граммирования понятии рекурсивного описания системы функций и ставит
его в фундамент всей теории.
Входными данными и результатами алгоритма в данном случае являют-
ся списки. Они годятся для сочленения данных любых типов. Элементарные
данные естественно считать атомами, внутренняя структура которых неви-
дима для наших алгоритмов. Будем считать, что задано некоторое множество
атомов, обязательно включающее true и false Эти два атома будем называть
логическими значениями.
Определение A.6.1. Списки задаются следующим индуктивным определе-
нием.

1. Пустой список () (обозначаемый также nil) является списком.

2. Если l1 ,. . . , ln , n > 1 — атомы либо списки, то (l1 , . . . , ln ) — также


список.

Равенство списков задается следующим индуктивным определением.

1. l = nil тогда и только тогда, когда l также есть nil.

2. (l1 , . . . , ln ) = (k1 , . . . , km ) тогда и только тогда, когда n = m и соответ-


ствующие li = ki .

Конец определения A.6.1.


Таким образом, поскольку понятие, задаваемое индуктивным определением,
должно строиться в результате конечного числа шагов применения опреде-
ления, мы исключаем списки, ссылающиеся сами на себя, и списки в нашем
рассмотрении изоморфны упорядоченным конечным деревьям, листьями ко-
торых являются nil либо атомы. Списки считаются равными в том и только
в том случае, если у них одинаковое число членов и их члены равны.
Пример A.6.2. Все списки (), (()), ((())) и т. д. различны. Различны также и
списки nil, (nil, nil), (nil, nil, nil) и так далее. Попарно различны и списки
((A, B), C), (A, (B, C)), (A, B, C), где A, B, C — различные атомы.
Конец примера A.6.2.
Определение A.6.3. Элементами списка (l1 , . . . , ln ) называются l1 , . . . , ln .
Вершины списка L задаются следующим индуктивным определением.
A.6. РЕКУРСИИ 851

1. Элементы списка являются его вершинами.

2. Вершины элементов списка являются его вершинами.

Длиной списка называется количество элементов в нем. Глубиной списка на-


зывается максимальное количество вложенных пар скобок в нем. Соедине-
нием списков (l1 , . . . , ln ) и (k1 , . . . , km ) называется список

(l1 , . . . , ln , k1 , . . . , km ).

Замена вершины a списка L на атом либо список M получается заменой


поддерева L, соответствующего a, на дерево для M . Замена обозначается
L[a | M ]. Через L[akM ] будем обозначать результат замены нескольких вхо-
ждений вершины a на M .
Конец определения A.6.3.
Списки естественно изоморфны упорядоченным конечным деревьям, у ко-
торых листьями являются атомы и nil. Соответственно, появляются три чи-
словые характеристики списка. Длина списка — это количество членов в
нем. Глубина списка — наибольшая длина пути в дереве, задаваемом данным
списком. Ширина списка — наибольшее число вершин на одном из уровней
дерева.
Если мы стремимся определить в некотором смысле множество программ,
то нужно определить элементарные операции над списками (примитивы).
Как всегда, выбор их допускает некоторый произвол, но известные базисы
почти изоморфны. Мы выберем следующий базис из трех функций:

выделяющая первый аргумент списка;


выделяющая хвост списка x, т. е.
HD(x),

список без первого элемента;


TL(x),

ADD(a, x), добавляющая атом либо список


первым членом к x;

трех констант nil, true, false и четырех предикатов:

проверяет, является ли x
элементом исходного множества U ;
ATOM

TRUE проверяет, что x — атом, равный true;


FALSE проверяет, что x — атом, равный false;
NULL проверяет, что x — пустой кортеж [ ].
852 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

Значениями предикатов считаются атомы true, false.


Пример A.6.4. Список (true, false, nil) выражается в нашем базисе как
ADD(true, ADD(false, ADD(nil, nil))).
Вообще, одноэлементный список с элементом A выражается как ADD(A, nil).
Конец примера A.6.4.
В связи с тем, что рассмотрено в предыдущем примере, вводится следующее
сокращение.
Выражение ADD(t1 , · · · ADD(tn , nil) . . . ) обозначается (t1 , . . . , tn ).
Если мы желаем рассмотреть алгоритмы над некоторой алгебраической
системой Σ, то элементы ее носителя рассматриваются как атомы, вводит-
ся новый предикат, например, SIGMA(x), выделяющий среди атомов те, ко-
торые являются элементами Σ, а константы, предикаты и функции данной
алгебраической системы вводятся в число примитивов.
Внимание!
Поскольку равенство отнюдь не для всех типов данных является вычисли-
мым предикатом (например, равенство действительных чисел либо равен-
ство объектов никакого вычислительного смысла не имеет), мы не считаем,
что алгебраическая система обязательно содержит в числе элементарных
предикатов предикат равенства.
Мы не считаем, что все функции алгебраической системы Σ всюду опре-
делены (но предикаты, как правило, считаются всюду определенными).
Пример A.6.5. Если мы импортировали как алгебраическую систему поле
действительных чисел с обычными алгебраическими операциями, то следу-
ющее выражение
(HD(X) + HD(TL(X)))/HD(TL(TL(X)))
делит сумму первых двух элементов списка на третий. Конечно же, можно
было записать алгебраические операции как двухместные функции, но до
такого педантизма мы в данном изложении не дойдем. Тем не менее стоит
отметить, что, как всегда, сохранение привычных способов записи при до-
бавлении новых структур приводит к новым сложностям. Скобки вокруг ал-
гебраического выражения и скобки вокруг выражения, превращающие его в
одноэлементный список, становятся неразличимы.
Конец примера A.6.5.
A.6. РЕКУРСИИ 853

Термы, построенные из заданных известных функций, констант и предика-


тов при помощи перечисленных выше операций, называются простейшими
композициями.
Внимание!
Примитивные функции считаются неопределенными в тех случаях, которые
не заданы явно в их определении. Например, не определено значение HD(x),
если x является атомом или nil. Соответственно, функции, импортированные
из некоторой алгебраической системы, являются неопределенными, если их
аргументы выходят за пределы носителя этой системы. Предикаты же счи-
таются определенными, но ложными.
Таким образом, мы обошли проблему отсутствия типов данных в нашем
языке, просто описывая каждый тип данных своим предикатом. Это — стан-
дартный способ определения типов данных в логическом языке.
Определение A.6.6. Условные термы сигнатуры σ задаются следующим ин-
дуктивным определением.

1. Терм является условным термом.

2. Если P — n-местный предикат, t1 , . . . , tn , r, s — условные термы, то

if P (t1 , . . . , tn ) then r else s fi

также условный терм.

Выражение P (t1 , . . . , tn ) называется условием терма, а r и s — его альтерна-


тивами.
Конец определения A.6.6.
Значение условного терма вычисляется следующим образом. Вычисля-
ются значения всех аргументов предиката P . Если они оказались определе-
ны, то вычисляется значение P . Если оно оказалось истиной, то вычисляется
r, если оно оказалось ложью, то s. Итак, условные термы представляют из
себя просто условные выражения. При вычислении условного терма вычи-
сляется лишь одна из его альтернатив.
Логический условный терм определяется индуктивно.

1. Выражение P (t1 , . . . , tn ), где P — предикат, а t1 , . . . , tn — произволь-


ные условные термы, является логическим условным термом.
854 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

2. Условный терм

if P (t1 , . . . , tn ) then r else s fi

является логическим, если обе его альтернативы являются логически-


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

if if P (t1 , . . . , tn ) then r1 else s1 fi then r else s fi

и
if if P (t1 , . . . , tn ) then if r1 then r else s fi else
if s1 then r else s fi fi
совпадают.
С помощью условных термов можно дать определение всех логических
связок.
Определение A.6.7. Пополним сигнатуру σ конечным числом функциональ-
ных символов f1 , . . . , fn арности соответственно m1 , . . . , mn . Обозначим по-
лученную сигнатуру σ1 Рекурсивная схема над σ — совокупность определе-
ний 
 f1 (x1 , . . . , xm1 ) ← t1 (x1 , . . . , xm1 )
... (A.2)

fn (x1 , . . . , xmn ) ← tn (x1 , . . . , xmn )
где все ti — условные термы сигнатуры σ1 , содержащие лишь переменные

Конец определения A.6.7.


x 1 , . . . , x mi .

Таким образом, совокупность функций fi рекурсивно определяется сама че-


рез себя. Рассмотрим несколько примеров.
Пример A.6.8. Последний элемент списка.

LAST(x) ← if NULL(TAIL(x)) then HEAD(x) else LAST(TAIL(x)) fi.

Конец примера A.6.8.


A.6. РЕКУРСИИ 855

Дадим точное определение рекурсии.


Прежде всего, рекурсия определяет частичные функции. Частичные функ-
ции могут быть частично упорядочены. При стандартном теоретико-множе-
ственном представлении функций такое упорядочение может быть описано
следующим образом. Функция f 4 g, если G{f } ⊆ G{g}, где G — график
функции f . Содержательно это означает, что g доопределяет f . Естественно,
что две функции, противоречащие друг другу (дающие разные значения для
одного и того же x0 ) просто несравнимы.
Нигде не определенная функция, функция ошибки ⊥ является наимень-
шим элементом в получившемся чуме.
Во всяком чуме можно определить естественную топологию. Окрестно-
стями функции f считаются все множества функций, замкнутые вверх (как
говорят, верхние конусы), содержащие f . Таким образом, любая окрестность
f содержит вместе с нею и все ее пополнения. Функция над функциями (фун-
кционал, как обычно ее называют) Φ непрерывна, если для каждой окрест-
ности ее результата OΦ(f ) можно найти окрестность ее аргумента Rf , такую,
что если g ∈ Rf , то Φ(g) ∈ OΦ(f ) . Это — стандартное топологическое опре-
деление непрерывности, копирующее и обобщающее определение для дей-
ствительных чисел.
Там, где появляется топология, появляются и пределы. В частности, пре-
дел возрастающей последовательности функций fn — наименьшая функция
lim fn , принадлежащая всем окрестностям fn .
n→∞

Лемма A.6.1. Любая возрастающая последовательность функций имеет


предел.
Непрерывный функционал перерабатывает предел последовательности в
предел своих значений на этой последовательности

Φ( lim fn ).
n→∞

Предложение A.6.2. Любой непрерывный функционал имеет неподвижную


точку.
Следствие A.6.3. Всякий непрерывный функционал имеет наименьшую не-
подвижную точку.
Теперь заметим, что, если условный терм t, содержащий свободную функ-
циональную переменную f , рассматривать как функционал λf. t(f ), то он
856 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

является непрерывным. Соответственно, он имеет наименьшую неподвиж-


ную точку.
Определение A.6.9. Рекурсивная функция f , определяемая схемой

f ← t(f )

это наименьшая неподвижная точка функционала λf. t(f ).


Конец определения A.6.9.
Такое определение позволяет, в частности, поставить вопрос о том, какие
стратегии вычисления рекурсивных функций корректны, а какие нет. Из не-
го, в частности, получается, что вычислять нужно всегда прежде всего са-
мую внешнюю функцию, если терм не условный, и условие терма, если терм
условный, заменяя эту функцию на ее определение. Если внешняя функция
уже является исходной либо импортированной, то вычисляется самая внеш-
няя рекурсивная функция в одном из ее аргументов, и так далее. В любом
случае заменяться на свое определение должна рекурсивная функция ли-
бо рекурсивный предикат, у которого нет объемлющих вызовов рекурсивно
определенных функций.
Если же мы попытаемся заменять внутреннее определение, то мы можем
выдать ошибку в том случае, если внутренняя функция не определена, но
она не вызывается из-за того, что ее вызов в определении внешней функции
оказывается в невыполненной альтернативе условного терма.
Другое содержательное истолкование такого абстрактного определения
касается уже самого графика рекурсивно определенной функции. Вначале
мы определяем значения t(f ), подставляя вместо f функцию ошибки ⊥, и
тем самым задается базис рекурсии, когда значение t(f ) определяется без
всякого обращения к f . Затем мы подставляем в t получившуюся функцию
f1 , определяя f2 = λx. t(f1 ), и так далее.
Пример A.6.10. Рассмотрим определение сложения (??). Тогда Add0 опре-
делено лишь для y = 0 и всегда дает x. Add1 определено уже и для y = 1 и
для него выдает x + 1 и так далее. Объединением всех этих ограниченных
рекурсий и является Add.
Конец примера A.6.10.
Построенное нами определение естественно обобщается на тот случай, ко-
гда несколько функций одновременно задаются рекурсивной схемой общего
A.6. РЕКУРСИИ 857

вида. Во-первых, топология на кортежах функций [f1 , . . . , fn ] является пря-


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

[f1 , . . . , fn ] 4 [g1 , . . . , gn ]

тогда, когда для всех i fi 4 gi . Точно так же доказывается, что любая воз-
растающая последовательность кортежей функций имеет предел, и функция,
определяемая рекурсивной схемой общего вида — наименьшая неподвижная
точка функционала, перерабатывающего вектор функций f~ в вектор функций
~t(f~), или, другими словами, функционала λf~. ~t(f~).
Упражнения к § A.6
A.6.1. Постройте деревья, соответствующие спискам из примера A.6.2.

A.6.2. Постройте индуктивные определения длины и высоты списка.

A.6.3. Является ли nil элементом списка (())? А списка ((()))? А списка ((), ())?

A.6.4. Можно ли в определении списка заменить второй пункт на следую-


щий: конечное множество атомов и списков — список?

A.6.5. Пусть L есть (A, ((D, E), C), B), a есть (D, E). Постройте L[a | nil],
L[a | true], L[a | (F, F, F )].

A.6.6. Пусть L есть (A, ((D, E), C), B, (D, E)), a есть (D, E). Постройте L[aknil],
L[aktrue], L[ak(F, F, F )].

A.6.7. Имеет ли смысл выражение nil[nil|A]? Если да, то чему оно равно?

A.6.8. Дайте индуктивное определение L[a | M ].

A.6.9. (Для тех, кто знаком с элементарными понятиями топологии). Пусть


на импортированной алгебраической структуре задана некоторая то-
пология и все импортированные функции непрерывны. Все остальные
атомы считаются изолированными точками. Если уже определены окрест-
ности l1 , . . . , ln , то окрестностями (l1 , . . . , ln ) являются прямые произ-
ведения окрестностей li .
Верно ли, что простейшая композиция также непрерывна на своей обла-
сти определения?
858 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

A.6.10. Какие функции действительных чисел будут простейшими компози-


циями, если импортировать поле действительных чисел?

A.6.11. А если импортировать кольцо действительных чисел?

A.6.12. Функция f инвариантна относительно данного элемента a списка


L, если для любого M f (L[a|M ]) = f (L)[akM ]. Всегда ли можно по
простейшей композиции f определить такие n и m, что f инвариантна
относительно всех вершин, стоящих не ранее чем на n-том месте либо
на глубине не менее чем m?

A.6.13. Переносятся ли результаты упражнения A.6.12 на условные термы?

A.6.14. Определите предикат, проверяющий, является ли его аргумент спис-


ком.

A.6.15. Определите предикат, проверяющий, является ли его аргумент одно-


элементным списком.

Постройте функции, вычисляющие:

A.6.16. Весь кортеж кроме последнего элемента HEAD*.

A.6.17. Добавление элемента в качестве последнего члена списка ADD*.

A.6.18. Перестановка членов кортежа в обратном порядке INV.

A.6.19. Вытягивание списка в линейный LINE, то есть построить список из


всех атомов, находящихся в данном списке, в списках, являющихся его
членами, и т. д.

A.6.20. Применение функции f (x) ко всем членам списка.

A.6.21. Проверка предиката P (x) для всех членов списка (вычисление логи-
ческой формулы ∀x ∈ L P (x)).

A.6.22. Постройте алгоритм CONCAT, вычисляющий соединение списков как


кортежей.

A.6.23. Постройте алгоритм, проверяющий, выполнен ли предикат P (x) хо-


тя бы для одного члена списка, т. е. вычисляющий логическую формулу
∃x ∈ L P (x).
A.6. РЕКУРСИИ 859

A.6.24. Постройте всюду определенный предикат, проверяющий является ли


его элемент одноэлементным списком.

A.6.25. То же самое для списка из двух элементов.

A.6.26. Список назовем чистым, если он удовлетворяет следующему индук-


тивному определению.

1. nil, true, false — чистые списки.


2. Если все элементы списка чистые, то и список чистый.

Постройте предикат, проверяющий,является ли список чистым.

A.6.27. Постройте предикат, проверяющий равенство двух чистых списков.


Предикат должен быть всюду определенным.

A.6.28. Пусть задан предикат EQ(x, y), проверяющий равенство произволь-


ных атомов. Построить предикат, проверяющий равенство произволь-
ных списков.

A.6.29. Есть ли в чуме функций наибольший элемент? Если его нет, есть ли
максимальные и чем они являются?

A.6.30. Есть ли у функции минимальная окрестность, если есть. из чего она


состоит?

A.6.31. Что является окрестностями ошибки ⊥?

A.6.32. Перепишите понятие предела возрастающей последовательности в


терминах лишь частичного порядка. Чем является предел последова-
тельности для множества ее членов с точки зрения частичного поряд-
ка?

A.6.33. Мы воспользовались неубыванием непрерывного функционала

∀f, g(f 4 g ⇒ Φ(f ) 4 Φ(g)).

Докажите это утверждение.

A.6.34. Если у функций f и g есть верхняя грань, то каким свойством эти


функции обладают и чем является эта грань?
860 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

A.6.35. Если у функций f и g есть нижняя грань, то каким свойством эти


функции обладают и чем является эта грань?

A.6.36. Разберитесь, что вычисляет функция

f (x) ← if x > 100 then x − 11 else f (f (x + 10)) fi.

A.6.37. Верно ли, что совокупность функций, определяемая схемой



 f1 (x11 , . . . , x1n1 ) ← t1 (x11 , . . . , x1n1 )
···

fk (xk1 , . . . , xknk ) ← tk (xk1 , . . . , xknk )

является наименьшей совместной неподвижной точкой всех функцио-


налов λfi . ti (f1 , . . . , fk )?

§ A.7. СОВМЕСТНОСТЬ И ПАРАЛЛЕЛИЗМ

Совместное и параллельное исполнение характеризуются с точки зрения


математических моделей тем, что время, в котором происходит исполнение
программы, рассматривается как частично-упорядоченное множество. Если
два момента времени несравнимы, то соответствующие действия могут ис-
полняться в любом порядке. Но заметим, что совместность и параллелизм да-
ют существенно разные вычислительные модели. Лучше всего рассмотреть
это на элементарном примере.
Пример A.7.1. Сильная трехзначная логика Клини.
В программировании мы все время должны принимать во внимание эф-
фект неудачи либо провала. Клини предложил следующее расширение клас-
сической логики на случай вычислений, не всегда приводящих к результату.
В приведенных таблицах u означает, что аргумент зациклился либо выдал
ошибку (распознать и разделить заранее два этих случая в принципе невоз-
можно).
& t f u ∨ t f u ⇒ t f u ¬
t t f u t t t t t t f u t f
f f f f f t f u f t t t f t
u u f u u t u u u t u u u u
A.7. СОВМЕСТНОСТЬ И ПАРАЛЛЕЛИЗМ 861

Таким образом, если один из аргументов гарантирует значение связки не-


зависимо от значения другого аргумента, это значение выдается. В програм-
мировании часто говорят (используя теорию как заклинание), что пользуют-
ся этой логикой. Но последовательными программами она реализована быть
не может. Чтобы вычислить значение связки в логике Клини, нужно запу-
стить два процесса вычисления аргументов параллельно, и завершить вычи-
сление, если хоть один из них выдал ответ, гарантирующий значение связки.
Если один из процессов выдал ошибку, второй должен, не обращая на это
внимание, продолжать вычисления.
Если аргументы вычисляются совместно, то может выполняться (даже
если совместность перенесена на уровень элементарных действий, реализу-
ющих вычисления аргументов) один из них, а про второй можно забыть и не
получить результата.
Если же совместность дополнить предписанием переходить к следую-
щим элементарным действиям каждого процесса лишь после того, как будут
закончены оба действия либо одно закончится, а другой процесс завершится
ошибкой, то опять можно вычислить значения в логике Клини.
Конец примера A.7.1.
Данный пример иллюстрирует два из трех базовых случаев параллелизма,
выделенных в теории программирования.

1. &-параллелизм. Процессы идут независимо друг от друга, пока все они


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

2. ∨-параллелизм. Процессы идут независимо друг от друга, пока один из


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

3. Конвейерный параллелизм. Выполняется по одному элементарному дей-


ствию из каждого процесса, затем они обмениваются данными, и вы-
числение продолжается.5
Этот вариант параллелизма был исторически первым разработанным и внедренным в
практику вычислений. Во время Великой французской революции потребовалось срочно
5

пересчитать все артиллерийские, геодезические и навигационные таблицы в связи с перехо-


862 ПРИЛОЖЕНИЕ A. МАТЕМАТИЧЕСКИЕ МОДЕЛИ

4. Общий параллелизм. Процессы выполняются независимо, пока не вой-


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

дом на метрическую систему. Было усажено несколько десятков грамотных солдат, которые
выполняли сложение либо вычитание результатов, полученных ими от заранее установлен-
ных товарищей. По команде они все начинали действия, по другой — передавали результаты
кому следует. В итоге мало того, что таблицы были пересчитаны в рекордно малое время и с
низкими затратами, они еще и стали самыми лучшими в мире (пока русские артиллеристы
под руководством гр. Аракчеева не составили свои таблицы).
Приложение B

Методологические результаты

В данной главе собраны в виде краткого обзора (или, скорее, конспекта)


результаты теории и анализа, которые:
• не столь прямо применимы, как собранные в предыдущей;

• как правило, большей частью опускаются в современных вузовских


курсах, делающих упор на ‘позитивном’ знании;

• полностью перевернули за XX век истинно научное мировоззрение.

Желающим подробнее ознакомиться с данными результатами рекомен-


дуем изучать фундаментальную теорию (стандартный совет литредакторов
советского времени плохим писателям был «Читайте классиков!»). Настоя-
щего образования никогда ничто не заменит. Другое дело, что чаще всего оно
большей частью результат самообразования (и порою попадания в удачную
неформальную группу).
Внимание! Мировоззрение позитивизма (в широком смысле, включая
сюда структурализм и фаллабилизм) или «квазирелигию прогресса» мы не
считаем по-настоящему научным мировоззрением. Их адепты предпочита-
ют закрывать глаза на существующие трудности и увлекаться слишком упро-
щенными моделями. Тем самым, на словах декларируя прогресс, на деле они,
путем замалчивания недостатков и отсутствия серьезного анализа ошибок,
способствуют тупиковому развитию либо застою.

§ B.1. ЛОГИЧЕСКИЕ ПАРАДОКСЫ

Парадокс — рассуждение либо высказывание, в котором, пользуясь сред-

863
864 ПРИЛОЖЕНИЕ B. МЕТОДОЛОГИЧЕСКИЕ РЕЗУЛЬТАТЫ

ствами, не выходящими (по видимости) за рамки логики, приходят к заведо-


мо неприемлемому результату. Ввиду того, что парадоксы обнажают скры-
тые концептуальные противоречия и переводят их в прямые и открытые,
они, согласно законам творческого мышления, помогают при развитии но-
вых идей и концепций.
Первым примером логического парадокса явился парадокс лжеца, припи-
сываемый Эпимениду. Простейшей формой его является следующее рассу-
ждение:
Если некто произнес лишь одно предложение: «Я лгу,» — сказал
ли он правду? Если предположить, что он сказал правду, то он
солгал, а если он солгал, то он сказал правду.
Заметим, что это рассуждение никак не зависит от закона исключенного тре-
тьего, оно доказывает утверждение формы A ≡ ¬ A. Более того, оно не за-
висит от семантики утверждений, и поэтому может считаться чисто синтак-
сическим парадоксом, который показывает, что утверждение, ссылающееся
само на себя, часто просто не может быть корректно проинтерпретировано.
Многие известные парадоксы восходят к парадоксу лжеца. Рассмотрим, на-
пример, парадокс Рассела:
Пусть R — множество всех множеств, не являющихся собствен-
/ x}. Тогда Z ∈ Z означает,
ными элементами, т.е. R = {x | x ∈
что Z ∈
/ Z.
Более тонко обыграна крайняя опасность автореференции (предложений,
ссылающихся на самих себя) в парадоксе Карри.
Пусть A — некоторое высказывание. Пусть B — высказывание
“Если B , то A.” Допустим B . Тогда B ⊃ A. Значит, из B следует
A, и B доказано. Но тогда доказано и A.
Таким образом, Карри показал, что обычная импликация в любой системе с
автореференцией позволяет вывести любое предложение, что является гру-
бой формой противоречия (противоречивость по Карри.)
Новый класс логических парадоксов был открыт Берри, который ввел в
рассмотрение сложность объекта.
Предложений, содержащих менее ста букв, конечное число. По-
этому с их помощью можно определить лишь конечное число на-
туральных чисел. Поэтому есть наименьшее число n0 , не опре-
делимое таким способом. Но тогда фраза “Наименьшее число, не
B.1. ЛОГИЧЕСКИЕ ПАРАДОКСЫ 865

определимое при помощи предложения, содержащего менее ста


символов” содержит менее ста символов и определяет n0 .

Конструкция парадокса Берри интенсивно используется в современной те-


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

Утренняя звезда — то же, что планета Венера. Сегодня вечером


я видел Венеру. Значит, я видел Утреннюю звезду.

Конструкция данного парадокса использована в доказательстве теоремы Рай-


са о неразрешимости нетривиальных свойств вычислимых функций (един-
ственные свойства вычислимых функций, которые могут определяться про-
граммой — тождественно истинное и тождественно ложное) и теоремы о
невозможности нетривиальных точных предсказателей (Е. Е. Витяев).
Как логические парадоксы часто трактуются законы материальной им-
пликации — “из лжи следует все, что угодно” и “истина следует из всего, что
угодно,” поскольку они позволяют получить формулы A ⇒ B, в которых A и
B никак не связаны по смыслу. Эти аномалии явились стимулом для развития
модальных, паранепротиворечивых и релевантных логик, в которых данные
парадоксы частично преодолеваются. На самом деле полностью преодолеть
их невозможно, поскольку любая успешная формализация является сильным
огрублением.
Еще один класс парадоксов, возникающих на границе логики и матема-
тики, основан на применении точных методов к неточным понятиям.

Человек, у которого на голове нет ни одного волоса — лыс. Если у


лысого вырастет еще один волосок, он останется лысым. Значит,
все люди лысые.

Данный класс парадоксов интенсивно используется при развитии ультраин-


туиционистской математики, имеющей дело с процессами, завершимыми в
реальное время.
866 ПРИЛОЖЕНИЕ B. МЕТОДОЛОГИЧЕСКИЕ РЕЗУЛЬТАТЫ

§ B.2. ТЕОРЕМЫ ТАРСКОГО И ГЕДЕЛЯ

Теория непротиворечива, если в ней нельзя вывести противоречие (фор-


мулу, эквивалентную лжи). Модель теории — интерпретация, в которой ис-
тинны все аксиомы. Теорема о корректности (Сколем, Гедель) классической
логики утверждает, что в любой модели теории истинны все ее теоремы. Та-
ким образом, логический вывод сохраняет истинность в интерпретации.
Теорема Геделя о полноте утверждает, что любая непротиворечивая те-
ория имеет модель. Тем самым оказывается, что классическое понятие тав-
тологии (формулы, истинной в любой интерпретации) описывается при по-
мощи сравнительно простого исчисления (классической логики предикатов).
Теорема о полноте эквивалентна аксиоме выбора, так что даже в принци-
пе никакого построения конкретной модели она дать не может. Конечно, в
частных случаях такую модель можно построить, но известные способы «по-
строения» моделей для нетривиальных случаев сводятся к преобразованию
какой-либо иной конструкции, созданной при помощи аксиомы выбора, и ко-
торая сама построена быть не может (например, ультрафильтра). Из теоремы
Геделя о полноте следует теорема компактности Мальцева: если любое ко-
нечное число аксиом теории имеет модель, то и вся теория имеет модель.
В самом деле, для вывода противоречия используется конечная конструкция
доказательства, и в ней может быть использовано лишь конечное число ак-
сиом.
Теорема Геделя о неполноте говорит, что любая теория с разрешимым
множеством аксиом, содержащая натуральные числа и дающая возможность
выразить хотя бы примитивно-рекурсивные функции, неполна. Теорема Ге-
деля о неполноте доказывается при помощи построения, по сути дела являю-
щегося одним из парадоксов автореференции. А именно, строится формула,
утверждающая свою собственную недоказуемость. Она не может быть дока-
зана, потому что тогда мы получили бы прямое противоречие, она не может
быть и опровергнута, потому что тогда мы получили бы доказательство ее
недоказуемости и, следовательно, ее обоснование.
Причины, по которым возникла теорема Геделя о неполноте, проясняет
теорема Тарского о невыразимости истины. Если теория достаточно сильна,
чтобы выразить подстановку, то в ней невыразимо понятие истинности ее
формул. В противном случае мы могли бы построить формулу, утверждаю-
щую: «Я лгу.» Поэтому теорему о неполноте обойти практически невозмож-
но, любая попытка ее обойти сама дает контрпример.
Теорема о существовании нестандартных моделей является следствием
B.3. ИДЕАЛЬНЫЕ И РЕАЛЬНЫЕ ПОНЯТИЯ ПО ГИЛЬБЕРТУ 867

теоремы полноты. Поскольку, в частности, для любого бесконечного уни-


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

Все подмножества нестандартной действительной оси сохраня-


ют все свойства стандартных множеств. Множество стандартных
действительных чисел ограничено сверху любым положительным
бесконечно большим числом. Значит, должно быть либо наиболь-
шее стандартное действительное число, либо наименьшее беско-
нечно большое. Но для любого стандартного x x + 1 также стан-
дартно, а для бесконечно большого x x − 1 также бесконечно.

Оба этих парадокса основаны на том, что свойство “быть стандартным” при-
надлежит метаязыку. Первый из них послужил основой теории полумно-
жеств, в которой классы могут быть подклассами множеств.

§ B.3. ИДЕАЛЬНЫЕ И РЕАЛЬНЫЕ ПОНЯТИЯ ПО ГИЛЬБЕРТУ

Нестандартные модели явно и бескомпромиссно высветили то, что по-


нималось ведущими мыслителями еще до их построения. Часто мы строим
в математике идеальные понятия, которые не имеют никакой (вычислитель-
ной) интерпретации, и при их помощи получаем реальные результаты.
Это наблюдение Д. Гильберт сделал основой своей методологической кон-
цепции математического знания. Согласно методологии Гильберта, основ-
ная часть математических понятий идеальна, и не стоит даже пытаться дать
им прямую интерпретацию. Но идеальные понятия могут привести к реаль-
ным результатам, которые, на самом деле, и составляют основную ценность
математики для приложений. Гильберт считал, что без идеальных понятий
868 ПРИЛОЖЕНИЕ B. МЕТОДОЛОГИЧЕСКИЕ РЕЗУЛЬТАТЫ

практически невозможно было бы достичь большинства ценных реальных


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

§ B.4. ПАРАДОКС ИЗОБРЕТАТЕЛЯ

Д. Пойа ввел понятие парадокса изобретателя независимо от концепции


идеальных объектов Д. Гильберта. Он использовал наблюдение, что при до-
казательстве по индукции часто необходимо усиливать доказываемое пред-
ложение, и индуктивное утверждение становится намного сложнее конеч-
ного результата. Эта необходимость усиливать результат, чтобы его строго
обосновать, на первый взгляд кажется парадоксальной.
Парадокс изобретателя связан со следующей логической и методологи-
ческой проблемой. В доказательствах порою встречаются вспомогательные
утверждения, более сложные, чем извлекаемые из них следствия. Можно ли
хотя бы в принципе устранить окольные пути в доказательствах, когда мы
доказываем лемму лишь затем, чтобы в дальнейшем применить ее в частных
случаях? Доказательства без окольных путей называют также прямыми.
Логические корни парадокса изобретателя оказались исключительно глу-
боки и связывают его с концепцией идеальных понятий и с современными
методологическими рассмотрениями.
Первым в данном ряду явился результат Хао Вана (1954 г.). Он постро-
ил последовательность примитивно-рекурсивных функций fn , таких, что для
доказательства утверждения ∀xfn (x) = 0 необходимо воспользоваться ин-
дукцией по формулам, содержащим не менее чем n кванторов. Таким обра-
B.5. ТИПЫ И ПОРЯДКИ 869

зом, даже в принципе в математических доказательствах внешне простых пред-


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

(n раз).
...2
o
22

Естественно, что сложность лемм при продвижении по данной последова-


тельности быстро возрастает.
В дальнейшем Р. Л. Гудстейн (1974) показал, что чем эффективнее про-
грамма, тем более высокие теоретические концепции могут понадобиться
для доказательства ее правильности.
Парадокс изобретателя показывает полную методологическую несостоя-
тельность редукционизма и эмпиризма (в том числе и такой его философской
профанации, как утилитаризм). Без концепций и идей высших уровней чело-
век обречен на умственное и духовное пресмыкание!

§ B.5. ТИПЫ И ПОРЯДКИ

Парадокс изобретателя остро ставит вопрос о критериях измерения слож-


ности формул и, в более общем виде, об иерархии идеальных концепций.
Простой, но весьма действенный, способ оценки уровня понятий и объектов
дал Б. Рассел. Он предложил классифицировать объекты по типам. Исходные
данные — объекты нулевого типа. Множества исходных данных и функции
над ними — объекты первого типа. Множества и функции над объектами
первого типа — объекты второго типа.
Соответственно, логические утверждения делятся на утверждения пер-
вого порядка, в которых нет ни переменных, ни кванторов по объектам вы-
ше нулевого типа, второго порядка, когда появляются кванторы по объектам
первого типа, и так далее. Классическая логика утверждений второго и бо-
лее высоких порядков уже неформализуема.
Оценка уровня теоретических концепций по уровню используемых объ-
ектов и порядку высказываний о них является прекрасным эвристическим
методом.
870 ПРИЛОЖЕНИЕ B. МЕТОДОЛОГИЧЕСКИЕ РЕЗУЛЬТАТЫ

Внутри утверждений одного и того же порядка есть также важная с прак-


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

Есть важнейшее наблюдение, впервые явно высказанное Ю.


Л. Ершовым, а неявно следующее из трудов С. К. Клини. Вычи-
слимость определена на первом уровне в самых разных аспек-
тах. Абсолютное определение вычислимости (например, маши-
ны Тьюринга) однозначно, как следует из тезиса Черча. Относи-
тельное также достаточно однозначно. Определение с ресурсны-
ми ограничениями слегка зависит от кодирования данных, но воз-
никающие неоднозначности легко преодолимы.
Для объектов второго типа вычислимость определяется уже
неоднозначно. Более того, определив вычислимость для объектов
второго типа, мы опять-таки сталкиваемся с неоднозначностью
ее распространения на третий, и так далее.

У обычного человека уже утверждения первого порядка с тремя перемена-


ми кванторов, а второго порядка с одной переменой кванторов, вызывают
затруднения при понимании. Примером такого утверждения является стан-
дартное определение непрерывности или предела. В математике объекты в
данный момент доходят до пятого типа. Примеры утверждений с семью пе-
ременами кванторов по четвертому-пятому типу дают современные теория
динамических систем, алгебра, логика, топология. Примеры явно сформули-
рованных принципов высоких типов можно найти, например, в книге [54] и
в последующих книгах Матросова и Васильева.
Заметим, что типы традиционных языков программирования, например,
языков Pascal и C++, не поднимаются в классификации Рассела выше перво-
го, причем сущности первого типа реализованы некорректно.

§ B.6. ЧИСТЫЕ ТЕОРЕМЫ СУЩЕСТВОВАНИЯ

Развитие современных логических методов привело к новым логическим


парадоксам. Например, Брауэр подметил следующий парадокс классическо-
го существования:
B.6. ЧИСТЫЕ ТЕОРЕМЫ СУЩЕСТВОВАНИЯ 871

В любой достаточно сильной классической теории имеется дока-


зуемая формула вида ∃x A(x), для которой нельзя построить ни-
какого конкретного t, такого, что доказуемо A(t).
В частности, нельзя построить в теории множеств ни одной нестандартной
модели действительных чисел, хотя можно доказать существование таких
моделей.
На самом деле чистые теоремы существования появляются уже в началь-
ном курсе математического анализа. Например, в принципе, при любом рас-
ширении понятия алгоритма, нельзя построить верхнюю грань любого огра-
ниченного множества действительных чисел, и даже найти наименьшее чи-
сло в любом непустом множестве натуральных чисел. Эти задачи могут ре-
шаться лишь в частных случаях.
Корни чистых теорем существования лежат в классической логике. Если
классическая теория неполна, то в ней обязательно есть чистые теоремы су-
ществования. А любая теория, содержащая натуральные числа, сложение и
умножение, неполна.
Если есть чистые теоремы существования, то теория неконструктивна.
Практически важными примерами теорий, где классическая логика конструк-
тивна — элементарная геометрия и элементарная алгебраическая теория дей-
ствительных чисел, в которой нет понятия натурального числа.
Для того, чтобы избавиться от неконструктивности, Брауэр предложил
модифицировать логику. Предложенная им модификация классической ло-
гики (удаление закона исключенного третьего и эквивалентного ему прин-
ципа снятия двойного отрицания) привела к логике, которая сохраняет кон-
структивность для теорий с числами — интуиционистской логике. Это была
первая из построенных конструктивных логик.
В конструктивных логиках формулы интерпретируются не через логиче-
ские значения, а через решения связанных с ними задач. Например, формула
A ∨ ¬A означает наличие ответа на вопрос, истинна A или ложна. Поэтому
обычно в них говорят не об истинности формул, а об их реализуемости.
В настоящее время известны следующие классы конструктивных логик.
1. Интуиционистская логика, которая адекватна вычислениям, где огра-
ничения на время и память несущественны (логика принципиальной
вычислимости). Интуиционистская логика максимально близка к клас-
сической, отличаясь от нее отсутствием закона исключенного третьего
(этого закона нет и в других конструктивных логиках). Ее модифика-
ция: интуиционистская логика с сильным отрицанием, или симметрич-
872 ПРИЛОЖЕНИЕ B. МЕТОДОЛОГИЧЕСКИЕ РЕЗУЛЬТАТЫ

ная интуиционистская логика, вводит в рассмотрение еще и процесс


анализа ошибок. Сильное отрицание сохраняет правило снятия двой-
ного отрицания и законы формулировки отрицаний, но полностью от-
казывается от правила приведения к абсурду. Соджержательно оно мо-
жет истолковываться как явное построение контрпримера.
2. Линейные логики, в которых рассматриваются вычисления, произво-
димые при ограничениии на ‘денежный’ ресурс. Тем самым мы мо-
жем иметь возможность реализовать A и B, но не оба вместе. Харак-
теристическим признаком для таких логик является отсутствие закона
A ⇒ A&A.
3. Нильпотентные логики, в которых вычисления производятся при огра-
ниченном времени. Таким образом, мы не можем не тратить выделен-
ный нам ресурс, даже если ничего не делаем. Характеристическим при-
знаком для них является правило исключенного застоя
A⇒A
¬A
Эти логики лучше всего описывают программирование от состояний.
4. Реверсивные логики, когда, наоборот, рассматриваются лишь обрати-
мые действия. Они описывают, например, вычисления в компьютере
на сверхпроводниках.
5. Когерентные логики, (часто называемые также категорными), когда
рассматриваются вопросы согласования данных. Они описывают про-
цессы приведения данных от одного типа к другому.

§ B.7. ДОКАЗАТЕЛЬСТВА И ПРОГРАММЫ

В конструктивных логиках доказательство не просто дает принципиаль-


ную возможность построить требуемый объект. При правильно подобранном
логическом инструментарии оно дает структуру алгоритма решения задачи.
При этом обычно выполняются следующие соотношения:
• Кванторы существования соответствуют значениям нулевого типа (зна-
чениям исходных типов). В целевой формуле они соответствуют ре-
зультатам создаваемой программы, в аксиомах — выходным данным
имеющихся методов либо подпрограмм.
B.7. ДОКАЗАТЕЛЬСТВА И ПРОГРАММЫ 873

• Конъюнкции соответствуют записям. Во многих конструктивных логи-


ках они естественно объединяются в промежуточных формулах с кван-
торами существования, образуя формулу, описывающую задачу постро-
ения структуры данных:

∃x1 , . . . , xn (A1 & · · · &Ak ).

В этой структуре xi представляют собой данные, а Ai — структуры


данных либо методы.

• Импликации соответствуют преобразованиям. A ⇒ B означает по-


строение преобразования решений A в решения B.

• Кванторы всеобщности соответствуют параметрам создаваемых про-


цедур. Часто они объединяются с импликацией в группу:

∀x1 , . . . , xn (A1 & · · · &An ⇒ B),

выражающую задачу построения функционала с параметрами-данны-


ми xi и параметрами высших типов Aj .

• Дизъюнкция соответствует в разных контекстах условному оператору


или структуре с альтернативными полями и дескриптором.

• Обычное отрицание порождает призраков. Оно доказывается приведе-


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

• Сильное отрицание соответствует явно пойманной ошибке, которую


предстоит проанализировать.
Эти соотношения можно рассматривать как эвристики достаточно высоко-
го уровня и общности, применимые для получения плодотворных аналогий
и нетривиальных предупреждений при анализе понятий программирования.
Точное их применение требует серьезного теоретического анализа и практи-
ческого чутья в каждом отдельном случае.
Не только отрицания порождают призраков. Безобиднейшая с виду фор-
мула, подобная
∀x, y, z(A(x, y)&B(y, z) ⇒ C(x, z))
874 ПРИЛОЖЕНИЕ B. МЕТОДОЛОГИЧЕСКИЕ РЕЗУЛЬТАТЫ

может превратить в призраки все построения y. Поэтому в доказательстве


имеются большие бездействующие блоки, которые служат для обоснования
строящегося алгоритма.
В принципе, если у нас есть адекватная конструктивная формализация,
программа может быть чисто механически извлечена из конструктивного до-
казательства. Получающиеся конструкции часто требуют дальнейшего пре-
образования, поскольку они сплошь и рядом пользуются функционалами выс-
ших типов и порою недостаточно эффективны (второе гораздо реже).
Более того, такой подход единственно разумный в задаче обоснования
программ. Это обосновано теоремой о верификации, упорно замалчиваемой
научным сообществом уже 20 лет1 :
Доказательство правильности программы может быть без существенного
увеличения длины перестроено в доказательство, синтезирующее програм-
му, удовлетворяющую заданным спецификациям.
Если в теории происходит увеличение длины в малое количество раз, на
практике это означает, что результат короче и проще. Отсюда следует вывод

Внимание!

Если вам необходимо построить доказательно правильную про-


грамму, делайте это с самого начала. Не пытайтесь сначала напи-
сать ее, а затем доказывать ее правильность!

§ B.8. ОСНОВНЫЕ ПОНЯТИЯ НЕФОРМАЛИЗУЕМОСТИ

И, наконец, последний класс логических парадоксов возникает на гра-


ницах между формализованными и неформализуемыми понятиями. Рассмо-
трим два из них.
Витгенштейн утверждает, что «Все, что может быть сказано, мо-
жет быть сказано ясно. А о чем не следует говорить, о том нужно
молчать.» Поскольку “сказано ясно” означает “быть формализо-
ванным,” а ни одно сложное понятие не может быть полностью
формализовано, нужно молчать обо всем, что выходит за рамки
чистой логики предикатов.
Ну, это в десять раз меньше, чем замалчивались открытия кн. Белосельского-Белозерско-
го.
1
B.8. ОСНОВНЫЕ ПОНЯТИЯ НЕФОРМАЛИЗУЕМОСТИ 875

(Аргумент Саймона) Все, что может быть выражено точно, может


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

Эти парадоксы привели к появлению теории неформализуемых понятий, но,


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

логом человека и компьютера.


Вообще говоря, трудно представить себе множество, хуже подходящее для моделирова-
ния логических значений, чем действительные числа.
3
876 ПРИЛОЖЕНИЕ B. МЕТОДОЛОГИЧЕСКИЕ РЕЗУЛЬТАТЫ

Соответственно, даже если B формально не следует из A, часто для под-


держания концептуальной непротиворечивости лучше принять его, приняв
A. Таким образом, и следиствие может быть концептуальным.
Далее, для описания понятий внутри одной теории (которая теперь рас-
сматривается как формализм в данный момент и для данной цели) наилуч-
шей оказывается классическая логика.
И, наконец, сложные формализованные понятия нормальный человек по-
нимает как неформализуемые. Он пытается вывести концептуальные след-
ствия и устранить концептуальные противоречия, подводя под формализм
некоторую идею. Именно в этом причина самых глубоких непониманий меж-
ду программистами и людьми и самых больших недоразумений при исполь-
зовании программных систем.
Краткое изложение основ теории неформализуемости дано в [63].

Появление и бурное развитие т. н. расплывчатой логики связано скорее всего с прагмати-


ческими и психологическими причинами: гораздо легче сказать, что вывод верен с досто-
верностью 0.9, чем прямо сказать: «Из сделанных предположений следует такой-то вывод.»
Тем более, что указание оценки, меньшей 1, заодно полностью избавляет эксперта от ответ-
ственности за неудачу.
Приложение C

Знания, данные, умения

Приложения теории в действительно сложных ситуациях либо к действи-


тельно сложным системам — то место, где без устойчивого и глубокого ми-
ровоззрения не обойтись. Здесь нужны знания и умения высокого уровня.
Поэтому сначала разберемся с данными понятиями.

§ C.1. АНАЛИЗ ПОНЯТИЙ

Тремя базовыми элементами как практической, так и теоретической ра-


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

— Знания должны прежде всего преобразовываться. Далее, их нужно


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

заться тяжелой. Все знают, как мучаются хорошие переводчики стихов. А А. Швейцер, так

877
878 ПРИЛОЖЕНИЕ C. ЗНАНИЯ, ДАННЫЕ, УМЕНИЯ

— Умения прежде всего применяются. Помимо этого, они преобразуют-


ся для обеспечения гибкости или приспособления к изменившимся усло-
виям. Далее, они обобщаются и пересматриваются.
При каждом применении и при каждом пересмотре существующего зна-
ния его конкретные формы видоизменяются. Самым часто применяемым пре-
образованием знания является его конкретизация. Например, применением
теоремы, обратной к теореме Пифагора, является возможность построения
прямого угла с помощью веревки, разделенной на 12 частей. Принцип беско-
нечного спуска является одной из конкретизаций возвратной индукции. Все
многочисленные понятия гомоморфизма в алгебре являются конкретизация-
ми общего понятия морфизмов алгебраических систем.
Факты могут быть включены в базу имеющихся знаний, лишь если они
организованы при помощи суждений более высокого уровня.
Главной характеристикой знания является его гибкость, возможность вы-
ражать одно и то же в различных формах. Именно это позволяет исключи-
тельно широко применять настоящие знания, но порою затрудняет понима-
ние обычными людьми того, что говорят действительно знающие специали-
сты, поскольку они не могут вообразить себе, что столь различные высказы-
вания эксперта являются всего-навсего различными выражениями одной и
той же идеи.2 Более того, творческие и знающие люди обычно даже не мо-
гут думать на внешнем ‘естественном’ языке. Они вынуждены переводить
внутренние структуры в выражения общепринятого языка, чтобы результаты
рассуждений могли понять другие.
В XVIII веке забытый русский мыслитель князь Белосельский-Белозер-
ский опубликовал книгу [7], получившую прекрасный отзыв И. Канта. Кон-
цепция «Дианиологии» была возвращена в научный обиход трудами А. Гу-
лыги и затем развита с современных позиций (см. [62])
Стержнем концепции Белосельского-Белозерского является то, что насто-
ящие знания и умения делятся на уровни, далеко отстоящие друг от друга,
и не смог перевести свою книгу о Христе с французского на немецкий и в конце концов
переписал ее заново.
Интересны комментарии Швейцера. Он сказал: «Французский язык больше подходит для
выражения общих идей, а немецкий — для частностей и подробностей». Если столь разли-
чаются сферы, в которых эффективно применимы естественные языки, то что же говорить
об искусственных и полуискусственных!
Здесь частенько требуется перевод даже для двух специалистов, формально говорящих на
одном и том же языке, но пользующихся различными парадигмами.
2
Конечно, эти формулировки делают упор на различных ее аспектах.
C.2. УРОВЕНЬ НАСЕКОМОГО 879

а между уровнями лежат громадные пространства квазизнаний, умствова-


ния, мудрствования, квазиумений (заносчивой халтуры). Игнорирование то-
го, что знания и умения имеют качественно разные уровни, которые зачастую
плохо согласуются между собой, но по отдельности успешно применимы,
является одной из бед современных научных исследований и практических
систем. А игнорирование того, что нельзя переползти с одного уровня на дру-
гой (можно либо перепрыгнуть, но это рискованно, либо быстро пробежать,
не задерживаясь, но после основательной подготовки), является одной из бед
современной системы обучения. Переученный человек обычно хуже чуть не-
доученного.
Концепцию Белосельского-Белозерского будем называть концепцией кня-
зя или сферами разума. Она была оценена критиком, стоявшим на высшей
ступени человеческого рационального мышления, но она же грубейшим обра-
зом противоречила примитивной концепции рационализма, которую агрес-
сивно вбивали в головы просветители.
Основная методологическая концепция современной науки в области со-
отношения данных, знаний и умений — то, что данные и умения порожда-
ют знания путем эмпирического либо теоретического обобщения. А знания
позволяют упростить умения до методов применения знаний к соответству-
ющим данным. Картина слишком хорошая, и она, как и всякая благостная
сказочка, обязана быть не просто неверной, но и быстро заводящей в тупик.
С логической точки зрения простейший способ учесть рефлексию и вне-
сти коррективы в данную картину — распределить знания и умения по ти-
пам, как это принято в математике со времен Рассела. При конструктивной
интерпретации знаний сложность информации, требуемой для применения
знания, приблизительно соответствует типу объекта, необходимого для его
реализации.

§ C.2. УРОВЕНЬ НАСЕКОМОГО

Низший уровень знаний и умений примерно соответствует уровню на-


секомого. Здесь имеется единая циклическая жизненная программа, выпол-
нение которой разнообразится непосредственными реакциями на внешние
раздражители. И сама программа, и реакции заложены заранее и совершен-
ствованию в течение жизни не подлежат.
Преимущество такого уровня — то, что насекомое почти невозможно сбить
с толку, оно все равно будет выполнять свою программу, и это может ока-
880 ПРИЛОЖЕНИЕ C. ЗНАНИЯ, ДАННЫЕ, УМЕНИЯ

заться единственным шансом на выживание в совершенно непредсказуемых


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

§ C.3. СТЕРЕОТИПНОЕ РЕАГИРОВАНИЕ

Следующий уровень — уровень условных рефлексов или непосредствен-


ного (стереотипного) реагирования. Условный рефлекс состоит в распозна-
вании ситуации и применении в ней некоторого фиксированного действия с
тем, чтобы получить желаемый результат. Это действие уже целенапра-
вленное, такое существо уже обучаемо.
Центр тяжести стереотипного реагирования лежит в системе распознава-
ния, которая и является соответствующим ему знанием.
С логической точки зрения данные, участвующие в процессе стереотип-
ного реагирования, могут рассматриваться как элементарные факты вида
P (c1 , . . . , cn ) или ¬ P (c1 , . . . , cn ) (C.1)
или, в самом общем случае, как их булевы комбинации. На самом деле и буле-
выми комбинациями чаще всего являются конъюнкции фактов, дизъюнкции
уже слишком тяжело постигаемы. Соответственно, вовлеченные в процесс
реагирования знания можно описать в форме
∀x1 , . . . , xn (A1 (~x) & · · · & Ak (~x) ⇒ Q(~x)), (C.2)

где Ai — либо предикаты, либо их отрицания.


Пример C.3.1. Примером стереотипного реагирования, переведенного на
уровень насекомого, являются шахматные программы разыгрывания стан-
дартных эндшпилей. Строится полная таблица позиций данного эндшпиля
(скажем, ферзь против ладьи), и для каждой позиции путем полного пере-
бора помечается, выигрышная она, проигрышная или ничейная, причем при
выигрышных и проигрышных дополнительно отмечается число шагов до вы-
игрыша либо, соответственно, проигрыша. Теперь достаточно распознать си-
туацию в том смысле, что просмотреть все позиции, которые могут получить-
ся из текущей за один ход, и сделать данный ход.
C.3. СТЕРЕОТИПНОЕ РЕАГИРОВАНИЕ 881

Конец примера C.3.1.


Пример C.3.2. Стереотипное реагирование — тот тип знаний и умений, ко-
торому обучают на фирменных курсах и который является наиболее рас-
пространенным в американской системе образования (в противовес европей-
ской). В результате человек запоминает, скажем, для компьютерной системы,
какую кнопку надо для какого действия нажимать, но требует переучивания
при появлении новой версии системы, а тем более новой системы, хотя бы и
построенной на тех же принципах.
Конец примера C.3.2.
На данном уровне мышления возможны два способа обучения. Во-первых,
затверживание новой ситуации при помощи, как правило, нескольких повто-
рений. Во-вторых, свертка нескольких обычно следующих друг за другом
стереотипных действий в новое единое действие.
Классическим примером системы знаний, построенной по данному прин-
ципу, является система предписаний «Талмуда». Четко расписано, что в ка-
ких ситуациях еврей должен делать, и чего он делать не должен. Но, конечно
же, и жизнь изменяется, и список ситуаций не может быть полным.
На уровне стереотипного реагирования действуют большинство специа-
листов не очень высокого класса, независимо от формального названия их
специальности. Но типичен данный уровень скорее для рабочего и техника.
Логически именно данному уровню соответствуют основные построения
индуктивной логики, начиная с Ф. Бэкона и кончая современными методами
планирования, основанного на распознавании образов.
Далее, на этом же уровне организована база знаний типичного эрудита.
Он чисто формально достает по ключевым словам сведения, которые кажут-
ся ему необходимыми.
Тест на данный уровень — поставить задачу, требующую планирования
на 2–3 шага вперед.
Первые зачатки критического мышления (здравый смысл) также появля-
ются на данном уровне.
Достоинством стереотипного реагирования является быстрота ответа на
знакомую ситуацию, соединенная с возможностью перевести незнакомую
ситуацию в разряд знакомых. Быстрота стереотипного реагирования, тем не
менее, порою хуже реакции насекомого, поскольку все-таки сначала нужно
распознать ситуацию, тем более, если она не является проходной.
Еще одним достоинством является стимул к обучению, когда ситуация
распознается как не подходящая ни под одно из стандартных действий.
882 ПРИЛОЖЕНИЕ C. ЗНАНИЯ, ДАННЫЕ, УМЕНИЯ

Недостатками являются, во-первых, возможность растерянности и пани-


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

§ C.4. ТУПОСТЬ

Это пространство находится между сферами фиксированной программы


и стереотипного реагирования.
Тупой человек не может распознать ситуацию, но пытается применить
то, что он видел в качестве успешных действий других. Ярче всего она опи-
сана в русских сказках про дурня (или же в стихотворении Кирши Данилова,
включенном в «Азбуку» Л. Н. Толстого). Лекарство от тупости — как пра-
вило, еще большая тупость: затвердить несколько фиксированных правил
(очень мало, чтобы не сбиваться), и выполнять данную систему правил как
программу с инстинктивными реакциями.
***
Следующие уровни связаны уже с преобразованием знаний и умений.
Но преобразования низшего уровня не определяют однозначно возможных
преобразований высших уровней, и это проявляется уже на втором уровне
знаний и умений. А именно, преобразовывать простейшие правила стерео-
типного реагирования можно двумя способами. Во-первых, можно строить
длинные их цепочки, планируя на несколько шагов вперед, во-вторых, мож-
но преобразовывать сами условия. Эти два уровня приводят к двум разным
типам мышления. Мы начнем с первого из них, когда строятся комбинации
стереотипных действий.

§ C.5. КОМБИНАЦИОННОЕ (КОМБИНАТОРНОЕ) ПЛАНИРОВАНИЕ

Впервые данный уровень мышления был явно выделен в шахматах. Ком-


бинация в шахматах — совокупность форсированных ходов, в ходе которых
игрок несет либо может понести некоторый материальный ущерб с тем, что-
бы по ее завершении получить преимущества. Итак, в комбинации есть це-
лая последовательность (и, возможно, разветвленная) действий, результат ка-
ждого из которых предсказуем, но настоящей целью осуществляющего ком-
бинацию является лишь результат последнего действия. В некотором смысле
C.5. КОМБИНАЦИОННОЕ (КОМБИНАТОРНОЕ) ПЛАНИРОВАНИЕ 883

начальные действия подготавливают почву для заключительных.


Комбинационные игроки хорошо справляются также с т. н. позициями,
требующими конкретного расчета, когда нужно предусматривать результаты
последовательностей практически вынужденных ходов.
Комбинационному дарованию в шахматах соответствует тактическое пла-
нирование в военных действиях, когда, как правило, недостаточно рассчи-
тывать на шаг вперед, а нужно иметь в виду целую последовательность дей-
ствий и ответов на возможные контрдействия противника, с тем, чтобы вы-
полнить стоящую перед соединением задачу.
Комбинационному планированию соответствует логический вывод, не со-
держащий лемм (нормализованный логический вывод). Если входящие в не-
го формулы имеют вид C.2, то вывод практически сводится к тому, что в «ис-
кусственном интеллекте» называется системой продукций.
Если же план разветвленный, то продукций недостаточно, и нужны фор-
мулы, превосходящие даже то, что заложено в языке PROLOG:
∀~x(A1 (~x) & · · · & Ak (~x) ⇒ B1 (~x) ∨ · · · ∨ Bm (~x)). (C.3)
Комбинационное планирование, как показали эксперименты с животны-
ми, тот высший уровень, на который они могут подняться.
Надо заметить, что стандартное применение традиционной логики выво-
дит нас именно на данный уровень. Это особенно четко прослеживалось в
ее схоластической традиции с длинными цепочками силлогизмов и правил
вывода.
Данный уровень типичен для программиста-кодировщика, инженера, ис-
кусного рабочего.
Одной из опасностей, возникающей при владении данным уровнем, явля-
ется соблазн поиска лобовых решений там, где надо было бы переформу-
лировать задачу в соответствии с системой ценностей и искать новую цель.
Такой человек обычно рвется к победе, игнорируя то, что она грозит стать
пирровой.
Достоинства комбинационного планирования. Во-первых, возможность
полной перемены декораций в результате последовательности хорошо спла-
нированных действий. Комбинатор успешно ищет новое решение там, где
другие не могут соединить несколько старых.
Во-вторых, человек, комбинирующий свои приемы, вынужден пересма-
тривать их на предмет согласованности, и поэтому даже база стандартных
приемов у него обычно гораздо лучше структурирована, чем у того, кто ра-
ботает на уровне условных рефлексов.
884 ПРИЛОЖЕНИЕ C. ЗНАНИЯ, ДАННЫЕ, УМЕНИЯ

И, наконец, комбинатор обычно оптимистичен, поскольку мир кажется


ему единым, и наслаждение от поиска новых решений добавляет положи-
тельного настроя в его жизнь.
Недостатками являются, во-первых, необходимость заранее продумать и
спланировать действия, что, как правило, противоречит быстроте реакции.
Во-вторых, ненадежность создаваемых планов, даже если каждое действие
вполне надежно и результат его предсказуем. Чем длиннее и разветвленнее
план, тем он уязвимее.
Еще один недостаток людей, которым свойственно комбинаторное мыш-
ление — плохая приспособляемость к ординарным ситуациям. Зачастую они
просто обостряют ситуацию, переводя ее в чрезвычайную, лишь потому, что
уже не могут выносить выжидания. Это может локально улучшить положе-
ние комбинатора, но стратегически он проигрывает.
Люди, владеющие комбинаторным мышлением вместе с логическими ли-
бо математическими орудиями, являются мощным инструментом обскуран-
тизма, поскольку они моментально видят несогласованности как новых кон-
цепций с тем, что считается почти догмой, так и внутри самих новых кон-
цепций. Именно на данном уровне работали квалификаторы Священной Ин-
квизиции.

§ C.6. ГЛУПОСТЬ

Пространство между стереотипным реагированием и комбинационным


планированием — типичная глупость. Человек не желает пользоваться из-
вестными ему рецептами, пытается рассчитывать вперед, но сам себя запу-
тывает в своих расчетах, не принимая во внимание возможные (а то и наибо-
лее вероятные) последствия своих действий.
В данной области находится и методичность как форма классической
немецкой глупости. Методичность не имеет никакого отношения к методу.
Методичный человек планирует свои действия намного вперед и теряется
при нарушениях режима, регламента и т. п. Но в неизменной, полностью ру-
тинной, обстановке методичность может быть выигрышным способом пове-
дения.
Методичность немного похожа и на уровень насекомого, но принципи-
ально отличается от него и сложностью программы, и реакцией на непреду-
смотренные ситуации.
Методичность, как показали личным примером И. Кант и лорд Кавендиш,
может быть прекрасным способом для человека, имеющего прочное положе-
C.7. СТРАТЕГИЧЕСКОЕ ПЛАНИРОВАНИЕ И ПРЕОБРАЗОВАНИЕ ДЕЙСТВИЙ 885

ние и гарантированный комфортный прожиточный минимум, а также живу-


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

СТРАТЕГИЧЕСКОЕ ПЛАНИРОВАНИЕ И ПРЕОБРАЗОВАНИЕ ДЕЙ-


СТВИЙ
§ C.7.

Возможно идти и с другой стороны. Часто достаточно преобразовать ру-


тинную формулу, чтобы она повернулась другой стороной, но для этого нуж-
но владеть системой преобразований утверждений, понятий и умений.
Впервые такую систему преобразований пытались систематически раз-
вить мудрецы-раввины, столкнувшиеся с необходимостью выполнять кано-
низированные, но зачастую противоречивые требования Талмуда, да еще и
принимать согласно тому же Талмуду решения в непредусмотренных ситуа-
циях.
Пример C.7.1. В «Талмуде» предусмотрены наказания тому, кто присвоил
овцу или осла другого еврея, но не указано наказание тому, кто присвоил козу.
Раввины назначали его по следующей логике: коза ценнее овцы, но менее
ценна, чем осел. Поэтому и наказание должно быть больше, чем за овцу, но
меньше, чем за осла.
Конец примера C.7.1.
В традиционной логике зачатки такой системы преобразований были (на-
пример, обращение и превращение суждений).
Пример C.7.2. Например, предложение

Ни один русский не побывал на Луне (C.4)

может быть обращено как

Те, кто побывали на Луне — не русские (C.5)

и превращено как

Луна — то место, где не побывал никто из русских. (C.6)

Конец примера C.7.2.


886 ПРИЛОЖЕНИЕ C. ЗНАНИЯ, ДАННЫЕ, УМЕНИЯ

Логически уровень преобразований представляется как применения про-


позициональных и предикатных эквивалентностей. Данная модель позволя-
ет увидеть качественный скачок при переходе от комбинаторного уровня к
данному. Эквивалентность даже пропозициональных высказываний — экс-
поненциально трудная задача, и, таким образом, преобразование объективно
намного труднее композиции.
Если план разветвленный, то чисто формально комбинаторное мышление
становится не менее сложным, чем преобразования.
Вторая логическая характеризация данного уровня — классические Π-
формулы вида ∀~x A(~x), где A — произвольная бескванторная формула, и Σ-
формулы вида ∃~x A(~x).
Еще одна логическая характеризация — конструктивные формулы вида
A ⇒ B, где посылка и заключение — любые классические пропозициональ-
ные формулы.
Функциональная характеристика — алгебра функций.
Сфера преобразований — уровень деятельности программиста-постанов-
щика, отличного рекламного агента и хорошего дипломата, хорошего мате-
матика (особенно теоретика), хорошего философа.3
На этом уровне работают и работали большинство из тех, кто стали при-
знанными гениями. В частности, он четко прослеживается у Эдисона, Бер-
банка, Наполеона I.
Оптимизм предыдущего уровня сменяется здесь некоторым пессимиз-
мом. Поскольку человек видит, как можно преобразовывать понятия и цели,
он начинает, почти как Экклезиаст, восклицать:
— Бывает нечто, о чем говорят: «смотри, вот это новое»; но это было уже
в веках, бывших прежде нас. 4
Такой человек ясно видит, что то, что выдают за новое — всего лишь
старое вино, влитое в новые мехи и сверху расцвеченное новыми узорами.
И, вместе с тем, как тот же Экклезиаст, человек на данном уровне, если не
теряет ориентировки на высшие ценности, начинает благодарить Бога либо
судьбу за то, что она дала ему умственное зрение:
— У мудрого глаза его — в голове его, а глупый ходит во тьме. Но узнал
я, что одна участь постигает их всех.5
Слово хороший включает в себя ограничения и сверху и снизу: существенно выше сред-
него уровня, но не выдающийся.
3

4
Экклезиаст, гл. 1, стих 10.
5
Экклезиаст, гл. 2, стих 14.
C.8. РЕЛЯТИВИЗМ 887

Заметим, что применять композиции и преобразования вместе крайне опас-


но, поскольку ненадежности обоих способов великолепно дополняют друг
друга.
Достоинства уровня преобразований.
Прежде всего, таковой является возможность представить имеющиеся
знания в нужной форме либо перенести их на случай, подобный рассмотрен-
ным, но не рассмотренный ранее. Пожалуй, здесь впервые знания становятся
настоящими знаниями.
Далее, преобразования позволяют лучше структурировать пространство
знаний, и заставляют нас иметь единую модель мира, что обеспечивает цель-
ность восприятия.
И, наконец, преобразования позволяют трезво оценить ситуацию, опреде-
лить, в чем заключается стратегический выигрыш, и идти к нему, невзирая
на частные поражения.
Недостатки. Прежде всего, это отсутствие дальнего расчета и игнориро-
вание побочных эффектов преобразований. Второй недостаток — отсутствие
быстрой реакции на мимолетные возможности. И, наконец, слишком часто
преобразования ограничены узкой областью и базируются на излишне жест-
кой модели мира, что ведет к узости кругозора человека, работающего на
данном уровне.

§ C.8. РЕЛЯТИВИЗМ

Пространство, промежуточное между сферой и преобразований и пре-


дыдущими сферами описывается сложнее, потому что сферу преобразова-
ний нельзя однозначно поставить выше сферы комбинаторного мышления.
Основной чертой релятивизма является наглое передергивание фактов, по-
ложений, законов. Поскольку по виду эквивалентные предложения часто не
имеют между собой ничего общего, а, соединяя комбинаторику и преобразо-
вания, можно из формально противоречивой системы знаний6 вывести все,
что угодно, человек, попавший в данное пространство, зачастую стремитель-
но превращается в законченного скептика и циника, тем более, что ему при
этом способствуют и практика, и теория нынешнего общества.
Другой стороной данного пространства является еще один возможный
вывод, который делают люди, имеющие внутренний иммунитет к огульному
Которой является практически любая система знаний человечества, кроме нескольких
точных наук.
6
888 ПРИЛОЖЕНИЕ C. ЗНАНИЯ, ДАННЫЕ, УМЕНИЯ

скептицизму. Поскольку лишь точные науки выдерживают проверку комби-


нацией композиций и преобразований, то лишь они имеют право на суще-
ствование, а гуманитарное знание знанием просто не является.7

§ C.9. ВЛАДЕНИЕ МЕТОДОМ

Метод — это общий способ преобразования планов. Примерами методов


могут служить метод динамических систем и метод диаграммного поиска в
математике. К несчастью, изложение существующих методов в других на-
уках не доведено до такой степени точности и конкретности, чтобы на них
можно было сослаться здесь.
С логической точки зрения метод выражается как сколь угодно сложная
формула классического исчисления предикатов (либо Π-формула классиче-
ского исчисления предикатов второго порядка). Альтернативная логическая
характеристика — конструктивное исчисление высказываний либо ограни-
ченное конструктивное исчисление предикатов.
Функциональная характеристика — функционалы второго порядка, орга-
низованные в достаточно богатую структуру, которая может рассматриваться
как функционал третьего-четвертого порядка.
Характерная черта такого уровня — взаимосогласованные модели мира
и действий. Это создает внутренне замкнутую систему знаний, и зачастую
человек, работающий на уровне метода, владеет одним-единственным мето-
дом, которого ему хватает.
Применение метода требует подстановки в него целых планов и условий,
это — конкретизация понятия высшего уровня, которая сама по себе является
алгоритмически трудной операцией.8
Сфера метода — уровень, присущий рабочим-асам, исключительно вы-
сококвалифицированным инженерам и врачам европейской медицины, ква-
лифицированным ученым, особенно теоретикам в точных науках и приклад-
никам-математикам, не замыкающимся в узкой области, гроссмейстерам и
полководцам-стратегам, программистам-аналитикам.
Пример борьбы метода против преобразований — кампания Кутузова про-
тив Наполеона. Наполеон блестяще владел преобразованиями композицион-
ных планов, что на уровне сражений и тактических маневров кажется стра-
Как сказал автору один математик, когда появились первые работы по формализации
неформализуемых понятий: «Если понятию не может быть придан однозначный точный
7

смысл, то оно просто не является понятием».


8
Известно, что унификация по типам выше первого неразрешима.
C.10. УМНИЧАНЬЕ, МЕССИАНСТВО 889

тегией, но, столкнувшись с настоящей стратегией, безнадежно проиграл, по-


теряв свою армию и сохранив в целости российскую, причем выигрывая все
битвы, в том числе и спасая стратегически совершенно безнадежные.
Уровень метода соответствует тому, что часто называют системным взгля-
дом. Именно поэтому системный подход при систематическом изложении
практически неизбежно сбивается к применениям одного из методов, чаще
всего метода динамических систем.
Сфера метода — это типичный уровень творческого лидера, основателя
научной школы либо (возможно, даже не тоталитарной) секты. При отсут-
ствии вкуса к руководству и коллективному действию это — оптимальный
уровень советника и эксперта. Но выводы данного советника требуют пере-
проверки на другом уровне. Если он дает предупреждение, то он, как прави-
ло, прав. Если он дает положительное заключение, это должно насторажи-
вать.
Сфера метода — исключительно оптимистичный уровень. Цельная кар-
тина Мира и Идей способствует активному (практическому либо духовному)
действию человека, прочно вставшего на этот уровень, а высота уровня в
значительной степени страхует его от последствий неизбежных тактических
ошибок, что еще добавляет настоящего оптимизма, а именно, устойчивости
к неудачам.
Недостаток уровня метода — прежде всего, иллюзия завершенности зна-
ния, которая возникает из-за единой модели мира и действий. Человек, в
совершенстве владеющий методом, зачастую считает, что практически все
можно сделать данным методом. Человек данного уровня часто может также
недооценить последствия мелких тактических неудач, и тогда его здоровый
оптимизм оборачивается своей уязвимой стороной (которая есть у любого
метода и любой стратегии). Абсолютизация метода и успехи, связанные с
его применением, вызывают некритическое восприятие и у самого человека,
и (еще чаще) у окружающих его учеников и почитателей.
Еще один общий недостаток высших уровней (начинающихся именно с
уровня метода) — тот, что на них трудно удержаться. Чтобы стоять на месте,
здесь нужно бежать, а чтобы попасть в другое место,— бежать вдвое быстрее
(Л. Кэррол).

§ C.10. УМНИЧАНЬЕ, МЕССИАНСТВО

Это — два различных проявления пространства перед сферой метода.


Человек, на самом деле не овладев методом, пытается его применять и за-
890 ПРИЛОЖЕНИЕ C. ЗНАНИЯ, ДАННЫЕ, УМЕНИЯ

путывается в конкретизациях высокоуровневых понятий. Это — умничанье.


Такой человек сам себя приводит к неправильным решениям в простейших
ситуациях, где надо было бы перейти на более низкие уровни.
Пример сползания со сферы метода в пространство умничанья, привед-
ший к катастрофической стратегической неудаче — реализация плана Шлиф-
фена германской армией в августе 1914 г.
Мессианство — человек, достаточно овладевший методом, чтобы его при-
менять, но недостаточно им овладевший, чтобы понять, когда применять его
не нужно (смотри, например, труды многих апостолов ООП).
Заметим, что человек, устойчиво вставший на сферу метода, хотя, воз-
можно, и абсолютизирует свой метод, тем не менее четко чувствует случаи,
когда применять его не стоит. Его защитной реакцией в подобных ситуациях9
является характеристика возникшего положения либо рассматриваемого во-
проса как такого, в котором нельзя пользоваться средствами слишком высо-
кого уровня. При этом говорятся слова, подобные таким: «Ну, это не научная,
а житейская (бюрократическая, техническая) проблема».

§ C.11. МНОГОУРОВНЕВОЕ МЫШЛЕНИЕ

Некоторые элементы данного уровня часто соединяют с системным под-


ходом, но системный подход естественно располагается на предыдущем уров-
не, и подобное соединение приводит лишь к выбросу в пространство мудр-
ствования (смотри далее).
Логическим аппаратом данного уровня служит классическая теория не-
формализуемых понятий [63] и полная конструктивная логика.
Функциональной моделью здесь является пространство функционалов
конечных типов.
Этот уровень требует альтернативных моделей мира и действий, каждая
из которых внутренне согласована, и которые дают многомерный взгляд на
проблему.
Он органичен для изобретателя высокого класса, системного аналитика,
склоняющегося к восточному подходу в медицине врача (т. е. лечащего боль-
ного, а не болезнь).
Недостатки. По сравнению с предыдущей сферой, человек на данной сфе-
ре зачастую становится менее активным. Он теряет ощущение целостности,
приобретенное после овладения методом. Он начинает видеть недостатки и
9
Кстати, почти всегда оправданной.
C.12. МУДРСТВОВАНИЕ, ИНТУИТИВИЗМ 891

ловушки в слишком многих местах. Он не может быть единомышленником


даже самому себе. В любой схватке он пытается, даже встав на одну из сто-
рон, морально оказаться над битвой.
Такой человек все время подмечает недостатки в положительно охарак-
теризованном решении и достоинства в большинстве неудач. А это может
сбить с толку прямолинейно мыслящих коллег.

§ C.12. МУДРСТВОВАНИЕ, ИНТУИТИВИЗМ

На полпути к уровню многоуровневого мышления застрял интуитивизм,


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

§ C.13. ДАО

Этот китайский термин лучше всего выражает исключительно плохо пе-


редаваемое словами состояние понимания целостности мира, утерянной на
предыдущей сфере. Много говорить об этом не будем, тем более, что до этого
уровня поднимаются единицы, а удерживаются на нем считанные по паль-
цам личности, и «Говорящий не знает, знающий — не скажет».
Логический аппарат данного уровня еще не разработан. Функциональная
модель, пожалуй, бестиповое λ-исчисление.
Линий поведения у человека, достигнувшего данного уровня, бесконеч-
но много, но общая их идея сводится к одной из двух: либо отшельничество
(формальное или фактическое) и недеяние, либо полностью незаинтересо-
ванное активное действие. Второе встречается намного реже.

§ C.14. ЛЖЕПРОРОКИ

Это те, кто застряли в пространстве перед уровнем дао. Они принимают
свои озарения, которые даются им ценой максимального напряжения интел-
лектуальных и духовных сил, за абсолютную истину, и тем самым убивают
элементы10 Истины, которые в них содержатся. Хотя такой самообман и по-
нятен, вред, приносимый подобными людьми, как правило, субъективно ис-
кренними, слишком велик.
10
Порою весьма значительные.
892 ПРИЛОЖЕНИЕ C. ЗНАНИЯ, ДАННЫЕ, УМЕНИЯ

§ C.15. ХИМЕРЫ И ВЫМЫСЛЫ

Комментариев нет.
Общая картина сфер знаний и умений представлена на рис. C.1.

Химеры
Лжепророки
Мудрствование
Умничанье
Релятивизм
Глупость

Тупость

Коллапс

Насекомое
Стереотипы

Комбинации
Планы
Метод
Многоуровневость
Дао
Рис. C.1. Сферы Белосельского-Белозерского
Литература

[1] Абрамов С.А., Гнездилова Г.Г. и др. Задачи по программированию —


М.: Наука, 1988. — 224 с.

[2] Абрамов В.Г., Трифонов Н.П., Трифонова Г.Н. Введение в язык Па-
скаль: Учеб. пособие. — М.: Наука, 1988. — 320 с.

[3] С. Алагич, М. Арбиб. Проектирование корректных структурирован-


ных программ. М.: Радио и связь, 1984, 264 с.

[4] Творчество как точная наука. Теория решения изобретательских за-


дач.М.: Советское радио, 1979, 184 с.

[5] И. О. Бабаев, М. А. Герасимов, Н. К.Косовский и др. Интеллектуаль-


ное программирование. Турбо Пролог и Рефал на первсональных ком-
пьютерах. СПб.: СПбГУ, 1992,— 167 с.

[6] Бардинова Т.Ю. и др. От Паскаля к Аде. — М.: Финансы и статистика,


1990 — 255 с.

[7] А. М. Белосельский-Белозерский. Dianyologie ou tableau philosophique


de l’entedment. Dresden, 1790.

[8] А. П. Бельтюков. Язык программирования Рефал (учебное пособие для


студентов-математиков). Ижевск, УдГУ, 1989,— 62 с.

[9] М. М. Бежанова, Л. А. Москвина. Практическое программирование.


Приемы создания программ на языке Паскаль. М.: Научный Мир, 2001,
270 с.

[10] М. М. Бежанова, И. В. Поттосин. Современные понятия и методы про-


граммирования. М.: Научный мир, 2000.

893
894 ЛИТЕРАТУРА

[11] Б. У. Боэм. Инженерное проектирование программного обеспечения.


М.: Радио и связь, 1985.

[12] И. Братко. Программирование на языке ПРОЛОГ для искусственного


интеллекта. М.: Мир, 1990, 560 с.

[13] А. Л. Брудно. Программирование в содержательных обозначениях. М.:


Наука, 1968, 142 с.

[14] Ф. П. Брукс Как проектируются и создаются программные комплек-


сы. М.: Мир, 1979.

[15] Ф. П. Брукс. Мифический человеко-месяц, или как создаются про-


граммные системы. СПб: Символ-Плюс, 1999.

[16] Г. Буч. Объектно-ориентированное проектирование с примерами при-


менения. М.: Конкорд, 1992.

[17] Г. Буч, Д. Рамбо, А. Джекобсон. Язык UML. Руководство пользователя.


М.: ДМК, 2000.

[18] А. М. Вендров CASE-технологии. Современные методы и средства


проектирования информационных систем. М.: Финансы и статистика,
1998.

[19] Вирт Н. Систематическое программирование. Введение: Пер. с англ.


- М.: Мир, 1977. – 183 с.

[20] Вирт Н. Алгоритмы + структуры данных = программы: Пер. с англ. -


М.: Мир, 1985. – 406 с.

[21] Систематический подход к программированию. М.: Наука, 1988, 207


с.

[22] Гамма Э., Хелм Р., Джонсон Р., Влрссидес Дж. Приемы объектно-
ориентированного проектирования. Паттерны проекти-рования. —
СПб: Питер, 2001. — 368 с.

[23] Р. Гантер. Методы управления проектированием программного обес-


печения. М.: Мир, 1981.
ЛИТЕРАТУРА 895

[24] С. К. Годунов, В. С. Рябенький. Разностные схемы. М.: Наука, 1973,


398 стр.

[25] Гололобов В. И., Чеблаков Б. Г., Чинин Г. Д. Описание языка ЯРМО.


Препринт ВЦ СО АН СССР, 1980, № 247, 278.

[26] Д. Грис. Конструирование компиляторов для цифровых вычислитель-


ных машин. — М.: Мир, 1975

[27] Д. Грис. Наука программирования. М.: Мир, 1984.

[28] Рутен Ф.Гурин, Сергей А.Романенко ЯЗЫК ПРОГРАММИРОВАНИЯ


РЕФАЛ ПЛЮС. http://www.rational.com/products/rup/index.jsp

[29] У. Дал, Э. Дейкстра, К. Хоор. Структурное программирование. М.:


Мир, 1975, 246 с.

[30] Дейкстра Э., Дисциплина программирования. М.: Мир, 1978, 278 с.

[31] Дейт К. Введение в системы баз данных. М.: Наука, 1980, 463 с.

[32] А. П. Ершов. Организация АЛЬФА-транслятора. / В кн. АЛЬФА - си-


стема автоматизации программированияњ под. ред. А.П. Ершова. —
Новосибирск: Наука. — 1967. — с. 35-70

[33] Ершов А. П. О сущности трансляции. Препринт № 6, Новосибирск:


ВЦ СО АН СССР, 1977.– 33 с.

[34] Ершов Ю. Л., Палютин Е. А. Математическая логика. М.: Наука, 1987.

[35] Зуев Е.А. Программирование на языке Turbo-Pascal 6.0, 7.0. - М.: Ве-
ста, Радио и связь, 1993. - 384 с.

[36] Йенсен К., Вирт Н. Паскаль. Руководство для пользователей и описа-


ние языка: Пер. с англ. - М.: Финансы и статистика, 1982, 150 с.

[37] Э. Йодан. Структурное проектирование и конструирование про-


грамм. М.: Мир, 1979, 416 с.

[38] С. К. Клини. Введение в метаматематику. М.: ИЛ, 1957.

[39] Д. Э. Кнут. Все про TEX. Протвино, АО RDTEX, 1993, 576 c.


896 ЛИТЕРАТУРА

[40] Д. Э. Кнут. Искусство программирования для ЭВМ. т.1–3. М.: Мир,


1976.

[41] Р. Ковальски. Логика в решении проблем. М.: Наука, 1990, 280 с.

[42] Т. Кормен, Ч. Лейзерсон, Р. Ривест. Алгоритмы. Построение и анализ.


МЦНМО, Москва, 1999, 960 с.

[43] И. Котельников, П. Чеботаев. Издательская система LATEX2ε. Новоси-


бирск: Сибирский хронограф, 1998.

[44] В. Е. Котов. Введение в теорию схем программ. М.: Наука, 1978, 257 с.

[45] В. Е. Котов, В. К. Сабельфельд. Теория схем программ. М.: Наука, 1991,


248 с.

[46] Лавров С.С., Гончарова Л.И. Автоматическая обработка данных. Хра-


нение информации в памяти ЭВМ (Серия: Библиотечка программи-
стањ). - М.: Наука, 1971

[47] В. Е. Ларичев. Колесо времени. Новосибирск: Наука, 1986.

[48] Д. Я. Левин. Язык сверхвысокого уровня Сетл и его реализация. Ново-


сибирск: Наука, 1983, 158 с.

[49] Д. Я. Левин. Инструментальный комплекс программирования на осно-


ве языков высокого уровня. М.: Наука, 1987, 197 с.

[50] А. А. Леоненков. Самоучитель UML. СПб.: БХВ-Петербург, 2001, 304с.

[51] Р. Лингер, Х. Миллс, Б.Уитт. Теория и практика структурного про-


граммирования. М.: Мир, 1982, 408 с.

[52] В. В. Липаев и др. Технология проектирования комплексов программ


АСУ. М.: Радио и связь, 1983.

[53] Б. Лисков, Дж. Гатэг. Использование абстракций и спецификаций при


разработке программ. М.: Мир, 1989, 424 с.

[54] В. М. Матросов, С. Н. Васильев, Л. Ю. Анапольский. Метод сравнения


в математической теории систем. Новосибирск: Наука, 1980.

[55] Д. Мартин и др. XML для профессионалов. М.: Лори, 2001.


ЛИТЕРАТУРА 897

[56] В. В. Милашевич. Опережающее обучение и его место в познании.


Владивосток, ДВНЦ АН СССР, 1986, 26с.

[57] М. Минский. Вычисления и автоматы. Мир, М.: 1971, 365 с.

[58] Н. Н. Непейвода.О построении правильных программ. Вопросы кибер-


нетики, вып. 46 (1978), стр. 88–122.

[59] Н. Н. Непейвода. Об одном методе построения правильной програм-


мы из правильных подпрограмм. Программирование, 1979, №1, стр.
11–21.

[60] Н. Н. Непейвода. Логический подход к программированию. Алгоритмы


в современной математике и ее приложениях. т. 2, Новосибирск, ВЦ
СО АН СССР, 1982.

[61] Н. Н. Непейвода (N. N. Nepejvoda). Some analogues of partial and


mixed computations in the logical programming approach. New Generation
Computing, 17 (1999), 309–327.

[62] Н. Н. Непейвода. Уровни знаний и умений. Тр. Научно-исследователь-


ского семинара логического центра ИФ РАН, т. 14 (2000), с. 9–35.

[63] Н. Н. Непейвода. Прикладная логика. Новосибирск: НГУ, 2000, 491 с.

[64] Ю. В. Новоженов и др. Объектно-ориентированные CASE-средства


// СУБД. 1996, № 5–6, 119–125 с.

[65] Пересмотренное сообщение об АЛГОЛе 68. Мир, М.: 1979, 533 с.

[66] В. Н. Пильщиков. Сборник упражнений по языку Паскаль: Учеб. посо-


бие для вузов. М.: Наука, 1989, 160 с.

[67] Д. Райли. Абстракция и структуры данных. Вводный курс. М.: Мир,


1993. 751 с.

[68] Б. Ренделл и Л. Рассел. Реализация АЛГОЛА 60, М.: Мир, 1967. - 475
с. (Randell B, Russel L.J. ALGOL 60 Implementation, Academic Press,
London, New York, 1964)

[69] Базисный Рефал и его реализация на вычислительных машинах. — М.:


ЦНИПИАСС, 1977. — 258 с.
898 ЛИТЕРАТУРА

[70] Ричардс и др. Oracle r 7.3. Энциклопедия пользователя. Киев: «Диа-


Софт», 1997.

[71] Case-технологии. Практическая работа в Rational Rose. М.: Бином,


2001, 270 с.

[72] Турчин В. Ф. Базисный Рефал. Описание языка и основные приемы про-


граммирования. (Методические рекомендации). — М.:ЦНИПИАСС,
1974. — 96 с.

[73] Уэзерелл Ч. Этюды для программистов: Пер. с англ. — М.: Мир, 1982.
— 288 с.

[74] Фаронова В. В. Программирование на персональных ЭВМ в среде


TURBO-Паскаль. — М.: Изд-во МГТУ, 1990. — 580 с.

[75] Языки программирования Ада, Си, Паскаль. Сравнение и оценка / Под


ред. А.Р.Фьюэра, Н.Джехани: Пер. с англ. под ред.В.В.Леонаса. - М.:
Радио и связь, 1989. - 368 с.

[76] Хоар Ч. Взаимодействующие последовательные процессы. М.: Мир,


1989.

[77] Дж. Хьюз, Дж. Мичтом. Структурный подход к программированию.


М.: Мир, 1980, 278 с.

[78] М. Ш. Цаленко. Моделирование семантики в базах данных. М.: Наука,


1989, 288 с.

[79] Ч. Чень, Р. Ли. Математическая логика и автоматическое доказа-


тельство теорем. М.: Наука, 1983, 358 с.

[80] Г. С. Цейтин. На пути к сборочному программированию, Программи-


рование, № 1, 1990, стр. 78–92

[81] А. А. Шалыто. SWITCH-технология. Алгоритмизация и программиро-


вание задач логического управления. СПб.: Наука, 1998, 628 с.

[82] Bacus, J. Can programming be liberated from the von Neuman style?
Comm. ACM, 21, 1978.
ЛИТЕРАТУРА 899

[83] P. Deransart, A. Ed-Dbali, and L. Cervoni. Prolog: The Standard. Springer-


Verlag, New York, 1996.

[84] John C. Mitchell. Foundations of programming languages. MIT Press,


1996, 846p.

[85] IEEE IDEF0. “Standard Users Manual for the ICAM Function Modeling
Method — IDEF0”. IEEE draft standard P1320.1.1. 1997.

[86] Turbo Pascal 5.5. Object-Oriented Programming Guide. Borland


Internanional, Inc. 1989.

[87] Peterson W.W. Addressing for random-access storage. IBM Journal of


Research and Development, 1, 1957.

[88] Rational Unified Process. Copyright c 2001 Rational Software


Corporation. http://www.rational.com/products/rup/index.jsp

[89] Rumbaugh J., Jacobson I., Booch G. The Unified Modelling Language.
Reference Manual. Addison-Wesley, 1999.— 550p.

[90] Turchin V. F.. Refal-5, Programming Guide and Reference Manual. New
England Publishing Co., Holyoke, 1989.
Предметный указатель

Артефакт, 181
CSS, см. Таблицы стилей Архитектура
C

фон Неймановская, 15
Атом, 730, 769
dll, см. днамически загружаемая библио-
D
Атрибуты
тека368 атома в LISP, 792
Аттестация, 189
D. Hilbert (Д. Гильберт), 867
H
Б
База
данных, 752, 814
R
знаний, 752, 815
Rational Unified Process, 180, 829–830
Библиотека, 39–44
динамически загружаемая, 368
W

периода исполнения, 9, 15
WYSIWYG, 778

А периода трансляции, 8, 15
Автомат, 845 среды программирования, 14
Мили, 847 стандартная, 13, 15
Мура, 845, 847
В
входные символы, 845
Верификация, 189
конечный, 560, 564, 566, 573, 610,
Выражение
языка Рефал, 730
состояния, 845
611, 617, 619, 621, 845
Выражения, 247
Адрес, 16
Вычисление
Адресность, 16
квазипараллельное, 51
Алгоритм
параллельное, 51
Маркова, 729, 849
последовательное, 51
Алгорифм Маркова, см. Алгоритм Мар-
смешанное, 170
кова
совместное, 52, 52–54
Анализ, 201
слабое, 53
Анализ осуществимости, 194
Вычисления
Арность, 839
гарантированные, 503, 808

900
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ 901

точность, 808 Интерпретатор, 14


Интерпретация
Г вычислительная, 840
Глупость, 884 Исполнение
Граф неудачное, 755
состояний и переходов, см. Диа- при данной оценке, 841
грамма переходов схемы программ, 841
Гроздь, 529 успешное, 755, 841
Использование, 195
Д
Исследования, 194
Данные, 877
История
изоморфизм, 664
исполнения, 842
Дао, 891
Декомпозиция программ, 44 К
Детерминатив, 751 Канал связи, 17
Диаграмма Класс
переходов, 567, 610 абстрактный, 717
Дизъюнкт Клауза, см. Педложение752
Хорнов, 748 Кодирование, см. Рализация185
Документ Команда, 16
объектная модель, 786 Комментарий, 38–39
Доступ к данным Комментирование, 110
программируемый, 511 Компилятор, 14
Компиляция
З
условная, 84
Замена
Конкретизация, 729
значения переменной, 842
Константа, 59, 751
Записи
Конституента, 600
вариантные, 520
Конструирование, 194, 201
Запись, 515
Конструкция, 7
Запроцедуривание, 417
Контейнер, 59
Засылка, см. Присваивание
Контекст
Знания, 877
предоставляемый, 455
Значение
Кэш
константное, 59
данных, 19
И команд, 19
Импликация
Л
Хорнова, 747
Литерал, 59
Имя, 60
Инициаторы работ, 202 М
902 ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ

Массив, 524 Наследование, 714


гибкий, 525
динамический, 525 О
статический, 525 Обзоры, 189
Машина Область
Тьюринга, 848 экранная, 642
потоков данных, 21, 763 Образец, 733
Метавыражение, 729, 732 Обратная польская запись, 699
Метакодирование, 733 Объединение, 519
Метод, 888 неразмеченное, 520
виртуальный, 715 размеченное, 519
конечный, 716 тегированное, 520
объекта, 708 Объект, 708
статический, 715 искусственный, 831
Методика, 107 квазиискусственный, 831
Методичность, 884 Окружение, 767
Методология, 107 Операнд, 16
Множество, 534 Оператор, 7, 22
автоматно разрешимое, 847 Описания, 44
регулярное, см. автоматно разре- Определение требований, 184, 201
шимое Ординалы, 638
Модели Откат, 756
нестандартные, 866 Отладчик, 9, 15
уровня анализа, 203 Отождествление, 729
уровня конструирования, 203 Оценка, 195
Моделирование переменных, 841
пользовательского интерфейса, 201 результатов итерации, 202
Модель Ошибка
Янова, 844 задания, 191
жизненного цикла
П
каскадная, 188–189
Память, 15, 59
классическая, 186–188
ассоциативная, 21, 746
общепринятая, 184–186
Парадокс, 863
строгая каскадная, 191
Берри, 864
фазы-функции, 193–196
Брауэра, 870
интерпретационная, 840
Карри, 864
Модуль, 42
Рассела, 864
загрузочный, 14
изобретателя, 868
Н лжеца, 864
Паттерн, см. Шаблон
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ 903

Паттерны, 163 Прерывание, 138


Патч, 165 Приведение, 23
Переатестация, 189 Приоритет, 142
Переменная, 59, 751 Присваивание, 16, 22
анонимная, 751 Программа, 181
Переход, 119 линейная, 37
Перечисление, 499, 499–501 старение, 182
Пиксель, 641 Программирование, 194
Планирование итерации, см. Опреде- восходящее, 131
ление требований нисходящее, 130
Подпрограмма, 23, 362–481 объектно=ориентированное, 118, 151–
клиент, 369
сервер, 369 от образца, 156
155

Подстановка, 729 от образцов, 164


Поле от переиспользования, 118, 155
данных, 752 от приоритетов, 118, 142
записи, 515 от событий, 135–142, 359
зрения, 752 от состояний, 118–124, 134, 160,
Поле зрения
Рефал, 730 от шаблонов, 118
179, 560–633

Поле памяти отсобытий, 118


Рефал, 730 сборочное, 157
Полиморфизм, 406 сентенциальное, 118, 132–134, 567,
Понятие
идеальное, 867 специализирующее, 155, 167
728–789

реальное, 867 структурное, 118


Понятия функциональное, 118, 147–151, 791–
внешние, 122
внутренние, 122 Проектирование, 185
804

Порядок Проецирование, 733, 734


высказывания, 869 Противоречие
Последовательное соединение, 843 проблемное, 188
Постусловие, 168 Процессор, 16, 59
Поток Путь
данных, 573 окольный, 868
Предикат, 751
отсечения, 757 Р
Предложение, 752 Разветвление, 843
Предусловие, 168 Развитие, 186
Препроцессор, 14 Разметка, 774
904 ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ

Разработка, 184 Стек


Реализация, 185, 202 контекстов, 397–398
Реальность Схема
виртуальная, 831 Янова, 611, 844
Регистры, 19 программ, 839, 839
Редактор, 8 Янова, см. Схема Янова
связей, 15 структурированная, 843
Рубрикация, 768
Т
С ТРИЗ, 188
Связь вход=выход, 168 Таблицы
Селектор, 511 стилей, 777, 783
Семантика, 102 Тег, 62, 619, 774
алгебраическая, 104 Тегирование, 62, 774
интерпретационная, 103 Терм, 839
логическая, 103 Тестирование, 186, 202
трансляционная, 103 Тип
функциональная, 103 данных, 59
Сигнатура, 839 логический, 869
Символы Токен, 769
пробельные, 39 раскрытие, 769
Система Транслятор, 8
аксиоматическая, 115 Трассировка
ассоциативная, 115 требования, 219
коммутационная, 114
программирования, 8 У
продукций, 113 Умения, 878
файловая, 8 Универс, 840
функций, 114 полных состояний, 844
Словарь, см. Сигнатура Унификация, 748
Слово, 561 Управляющее устройство, 16
Смысл, 102 Условие
Сообщение, 341 корректности относительно ошиб-
Сопровождение, 182, 184, 186 ки, 840
Состояние, 119 Успех, 753
памяти, 841
Ф
схемы программ, 841
Факт, 753
Спецификация системы, 184
Формат
Среда
операционная, 8
TEX, 769
TEXа, 766
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ 905

Формула
Хорнова, 747
FORTRAN, 7, 24–26, 68, 71, 80,

Функтор, 751
102, 251, 273, 326, 362, 381, 382,

Функция
414

расстановки, 529
Java, 33

рекурсивная, 856
LISP, 791–804

сигнализирующая, 638
Lisp, 5, 12

Функция моделирования, 203


Pascal, 28
Perl, 140

Х
PL/1, 26

Хеширование, см. Функция расста-


Plankalkul, 791

новки
Prolog, 5, 12, 746–763
SNOBOL, 134
Ц
Алгол 68, 4, 7, 11, 27, 40, 42, 52,
SNOBOL-A, 134
Цель, 752
Цикл
Рефал, 5, 12, 13, 59, 132, 729–745
58, 280
Бема-Джакопини, 129, 843, 845
С, 29
Кнута, 843
Симула-67, 26
Ч прагматическое расширение, 13
Чум разметки, 614, 617, 765
фундированный, 639 LATEX, 615, 617, 766

Ш
TEX, 615, 765

Шаблон, 722
HTML, 615, 774
plain TEX, 766

Э
SGML, 774

Эквивалентность реализация, 7
XML, 615, 617, 781

схем реализованный, 14
логико-термальная, 843 эталонный, 13
функциональная, 842 Языки
Эклектика, 111 операционные, 7
Эпименид, 864 традиционные, 7, 15, 22
форматные, 39
Я
Ячейка, 16
Язык
Ящик
серый, 161
Ada, 31
черный, 161
Algol 60, 25, 26, 382, 390, 392, 395,

Algol 68, см. Алгол 68


397, 414, 417, 446
Оглавление

Предисловие i

I Базовые понятия 1
1. Введение в систему понятий программирования
1.1. Языки и системы программирования . . . . . . . . . . . . . .
3

1.1.1. Сравнение программ на разных языках . . . . . . . . .


3

1.1.2. Язык, его реализация и среда программирования . . .


3

1.2. Модель вычислений фон Неймана и традиционные языки . . .


6

1.3. Базовые конструкции языка . . . . . . . . . . . . . . . . . . . .


15

1.3.1. Линейные программы и их компоненты . . . . . . . . .


37

1.3.2. Не выполняемые вычислителем фрагменты программы


37

1.3.3. Подключение внешней информации (библиотек) . . .


38

1.3.4. Описания . . . . . . . . . . . . . . . . . . . . . . . . . .
39

1.3.5. Действия . . . . . . . . . . . . . . . . . . . . . . . . . .
44

1.4. Структура вычислений и структура текста программы . . . . .


46

1.4.1. Последовательное, параллельное и совместное испол-


48

нение . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.4.2. Управление порядком вычислений . . . . . . . . . . . .
48

1.5. Работа со значениями . . . . . . . . . . . . . . . . . . . . . . .


54
59

2. Синтаксис, семантика и прагматика языка программирования


2.1. Синтаксис, семантика . . . . . . . . . . . . . . . . . . . . . . .
69

2.2. Прагматика . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
70

2.3. Абстрактное и конкретное представления программы . . . . .


81

2.3.1. Абстрактное представление . . . . . . . . . . . . . . .


86

2.3.2. Структура абстрактного синтаксиса . . . . . . . . . . .


86
88

906
ОГЛАВЛЕНИЕ 907

2.3.3. Абстрактный вычислитель и абстрактная структура . . 93


2.3.4. Вызовы как пример синтаксических схем . . . . . . . . 99
2.4. Принципиальные трудности, связанные с семантикой . . . . . 100

3. Стили программирования, или программирование с птичьего по-


лета
3.1. Стили программирования . . . . . . . . . . . . . . . . . . . . . 107
106

3.2. Программирование от состояний . . . . . . . . . . . . . . . . . 118


3.3. Структурное программирование — самый распространенный
стиль . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
3.4. Сентенциальное программирование . . . . . . . . . . . . . . . 132
3.5. Программирование от событий . . . . . . . . . . . . . . . . . . 135
3.6. Программирование от приоритетов . . . . . . . . . . . . . . . 142
3.7. Функциональное программирование . . . . . . . . . . . . . . . 147
3.8. Объектно-ориентированный подход . . . . . . . . . . . . . . . 151
3.9. Три технологических стиля программирования . . . . . . . . . 155
3.9.1. Программирование от переиспользования . . . . . . . 156
3.9.2. Программирование от образцов . . . . . . . . . . . . . 164
3.9.3. Специализирующее программирование . . . . . . . . . 167
3.10. Общие выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . 177

4. Понятие жизненного цикла программного обеспечения и его мо-


дели
4.1. Введение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
181

4.2. Модели традиционного представления о жизненном цикле . . 184


4.2.1. Общепринятая модель . . . . . . . . . . . . . . . . . . 184
4.2.2. Классическая итерационная модель . . . . . . . . . . . 186
4.2.3. Каскадная модель . . . . . . . . . . . . . . . . . . . . . 188
4.2.4. Модель фазы-функции . . . . . . . . . . . . . . . . . . 193
4.3. Итеративные модели жизненного цикла . . . . . . . . . . . . . 196
4.3.1. Базовые технологические принципы итеративного про-
ектирования . . . . . . . . . . . . . . . . . . . . . . . . 199
4.3.2. Итеративная модификация модели фазы-функции . . . 202
4.3.3. Параллельное выполнение итераций . . . . . . . . . . 209
4.3.4. Моделирование итеративного наращивания возможно-
стей системы . . . . . . . . . . . . . . . . . . . . . . . . 211
4.4. Требования к программному изделию и жизненный цикл . . . 214
4.4.1. Проблемы определения и анализа требований . . . . . 215
908 ОГЛАВЛЕНИЕ

4.4.2. Трассировка требований . . .


4.4.3. Учет трассировки требований в модели жизненного
. . . . . . . . . . . . . . 218

цикла . . . . . . . . . . . . . .
4.4.4. Особенности первой итерации
. . . . . . . . . . . . . . 224

4.4.5. Фаза завершения . . . . . . .


. . . . . . . . . . . . . . 226

4.5. Итоги и перспективы . . . . . . . . .


. . . . . . . . . . . . . . 231
. . . . . . . . . . . . . . 235

II Структуры программирования 244


5. Выражения
5.1. Операции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
247

5.2. Логические выражения . . . . . . . . . . . . . . . . . . . . . . 252


5.3. Приоритет операций . . . . . . . . . . . . . . . . . . . . . . . . 260

6. Разветвление вычислений
6.1. Разбор случаев . . . . . . . . . . . . . . . . . . . . . . . . . . .
269

6.1.1. Цепочка условий . . . . . . . . . . . . . . . . . . . . .


269

6.1.2. Переключение . . . . . . . . . . . . . . . . . . . . . . .
269

6.1.3. Типы данных, связанные с разветвлением . . . . . . .


273

6.2. Табличное задание разветвлений и оператор выбора Дейкстры


278

6.2.1. Таблицы решений . . . . . . . . . . . . . . . . . . . .


281

6.2.2. Охраняемые команды . . . . . . . . . . . . . . . . . . .


281

6.2.3. Условные выражения . . . . . . . . . . . . . . . . . . .


283
285

7. Циклические вычисления
7.1. Мотивация циклических вычислений . . . . . . . .
287

7.2. Потоковая обработка . . . . . . . . . . . . . . . . .


. . . . . . 287

7.2.1. Порождение элементов потока . . . . . . . .


. . . . . . 292

7.2.2. Фильтрация потока . . . . . . . . . . . . . .


. . . . . . 293

7.2.3. Потоки и данные . . . . . . . . . . . . . . .


. . . . . . 297

7.2.4. Потоки и цикл for . . . . . . . . . . . . . . .


. . . . . . 300

7.3. Логическая структура цикла . . . . . . . . . . . . .


. . . . . . 326

7.3.1. Инвариант и параметры цикла . . . . . . . .


. . . . . . 329

7.3.2. Цикл Дейкстры и цикл-‘паук’ . . . . . . . .


. . . . . . 329

7.3.3. Совместный цикл . . . . . . . . . . . . . . .


. . . . . . 332

7.3.4. Входные и выходные потоки. Сопрограммы


. . . . . . 333
. . . . . . 335
ОГЛАВЛЕНИЕ 909

7.3.5. Понятие обстановки вычислений. Действия, меняю-


щие обстановку . . . . . . . . . . . . . . . . . . . . . .
7.4. Абстрактно-синтаксическое представление циклов . . . . . .
343

7.4.1. Представление циклов . . . . . . . . . . . . . . . . . .


347

7.4.2. Структурные переходы . . . . . . . . . . . . . . . . . .


347

7.4.3. Исключения . . . . . . . . . . . . . . . . . . . . . . . .
348
358

8. Подпрограммы
8.1. Виды подпрограмм . . . . . . . . . . . . . . . . . . . . . . . .
362

8.2. Именование процедур . . . . . . . . . . . . . . . . . . . . . . .


364

8.3. Контексты и обстановки. Локализация имен . . . . . . . . . .


371

8.4. Вызов процедуры . . . . . . . . . . . . . . . . . . . . . . . . .


377

8.4.1. Семантика вызова процедуры . . . . . . . . . . . . . .


390

8.4.2. Статическая и динамическая цепочки . . . . . . . . . .


390

8.4.3. Понятие экземпляра процедуры . . . . . . . . . . . . .


393

8.5. Параметризация . . . . . . . . . . . . . . . . . . . . . . . . . .
396

8.5.1. Назначение параметризации . . . . . . . . . . . . . . .


400

8.5.2. Полиморфизм и вызовы с переменным числом пара-


400

метров . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.5.3. Механизмы передачи параметров . . . . . . . . . . . .
406

8.5.4. Рекомендации по использованию параметров . . . . .


410

8.5.5. Параметры-процедуры и параметры-функции. Проце-


426

дурный тип . . . . . . . . . . . . . . . . . . . . . . . .
8.6. Развитие языковых средств модульности в языках линии Turbo
437

8.6.1. Поддержка модульности в стандартном языке Pascal .


Pascal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447

8.6.2. Модули в TURBO-системах программирования . . . .


447

8.6.3. Пример использования модуля . . . . . . . . . . . . . .


453

8.7. Рекурсивные программы . . . . . . . . . . . . . . . . . . . . .


458

8.7.1. Сопоставление итеративной и рекурсивной схем . . .


463

8.7.2. Рекурсия через параметры . . . . . . . . . . . . . . . .


465

8.7.3. Пример для самостоятельного анализа . . . . . . . . .


469

8.8. Процедуры в разных моделях вычислений . . . . . . . . . . .


472
475

9. Структуры данных
9.1. Общие концепции структурирования данных . . . . . . . . . . 482
482

9.1.1. Структура программы и структуры данных . . . . . . . 482


9.1.2. Базовые структуры данных и конструкторы структур . 485
910 ОГЛАВЛЕНИЕ

9.1.3. Операции над вновь определяемыми типами данных


9.1.4. Типизация и стили . . . . . . . . . . . . . . . . . . .
. 488

9.1.5. Абстрактные типы данных . . . . . . . . . . . . . . .


. 493

9.2. Базовые и выводимые типы . . . . . . . . . . . . . . . . . . .


. 494

9.2.1. Перечисления . . . . . . . . . . . . . . . . . . . . . .
. 499

9.2.2. Булевский тип . . . . . . . . . . . . . . . . . . . . . .


. 499

9.2.3. Тип литерных . . . . . . . . . . . . . . . . . . . . . .


. 501

9.2.4. Тип целых . . . . . . . . . . . . . . . . . . . . . . . .


. 501

9.2.5. Вещественные типы . . . . . . . . . . . . . . . . . .


. 502

9.3. Структурные типы . . . . . . . . . . . . . . . . . . . . . . . .


. 504

9.3.1. Наборы компонент . . . . . . . . . . . . . . . . . . .


. 508

9.3.2. Записи . . . . . . . . . . . . . . . . . . . . . . . . . .
. 510

9.3.3. Объединения . . . . . . . . . . . . . . . . . . . . . . .
. 515

9.3.4. Массивы . . . . . . . . . . . . . . . . . . . . . . . . .
. 519

9.3.5. Множества . . . . . . . . . . . . . . . . . . . . . . . .
. 524

9.4. Рекурсивные структуры данных . . . . . . . . . . . . . . . .


. 534

9.4.1. Списочные структуры . . . . . . . . . . . . . . . . .


. 541

9.4.2. Указательные типы . . . . . . . . . . . . . . . . . . .


. 544

9.4.3. Специальные рекурсивные структуры . . . . . . . . .


. 547

9.4.4. Деревья . . . . . . . . . . . . . . . . . . . . . . . . . .
. 550
. 552

III Методы программирования 555


10. Методы программирования от состояний
10.1. Основные структуры программирования от состояний . . . . 561
560

10.2. Задача подсчета длин слов текста и задание конечного автомата561


10.2.1. Постановка задачи и первичный анализ . . . . . . . . . 561
10.2.2. Построение графа состояний . . . . . . . . . . . . . . . 563
10.2.3. Табличное представление графа состояний конечного
автомата . . . . . . . . . . . . . . . . . . . . . . . . . . 567
10.2.4. Ручная трансляция диаграмм переходов . . . . . . . . 572
10.2.5. Представления, ориентированные на автоматические
преобразования диаграмм переходов . . . . . . . . . . 579
10.2.6. Обсуждение решения . . . . . . . . . . . . . . . . . . . 588
10.3. Синтаксические таблицы . . . . . . . . . . . . . . . . . . . . . 593
10.3.1. Расширение сферы применения конечных автоматов:
анализатор как система связанных конечных автоматов 593
ОГЛАВЛЕНИЕ 911

10.3.2. Задача анализа простых выражений . . . . . . . . . . . 595


10.3.3. Синтаксические таблицы и рекурсивный спуск . . . . 601
10.3.4. Преобразования грамматики, сохраняющие язык. Вы-
числительная мощность синтаксических таблиц . . . . 604
10.3.5. Построение графа состояний . . . . . . . . . . . . . . . 607
10.4. Диаграммы состояний и переходов. Их связь с математиче-
скими моделями . . . . . . . . . . . . . . . . . . . . . . . . . . 609
10.5. Программные представления графа состояний . . . . . . . . . 612
10.5.1. Требования к автоматической трансляции таблиц . . . 613
10.5.2. Языки разметки и автоматическая трансляция таблиц . 614
10.5.3. Автоматное преобразование структурированных текстов617
10.6. Переход от данных к конечному автомату . . . . . . . . . . . . 625

11. Методы, основанные на рекурсии


11.1. Механизмы рекурсии . . . . . . . . . . . . . . . . . . . . . . .
634

11.2. Закрашивание замкнутых областей . . . . . . . . . . . . . . .


635

11.3. Переборные алгоритмы и рекурсия . . . . . . . . . . . . . . .


641

11.3.1. Перебор/генерация вариантов с возвратами . . . . . .


645

11.4. Лабиринт . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
646

11.4.1. Блуждание по лабиринту и закраска области . . . . . .


661

11.4.2. Абстрактное и конкретное представления данных . . .


662

11.4.3. Абстрактное представление лабиринта . . . . . . . . .


664

11.4.4. Поиск пути в лабиринте . . . . . . . . . . . . . . . . .


666

11.5. Рекурсия при обработке символьной информации . . . . . . .


673

11.6. Рекурсия в транслирующих программах . . . . . . . . . . . . .


684

11.6.1. Синтаксический распознаватель простых выражений .


690

11.6.2. Метод рекурсивного спуска . . . . . . . . . . . . . . .


690

11.6.3. Обратная польская запись выражений: понятие, алго-


692

ритмы вычисления и построения . . . . . . . . . . . . 699

12. Объектно-ориентированный подход


12.1. Объекты . . . . . . . . . . . . . . . . . . . . . . . . . .
708

12.1.1. Объекты как структуры данных и права доступа


. . . . 708

12.1.2. Наследование и полиморфизм . . . . . . . . . .


. . . . 709

12.1.3. Множественное наследование и интерфейсы . .


. . . . 714

12.2. Объектная модульность . . . . . . . . . . . . . . . . . .


. . . . 718
. . . . 722
912 ОГЛАВЛЕНИЕ

13. Сентенциальные методы


13.1. Конкретизация . . . . . . . . . . . . . . . . . . . . . . . . . .
728

13.1.1. Структура данных . . . . . . . . . . . . . . . . . . . .


. 729

13.1.2. Модель вычислений и Рефал-программа . . . . . . .


. 729

13.1.3. Дополнительные возможности . . . . . . . . . . . . .


. 733

13.1.4. Развитие языка и его диалекты . . . . . . . . . . . . .


. 739

13.2. Унификация . . . . . . . . . . . . . . . . . . . . . . . . . . .
. 744

13.2.1. Общие концепции . . . . . . . . . . . . . . . . . . . .


. 746

13.2.2. Поле зрения, поле памяти и PROLOG-программа . .


. 746

13.2.3. Управление исполнением программы . . . . . . . . .


. 751

13.2.4. Динамическое пополнение и порождение программы


. 755

13.3. Языки разметки . . . . . . . . . . . . . . . . . . . . . . . . .


. 762

13.3.1. TEX и LATEX . . . . . . . . . . . . . . . . . . . . . . . .


. 765

13.3.2. Языки разметки для Internet . . . . . . . . . . . . . .


. 765

13.4. Применения сентенциального программирования . . . . . .


. 774

13.4.1. Аналитические преобразования . . . . . . . . . . . .


. 787

13.4.2. Сентенциальные методы в традиционных языках . .


. 787
. 788

14. Функциональное программирование


14.1. Структура данных . . . . . . . . . . . . . . . . . . . . . . . . . 791
791

14.2. Модель вычислений . . . . . . . . . . . . . . . . . . . . . . . . 793


14.3. Объекты и LISP . . . . . . . . . . . . . . . . . . . . . . . . . . 800

15. Моделирование
15.1. Модели и вычисления . . . . . . . . . . . . . . . . . . . . . . .
805

15.2. Моделирование времени . . . . . . . . . . . . . . . . . . . . .


808

15.3. Информационные системы с временем . . . . . . . . . . . . .


810

15.4. Моделирование и информационные системы . . . . . . . . . .


811

15.4.1. Информационное обеспечение моделирования . . . . .


814

15.4.2. Информационные системы для моделей принятия ре-


816

шений . . . . . . . . . . . . . . . . . . . . . . . . . . .
15.5. Системы с дискретными событиями . . . . . . . . . . . . . . .
817

15.6. UML-моделирование и RUP . . . . . . . . . . . . . . . . . . .


819
829

16. Подведение итогов 831


ОГЛАВЛЕНИЕ 913

A. Математические модели
A.1. Несколько терминов . . . . . . .
837

A.2. Вычислительные интерпретации


. . . . . . . . . . . . . . . . . 837

A.3. Модели Янова . . . . . . . . . .


. . . . . . . . . . . . . . . . . 839

A.4. Автоматы и машины Тьюринга .


. . . . . . . . . . . . . . . . . 844

A.5. Алгоритмы над структурами . .


. . . . . . . . . . . . . . . . . 845

A.6. Рекурсии . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . 848

A.7. Совместность и параллелизм . .


. . . . . . . . . . . . . . . . . 849
. . . . . . . . . . . . . . . . . 860

B. Методологические результаты
B.1. Логические парадоксы . . . . . . . . . . . . .
863

B.2. Теоремы Тарского и Геделя . . . . . . . . . . .


. . . . . . . . . 863

B.3. Идеальные и реальные понятия по Гильберту .


. . . . . . . . . 866

B.4. Парадокс изобретателя . . . . . . . . . . . . .


. . . . . . . . . 867

B.5. Типы и порядки . . . . . . . . . . . . . . . . .


. . . . . . . . . 868

B.6. Чистые теоремы существования . . . . . . . .


. . . . . . . . . 869

B.7. Доказательства и программы . . . . . . . . . .


. . . . . . . . . 870

B.8. Основные понятия неформализуемости . . . .


. . . . . . . . . 872
. . . . . . . . . 874

C. Знания, данные, умения


C.1. Анализ понятий . . . . . . . . . . . . . . . . . . . . . . . .
877

C.2. Уровень насекомого . . . . . . . . . . . . . . . . . . . . . .


. . 877

C.3. Стереотипное реагирование . . . . . . . . . . . . . . . . .


. . 879

C.4. Тупость . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . 880

C.5. Комбинационное (комбинаторное) планирование . . . . . .


. . 882

C.6. Глупость . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . 882

C.7. Стратегическое планирование и преобразование действий


. . 884

C.8. Релятивизм . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . 885

C.9. Владение методом . . . . . . . . . . . . . . . . . . . . . . .


. . 887

C.10. Умничанье, мессианство . . . . . . . . . . . . . . . . . . .


. . 888

C.11. Многоуровневое мышление . . . . . . . . . . . . . . . . . .


. . 889

C.12. Мудрствование, интуитивизм . . . . . . . . . . . . . . . . .


. . 890

C.13. Дао . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . 891

C.14. Лжепророки . . . . . . . . . . . . . . . . . . . . . . . . . .
. . 891

C.15. Химеры и вымыслы . . . . . . . . . . . . . . . . . . . . . .


. . 891
. . 892

Вам также может понравиться