Вы находитесь на странице: 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 могло привести к ошибке. Этот пример пока-
зателен: стремление к универсализации не всегда благо. Так что возведением
в степень там, где эта операция предусмотрена, во избежание неприятностей
лучше пользоваться лишь для целых чисел и для в