В.Ш. Кауфман - Языки программирования - концепции и принципы (1990) (1160787), страница 45
Текст из файла (страница 45)
указывают цену компромисса. С третьей стороны, показывают, от каких
заманчивых возможностей (например, контроль содержательных ролей) приходится
отказываться ради простоты определения, реализации и использовании ЯП, помня
о неумолимом законе распространения сложности.
В связи с управлением видимостью интересно понять, почему в Модуле-2
остался присоединяющий оператор (остался от Паскаля). Казалось бы, это
полный аналог указателя контекста и неявные (локальные) объявления полей
записи - очевидное противоречие с концепцией явных объявлений.
Однако серьезного противоречия нет. Скорее наоборот, можно и здесь
усмотреть следование принципу чемоданчика. Только понимать его нужно глубже,
трактуя "совершенно необходимо" не только в чисто техническом, но и в
"социальном" смысле. Во-первых, область действия неявных объявлений полей
строго ограничена - между DO и END одного оператора. Во-вторых, как было
показано, становятся менее нужными переименования (с такими своими
проблемами, как синонимия - доступ к одному объекту по различным именам; это
очень ненадежно; почему?). В-третьих, присоединяющий оператор допускает
весьма эффективную реализацию. Запись, с которой предполагается работать,
можно разместить в сверхоперативной памяти, на рабочих регистрах и т.п. С
этой точки зрения отказаться от присоединяющего оператора - решение
сомнительное.
Однако чтобы понять, почему присоединяющий оператор совершенно
необходим в Модуле-2, нужно привлечь соображения социального характера, явно
учитывающие потенциальную нишу для этого языка. Присоединяющий оператор
совершенно необходим в Модуле-2 потому, что он имеется в Паскале (к нему
привыкли те самые пользователи, которые с большой вероятностью начнут
пользоваться Модулой-2 как естественным преемником Паскаля (если угодно, его
модульным диалектом)), т.е. у этих ЯП - потенциально пересекающиеся ниши.
Обратите внимание: закон консерватизма ниш в данном случае работает не
против нового ЯП, а за него, потому что Модулу-2 следует рассматривать не
как конкурента Паскаля, а как его естественное развитие, учитывающее
консерватизм ниши. [Правда, различные развития Паскаля вполне могут
конкурировать (и реально конкурируют!) между собой и, в частности, с
Модулой-2.]
12.7.2. Инкапсуляция
Особенно наглядно принцип чемоданчика проявляется в методе М-инкапсуля-
ции. Обсуждая необходимость приватной части в Аде, мы привлекали
реализаторскую позицию (соображения эффективности реализации - без приватной
части компилятор не в состоянии распределять память под объекты приватных
типов). И отмечали, что при этом нарушается согласованность с концепцией
разделения спецификации и реализации, а также пошаговой детализации.
Другими словами, эффективность реализации в этом случае достигается за
счет ряда нарушений общих принципов и усложнения языка (кстати, тоже
нарушение общего принципа, а именно принципа чемоданчика).
Анализируем проблему по принципу чемоданчика. Что совершенно
необходимо? - Инкапсуляция как средство достижения надежности и целостности.
Ищем компромисс между потребностями и возможностями простых проектных
решений. Находим его в отказе от особо эффективной реализации (по сути - от
статического распределения памяти под инкапсулированные объекты - сравните
работу с А- и М-сетями). Следствие - возможность отказаться от весьма
неприятной приватной части и тем самым обеспечить соблюдение четких
принципов проектирования программы (точное разделение спецификации и
реализации между двумя категориями модулей).
Главная цель достигнута. ЯП стал проще (и технологичнее), так как
распределение памяти под составные инкапсулированные объекты должно
выполняться не компилятором, а динамически - генератором "непрозрачных"
указателей.
Ограничение непрозрачных типов ссылочными и отрезками предопределенных
типов позволяет компилятору выделять память для них как для скаляров (по
одной "единице" памяти). Остальное выполняется в динамике процедурами
модуля-экспортера. Изящное решение.
Упражнение. Докажите неформальную теорему : отсутствие приватной части
в сочетании с раздельной компиляцией спецификаций и тел модулей влечет
динамизм составных инкапсулированных типов. Как следствие - ограничение
непрозрачных типов ссылочными или скалярными.
12.7.3. Обмен
Мы видели, как вся мощь А-модели использовалась для управления обменом.
Но вся мощь потребовалась именно потому, что А-модель претендует на
удовлетворение отнюдь не минимальных потребностей. Например, можно создавать
файлы с совершенно произвольными (а не только предопределенными) типами
элементов - именно это потребовало родовых пакетов обмена. Аналогичные
претензии удовлетворяются при создании драйверов специальных устройств
обмена - именно это потребовало развитых спецификаций представления.
С другой стороны, для реализации драйверов привлекается универсальный
аппарат управления асинхронными процессами. Когда он уже имеется в ЯП, такое
решение может показаться даже изящным. Однако на уровне машинных команд
организация взаимодействия с аппаратной задачей может существенно отличаться
от организации взаимодействия "полностью программных" задач. Мы уже отмечали
это, приводя пример драйвера клавиатуры.
Так что компилятор вынужден выделять драйверы и все-таки
программировать их не так, как другие задачи. Выделять драйверы приходится
по спецификациям представления. Итак, применение универсального аппарата
асинхронных процессов для реализации драйверов заставляет сначала тщательно
замаскировать то, что затем приходится столь же тщательно выискивать.
Создавать трудности, чтобы потом их преодолевать - не лучший принцип не
только в программировании.
Наконец, хотя асинхронность внешних устройств - одна из причин
появления в ЯП асинхронных процессов, совершенно не очевидно, что для
создания драйверов требуется столь абстрактный (и дорогой в реализации)
аппарат, как рандеву из А-модели.
Итак, с учетом ориентации Модулы-2 в основном на однопроцессорные
компьютеры (но с асинхронными устройствами обмена) взглянем на управление
обменом, руководствуясь принципом чемоданчика.
Что совершенно необходимо? Дать возможность писать драйверы,
обеспечивающие взаимодействие основной программы с асинхронно работающим
устройством обмена.
Снова ищем компромисс между потребностями в асинхронных процессах и
возможностями простых проектных решений.
Находим его в отказе, во-первых, от того, чтобы, чтобы драйвер работал
асинхронно с основной программой (оставаясь программной моделью аппаратной
задачи), и, во-вторых, от абстрактного механизма взаимодействия относительно
равноправных задач (подобного рандеву).
Действительно, минимальные потребности состоят в том, чтобы основная
программа (точнее ее часть, драйвер устройства) имела лишь возможность:
а. запустить устройство для выполнения конкретного обмена;
б. продолжать работать, пока устройство исполняет задание;
в. реагировать на завершение обмена (на факт выполнения устройством
задания).
Именно такие минимальные (совершенно необходимые) возможности
управления устройствами встроены в Модулу-2. Точнее говоря, то, что мы
назвали аппаратной задачей, в Модуле-2 называется периферийным (асинхронным)
процессом.
[Периферией обычно называют совокупность устройств обмена. В Модуле-2
имеются еще и квазипараллельные процессы (сопрограммы).]
Рассмотрим на примере периферийные процессы в Модуле-2. Как и в Аде, в
Модуле-2 обмен требует всей мощи ЯП. По крайней мере, существенно
используется основной механизм абстракции - модули. Определяемые реализацией
("системно-зависимые") имена инкапсулированы в предопределенном модуле
"Система" (SYSTEM). Так как транзит импортированных имен запрещен, то любые
модули, где применяются системно-зависимые имена, должны явно импортировать
модуль "Система". (По этому признаку системно-зависимые модули легко
распознавать).
Модуль "Система" экспортирует, в частности, типы "Адрес" (ADDRESS),
"Слово" (WORD - машинное слово), "Процесс" (PROCESS - к этому типу относятся
как сопрограммы, так и периферийные процессы), А также процедуры для работы
с объектами этих типов: "НовыйПроцесс" (NEWPROCESS), "Переключить"
(TRANSFER) и "ПереключитьСЗаказом" (IOTRANSFER).
Так что в системно-зависимых модулях (в том числе в драйверах) можно
работать с машинными адресами, словами и процессами. Последние
характеризуются двумя компонентами - телом (представленным некоторой
процедурой) и рабочей областью, в свою очередь характеризуемой начальным
адресом и размером (представленным натуральным числом).
Процедура
НовыйПроцесс (P, A, n, p1)
создает новый процесс (объект типа "Процесс" с телом P и рабочей областью с
начальным адресом A и размером n) и присваивает его переменной p1 типа
"Процесс". Новый процесс при этом не запускается (ведь процессор один),
продолжает выполняться текущий процесс (основная программа также считается
процессом).
Переключение на новый процесс осуществляется процедурой
Переключить (p1, p2);
При этом текущий процесс приостанавливается и присваивается переменной p1, а
активным становится процесс-содержимое переменной p2. [Напомним, что это
сопрограммы; p2 начинает работать с начала своего тела или с того места, где
ранее приостановился.]
Процедура
ПереключитьСЗаказом (p1, p2, A);
делает то же, что и предыдущая, но еще и заказывает переключение снова на p1
после прерывания по адресу A.
Именно эта процедуоа и позволяет обеспечить указанные выше потребности
б) и в). Для этого достаточно указать в качестве p2 процесс, который должен
работать асинхронно (параллельно) с устройством, а в качестве адреса A
указать адрес вектора прерываний, приписанный управляемому устройству.
Тогда, если непосредственно перед выполнением процедуры
ПереключитьСЗаказом запустить обмен с устройством, то текущий процесс,
приостановившись на этой процедуре, будет ждать прерывания,
свидетельствующего о завершении обмена. При этом параллельно с устройством
будет работать процесс p2. А после прерывания произойдет заказанное
переключение снова на p1, т.е. на процесс, запустивший обмен с устройством
(с заказом прерывания). Обычно обмен запускается засылкой единицы в
соответствующий разряд регистра состояния устройства - так реализуется
указанная выше потребность "а" .
Вот такими скупыми средствами реализовано в Модуле-2 управление
периферийными процессами. С точки зрения языка не понадобилось вообще ничего
нового, а с точки зрения модуля "Система" - всего одна процедура
ПереключитьСЗаказом. Так действует принцип чемоданчика! Чтобы лучше понять
взаимодействие описанных средств, приведем (с переводом идентификаторов на
русский язык) модуль обмена с телетайпом из авторского описания Модулы-2.
Драйвер на Модуле-2. Чтобы все в нижеследующей программе (модуле
"Телетайп") было понятно, нужно сказать несколько слов о приоритетах
процессов. Приоритет - это целое число, характеризующее срочность процесса.
Приоритет связывается с каждым модулем и с каждым устройством, посылающим
прерывания. Исполнение программы может быть прервано тогда и только тогда,
когда приоритет прерывающего устройства выше приоритета исполняемого
(текущего) процесса. Приоритет процессора (т.е. приоритет текущего процесса)
можно временно понизить процедурой УменьшитьПриоритет (LISTEN) из модуля
"Система". Нужно это для того, чтобы разрешить прерывания от устройств.
1 MODULE Телетайп [4] ; (* приоритет этого модуля равен 4 *)
2 FROM Система IMPORT Слово, Процесс, НовыйПроцесс,
Переключить, ПереключитьСЗаказом, УменьшитьПриоритет ;
3 EXPORT Печатать ;
4 CONST N = 32 ; (* размер буфера литер *)
5 VAR n : INTEGER ; (* текущее количество литер в буфере *)
6 Класть, Брать : [1..N] ; (* индексы в буфере, отмечающие,
куда класть и откуда брать литеры *)
7 Буф : ARRAY [1..N] OF CHAR ; (* буфер, массив литер *)
8 Дай, Возьми : Процесс ;
9 РабОбл : ARRAY [0..20] OF Слово ;
(* рабочая область драйвера *)
10 РегСост [177564B] : BITSET ; (* регистр состояния
телетайпа *)