MODULA-2
Появление языка Pascal дало разработчикам программного обеспечения строгий и выразительный инструмент для создания ясных и хорошо структурированных программ, - тот самый инструмент, который был столь необходим в условиях известного «кризиса программного
обеспечения», становившегося все острее в то время. Однако неверным было бы считать, что все трудности информатики остались позади. Действительно, новый язык вполне адекватно поддерживал парадигму структурного программирования, которая позволяла справляться со сложными задачами путем декомпозиции их на более простые, легче решаемые. Декомпозиция является великолепным средством борьбы со сложностью, позволяя человеческому разуму с ограниченными возможностями охватывать как всю задачу в целом, так и каждую ее часть детально, переходя с одного уровня абстракции на другой. Однако у декомпозиции есть и другая сторона.
Разбив задачу на несколько относительно независимых, слабо связанных подзадач, мы получаем возможность привлечь к работе над проектом нескольких программистов. Если каждый из них получит ясную спецификацию своей части, в которой оговорено, какие данные поступают на вход и какие требуются на выходе, каждый из них сможет выполнять работу параллельно. Кроме того, каждая часть может в свою очередь быть подвержена декомпозиции, что позволяет произвести дальнейшее распараллеливание. Таким образом, можно обеспечить возможность согласованной коллективной работы над проектом при условии, что каждая часть строго соответствует спецификациям (что возможно в процессе тестирования). Разумеется, не следует думать, что структурное программирование возникло именно благодаря языку Pascal, скорее наоборот. Идеи структурного программирования витали в воздухе задолго до его появления. Да и проекты на FORTRAN и COBOL выполнялись не в одиночку; однако именно в Pascal’е идеи структурного программирования были воплощены Виртом в рафинированном виде.
Однако именно коллективная работа над проектом явилась наиболее слабой стороной Pascal’я. Язык, задуманный изначально как учебный, не содержал в себе никаких средств для разделения программы на независимые части – модули – с последующей сборкой модулей в единое целое. Для компиляции программы весь ее исходный текст должен быть собран в единое целое. Такой подход имеет ряд недостатков. Пожалуй, главный из них – огромная нагрузка на компилятор. Достаточно поменять одну строчку в процедуре, чтобы весь проект в тысячи или десятки тысяч строк пришлось перекомпилировать заново. Те, кому довелось работать с оборудованием 80-х годов выпуска, должны помнить, насколько это небыстрый процесс. Да и «склейка» большого файла из отдельных кусков тоже не слишком приятная задача.
Конечно же, различные реализации компиляторов по-разному пытались бороться с этой проблемой (впрочем, без особого успеха). Так, один из компиляторов для PDP-11 позаимствовал из языка C конструкцию #include, избавив программиста от сборки проекта в один исходный файл. Впрочем, эта полумера не избавляла от необходимости полной перекомпиляции при внесении малейшего изменения в программу. Другое расширение языка позволяло компилировать фрагменты программы отдельно, каждый фрагмент в свой объектный модуль. При этом «внешние» процедуры описывались одним лишь заголовком с ключевым словом external. Затем компоновщик собирал объектные модули воедино в загрузочный образ программы. Такой подход, получивший название независимой компиляции, позволил избежать полной перекомпиляции проекта, выигрывая немало времени.
Впрочем, независимая компиляция таит в себе большую опасность. Компилятор никоим образом не может проверить соответствие объявления процедуры и ее реализации, полагаясь лишь на внимательность программиста. Практика же показывает, что любая технология, полагающаяся на внимательность и ответственность человека, обречена на провал. Ошибки, связанные с неправильной передачей параметров между раздельно скомпилированными
процедурами, не так легко обнаружить, поскольку система контроля типов здесь бессильна.
Выход из создавшейся ситуации предложил тот же Вирт, разработав следующий язык программирования – MODULA-2. (Этому проекту предшествовал другой, экспериментальный язык MODULA, который, впрочем, не вышел за стены лаборатории Вирта и о котором подробно рассказывать я не вижу смысла).
Синтаксис MODULA-2 весьма напоминает Pascal, и понять программу на MODULA-2, зная Pascal, не составит никакого труда. Набор операторов практически не изменился, встроенные типы данных также не претерпели существенных изменений, равно как и основные структуры. Впрочем, разработчики получили небольшой подарок: поскольку многие высказывали претензии к операторным скобкам Pascal’я begin/end, весьма громоздким по сравнению
с {/}, столь привычным пишущим на C, многие операторы (операторы цикла, условные операторы и др.) начинаются неявной открывающей скобкой, делая многочисленные begin’ ы ненужными. Таким образом, многие операторы являются составными автоматически и требуют лишь явного закрытия посредством end. (Хотя такой подход может обескуражить знатоков Pascal, однако поклонники Visual Basic вряд ли будут этим удивлены).
Однако все это мелочи по сравнению с главной идеей: в языке появилось понятие модуля. Фактически модуль – это самостоятельно компилируемый фрагмент программы, состоящий из двух основных частей: интерфейса и реализации. Интерфейс – это некий контракт, который модуль обязуется соблюдать. В нем описываются сигнатуры процедур и функций, а также объекты, к которым можно обращаться извне (разумеется, слово «объекты» не следует понимать в смысле, привычном для ООП). Реализация – это закрытая часть модуля, содержащая его физическое воплощение в виде тел подпрограмм, локальных переменных и т.д.
В результате компиляции модуля образуются два файла: файл описания интерфейса и объектный файл. Для того, чтобы воспользоваться модулем, необходимо его импортировать. При импорте модуля используется описание его интерфейса и тем самым гарантируется столь же строгий контроль типов, как и в пределах локального модуля. При этом отслеживается соответствие версии модулей. Например, если модуль A импортирует модуль B и при этом в модуль B были внесены изменения, повлекшие изменение интерфейса, модуль A также потребует перекомпиляции. Если же изменения коснулись лишь реализации, а интерфейс остался неизменным, перекомпиляция A не потребуется. Таким образом, MODULA-2 не полагается на внимательность и аккуратность программиста, а подвергает согласованность интерфейсов строгому контролю при каждой компиляции.
Из других особенностей языка я бы отметил некоторые низкоуровневые средства, напоминающие C, для доступа к регистрам ввода/вывода и обработки прерываний, позволяющие использовать MODULA-2 для управления объектами в реальном времени, а также поддержку распараллеливания вычислений на уровне стандартной библиотеки.
Следует отметить, что по непонятным мне причинам язык не получил широкого распространения, несмотря на явные преимущества перед своим предшественником. Хотя Вирт доказал на деле гибкость и мощность нового языка, разработав на нем операционную систему Medos и все программное обеспечение для довольно интересной мини-ЭВМ Lilith, программисты не спешили оставить полюбившийся им Pascal и перейти на MODULA-2.
В заключение – исходный текст программы для рисования кривой Серпински (заимствовано из Вирт Н. Программирование на языке Модула-2: Пер. с англ. – М.: Мир, 1987):
MODULE Serpinsky;
FROM Terminal IMPORT Read;
FROM LineDrawing IMPORT width, Height, Px, Py, clear, line;
CONST
SqrSize = 512;
VAR
i, h, x0, y0: CARDINAL;
ch: CHAR;
PROCEDURE A(k: CARDINAL);
BEGIN
IF k > 0 THEN
A(k-1);
line(7, h);
B(k-1);
line(0, 2*h);
D(k-1);
line(1, h);
A(k-1)
END
END A;
PROCEDURE B(k: CARDINAL);
BEGIN
IF k > 0 THEN
B(k-1);
line(5, h);
C(k-1);
line(6, 2*h);
A(k-1);
line(7, h);
B(k-1)
END
END B;
PROCEDURE C(k: CARDINAL);
BEGIN
IF k > 0 THEN
C(k-1);
line(3, h);
D(k-1);
line(4, 2*h);
B(k-1);
line(5, h);
C(k-1)
END
END C;
PROCEDURE D(k: CARDINAL);
BEGIN
IF k > 0 THEN
D(k-1);
line(1, h);
A(k-1);
line(2, 2*h);
C(k-1);
line(3, h);
D(k-1)
END
END A;
BEGIN
clear;
i := 0;
h := SqrSize DIV 4;
x0 := CARDINAL(width) DIV 2;
y0 := CARDINAL(height) DIV 2 + h;
REPEAT
i := i + 1;
x0 := x0 – h;
h := h DIV 2;
y0 := y0 + h;
Px := X0;
Py := Y0;
A(i);
line(7,h);
B(i);
line(5, h);
C(i);
line(3, h);
D(i);
line(1, h);
Read(ch)
UNTIL (i = 6) OR (ch = 33C);
clear
END Serpinsky.