«TEXT AND MONOGRAPHS IN COMPUTER SCIENCE Editor David Gries Advisory Board E.L.Bauer, K.S.Fu, J.J.Horning, R. Reddy, D.C.Tsichritzis, W.M.Waite PROGRAMMING IN MODULA-2 NIKLAUS WIRTH Third, Corrected Edition ...»
МАТЕМАТИЧЕСКОЕ ОБЕСПЕЧЕНИЕ ЭВМ
Н.Вирт
ПРОГРАММИРОВАНИЕ НА ЯЗЫКЕ МОДУЛА – 2
TEXT AND MONOGRAPHS IN COMPUTER SCIENCE
Editor David Gries
Advisory Board E.L.Bauer, K.S.Fu, J.J.Horning,
R. Reddy, D.C.Tsichritzis, W.M.Waite
PROGRAMMING IN MODULA-2
NIKLAUS WIRTH
Third, Corrected Edition
МАТЕМАТИЧЕСКОЕ
ОБЕСПЕЧЕНИЕ
ЭВМ Н.Вирт ПРОГРАММИРОВАНИЕ НА ЯЗЫКЕ МОДУЛА-2ПРЕДИСЛОВИЕ РЕДАКТОРА ПЕРЕВОДА
ПРЕДИСЛОВИЕ
ПРЕДИСЛОВИЕ К ТРЕТЬЕМУ ИЗДАНИЮ
Часть 1
1. ВВЕДЕНИЕ
2. ПЕРВЫЙ ПРИМЕР
3. НОТАЦИЯ ДЛЯ ЗАПИСИ СИНТАКСИСА МОДУЛЫ
4. ПРЕДСТАВЛЕНИЕ ПРОГРАММ НА МОДУЛЕ
5. ОПЕРАТОРЫ И ВЫРАЖЕНИЯ
6. УПРАВЛЯЮШИЕ СТРУКТУРЫ
6.1. Операторы повторения (циклы)
6.2. Условные операторы
7. ЭЛЕМЕНТАРНЫЕ ТИПЫ ДАННЫХ
7.1. Тип INTEGER (целый)
7.2. Тип CARDINAL (натуральный)
7.3. Тип REAL (действительный)
7.4. Тип BOOLEAN (логический)
7.5 Тип CHAR (литерный)
7.6. Тип BITSET
8. ОПИСАНИЯ КОНСТАНТ И ПЕРЕМЕННЫХ
9. МАССИВЫ
Часть 2
10. ПРОЦЕДУРЫ
11. ПОНЯТИЕ ЛОКАЛЬНОСТИ
12. ПАРАМЕТРЫ
12.1. Параметры-переменные
12.2. Параметры-значения
12.3. Гибкие массивы-параметры
13. ПРОЦЕДУРЫ-ФУНКЦИИ
14. РЕКУРСИЯ
Часть З
15. ОПИСАНИЯ ТИПОВ
16. ПЕРЕЧИСЛИМЫЕ ТИПЫ
17. ТИП ДИАПАЗОН
18. ТИП МНОЖЕСТВО
19. ТИП ЗАПИСЬ
20. ЗАПИСИ С ВАРИАНТНЫМИ ЧАСТЯМИ
21. ДИНАМИЧЕСКИЕ СТРУКТУРЫ ДАННЫХ И УКАЗАТЕЛИ
22. ПРОЦЕДУРНЫЕ ТИПЫ
Часть 4
23. МОДУЛИ
24. РАЗДЕЛ ОПРЕДЕЛЕНИЙ И РАЗДЕЛ РЕАЛИЗАЦИИ
25. РАЗБИЕНИЕ ПРОГРАММЫ НА МОДУЛИ
26. ЛОКАЛЬНЫЕ МОДУЛИ
27. ПОСЛЕДОВАТЕЛЬНЫЙ ВВОД И ВЫВОД
28. ЭКРАННЫЙ ВВОД И ВЫВОД
Часть 5
29. СРЕДСТВА ПРОГРАММИРОВАНИЯ НИЗКОГО УРОВНЯ
30. ПАРАЛЛЕЛЬНЫЕ ПРОЦЕССЫ И СОПРОГРАММЫ
31. УПРАВЛЕНИЕ ВНЕШНИМИ УСТРОЙСТВАМИ, ПАРАЛЛЕЛЬНОСТЬ И
ПРЕРЫВАНИЯСообщение о языке программирования Модула-2
1. ВВЕДЕНИЕ
2. СИНТАКСИС
3. СЛОВАРЬ И ИЗОБРАЖЕНИЕ
4. ОПИСАНИЯ И ПРАВИЛА ВИДИМОСТИ
5. ОПИСАНИЯ КОНСТАНТ
6. ОПИСАНИЯ ТИПОВ
6.1. Основные типы
6.2. Перечисления
6.3. Тип диапазон
6.4. Тип массив
6.5. Тип запись
6.6. Тип множество
6.7. Тип указатель
6.8. Тип процедура
7. ОПИСАНИЯ ПЕРЕМЕННЫХ
8. ВЫРАЖЕНИЯ
8.1. Операнды
8.2. Операции
9. ОПЕРАТОРЫ
9.1. Присваивания
9.2. Вызовы процедур
9.3. Последовательности операторов
9.4. Условный оператор
9.5. Оператор выбора
9.6. Цикл с условием продолжения
9.7. Цикл с условием окончания
9.8. Цикл с шагом
9.9. Безусловный цикл
9.10. Оператор присоединения
9.11. Операторы выхода и возврата
10. ОПИСАНИЯ ПРОЦЕДУР
10.1. Формальные параметры
10.2. Стандартные процедуры
11. МОДУЛИ
12. СИСТЕМНО-ЗАВИСИМЫЕ ВОЗМОЖНОСТИ
13. ПРОЦЕССЫ
13.1. Порождение процессов и передача управления
13.2. Процессы устройств и прерывания
14. ЕДИНИЦЫ КОМПИЛЯЦИИ
ПРИЛОЖЕНИЕ 1 СИНТАКСИС МОДУЛЫ-2
Перекрестные ссылки
ПРИЛОЖЕНИЕ 2
СТАНДАРТНЫЕ ВСПОМОГАТЕЛЬНЫЕ МОДУЛИ
ПРИЛОЖЕНИЕ 3
ТАБЛИЦА ЛИТЕР КОДА ASCII
Литеры управления курсором
Литеры-разделители
ПРИЛОЖЕНИЕ 4
СИНТАКСИЧЕСКИЕ ДИАГРАММЫ МОДУЛЫ-2
ПРОГРАММИРОВАНИЕ
Перевод с английского В.А. Серебрякова и В.М. Ходукина под редакцией В.М. Курочкина МОСКВА «МИР» ББК 32. В 52 УДК 681. Вирт Н.1987. — 224 с, ил.
Книга известного швейцарского специалиста по системному программированию, знакомого советским читателям по переводам его книг «Введение в системное программирование» (М.: Мир, 1977) и «Алгоритмы + структуры данных = программы» (М Мир, 1985). Язык Модула-2 является преемником известного языка Паскаль и ориентирован на однопроцессорные малые ЭВМ. Книга сочетает в себе достоинства учебного пособия и справочного руководства по этому языку.
Для системных программистов, для специалистов, работающих с языком Модула-2.
1702070000- Редакция литературы по математическим наукам © 1985 by Springer-Verlag New York Inc. All rights reserved.
Authorized translation from English language edition published by Springer-Verlag Berlin — Heidelberg — New York — Tokyo © перевод на русский язык, «Мир»,
ПРЕДИСЛОВИЕ РЕДАКТОРА ПЕРЕВОДА
Более 15 лет назад появился язык Паскаль, который быстро завоевал популярность, получив широкое распространение. Создан он был для целей обучения программированию, однако очень скоро нашел другое поприще - системное программирование. И пожалуй, в дальнейшем (в том числе и у нас) он больше всего используется именно для создания программного обеспечения.Правда, целый ряд черт языка мешал этому. Прежде всего отсутствовала модульность. Немалую роль играли также такие моменты, как отсутствие в языке параллельных процессов, затруднения с организацией работы различных независимых устройств вычислительной машины и др. Видимо, все это привело Н.Вирта к идее разработки языка Модула, а затем и настоящей его модификации Модула-2. С того момента, когда этот язык стал известен системным программистам в СССР, число его сторонников постоянно увеличивалось..Можно смело рассчитывать на то, что в ближайшее время, кроме зарубежных, мы будем располагать целым рядом высококачественных отечественных трансляторов, в большей степени приспособленных к нашей специфике работы на ЭВМ.
Трудно объяснить причины успеха (или неуспеха) какого-либо языка программирования. Помимо привычки (возможно, основной причины, объясняющей распространенность, например, Фортрана), есть еще какие-то Факторы. И не исключено, что весьма существенным для языков, создаваемых Н.Виртом, и в частности для Модулы-2, является относительная простота: при всей широте возможностей и мощности изобразительных средств описание его требует всего страниц ("Сообщение о языке программирования Модула-2" в настоящей книге). Это, конечно, существенно облегчает изучение языка и его использование.
Следует все же сказать, что в языке не все может нравиться. В таких оценках много субъективного, но тем не менее хочется отметить, например, отсутствие динамических массивов, бедность аппарата параллельных процессов и средств их взаимодействия, отсутствие способов гибкого задания отображения типов на Физическую память машины. Создалось впечатление, что сложные системные программы будут ориентированы на те ЭВМ, для которых они пишутся, и перенос программ с одних ЭВМ на другие будет затруднен. Впрочем, язык Модула-2 не следует рассматривать как окончательно сформированный и законченный, и возможно, что как Предисловие редактора перевода по инициативе самого Н.Вирта, так и в результате накопления опыта работы с языком в нем будут происходить изменения. Это, в частности, подтверждается дрейфом языка, наблюдаемым в различных авторских публикациях описаний языка. В частности, в 1985 г. появился препринт (N.Uirth. A fast and compact compiler for Modula-2. O.Gutknecht. Compilation of data structures: an new approach to efficient Modula-2 symbol files, duly 1985, #64, Institut fur informatic, ETH-Zentrum, Zurich, Switzerland), в котором Н.Вирт требует объявления объектов (констант, переменных, процедур) до их использования. Для процедур разрешено предварительное описание заголовков.
В настоящем переводе,- выполнявшемся с третьего английского издания, часть идентификаторов в программах не переведена на русский язык. Неизменными остались идентификаторы в тех модулях, которые могут войти в библиотеки Модулы-2 в качестве стандартных. При подготовке русского издания переводчики и редакторы пользовались средствами современной вычислительной техники.
Будем надеяться, что книга окажется очень интересной для советских читателей и принесет большую пользу, в первую очередь разработчикам программного обеспечения. Кроме того, первая (и основная) часть книги, задуманная скорее как введение в язык программирования Модула-2, а не как его строгое определение, может служить прекрасным учебником по программированию, написанным свойственным Н.Вирту четким языком, выдержанным в стиле структурного программирования и иллюстрированным весьма интересными примерами.
В.М.Курочкин
ПРЕДИСЛОВИЕ
Настоящая книга представляет собой введение в программирование вообще и руководство по программированию на языке Модула-2 в частности. Она ориентирована в основном на лиц, уже знакомых с элементами программирования и желающих систематизировать свои знания в этой области. Тем не менее в книгу включен вводный раздел для начинающих, где в сжатом виде представлены Фундаментальные понятия информатики, благодаря чему книга может служить и самоучителем. Используемая здесь система обозначений -это язык Модула-2, которому в большой мере присущ структурный подход. Он вырабатывает у изучающего стиль работы, широко известный под названием структурное программирование.Эта книга, служащая руководством по программированию на языке Модула-2, охватывает практически все его средства. В гл. 1 рассматриваются такие основные понятия, как переменная, выражение, присваивание, условный оператор, оператор цикла, а также массивы. Эта и вторая глава, в которой вводится важное понятие процедуры или подпрограммы, по существу, содержат материал стандартного вводного курса программирования. Глава 3 касается типов и структур данных, что составляет ядро курсов программирования повышенного типа. Четвертая глава посвящена понятию модуля, являющегося фундаментальным средством при разработке больших программных систем и при совместной работе коллективов программистов. Наиболее широко используемые служебные программы ввода и вывода даны в виде примеров модулей. И наконец, в гл. 5 описываются средства системного программирования, работа с внешними устройствами и мультипрограммирование. Книга содержит практические рекомендации по тому, как и где использовать конкретные средства языка. Эти рекомендации должны помочь читателю выработать хороший стиль программирования.
Язык Модула-2 - потомок и прямой наследник языков Паскаль [1] и Модула 2]. Паскаль был разработан как язык общего назначения и после его реализации в 1970 г. получил широкое распространение, а Модула возникла из экспериментов по мультипрограммированию и нацелена, следовательно, на аспекты, относящиеся именно к этой сфере приложений. Язык Модула был специфицирован и реализован в опытном порядке в 1975 г.
В 1977 г. в Цюрихе, в Институте информатики (Institut fur Informatic of ETH, Zurich) была начата работа по созданию новой вычислительной системы. Проект предполагал одновременную разработку аппаратуры и программного обеспечения. Эта система (позже названная Lilith) должна была программироваться на едином языке высокого уровня, который, следовательно, должен был, с одной стороны, удовлетворять требованиям проектирования в целом, а с другой - допускать программирование ее отдельных Фрагментов, описывающих взаимодействие с аппаратурой. В результате скрупулезного анализа проекта возник язык Модула-2, включающий все характерные черты Паскаля и дополненный важными понятиями модуля и мультипрограммирования.
Поскольку синтаксис нового языка соответствовал больше синтаксису Модулы, чем Паскаля, было выбрано название Модула-2. Далее мы будем использовать названия Модула и Модула-2 как синонимы.
От Паскаля язык отличается следующими основными средствами:
1. Понятие модуля и возможность его разбиения на раздел определений и раздел реализации.
2. Более систематизированный синтаксис, что облегчает изучение языка. В частности, каждая конструкция, начинающаяся с ключевого слова, заканчивается тоже ключевым словом (за исключением оператора REPEAT... UNTIL...), т.е. заключена в своего рода скобки.
3. Процесс - как ключевое понятие мультипрограммирования.
4. Так называемые средства программирования низкого уровня. позволяющие ослабить жесткий контроль типов и отображать данные, имеющие структуру Модулы-2, на память, не обладающую внутренней структурой.
5. Процедурный тип. который позволяет динамически присваивать процедуры переменным.
Первая реализация Модулы-2 заработала на PDP-11 в 1979 г., а первое определение языка было опубликовано в марте 1980 г. как сообщение о языке Института.информатики. С тех пор язык интенсивно используется в стенах нашего института. После годичной эксплуатации и проверок на различных приложениях в марте 1981 г. компилятор был передан внешним пользователям.
Интерес к компилятору быстро возрос, поскольку он оказался мощным инструментом разработки сложных систем и был реализован на широко распространенной мини-ЭВМ. Этот интерес вызвал необходимость написания руководства и учебника по языку. Сообщение о языке, содержащее сжатое определение языка Модула-2, включено в конец настоящего руководства в основном для облегчения ссылок на него. Оно осталось практически неизменным; в нем лишь опущены разделы, посвященные стандартным служебным модулям и использованию компилятора.
Английский оригинал книги был получен в виде, удобном для тиражирования, с помощью миниЭВМ Lilith, присоединенной к лазерному печатающему устройству Canon LBP-10. Параллельно с написанием книги автор разрабатывал программы, необходимые для Форматирования текстов (и управления печатающим устройством), а также проектировал интерфейс связи с устройством печати. Естественно, что все эти программы были написаны на Модуле (для Lillth).
Просто невозможно выразить заслуженную благодарность всем, кто оказал влияние на написание этой книги и на проект Модула-2. Большую пользу принес мне год (1976), проведенный в исследовательской лаборатории корпорации Xerox, и знакомство с некоторыми идеями, касающимися модульного программирования, содержащимися в языке Mesa [3]. Вероятно, очень важной была мысль о возможности эффективной реализации языка высокого уровня на миниЭВМ. Приношу свою благодарность также разработчикам Модулы, в особенности Л.Гайсмену, А.
Горангуру, Ч.Якоби и СЕ.Кнудсену, которые не только превратили Модулу в эффективный и надежный инструмент, но также часто (и очень мудро) предостерегали меня от включения в язык новых модных средств.
ПРЕДИСЛОВИЕ К ТРЕТЬЕМУ ИЗДАНИЮ
В третье издание книги включены изменения и модификации Модулы-2, сделанные в конце г. Изменения по сравнению с предыдущими изданиями отмечены символом ((). Одно существенное изменение касается модуля определений, который теперь не содержит экспортного списка, а сам Фактически представляет собой экспортный список (см. разд. 24). Дополнительно в приложение включены несколько стандартных модулей, оказавшихся весьма полезными. В основном они относятся к вводу и выводу, т.е. использованию клавиатуры, дисплея и Файловой системы.Н.Вирт Литература 1. N.Ulrth. The programming language PASCAL. Acta Informatica 1, 35-63 (1971).
2. N.Ulrth. Modula: a language for modular multiprogramming. Software - Practice and Experiment»,7,3J.G.Mitchel, W.Maybury, R.Sweet. Mesa Language Manual. Xerox PARC, CSL-78-1 (1978.
1. ВВЕДЕНИЕ Хотя данное руководство и предполагает знакомство читателя с основными понятиями информатики, однако, по-видимому, все же уместно начать с объяснения некоторых понятий и терминологии. Мы осознаем, что (за редким исключением) программы пишутся (более точно проектируются) с целью их интерпретации вычислительной машиной. Машина в этом случае выполняет некий процесс, т.е. последовательность действий в соответствии со спецификацией, заданной программой. Этот процесс также называется вычислением. Программа сама по себе - это не что иное, как текст. Поскольку она, как правило, определяет достаточно сложный процесс и должна делать это с максимальной точностью и учетом всех деталей, смысл этого текста должен быть определен очень строго. Такая строгость требует наличия некоторого Формализма, для которого теперь используется термин язык. Мы принимаем это название, хотя на языке обычно говорят, и он гораздо менее четко определен. Наша цель здесь - изучить Формализм, или язык, называемый Модула-2 (далее - просто Модула).
Программа обычно определяет процесс, который заставляет интерпретатор, т.е. ЭВМ, считывать данные (так называемый ввод) из некоторых источников и варьировать свои последующие действия в зависимости от вводимых данных. Имеется в виду, что программой определяется не единственный процесс, а целый класс вычислений (обычно неограниченный). Мы должны гарантировать, что во всех случаях эти процессы будут действовать в соответствии с заданными описаниями (или, следовало бы сказать, нашими ожиданиями). Мы могли бы проверить, что описание действительно удовлетворяется в случае единственного процесса вычислений, но в общем случае это невозможно, поскольку класс всех допустимых процессов слишком велик.
Добросовестный программист обеспечивает правильность своей программы путем ее тщательной разработки и анализа. Именно тщательная разработка - сущность профессионального программирования.
Задача проектирования программы еще больше осложняется тем, что программа должна не только полностью описывать класс вычислений, а часто также должна интерпретироваться (выполняться) различными интерпретаторами (вычислительными машинами). Раньше это требовало ручного перевода исходного вида программы в различные машинные коды, причем приходилось принимать во внимание разнообразные характеристики и ограничения этих кодов. С созданием языков высокого уровня, имеющих Формальные определения, и построением автоматических трансляторов, преобразующих программы в коды различных машин, трудности коренным образом уменьшились, хотя и не исчезли. В принципе Формальный язык следовало бы определять абстрактным образом, возможно аксиоматически, не делая никаких ссылок на реальную машину или на конкретный механизм интерпретации. Если бы это было достигнуто, то программисту нужно было понимать только сам Формальный язык. Однако такая общность часто слишком сильно ограничивает действия программиста и требует дополнительных затрат, поэтому во многих случаях он все же должен знать основные характеристики своей машины или машин. Тем не менее квалифицированный программист будет стремиться как можно меньше обращаться к специфическим характеристикам машины, а опираться исключительно на стандарт Формального языка, чтобы сохранить универсальность и мобильность программы. Язык Модула помогает решать эту задачу: машинные зависимости заключаются в особые объекты, используемые только в так называемом низкоуровневом программировании.
Из сказанного выше становится ясно, что процесс трансляции расположен между написанием программы и ее интерпретацией. Процесс этот называется компиляцией и состоит в переводе исходного текста в кодированное машинное представление. Качество компиляции может оказывать решающее влияние на эффективность последующей интерпретации программы. Мы подчеркиваем тот Факт, что может быть много компиляторов с одного языка (даже для одной ЭВМ): одни более, другие менее эффективные. Важно понимать, что эффективность характеристика реализации, а не языка, и, следовательно, необходимо разделить понятия "язык" и "реализация".
Подведем итог:
Программа - Фрагмент текста.
Она задает процесс вычислений.
Процесс осуществляется некоторым интерпретатором, обычно вычислительной машиной, интерпретирующей (выполняющей) программу.
Смысл программы задается Формализмом, называемым языком Программа задает некоторый класс вычислений, причем исходные данные играют роль параметра каждого конкретного процесса.
Перед выполнением текст программы транслируется компилятором в машинный код. Этот процесс называется компиляцией.
Разработка программы включает в себя обеспечение того, чтобы все члены упомянутого класса вычислений функционировали в соответствии с определением. Это осуществляется тщательной аналитической верификацией программы И избирательным тестированием характерных вариантов.
В программах по возможности следует воздерживаться от использования особенностей конкретных интерпретаторов (вычислительных машин). Только в этом случае можно быть уверенным, что смысл программы будет понят по описанию языка.
Компилятор - программа, переводящая исходный вид программы в коды конкретной машины.
Перед выполнением программа должна быть откомпилирована. Программирование в широком смысле слова подразумевает не только составление программы, но также и конкретную подготовку текста, его компиляцию, исправление ошибок, так называемую отладку и планирование тестов. Современный программист использует для этих целей много различных средств, включая редакторы, компиляторы и отладчики. Он также должен быть знаком с программным окружением этих компонент. Мы не будем касаться всех этих аспектов, а сосредоточим внимание на языке Модула.
2. ПЕРВЫЙ ПРИМЕР Проследим этапы разработки простой программы и поясним на ее примере некоторые фундаментальные понятия программирования и основные средства языка Модула. Рассмотрим следующую задачу: даны два натуральных числа х и у; надо вычислить их наибольший общий делитель (нод).
Приведем необходимые для решения этой задачи математические сведения.
1. Если х равен у, то х (или у) - искомый результат.
2. нод двух чисел не изменится, если большее из них заменить их разностью, т.е. вычесть из большего числа меньшее.
Если выразить это в математических терминах, то получим следующие правила:
1. нод(х,х) - х 2. Если х > у, то нод(х,у) - нод(х-у,у) Основной рецепт, так называемый алгоритм получения нод, таков: изменять числа х и у согласно правилу 2 так, чтобы их разность уменьшалась. Повторять это до тех пор, пока числа не станут равными. Правило 2 гарантирует, что при этих изменениях нод(х,у) все время остается одним и тем же, а правило 1 гарантирует, что в конце концов мы найдем результат.
2. Первый пример Теперь мы должны записать эти рекомендации в терминах Модулы. Первая попытка приводит к следующему наброску (первая версия). Заметим, что символ # означает "не равно".
"применить правило 2, уменьшив разность" Текст в кавычках представляет собой предложение естественного языка. Вторая версия уточняет первую, заменяя естественный язык Формальными терминами.
Этот Фрагмент текста - еще не готовая программа, но он уже демонстрирует одну существенную черту языка структурного программирования - иерархическую структуру. Вся первая версия -это один оператор, и он содержит другой подчиненный "оператор" (текст в кавычках). Во второй версии этот внутренний "оператор" детализирован, и появились новые подчиненные операторы, заменяющие значение х значением х - у. Такая иерархия операторов отражает структуру, лежащую в основе алгоритма.
Она явно видна, благодаря структуре языка, разрешающего вложение компонент программы друг в друга. Поэтому важно знать структуру, т. е. синтаксис языка до самых мельчайших деталей. В тексте мы отразили вложение или подчинение сдвигами строк. Хотя это и не требуется нормами языка, но существенно помогает пониманию программы.
Отражение внутренней структуры алгоритма в структуре текста программы - ключевая идея структурного программирования. По существу, невозможно понять смысл программы, если исчезнет ее структура, как это бывает, когда компилятор выдает машинный код. Мы должны иметь в виду следующее - программа бесполезна, если человек не может в ней разобраться и удостовериться в ее правильности.
Теперь приступим к получению из написанного выше Фрагмента законченной программы.
Понятно, что нужно задать действия, присваивающие начальные значения переменным х и у, и действие, делающее видимым результат. Казалось бы, для этой цели нам потребуется знание машинных средств связи с пользователем. Но поскольку мы не хотим обращаться к специфике конкретных машин, особенно в таком важном и часто встречающемся случае, как генерация выходной информации, введем абстракции средств связи, предполагая, что они будут в наличии (реализованы некоторым подходящим образом) на всех ЭВМ, на которых возможно программирование на Модуле. Эти абстракции, как показано ниже, принимают Форму стандартных операторов. Ввод данных осуществляется операцией Read (читать), а вывод операцией Write (писать). Мы можем, например, считать, что данные читаются с клавиатуры и пишутся на дисплей.
ReadCard (x);
ReadCard(y);
WriteCard(x,6) Процедура ReadCard читает число типа CARDINAL (т.е. целое неотрицательное) и присваивает его параметру (х). Процедура WriteCard выводит число типа CARDINAL, указанное ее первым параметром (х). Второй параметр (6) указывает количество позиций, выделяемое для представления этой величины на внешнем носителе. В следующей далее окончательной версии мы оформим наш текст так, что он станет настоящей программой на Модуле.
FROM InOut IMPORT ReadCard,WriteString, VAR x.y: CARDINAL;
WriteString("x="): ReadCard(x); WriteLn;
UriteString("y="): ReadCard(y); WriteLn;
WriteString("нод="); WriteCard(x,6); WriteLn;
Существенные добавления, сделанные на этом шаге, - это описания. в Модуле имена всех объектов, встречающихся в программе, таких, как переменные и константы, должны быть описаны. Описание вводит идентификатор (имя) объекта, определяет вид объекта (переменная, константа или что-либо еще) и указывает его общие неизменные свойства, такие, как тип переменной или значение константы.
Получившейся завершенной программе, называемой модулам, присваивается имя (нод), и она имеет следующий Формат:
Уместно сделать еше несколько замечаний относительно нашего примера. Процедуры WriteLn, WriteStrlng. ReadCard и WriteCard не являются частью самого языка. Они определены в другом модуле, называемом InOut, который считается доступным. Подборка таких полезных модулей будет приведена в последующих разделах книги с соответствующими пояснениями. Здесь же мы просто отметим: для того чтобы сделать модули доступными программе, их нужно импортировать. Это осуществляется включением имен нужных объектов в список импорта и указанием того, какому модулю они принадлежат.
Процедура WriteString по выводит текст в виде последовательной цепочки литер (предназначенные для вывода литеры заключены в кавычки). Этот вывод сообщает пользователю ЭВМ, что далее требуется ввод. Такое поведение - существенное свойство диалоговых систем. Процедура WriteLn заканчивает строку в выходном тексте.
На этом завершим обсуждение первого примера, которое было совершенно неформальным. Это допустимо, поскольку мы стремились объяснить уже готовую программу. Однако программирование - это разработка и создание новых программ. Для такой цели подходит лишь точное, Формальное описание нашего инструментального средства. В следующем разделе мы введем Формализм для точного описания правильных ("законных") программ. Этот Формализм позволяет строгим образом определить, удовлетворяет ли написанный текст нормам языка.
3. НОТАЦИЯ ДЛЯ ЗАПИСИ СИНТАКСИСА МОДУЛЫ
Формальный язык - бесконечное множество цепочек символов. Элементы этого множества называются предложениями языка. В случав языка программирования такими предложениями являются программы. Символы берутся из конечного множества, называемого словарем. Так как множество программ бесконечно и не может быть задано прямым перечислением, то вместо этого оно определяется правилами образования его элементов. Последовательности символов, которые могут быть образованы в соответствии с этими правилами, называют синтаксически правильными программами. Такой набор правил представляет собой синтаксис языка.Программы Формального языка соответствуют грамматически правильным предложениям разговорных языков. Каждое предложение имеет структуру и состоит из отдельных частей, таких, как подлежащее, сказуемое, дополнение. Аналогично, программа состоит из частей, называемых синтаксическими понятиями, таких, как операторы, выражения, описания. Если грамматическая конструкция А состоит из следующих друг за другом конструкций В и С, т.е. их конкатенации (сцепления) ВС, то мы будем называть В и С -синтаксическими Факторами и описывать А следующей синтаксической ФОРМУЛОЙ:
Если же А состоит либо из В, либо из С, мы будем называть В и С синтаксическими термами и выражать А в виде:
Для группировки термов и Факторов можно использовать круглые скобки. Следует заметить, что А, В и С обозначают синтаксические понятия описываемого Формального языка, символы равно "-", вертикальная черта "|", скобки "(",")" и точка "." -символы метанотации, называемые метасимволами. Введенная здесь метанотация называется расширенной формой Бэкуса-Наура (РБНФ).
Кроме конкатенации и выбора РБНФ позволяет выразить условное вхождение и повторение. Если конструкция А может состоять либо из В, либо из пустой цепочки, то это выражается в виде Если же А может состоять из конкатенации любого числа (включая нуль) конструкций В, то это обозначается Вот мы и объяснили, что такое РБНФ. Приведем несколько примеров того, как множества предложений описываются Формулами в РБНФ.
Очевидно, что сама РБНФ - это тоже Формальный язык. Если этот язык способен делать то, для чего предназначен (описывать Формальные языки), то уж по крайней мере он должен уметь описать сам себя» В приведенном ниже описании РБНФ на РБНФ мы используем следующие имена для синтаксических понятий:
СинтОператор : синтаксическая Формула СинтВыражение : список альтернативных термов СинТерм : конкатенация Факторов СинтФактор : единичное 'синтаксическое понятие или выражение в скобках Формальное определение РБНФ теперь можно задать следующим образом:
Синтаксис = { СинтОператор }.
СинтОператор = идентификатор "-" СинтВыражение ".".
СинтВыражение = СинТерм { "|" СинТерм }.
СинТерм = СинтФактор { СинтФактор }.
СинтФактор = идентификатор | цепочка | "(" СинтВыражение ")" I "[" СинтВыражение "]" Идентификаторами обозначены синтаксические понятия; цепочка -это последовательность литер, взятых из алфавита определяемого языка. Для представления идентификатора мы приняли широко используемое в языках программирования соглашение, а именно:
Идентификатор состоит из последовательности букв и цифр. начинающейся буквой. Цепочка состоит из последовательности любых литер, заключенных в кавычки (или в апострофы).
Формальное определение этих правил в терминах РБНФ дано в следующем разделе.
4. ПРЕДСТАВЛЕНИЕ ПРОГРАММ НА МОДУЛЕ
В предыдущем разделе был введен Формализм, которым в дальнейшем будет определяться структура правильно составленных программ. Этот Формализм, однако, определяет только то, каким образом представляются программы в виде последовательностей синтаксических элементов (лексем), но не литер. Этот "дефект" допущен намеренно: мы полагаем, что представление лексем (а значит, и программ) в терминах литер слишком сильно зависит от конкретной реализации, а для определения языка необходим более высокий уровень абстракции. Создание промежуточного уровня представления через последовательности лексем обеспечивает удобную развязку между языком и окончательным представлением программы, которое зависит от доступного набора литер. Как следствие этого мы должны принять ряд правил, регулирующих представление лексем в виде последовательности литер. Лексемы Модулы разделяются на следующие классы:идентификаторы, числа, цепочки, операции, разделители, комментарии.
Правила, регулирующие их представление в терминах стандартного набора литер ISO, следующие:
1. Идентификаторы - последовательности букв (* в оригинале только буквы латинского алфавита, в русском переводе книги используются также буквы русского алфавита, прописные и строчные. Прим. перев.*) и цифр, начинающиеся с буквы:
$ Идентификатор - Буква {Буква|Цифра>.
Вот примеры правильно составленных идентификаторов:
Алиса hello ЧернаяПтица oпepaторWHILE SR Примеры слов, не являющихся идентификаторами:
D'Alembert (содержит апостроф) Прописные и строчные буквы считаются различными.
Иногда идентификатор (например i) должен быть квалифицирован (уточнен) другим идентификатором (j). Эти выражается в том, что перед i размешается j и они разделяются точкой (j.i). Такой объединенный идентификатор называется квалифицированным (сокращенно КвалИдент). Его синтаксис:
$ КвалИдент * {Идентификатор "."} Идентификатор.
2. Числа могут быть целыми или действительными. Целые представляются последовательностями цифр. Действительные числа содержат десятичную точку и дробную часть. Кроме того, в действительном числе может присутствовать порядок. Он задается буквой Е (прописная латинская) и произносится как "умножить на десять в степени". Числа не должны содержать пробелов. Примеры правильно записанных чисел:
1981 1 3.25 5.1ЕЗ 4.0Е- А вот примеры последовательностей литер, которые не распознаются как числа:
1'000'000 не может быть апострофов 3.5Еn запрещены буквы в числе (за исключением Е) Точные правила образования чисел задаются следующим синтаксисом:
$ Действительное - ЦиФра{ЦиФра}"."{Цифра}[Порядок]Порядок - "Е" ["+"|"-"] Цифра {Цифра}.
ПРИМЕЧАНИЕ: Если за целым числом следует латинская буква В. то оно воспринимается как восьмеричное, если же за ним следует латинская буква Н, то как шестнадиатеричное.
3. Цепочка - последовательность любых литер, заключенная в кавычки. Очевидно, что для однозначного распознавания цепочки необходимо, чтобы она сама не содержала кавычек. Чтобы можно было записать и цепочку, содержащую кавычки, разрешается заключать ее вместо кавычек в апострофы. Однако в этом случае в ной не должно содержаться апострофов.
$ Цепочка - '"' {Литера}'"'|"'" {Литера} "'".
Примеры цепочек:
'Придворная молочница сказала: "Благодарствуйте!".' 4. Операции и ограничители - это или специальные литеры или ключевые слова. Последние пишутся прописными буквами и не должны использоваться в качестве идентификаторов. Стоит поэтому запомнить ключевые слова, перечисление в следующем далее списке; их смысл будет объяснен в последующих разделах.
AND ELSIF LOOP REPEAT
ARRAY END MOD RETURN
BEGIN EXIT MODULE SET
BY EXPORT MOT THEN
CASE FOR OF TO
CONST FROM OR TYPE
DEFINITION IF POINTER UNTIL
DIV IMPLEMENTATION PROCEDURE VAR
DO IMPORT QUALIFIED WHILE
ELSE IN RECORD WITH
Операции и ограничители, составленные из специальных литер:* умножение, пересечение множеств / деление, симметрическая разность множеств (* *) скобки комментария ^ операция разыменования,.;:.. | знаки пунктуации Последовательные лексемы принято разделять одним или несколькими пробелами. Однако необходимо это только в тех случаях, когда отсутствие пробелов привело бы к слиянию двух лексем в одну. Например, во Фрагменте "IF х = у THEN" пробелы нужны перед х и после у, а вокруг знака равенства они могут быть опущены.
5. Комментарии могут быть вставлены между любыми двумя лексемами. Они являются произвольными последовательностями литер, заключенными в скобки для комментариев (* и *).
Комментарии служат дополнительной информацией для человека и пропускаются компилятором.
Они могут также служить для задания режимов работы компилятора.
5. ОПЕРАТОРЫ И ВЫРАЖЕНИЯ
Языковая конструкция, задающая некоторое действие, называется оператором. Операторы могут истолковываться (исполняться), и это истолкование (исполнение) влечет за собой последствия, заключающиеся в том, что изменяется состояние вычислительного процесса, который задается совокупным значением всех переменных программы. Самое элементарное действие присваивания значения переменной. Присваивание имеет вид $ Присваивание = Обозначение ":=" Выражение.и соответствующее ему действие состоит из трех частей, выполняемых в такой последовательности:
1. Вычислить обозначение, определяющее некоторую переменную.
2. Вычислить выражение, получив некоторое значение.
3. Заменить значение переменной из п. 1 на значение выражения из п. 2.
Простые примеры присваиваний Здесь i получает значение 1, х - значение суммы у и z, прежние значения теряются. Заметьте, что следующие пары операторов, выполняемые последовательно, дают разные результаты:
Полагая начальное значение 1 равным 0, для первой пары получим i = 1, j = 2, в то время как вторая пара дает j = 0. Если мы захотим обменять значения переменных i и j, то последовательность операторов не даст желаемого результата. Мы должны ввести вспомогательную переменную, скажем к, для сохранения значения и задать три последовательных присваивания В общем случае выражение состоит из операндов и знаков операций. Его вычисление состоит из применения к операндам операций в предписанном порядке, как правило, слева направо.
Операндами могут быть константы, переменные или функции. (Функции будут описаны далее.) Вообще говоря, идентификация переменной требует в свою очередь вычисления обозначения:
здесь мы, однако, ограничимся лишь случаем использования простой переменной, изображаемой идентификатором. Арифметические выражения (существуют и другие выражения) включают числа, числовые переменные и арифметические операции. К последним относятся основные арифметические операции: сложение (+), вычитание (-), умножение (*) и деление. Все они будут подробно рассмотрены в разделе, посвященном основным типам данных. Здесь же достаточно упомянуть, что знак (/) зарезервирован для деления действительных чисел, а для целых мы используем в качестве знака операции ключевое слово DIV, что означает взятие целой части частного.
Выражение состоит из последовательных слагаемых. Запись эквивалентна Синтаксис выражения определяется правилами $ ПростоеВыражение = ["+" |"-"] Слагаемое $ ОперацияТипаСложения - "+"|"-"|"OR".
ПРИМЕЧАНИЕ: Пока читатель может считать, что синтаксические понятия Выражение и ПростоеВыражение эквивалентны. Различие между ними и смысл операций OR, AND и NOT будут разъяснены в разделе, посвященном данным типа BOOLEAN.
Аналогичным образом, каждое слагаемое состоит из множителей. Слагаемое эквивалентно и определяется синтаксически по правилам:
$ Слагаемое - Множитель $ {ОперацияТипаУмножения Множитель}.
$ ОперацияТипаУмножения - "*"|"/"|"DIV"|"MOD"|"AND"|"&".
Каждый множитель - это или константа, или переменная, или Функция, или выражение, заключенное в круглые скобки.
Примеры арифметических выражений:
Учитывая, что множитель в свою очередь тоже может быть выражением, очевидно, что синтаксис множителей рекурсивен.
$ Множитель = Число | Цепочка | Множество | $ Обозначение[ФактическиеПараметры] | Правила вычисления выражений в действительности очень просты: сложные ситуации встречаются весьма редко, но мы тем не менее укажем несколько основных правил, заслуживающих упоминания.
1. Каждой переменной в выражении должно быть предварительно присвоено значение.
2. Два знака операций не могут стоять рядом. Например, запись а*-b неправильна: нужно писать а*(-b).
3. При умножении нельзя пропускать знак операции. Например, запись 2n неправильна:
должно быть 2*n.
ОперацияТипаСложения.
5. При возникновении сомнений в правилах вычисления (т.е. старшинстве операций) используйте дополнительные скобки для уточнения. Например, а+b*с можно записать и как а+(b*с).
Присваивание - лишь одна из возможных Форм операторов. Другие Формы будут введены в следующих разделах. Мы перечислим эти Формы в виде синтаксического определения $ Оператор - [ Присваивание | ВызовПроцедуры | $ БезусловныйИикл | УсловныйОператор | $ ОператорВыбора | ОператорПрисоединения | Некоторые из этих Форм - структурированные операторы, т.е. их компоненты в свою очередь могут быть операторами. Таким образом, определение операторов, как и выражений, рекурсивно.
Самая фундаментальная структура языка - последовательность. Начисление - последовательность действий, где каждое действие задается некоторым оператором и исполняется после завершения предшествующего действия. Строгая временная упорядоченность -существенная предпосылка последовательного программирования. Если оператор S1 следует за S0, то мы указываем эту последовательную во времени связь точкой с запятой Этот разделитель операторов (не завершитель) указывает на то, что за действием, соответствующим S0, должно непосредственно следовать действие, соответствующее S1.
Последовательность операторов синтаксически определяется так:
$ ПослОператоров = Оператор {":" Оператор).
Синтаксис операторов подразумевает, что оператор может вообще не содержать литер. В таком случае оператор называют пустым, и, очевидно, он задает пустое действие. Эта диковинка среди операторов имеет определенный смысл. Пустой оператор позволяет вставлять точку с запятой в такие места, где она на самом деле избыточна, например в конец последовательности операторов.
6. УПРАВЛЯЮШИЕ СТРУКТУРЫ
Главная особенность ЭВМ - способность выполнять отдельные действия циклически либо выбирать одно из нескольких действий в зависимости от ранее вычисленных результатов. Таким образом, последовательность выполняемых действий не всегда совпадает с последовательностью соответствующих операторов. Последовательность действий определяется управляющими структурами, указывающими повторение, выбор либо условное выполнение заданных операторов.6.1. Операторы повторения (циклы) Наиболее общая ситуация - повторение одного оператора или последовательности под управлением некоторого условия. Повторение продолжается, пока условие остается истинным.
Это выражается оператором цикла с условием продолжения (ЦиклПока). Его синтаксис ЦиклПока - "WHILE" Выражение Соответствующее ему действие Вычислить условие, которое принимает Форму выражения со значением или TRUE (истина) или FALSE (ложь).
2. Если получилось значение TRUE, выполнить последовательность операторов, а затем повторить шаг 1. если значение условия - FALSE, то закончить выполнение.
Условное выражение в операторе цикла имеет тип BOOLEAN (булев, логический). Этот тип будет обсуждаться в разделе, посвященном типам данных. Здесь же достаточно знать, что простое сравнение -выражение типа BOOLEAN. Пример цикла был дан во вводном примере, где повторение заканчивалось, когда сравниваемые переменные принимали одинаковые значения. Вот еще примеры операторов цикла с условием продолжения.
1. Пусть вначале q = 0 и r = х; вычислить, сколько раз можно вычесть у из х, т.е. вычислить частное q = х DIV у и остаток r = х MOD у, если х и у - натуральные числа.
2. Пусть z = 1 и i = k, умножить z на х к раз, т.е. вычислить z = х^k, если r и k- натуральные числа.
Используя циклы, важно помнить следующее:
1. При каждом повторении должно происходить приближение к цели, т.е. к условию окончания.
Очевидным следствием этого шляется необходимость того, чтобы повторяющиеся вычисления влияли каким-либо образом на условие. Следующие условия либо неверны, либо зависят от некоторых предварительных условии, которые должны выполняться до начала выполнения цикла.
i := i — 2 (* i должно быть четным и положительным *) 2. Если условие не выполнено в самом начале, то цикл эквивалентен пустому оператору, т.е. не производит никаких действий.
3. Для того чтобы выяснить, каков эффект выполнения цикла, нужно установить соотношение, сохраняющееся при повторениях и называемое инвариантом. В приведенном выше примере деления инвариант - это уравнение q*у + r = х, выполняющееся перед началом каждого повторения. В примере возведения в степень инвариант - z*x^i = х^k, который вместе с условием 1=0 дает желаемый результат z = x^k.
4. Следует избегать повторения идентичных вычислений (хотя терпение ЭВМ безгранично и она не будет жаловаться). Простое правило - избегать внутри повторяющихся операторов выражений, в которых ни одна переменная не меняет своего значения. Например, оператор следует записать более эффективно как Кроме оператора цикла с условием продолжения имеется оператор цикла с условием окончания (ЦиклДо). Этот цикл выполняется да тех пор, пока условие не станет истинным.
$ ЦиклДо » "REPEAT" ПослОператоров "UNTIL" Выражение.
Существенное отличие его от первого заключается в том, что условие окончания проверяется всякий раз после (а не до) выполнения последовательности операторов. В результате последовательность всегда выполняется по крайней мере один раз. Преимущество состоит в том, что условие может содержать переменные, значение которых не определено до выполнения цикла.
REPEAT
REPEAT
Два приведенных типа операторов цикла - наиболее распространенные и простые конструкции повторения. Но существуют и другие, в особенности оператор цикла с шагом, который будет описан позже в соответствующем месте. Безусловный цикл -обобщение циклов с условием окончания и условием продолжения, поскольку он позволяет задавать условие окончания в различных местах повторяющейся последовательности операторов. Его завершение осуществляется оператором, состоящим из одного ключевого слова EXIT (выход). Хотя безусловный цикл и удобен в некоторых случаях, мы рекомендуем использовать операторы с пред-и постусловием, поскольку они более ясно выделяют единственное условие завершения в синтаксически очевидной точке.$ БезусловныйЦикл = "LOOP" ПослОператоров "END".
6.2. Условные операторы Условный оператор имеет вид $ УсловныйОператор * "IF" Выражение $ "THEN" ПослОператоров S {"ELSIF" Выражение "THEN" ПослОператоров) S ["ELSE" ПослОператоров] "END".
Следующий пример иллюстрирует общий вид условного оператора.
Его смысл очевиден из значения слов (IF - если; THEN - то: ELSE - иначе: ELSIF - иначе, если... ).
Следует, однако, помнить, что выражения Rl... R3 вычисляются одно за другим и что, как только одно из них даст значение TRUE, будет выполнена сооткетствующая ему последовательность операторов, после чего условнный оператор считается завершенным. Последующие условия при этом не проверяются. Примеры:
IF ODD(k) THEN Z := z*x END Рассмотренные нами структуры уже позволяют разработать носколько простых завершенных программ, описанных далее. Первый пример — расширение нашего вводного примера, вычисляющего наибольший общий делитель (нод) двух натуральных чисел х и у. Расширение состоит во введении переменных u и v и операторов, которые позволяют вычислить наименьшее общее кратное (нок) для х и у. Величины нок и нод связаны соотношением нок(х,у)*нод(х,у) = х*у MODULE ноднок:
FROM InOut IMPORT ReadCard,WriteLn,WriteString,WriteCard;
VAR x,y,u,v: CARDINAL:
WriteString("x="); ReadCard(x); WriteLn;
WriteString("y="); ReadCard(y);
y:=y-x; v=v+u WriteCard(x,6); WriteCard((u + v) D1V 2),6); WriteLn Это еще один пример вложенных управляющих структур. Повторение, выраженное оператором с предусловием, включает в себя условную структуру, выраженную условным оператором IF, который в свою очередь включает две последовательности операторов, состоящих каждая из двух присваивании. Эта иерархическая структура выделена соответствующими сдвигами "внутренних" частей.
Другой пример, демонстрирующий иерархическую структуру, вычисляет 1-ю степень действительного (REAL) числа х, где i -натуральное число.
MODULE Степень;
FROM InOut IMPORT ReadCard,WriteStrlng,WriteLn;
FROM RealInOut IMPORT ReadReal,Done,WriteReal;
VAR i: CARDINAL; x,z: REAL;
WriteString("x="); ReadReal(x);
WriteStrlng("^i="); ReadCard(i);
WriteReal(z,16); WriteLn;
WriteString("x="); ReadReal(x) Здесь операторы, вычисляющие степень, охвачены еще одной конструкцией повторения: каждый раз после получения результата запрашивается новая пара х и 1. Внешнее повторение управляется логической переменной Done, указывающей, действительно ли введено число х. (Эта переменная импортируется и ее значение устанавливается процедурой чтения ReadReal).
Лобовое вычисление степени многократными умножениями - операция вполне корректная, но не очень экономная. Мы теперь дадим более сложное и более эффективное решение. Оно базируется на следующих соображениях: цель повторения - достигнуть значения i = 0. Это получается последовательным уменьшением i при сохранении инварианта z*x^i = х0^i0, где х0 и i0 обозначают начальные значения х и i. Более быстрый алгоритм должен, следовательно, основываться на уменьшении i несколько большими шагами. Приведенное здесь решение делит i пополам. Но это подокно, только если i четно. Следовательно, если i нечетно, его нужно уменьшить на 1. Конечно, каждое изменение i должно сопровождаться коррекцией z с целью сохранения инварианта.
Отметим одну деталь: уменьшение i на 1 не выражается явно, а осуществляется последующим делением на 2. Еще отметим, что Функция ODD(i) (нечетный) равна TRUE, если i нечетное число, и равна FALSE в противном случае. Идентификаторы х и z обозначают действительные значения в отличие от целых значений. Следовательно, они могут представлять и дроби.
MODULE Степень;
FROM InOut IMPORT ReadCard,WriteString,WriteLn;
FROM RealInOut IMPORT ReadReal,Done,WriteReal;
VAR i: CARDINAL; x,z: REAL;
WriteString("x="); ReadReal(x);
WriteStrlng("^i="); ReadCard(l);
IF ODD(i) THEN z := z*x END;
WriteReal(z,16); WriteLn;
WriteString("x="); ReadReal(x) Следующий пример программы имеет структуру, почти совпадающую с предыдущей программой. В этом примере вычисляется логарифм по основанию 2 вещественного числа х, значение которого лежит между 1 и 2. Инвариант совместно с условием завершения (b = 0) определяет желаемый результат сумма = log2(х).
FROM InOut IMPORT WriteString,WriteLn;
FROM RealInOut IMPORT ReadReal,Done,WriteReal;
VAR x,а,b,сумма: REAL;
WriteString('x="); ReadReal(x);
сумма := сумма + b; а := 0.5*а WriteReal(сумма, 16); WriteLn;
WriteSlring("x="): ReadReal(x) Обычно процедуры вычисления стандартных математических Функций не требуют детального программирования, поскольку они могут быть получены из набора программ, аналогичного модулям ввода и вывода. Такой набор не совсем удачно называется библиотекой программ. В следующем примере, опять демонстрирующем использование оператора REPEAT, применим для вычисления косинуса и показательной Функции (ехр) подпрограммы из библиотеки, называемой MathLib0. Мы получим таблицу значений для затухающих колебаний.
Обычно набор стандартных процедур включает Функции sin, cos, ехр, In (натуральный логарифм), sqrt (квадратный корень) и arctan (арктангенс).
MODULE Колебания:
FROM InOut IMPORT ReadCard,WriteString,WriteLn;
FROM RealInOut IMPORT ReadReal,WriteReal;
FROM MathLib0 IMPORT exp,cos;
CONST dx = 0.19634953; (*Pi/16*) VAR i,n: CARDINAL;
WriteString("n="): ReadCard(n);
WriteString("r="): ReadReal(r); WriteLn;
у := exp(-r*x)*cos(x);
WriteReal(x,15); WriteReal(y,15); WriteLn END Колебания.
7. ЭЛЕМЕНТАРНЫЕ ТИПЫ ДАННЫХ
Мы раньше уже говорили о том, что все переменные должны быть описаны. Это означает, что их имена указываются в заголовке программы. Кроме введения имени (что дает компилятору возможность обнаружить и отметить неправильно написанные идентификаторы) описания также связывают с каждой переменной тип данных. Этот тип данных представляет собой статическую информацию о переменной, в отличие, например, от ее значения. Эта информация тоже может способствовать обнаружению в ошибочной программе таких несоответствий, которые могут быть обнаружены простым просмотром программы без ее выполнения.Тип переменной определяет множество ее возможных значений и операций, применимых к ней. Каждая переменная имеет единственный тип, который можно узнать из ее описания. Каждая операция требует операндов определенного типа и выдает результат тоже определенного типа.
Следовательно, из текста программы видно, применима ли данная операция к данной переменной.
В программе можно объявлять новые типы данных. Такие сконструированные типы обычно образуются как композиция основных типов. Существует некоторое количество наиболее часто используемых элементарных типов, называемых стандартными типами, которые являются, основными в языке и не требуют описания. О них будет рассказано в этом разделе, хотя некоторые из них уже возникали в предшествующих примерах.
Фактически тип имеют не только переменные, но и константы, Функции, операнды и операции (их результаты). В случае констант тип обычно выводим по записи самой константы, другими словами, из ее явного описания.
Сначала мы расскажем о стандартных типах Модулы, а затем рассмотрим Форму описаний переменных и констант. Другие типы данных и описаний будут изложены в последующих разделах.
7.1. Тип INTEGER (целый) Этот тип представляет целые числа, и любому значению типа INTEGER соответствует некоторое целое число. Операции, применимые к типу INTEGER, включают основные арифметические операции DIV разделить MOD остаток от деления Деление нацело, обозначаемое ключевым словом DIV, дает целую часть частного от деления первого операнда на второй.
(*Судя по данному примеру, то, как автор понимает целую часть отрицательного числа (операция truncate), не согласуется с описанием этой операции в книге Д. Кнута "Искусство программирования для ЭВМ" (М.:Мир, 1976,т. 1,с.68) - "наибольшее целое, меньшее или равное х". - Прим. перев.*) Операция MOD обозначает остаток целочисленного деления. Если мы определим то будет выполняться соотношение будут работать правильно, если i имеет тип INTEGER. Но в случае типа CARDINAL произойдет ошибка, поскольку получится -1. Фактически выражение i >= 0 для i типа CARDINAL всегда истинно. Вот правильная форма этого фрагмента программы:
Еще одно преимущество использования типа CARDINAL состоит в том, что вычислительная машина, использующая N битов для представления целых, обеспечит для типа CARDINAL диапазон 0 — 2^N-1, а максимальное значение INTEGER будет 2^(N-1)—1. Более того, умножение и деление обычно несколько быстрее выполняются над операндами типа CARDINAL.
Модула не разрешает использовать операнды типа INTEGER и CARDINAL в одном и том же выражении (так называемые смешанные выражения). Причина кроется в том, что машинные команды, реализующие операции, различны для этих двух типов. Ситуация несколько облегчается присутствием так называемых = n:
WriteReal(sl,16); s2 := 0.0;
REPEAT
WriteReal(s2,16); WriteLn;WriteString("n="); ReadCard(n) Главная причина явного разделения действительных и целых чисел - их различное внутреннее представление. Значит, и арифметические операции для разных типов реализуются разными командами. Модула, следовательно, запрещает выражения со смешанными операндами.
Можно, однако, преобразовать целые числа в действительные (более точно: внутреннее представление целых может быть преобразовано в представление с плавающей точкой) и обратно явными Функциями преобразования, а именно FLOAT(c) TRUNC(x) Функция FLOAT(c) имеет тип REAL и представляет значение величины с типа CARDINAL; TRUNC(x) представляет целую часть действительной величины х и имеет тип CARDINAL. Программист должен иметь в виду, что различные реализации Модулы могут предоставлять другие либо дополнительные Функции преобразования.
7.4. Тип BOOLEAN (логический) Значение типа BOOLEAN (булев, логический) имеет два логических, истинностных значения, обозначаемых стандартными идентификаторами TRUE и FALSE. Булевы переменные обычно обозначаются идентификаторами, имеющими смысл прилагательных, причем значение TRUE подразумевает наличие соответствующего свойства, a FALSE - его отсутствие. Имеется набор логических операций, которые вместе с переменными типа BOOLEAN образуют выражения этого типа. Логическими операциями являются AND (и) (обозначаемая также &), OR (или) и NOT (не) (обозначаемая также ~). Их смысл таков:
р OR q = "или р, или q, или оба равны TRUE" Точное определение операций, однако, немного другое, хотя результат тот же:
Эти определения подразумевают, - что второй операнд может и не вычисляться, если результат уже известен после вычисления первого операнда. Замечательное свойство этого определения заключается в том, что результат выражения может иметь смысл, даже если второй операнд не определен. Как следствие этого порядок операторов может оказаться существенным.
Иногда можно упростить логическое выражение применением простых правил преобразования. Особенно полезны законы де Моргана, задающие эквивалентность Сравнения выдают результат типа BOOLEAN, т.е. TRUE, если сравнение справедливо, и FALSE - если нет. Например, Такие сравнения синтаксически классифицируются как выражения, а два сравниваемых операнда - это простые выражения (см. раздел, посвященный выражениям и операторам).
Результат сравнения имеет тип BOOLEAN и может быть использован в управляющих структурах, таких, как условный оператор или операторы цикла. Знак # означает "не равно" (его синоним :
$ Выражение = ПростоеВыражение Следует отметить, что, подобно арифметическим операциям, среди логических операций тоже имеется отношение старшинства. NOT имеет наивысший приоритет, затем следует AND (называемая также логическим умножением), а затем OR (логическое сложение) и, наконец, операции сравнения. Как и в случае арифметических выражений, можно свободно применять скобки, чтобы явно выразить связь между операциями. Вот примеры логических выражений:
Заметим, что такая конструкция, как х < у AND z < w, запрещена.
Значения типа BOOLEAN можно сравнивать, причем не только на равенство. В частности, Следовательно, логическая импликация "из р следует q* выражается или как (NOT р) OR q, или как p "); n := 0; Read(ch);
n := n + 1; a[n] := ch; Write(ch); Read(ch) WriteLn; Переставить(n) END Перестановка.
Результаты, полученные в случае использования трех литер, таковы:
ABC ВАС СВА ВСА АСВ CAB
Каждая цепочка рекурсивных вызовов должна на каком-то шаге завершиться, и, следовательно, любая рекурсивная процедура должна содержать рекурсивный вызов внутри условного оператора. В приведенном примере рекурсия завершается, когда число объектов, которые нужно переставить, становится равным единице.Число возможных перестановок легко вывести из рекурсивного определения алгоритма.
Мы выразим это число как функцию np(n). Пусть задано п элементов, тогда существует n вариантов выбора элемента а[n], и при каждом Фиксированном а[n] мы получаем np(n—1) перестановок. Следовательно, общее число np(n) n*np(n—1). Очевидно, что np(1) = 1. Вычисление величины np можно теперь выразить как вызов рекурсивной процедуры.
PROCEDURE np(n: CARDINAL): CARDINAL;
Этот вариант вычислит результат более эффективно, чем рекурсивная версия. Причина в том, что каждый вызов требует некоторых дополнительных "административных" операций, на выполнение которых тоже расходуется время. Команды, обеспечивающие повторение, тратят меньше времени. И хотя эта разница не может быть слишком уж существенной, рекомендуется все же использовать циклические конструкции вместо рекурсивных всегда, когда это можно сделать без особых усилий. В принципе, это возможно всегда, однако циклическая версия в состоянии настолько усложнить алгоритм и затуманить его смысл, что минусов окажется намного больше, чем плюсов. Например, циклическая форма процедуры перестановки намного сложнее и запутаннее приведенной рекурсивной. Для иллюстрации полезности рекурсии дадим два дополнительных примера. Приводимые далее алгоритмы обычно возникают в задачах, решение в которых легко находится и объясняется с применением рекурсии.
Первый пример принадлежит классу алгоритмов, работающих с данными, структура которых тоже определяется рекурсивно. Характерной задачей такого рода является преобразование простых выражений в соответствующую постфиксную форму или польскую инверсную запись (Полиз), т.е. в Форму, при которой знак операции следует за операндами.
Выражение в данном случав будет определяться в виде РБНФ так:
Выражение - Слагаемое {("+"|"-") Слагаемое}.
Слагаемое - Множитель {("*"|"/") Множитель}.
Множитель - Буква | "(" Выражение ")" | "[" Выражение "]".
Обозначая слагаемые как Т0,Т1, а множители - как F0,F1, запишем правила преобразования:
Следующая далее программа Полиз вводит выражения с терминала и проверяет входную информацию на синтаксическую правильность. В случае правильного ввода информация в неизменном виде повторяется на выходе, если же ввод неверен, то такой выдачи информации не происходит. Процедура вывода Write импортируется не из стандартного модуля Terminal, а из модуля TextWindows (текстовые окна), управляющего размещением на экране так называемых окон и выдачей на них текста. Мы можем считать, что одно окно используется для повторения принятой входной информации, а второе - для выдачи преобразованных выражений, т.е. основной выходной информации.
Наша программа в точности отражает структуру синтаксиса плодных выражений.
Поскольку синтаксис рекурсивен, рекурсивна и сама программа. Такое точное отражение - лучшая гарантия правильности программы. Заметим также, что, аналогично, итерация в синтаксисе, выражаемая Фигурными скобками, ведет к итерации в программе, выражаемой оператором с условием повторения. В этой программе ни одна из процедур не вызывает себя непосредственно.
Здесь рекурсия имеет непрямой характер и возникает, благодаря обращению в процедуре Выражение к Слагаемое, которая в свою очередь обращается к Множитель, а уже Множитель обращается к Выражение. Очевидно, что непрямая рекурсия менее наглядна и заметна, чем прямая.
Этот пример иллюстрирует также применение локальных процедур. Примечателен тот Факт, что процедура Множитель описана локальной для процедуры Слагаемое, а та в свою очередь локальна в процедуре Выражение. Это сделано в соответствии с тем правилом, что объекты следует описывать преимущественно локальными в той области, где они используются.
Это правило иногда может оказаться не только желательным, но и необходимым, как иллюстрируется переменными ОпСлож (локальная в Слагаемое) и ОпУмнож (локальная в Множитель). Если бы эти переменные были описаны как глобальные, то программа не смогла бы работать. Для объяснения этого мы должны вспомнить то правило, что локальные переменные существуют (и под них выделяется память) лишь в тот промежуток времени, когда процедура активна. Непосредственным следствием этого в случае рекурсивного вызова является создание нового экземпляра локальной переменной. Таким образом, существует столько экземпляров переменной, сколько уровней рекурсии, из чего, в частности, заключаем, что программист должен заботиться о том, чтобы глубина рекурсии не оказалась чересчур большой.
FROM Terminal IMPORT Read:
FROM TextWindows IMPORT Window,OpenTextWindow,Write,WriteLn,CloseTextWindow;
VAR ch: CHAR; w0,wl: Window;
PROCEDURE Выражение;
PROCEDURE Слагаемое;
VAR ОпУмнож: CHAR;
PROCEDURE Множитель;
Write(w0,ch); Read(ch); Выражение;
Write(w0,ch); Read(ch); Выражение;
WHILE (ch < "a") OR (ch > "z") DO Read(ch) END;
Write(w0,ch); Read(ch) BEGIN (*Слагаемое*) Множитель;
WHILE (ch = "*") OR (ch = "/") DO Write(w0,ch); ОпУмнож := ch; Read(ch); Множитель;
Write(wl,ОпУмнож) END Слагаемое;
BEGIN (*Выражение*) Слагаемое;
WHILE (ch = "+") OR (ch = "-") DO Write(w0,ch); ОпСлож := ch; Read(ch);
Слагаемое; Write(w1,ОпСлож) END Выражение;
BEGIN OpenTextWindow(w0,50,50,300,400,"ввод");
(*Открыть текстовое окно*) OpenTextWindow(w1,400,100,300,400,"вывод");
Write(w0,">"); Read(ch);
WriteLn(w0); WriteLn(w1);
Write(w0,">"); Read(ch) CloseTextWindow(w1); CloseTextWindow(w0) Образец данных, обрабатываемых и генерируемых программой, приведен ниже.
>a*(b/[c-d]) abcd-/* Следующий пример программы демонстрирует рекурсию в применении к классу задач поиска решения методом проб и ошибок. В этом методе используется последовательное построение частичных "решений" и проверка их допустимости. Каждое новое частичное решение получается дополнением некоторого другого. Если полученное частичное решение неудовлетвооительно, то происходит возврат к частичному решению, из которого оно было получено, и попытка дополнить его другим способом. Поэтому такой подход называется еще поиском с возвратами. Выбор решения задачи осуществляется среди множества частичных решений. Для записи таких алгоритмов очень удобно применение рекурсии. Наш конкретный пример предназначен для поиска всех возможных размещений 8 Ферзей на шахматной доске так, чтобы ни один из них не находился под ударом остальных, т.е. на каждой вертикали, горизонтали и диагонали должно находиться не более одного Ферзя. Метод состоит в помещении на j-ю вертикаль (начиная с j = 8) еще одного Ферзя; при этом предполагается, что все вертикали справа уже содержат правильно размещенные Фигуры. Если на вертикали j нет допустимого места, то нужно пересмотреть размещение Ферзя на (j+1)-й вертикали. Информация, необходимая для заключения о том, доступна ли данная клетка, содержится в трех глобальных переменных типа массив: Гориз, Диагон1, Диагон2 так, что Гopиз[i]&Диaгoн1[i+j]&Диaгoн2[N+i-j] = "клетка на горизонтали 1 и вертикали J свободна" Программа использует набор процедур, импортируемых из модуля, называемого LineDrawing (вычерчивание прямых), чтобы представить выходные данные в легко воспринимаемой графической Форме.. В частности, вызов процедуры area(c,x,y,w,h) (* область(...) *) закрасит прямоугольник с координатами левого нижнего угла х и у, шириной w и высотой h, "цветом" с. Очевидно, что эта процедура может быть использована как для прочерчивания линий между полями шахматной доски, так и для закрашивания отдельных клеток. Рекурсия возникает непосредственно в процедуре НоваяВертикаль. Вспомогательные процедуры ПоставитьФерзя и УдалитьФерзя могли бы быть, в принципе, описаны локальными внутри процедуры НоваяВертикаль. Однако, поскольку существует только одна доска (представленная переменными ГОРИЗ, Диагон1, Диагон2), эти процедуры соответственно считаются приписанными глобальным данным, а следовательно, не должны быть локальными в (каждйй активации) НоваяВертикаль.
FROM LineDrawing IMPORT width,height,area,clear;
CONST N = 8; (* число вертикалей и горизонталей *) VAR x0,y0: CARDINAL; (* начальные координаты на доске *) ГОРИЗ: ARRAY [1..N] OF BOOLEAN;
(* Гopиз[i] = "на i-й горизонтали нет Ферзей" *) Диагон1: ARRAY [2..2*N] OF BOOLEAN;
(* Диaгoн1[i] = "на i-й нисходящей диагонали нет Ферзей *) Диагон2: ARRAY [1..2*N-1] OF BOOLEAN;
(* Диaгoн2[i] - "на i-й восходящей диагонали нет Ферзей *) PROCEDURE НарисоватьДоску;
VAR i,j,x,y: CARDINAL;
BEGIN clear(l):
FOR i := 1 TO N DO Гopиз[i] :- TRUE END;
FOR i := 2 TO 2*N DO Диaгoнl[i] := TRUE END;
FOR i := 1 TO 2*N-1 DO Диaгoн2[i] := TRUE END;
x0 := (Width-L) DIV 2; x := x0;
y0 := (height-L) DIV 2; у := y0;
area(3,x0,y0,L,L);
FOR i := 0 TO N DO area(0,x0,y,L,2); у := у + M;
area(0,x,y0,2,L); х : = х + М;
END END НарисоватьДоску;
PROCEDURE Пауза;
VAR n: CARDINAL;
BEGIN n := 50000:
REPEAT n := n-1 UNTIL n = END пауза;
PROCEDURE ПоставитьФерзя(1,J: CARD INAL):
BEGIN Гopиз[i] := FALSE;
Диaгoнl[i+j] := FALSE; Диaгoн2[N+i-j] := FALSE;
area(0,x0+2+(j-i )*M,y0+2+( i-j)*M,M-2,M-2) END ПоставитьФерзя;
PROCEDURE УбратьФерзя i, j: CARDINAL);
BEGIN Гopиз[i] := TRUE;
Диaгoн'l[i+j] := TRUE; Диaгoн2[N+i-j] := TRUE:
area(3,x0+2+(j-1 )*M,y0+2+(i-1)*М,М-2,м-2) END УбратьФерзя;
PROCEDURE НоваяВертикаль(j: CARDINAL);
VAR i: CARDINAL;
BEGIN i := N;
REPEAT
IF Гориз[il&Диагон1[i+j]&Диагон2[N+i-j] THEN ПоставитьФерзя(i,j);IF j > 1 THEN НоваяВертикаль(j-1) ELSE Пауза END;
УбратьФерзя(i,j) END;
i := i- UNTIL i = END НоваяВертикаль;
BEGIN НарисоватьДоску; НоваяВертикаль(N); clear(3) END Ферзи.
Вывод программы ферзи.
15. ОПИСАНИЯ ТИПОВ Каждое описание переменной задает тип этой переменной как ее неизменное свойство.
Типом может быть один из стандартных, примитивных типов либо же тип может быть описан в самой программе. Описания типов имеют Форму $ ОписаниеТипа = Идентификатор "=" Тип.
Им предшествует ключевое слово TYPE. Типы делятся на неструктурированные и структурированные. По существу каждый тип определяет множество значений, которые может принимать переменная данного типа. Значение неструктурированного типа -неделимый объект, в то время как величина структурированного типа имеет компоненты (элементы). Например, тип CARDINAL -неструктурированный: его значение неделимо. Не имеет смысла ссылаться, скажем, к третьему биту значения 13; тот Факт, что число может "иметь третий бит" или вторую цифру характеристика его (внутреннего) представления, которое намеренно оставлено скрытым.
В последующих разделах мы опишем способы описания неструктурированных и структурированных типов. Кроме стандартных типов, с которыми мы встречались до сих пор, как неструктурированные типы описываются перечислимый тип и тип диапазона. Из всех структурированных типов мы до сих пор имели дело только с массивами. Кроме этого существует еще тип запись и тип множество. Возможность введения структур, которые динамически изменяются во время исполнения программы, основана на понятии указателей. Эта возможность будет обсуждаться в отдельном разделе.
$ Тип = ПростойТип|ТипМассив|ТипЗапись| $ ТипМножество|ТипУказатель|ТипПроцедура.
$ ПростойТип = КвалИдент|Перечисление|ТипДиапазон.
Прежде чем приступить к рассмотрению различных способов гадания типов, сделаем одно общее замечание. Если тип Т описан так:
TYPE T = НекоторыйТип то эти два описания можно слить в одно:
VAR t: НекоторыйТип однако в этом случае тип переменной t не будет иметь явного имени.
Понятие типа играет важную роль в программировании, поскольку типы делят все множество переменных в программе на непересекающиеся классы. Ошибочные присваивания между элементами разных классов могут, следовательно, быть обнаружены простым просмотром текста программы без ее выполнения. Пусть имеются такие описания:
в этом случае присваивание b := i невозможно, поскольку типы переменных Ь и i несовместимы. Два типа называются совместимыми. если они описаны как равные либо удовлетворяют определенным правилам совместимости, которые будут обсуждаться далее.
Важным исключением из этих правил являются типы INTEGER и CARDINAL. По этой причине присваивание i := с допустимо.
Для демонстрации правил совместимости рассмотрим следующие описания:
TYPE А = ARRAY [0..99] OF CHAR;
В = ARRAY [0..99] OF CHAR;
В этом случае переменные типа А можно присваивать переменным типа С (и наоборот), но не переменным типа В. Однако допустимо присваивание b[i] := a[i], поскольку обе переменные имеют один тип CHAR.
16. ПЕРЕЧИСЛИМЫЕ ТИПЫ Можно описать новый неструктурированный тип перечисление или перечислимый тип, явно выписав все множество значений, которые принадлежат этому типу. Объявление типа Т = (cl,c2,...,сn) вводит новый, неструктурированный тип Т, для значений которого используются n идентификаторов-констант c1, c2.....cN. Только эти идентификаторы входят в значения данного типа. Синтаксис описания перечислимого типа следующий:
$ Перечисление = "(" СписИдент ")".
Операции над величинами такого типа должны описываться программистом как процедуры. Кроме операций присваивания возможны еще сравнения этих величин. Величины упорядочены: c1 -наименьшая, сп - наибольшая. Вот примеры перечислимых типов:
TYPE цвет - (красный,оранжевый,желтый,зеленый, голубой,синий,Фиолетовый);
ДеньНедели = (пн,вт,ср,чт,пт,сб,вс);
месяц = (янв.Февр,март,апр,май,июнь,июль, авг, сект,окт,нояб,дек) Порядковый номер константы ci можно получить, воспользовавшись стандартной Функцией ORD(ci): ее значение i-1. Например, ORD(красный) = 0, ORD(чт) = 3, ORD(дек) = 11.
Стандартный тип BOOLEAN - тоже перечислимый тип. Можно считать, что он имеет описание BOOLEAN = (FALSE,TRUE) Стандартные процедуры INC(x) и DEC(x) служат для присваивания переменной х соответственно следующего и предыдущего значения по сравнению с ее текущим значением.
17. ТИП ДИАПАЗОН Если известно (или предполагается), что переменная будет принимать значения только внутри некоторого определенного диапазона величин, то этот Факт можно выразить, описав так называемый тип диапазон. Допустим, например, что переменная ш принимает значения только из диапазона от 1 до N включительно. Запишем следующие описания:
(которые можно сократить до VAR i: [1..N] ).
Каждый диапазон имеет некоторый базовый тип - тип его значений. Все операции, определенные для базового типа, применимы также и к его диапазону. Единственное ограничение касается величин, которые могут быть присвоены переменным типа диапазон.
Синтаксис описания диапазона:
$ ТипДиапазон = [КвалИдент] $ "[" КонстВыражение ".." КонстВыражение "]".
где выражения обозначают границы диапазона и должны содержать только константы.
Приведем примеры описаний диапазонов.
ЛатБуква = ["А".."Z"] Цифра - ["0"..."9"] Индекс = INTEGER [1.. 100] РабочийДень = [пн..пт] Идентификатор, который может стоять перед квадратными скобками, указывает базовый тип диапазона. Этот идентификатор опускается, если базовый тип очевиден по виду границ. Но для целых это не всегда возможно. Если же идентификатор опущен в случае целых констант, то соблюдается следующее правило: если нижняя граница диапазона отрицательна, то базовым типом считается INTEGER, иначе CARDINAL. Отметим еще, что нельзя определять диапазон для типа REAL.
Тип диапазон имеет то преимущество, что дает дополнительную гарантию против некорректных присваиваний и может, следовательно, помочь в обнаружении ошибок. Следует, однако, указать, что проверки принадлежности величины диапазону происходят во время выполнения программы, поскольку такие ошибки не могут быть обнаружены лишь проверкой текста программы.
18. ТИП МНОЖЕСТВО Каждый тип данных определяет некоторое множество значений. В том случае, когда тип S является типом множество (множественный тип), то область его значений - набор всевозможных множеств, составленных из элементов некоторого определенного базового типа В. Например, если базовый тип В - диапазон то величинами типа S будут множества {}, {0},{1},{0,1}. Если базовый тип принимает п различных значений, то тип множество для него будет иметь 2^N различных значений.
Обозначение О соответствует пустому множеству. В предшествующем разделе нам уже встречался стандартный множественный тип BITSET. Он определяется как BITSET = SET OF [0..W-1] где W - длина слова используемой ЭВМ. Операции объединения, пересечения и вычитания множеств, а также операция IN (определения принадлежности множеству) применимы не только к типу BITSET, а ко всем множественным типам. Для указания типа константного множества перед списком элементов, заключенным в Фигурные скобки, должен стоять соответствующий идентификатор типа. Он может быть опущен в случае стандартного типа BITSET. Синтаксис описания множественного типа:
$ ТипМножество = "SET" "OF" ПростойТип.
Синтаксис представления множеств как операндов выражений был приведен в разделе, посвященном стандартному типу BITSET. Напомним, что операнды множественного типа образуются посредством заключения списка элементов в Фигурные скобки, перед которыми стоит идентификатор, указывающий тип множества (в случае типа BITSET он может быть опущен).
Базовый тип множества должен быть или перечислением, или диапазоном. Кроме того, реализации Модулы ограничивают число элементов базового типа для множества длиной слова или некоторым небольшим числом, кратным ей. Обычно это число равно 16 или 32.
Хотя эти правила и ограничивают общность понятия "множество", тем не менее тип множество - мощное средство, позволяющее выразить операции над отдельными битами операнда на высоком уровне абстракции, основанном на хорошо знакомом математическом понятии. Для работы с множествами предлагаются две стандартные процедуры (они разворачиваются компилятором непосредственно в последовательность команд, без использования вызова); здесь s должна быть переменной, ах- выражением базового типа для s.
INCL(s,x) включить элемент х в множество s EXCL(s,x) исключить элемент х из множества s В заключение этой главы опишем одно приложение типа BITSET, не отражающее непосредственно понятие множества, но ставшее тем не менее очень важным и полезным. Оно касается представления информации о растре сканирующего дисплея. Эта информация называется битовой картой экрана, поскольку каждая отдельная точка экрана представляется отдельным битом в памяти ЭВМ, причем 1 обозначает черный, 0 - белый цвет (или наоборот). Такое битовое представление удобно описывается как массив элементов типа BITSET. Предположим теперь, что мы должны представить в машине с длиной слова W экран дисплея с М строками, каждая из которых содержит N точек. (Мы также считаем, что N кратно W). Тогда соответствующее описание будет выглядеть так:
VAR КартаБитов: ARRAY [0..M*(N DIV W)-l] OF BITSET Закрашивание точки (элемента изображения) с координатами х, у можно теперь выразить следующей процедурой:
PROCEDURE ЗакраситьЧерным(х,у: CARDINAL);
INCL(KapтaБитов[(N*y + х) DIV W],x MOD W) END ЗакраситьЧерным Процедура ЗакраситьБелым просто использовала бы EXCL вместо INCL. Мы предполагаем, что число N кратно W и что 0 0 означает, что таблица полна *) PROCEDURE Инициализировать(VAR t: Таблица);
PROCEDURE Записать(t: Таблица;
VAR x: ARRAY OF CHAR; n: INTEGER);
(* занести пару х,п в таблицу строка х должна заканчиваться пробелом *) PROCEDURE Распечатать(t: Таблица) END РаботаСТаблицей.
MODULE XREF;
FROM InOut IMPORT Done,EOL,OpenInput,OpenOutput,Read,Write,WriteCard, WrlteString,CloseInput,CloseOutput;
FROM РаботаСТаблицей IMPORT ДлинаСлова,Таблица,переполнение, Инициализировать,Записать,Распечатать;
TYPE Alfa = ARRAY [0..9] OF CHAR;
CONST N =45; (* число ключевых слов *) VAR ch: CHAR.
i,k,l,m,r,номстp: CARDINAL;
T: Таблица;
идент: ARRAY [0..ДлинаСлова-1] OF CHAR;
ключ: ARRAY [1..N] OF Alfa;
PROCEDURE Копировать;
BEGIN Write(ch); Read(ch);
END Копировать;
PROCEDURE Заголовок;
BEGIN номстр := номстр + 1;
WriteCard(номстр,5); Write(" ") END Заголовок;
BEGIN Инициализировать(T);
ключ[1]:= "AND"; ключ[2]:= "ARRAY";
ключ[3]:="BEGIN"; ключ[4]:= "BITSET";
ключ[5]:= "BOOLEAN"; ключ[6]:="BY";
ключ[7]:= "CASE"; ключ[8]:= "CARDINAL";
ключ[9]:="CHAR"; ключ[10]:="CONST";
ключ[ll]:-"DIV"; ключ[12]: ="DO";
ключ[13]:="ELSE"; ключ[14]:="ELSIF";
ключ[17]:="EXPORT"; ключ[18]:="FALSE";
ключ[23]:="IN"; ключ[24]:="INTEGER";
ключ[25]:="LOOР"; ключ[26]:="MOD";
ключ[27]:="MODULE"; ключ[28]:="NOT";
ключ[31]:="POINTER"; ключ[32]:="PROCEDURE";
ключ[33]:="QUALIFIED"; ключ[34]:="RECORD";
ключ[351:="REPEAT"; ключ[36]:="RETURN";
ключ[4]:="ТУРЕ"; ключ[42]:="UNTIL";
ключ[43]:="VAR"; ключ[44]:="WHILE";
ключ[45]:="WITH";
OpenInput("MOD");
IF NOT Done THEN HALT END;
OpenOutput("XREF");
номстр := 0: Read(ch);
IF Done THEN Заголовок;
REPEAT
IF (CAP(ch) >= "A") & (CAP(ch) "9") & (CAP(ch) < "A") OR (CAP(ch) > "Z");l := 1; r := N; идент[k] := " ";
REPEAT m := (i+r) DIV 2; i := 0;(*двоичный поиск*) WHILE (идент[i]=ключ[m,i])&(идент[i]>" ") DO END;
IF идент[i] = ключ[m,i] THEN 1 := m + 1 END;
UNTIL l > r;
IF l = r + l THEN Записать(t,идент,номстр) END ELSIF (ch >= "0") & (ch 0 THEN WriteString("Переполнение таблицы");
WriteCard(переполнение,6); Write(EOL) Write(35C); Распечатать(Т); CloseInput; CloseOutput Теперь опишем модуль РаботаСТаблицей. Как видно из раздела описаний, он экспортирует приватный тип Таблица и операции над ним Записать и Распечатать. Обратите внимание, что структура таблиц, а значит, и алгоритмы поиска и доступа остаются скрытыми. Два наиболее вероятных способа организации таблиц -это двоичное дерево и хеш-таблица. Нами выбран первый способ. Таким образом, этот пример является дальнейшей иллюстрацией использования указателей и динамических структур данных. Модуль содержит процедуру поиска и вставки элемента в дерево, а также процедуру, которая обходит дерево и печатает содержимое таблицы (см. также раздел, посвященный динамическим структурам данных). Каждый узел дерева - запись, содержащая следующие компоненты: ключ, ссылки на левого и правого сыновей, заголовок списка, содержащего номера строк.
IMPLEMENTATION MODULE РаботаСТаблицей;
FROM InOut IMPORT Write,WriteLn,WriteInt;
FROM Storage IMPORT Allocate;
CONST ДлинаТаблицы = 3000;
TYPE УкДерево = POINTER TO Слово;
УкСписок = POINTER TO Элемент;
Элемент = RECORD номер: INTEGER;
следующ: УкСписок Слово = RECORD ключ: CARDINAL; (*индекс таблицы*) первый: УкСписок; (*голова списка*) левый,правый: УкДерево END;
Таблица = УкДерево;
VAR идент: ARRAY [0..ДлинаСлова] OF CHAR;
ТаблИндекс: CARDINAL;
ЛитТаб: ARRAY [0..ДлинаТаблицы-1 ] OF CHAR;
PROCEDURE Инициализировать(VAR t: Таблица);
BEGIN Allocate(t,SIZE(Слово)); t^.правый := NIL END Инициализировать;
PROCEDURE Поиск(р: УкДерево): УкДерево, (*поиск узла с именем, равным идент*) TYPE Отношение = (меньше,равно,больше);
VAR q: УкДерево;
r: Отношение; i: CARDINAL;
PROCEDURE сравн(k: CARDINAL): Отношение;
(*сравнение идент с ЛитТаб[k]*) VAR i: CARDINAL;
R: Отношение; х,у: CHAR;
BEGIN i := 0; R := равно;
LOOP x := идент[i]; у := ЛитТаб[k];
IF САР(х) # САР(у) THEN EXIT END;
IF x у THEN R := больше END;
END;
IF CAP(x)>CAP(y) THEN RETURN больше ELSE RETURN меньше END END сравн;
BEGIN q := р^. правый; r := больше;
WHILE q # NIL DO p := q;
r := сравн(р^.ключ);
IF r = равно THEN RETURN p ELSIF r = меньше THEN q,:= р^. левый ELSE q : = р^.правый END END;
Allocate(t,SIZE(Слово)); (*узел не найден, вставка*) IF q # NIL THEN WITH q^ DO ключ := ТаблИндекс; первый := NIL;
левый := NIL; правый := NIL END;
IF r = меньше THEN p^.левый := q ELSE р^.правый := q END;
i := 0;
(*скопировать идентификатор в таблицу ЛитТаб*) WHILE идент[i] > " " DO IF ТаблИндекс = ДлинаТаблицы THEN ЛитТаб [ТаблИндекс] := " "; идент[i] := " ";
переполнение : = ELSE ЛитТаб[ТаблИндекс] := идент[i];
ТаблИндекс := ТаблИндекс + 1; i := i + END END;
ЛитТаб [ТаблИндекс] := " ";
ТаблИндекс := ТаблИндекс + 1;
END;
RETURN q END Поиск;
PROCEDURE Записать(t: Таблица;
VAR x: ARRAY OF CHAR; n: INTEGER);
VAR p: УкДерево; q: УкСписок; i: CARDINAL;
BEGIN i := 0;
REPEAT идент[i] := x[i]; i := i + UNTIL (идент[i-1] = " ") OR (i = ДлинаСлова);
p := Поиск(t);
IF p = NIL THEN переполнение := ELSE Allocate(q,SIZE(Элемент));
IF q = NIL THEN переполнение := 3 ELSE q^.номер := n; q^следующ := p^.первый;
p^.первый := q END END END Записать;
PROCEDURE Распечатать(t: Таблица);
PROCEDURE ПечЭлем(р: УкДерево);
CONS L = 6;
N = (ШиринаСтроки-ДлинаСлова) DIV L;
VAR ch: CHAR;
i,k: CARDINAL; q: УкСписок;
BEGIN i :- ДлинаСлова + 1; k := р^.ключ;
REPEAT ch := ЛитТаб[k];
i:=i-1; k:=k+1; Write(ch) UNTIL ch 0 DO Write(" "); i := i- END;
q := р^.первый; i := N;
WHILE q # NIL DO IF i = 0 THEN WriteLn; i := ДлинаСлова + 1;
REPEAT Write(" "); i := i - UNTIL i = 0;
i := N END;
WriteInt(q^.номep,L);
q := q^.следуют;
END;
WriteLn END ПечЭлем;
PROCEDURE ОбходДерева(р: УкДерево);
BEGIN IF p # NIL THEN ОбходДерева(p^.левый);
ПечЭлем(р);
ОбходДерева(р^.правый);
END END ОбходДерева;
BEGIN WriteLn;
ОбходДерева(t^.правый) END Распечатать;
BEGIN ТаблИндекс := 0; идент[ДлинаСлова] := " ";
переполнение := END РаботаСТаблицей.
26. ЛОКАЛЬНЫЕ МОДУЛИ Модули, которые нам встречались до сих пор, следовало рассматривать как Фрагменты текста, стоящие "бок о бок". Но модули могут быть и текстуально вложенными. Непосредственное следствие этого - то, что вложенные модули не компилируются раздельно. Они называются локальными модулями и их единственная цель - скрыть детали описания внутренних объектов.
Каждый модуль задает область видимости идентификаторов. Подразумевается, что объекты, описанные в области (модуле), видимы только в ней. Отметим, что процедуры тоже образуют область видимости. Однако здесь имеются различия.
1. Для модулей диапазон видимости объекта может быть расширен включением его идентификатора в экспортный список модуля. Тогда идентификатор становится видимым в окружающей модуль области видимости. Для процедур это невозможно.
2. Идентификатор, видимый в области, окружающей процедуру, видим также и. внутри процедуры. Для модуля такое утверждение неверно, если только идентификатор не включен в импортный список этого модуля.
Правила видимости для модулей иллюстрируются следующими примерами:
VAR а,Ь: CARDINAL;
IMPORT a; EXPORT w,x;
VAR u,v,w: CARDINAL;
IMPORT u; EXPORT x,y;
VAR x,y,z: CARDINAL;
(* здесь видимы u,x,y,z *) (* здесь видимы a,u,v,w,x,y *) (* здесь видимы a,b,w,x *) Если идентификатор должен пересечь несколько границ областей, то он должен быть включен ровно в столько же импортных списков (или же модуль, содержащий идентификатор, должен импортироваться целиком). Расширение области видимости из внутреннего модуля наружу достигается экспортом, снаружи вовнутрь - импортом. Правила полностью симметричны.
Рассмотрим теперь следующую структуру модулей N1, N2 и N3, вложенных в модуль М:
VAR a: CARDINAL;
(* здесь видим только b *) (* здесь видим только с *) IMPORT b,с; (* здесь видимы b и с *) N3 импортирует идентификаторы b и с, описанные соответственно в модулях N1 и N2. Эти идентификаторы экспортированы в окружение М из локальных модулей. Если заменить модуль М "внешней средой" (в которой нельзя описывать локальные объекты), то модули N1, N2 и N превратятся в глобальные модули, обсуждавшиеся в предшествующем разделе (*небольшое различие имеется,поскольку экспорт из глобальных модулей может быть только квалифицируемым. Кроме того, следует иметь в виду, что у глобальных модулей существуют раздел описаний и раздел реализации. - Прим. перев.*). Фактически правила видимости для локальных и глобальных модулей совпадают. Глобальный модуль, т.е. единицу компиляции, можно назвать локальным во внешней среде.
Предположим теперь, что переменная с, экспортируемая из N2, тоже называется b. Это привело бы к коллизии имен, потому что идентификатор Ь уже известен в М (Ь экспортирован из N1). Эту проблему можно обойти, применяя квалифицируемый экспорт точно так же, как для глобальных модулей. Теперь объекты с именем Ь, принадлежащие N1 и N2, могут именоваться как N1.b и N2.b соответственно.
Квалифицируемый экспорт обязателен для глобальных модулей, потому что разработчик глобального модуля не знает, существует ли выбранный им идентификатор в окружающей программной среде. Для локальных модулей квалифицируемый экспорт - скорее исключение, чем правило, поскольку программист знает их окружение и, следовательно, может выбрать идентификаторы так, чтобы избежать конфликта имен.
Последнее замечание прямо касается различий модулей и процедур. И те и другие образуют некоторую область видимости (вложенную в их окружение). Но если единственной Функцией модуля является создание новой области видимости, то процедура, кроме того, образует новую область существования ее локальных объектов: они исчезают при завершении процедуры.
В случае модуля его локальные объекты возникают в' момент создания окружающей его области существования и продолжают существовать, пока эта область не исчезнет. Однако случай модулей, локальных в процедуре, на практике встречается редко (если только не рассматривать всю программу как процедуру). Синтаксис локального модуля подобен синтаксису программного модуля:
$ "MODULE" Идентификатор [Приоритет] ";" $ {Импорт} [Экспорт] Блок Идентификатор.
$ Приоритет = "[" КонстВыражение "]".
(Назначение параметра "приоритет" будет обсуждаться в разделе, посвященном параллельному исполнению.) Следующий далее пример программы демонстрирует применение локального модуля.
Цель программы - читать текст, являющийся описанием синтаксиса с помощью РБНФ, проверять правильность записи правил РБНФ и генерировать таблицу перекрестных ссылок для вводимого текста. Должны быть напечатаны две таблицы: в одной будут содержаться терминалы, т.е. строки, заключенные в кавычки, и идентификаторы, состоящие только из прописных букв, а в другой нетерминалы, т.е. остальные идентификаторы.
Приведенная спецификация предполагает разбиение программы, подобное тому, что было у программы XREF в предшествующем разделе. Мы проведем дальнейшее разбиение задачи просмотра текста на чтение отдельных символов РБНФ, т.е. лексический анализ текста, и проверку правильности правил РБНФ, т.е. синтаксический анализ. Программа тогда будет состоять из главного модуля, называемого РБНФ, который импортирует РБНФСканер (производящий лексический анализ) и модуль РаботаСТаблицей (запоминающий и печатающий данные). Модуль РаботаСТаблицей взят без изменений из предыдущего раздела. Все три модуля, кроме того, импортируют модуль InOut.
Главная программа работает в соответствии с нисходящей стратегией разбора, подобной стратегии, которая использована в разделе, посвященном рекурсии. Разница в том, что элементами текста считаются не литеры, а символы РБНФ, получаемые по одному с помощью вызова процедуры ВзятьЛексему из модуля РБНФСканер. Кроме самой процедуры ВзятьЛексему импортируются ее результаты: переменные лекс, ид, номстр. Переменной ид присваивается строка литер, обозначающая лексический элемент, если введенный элемент - идентификатор или строка литер. Отметим, что лекс имеет тип Лексема, который также определен в модуле РБНФСканер.
DEFINITION MODULE РБНФСканер;
ТУРЕ Лексема = (идент,литерал,лкрск,лквск,лфск, верт,равно,точка,пкрск,пквск,пфск,другая);
CONST ИдентДлина = 24;
VAR лекc: Лексема; (* следующая лексема *) ид: ARRAY [0..ИдентДлина] OF CHAR;
номстр: CARDINAL;
PROCEDURE ВзятьЛексему;
PROCEDURE ОтметитьОшибку(n: CARDINAL);
PROCEDURE ПропускСтроки;
END РБНФСканер.
Этот пример еще раз иллюстрирует тот Факт, что знание раздела описаний импортируемого модуля как необходимо, так и достаточно для написания импортирующего модуля.
FROM InOut IMPORT Done,EOL,OpenInput,OpenOutput, Read,Write,WriteLn,WriteCard,WriteString, CloseInput,CloseOutput;
FROM РБНФСканер IMPORT Лексема,лекc,ид,номстр,ВзятьЛексему, ОтметитьОшибку,ПропускСтроки;