В.Ш.КАУФМАН

 

                                                                                         ЯЗЫКИ

ПРОГРАММИРОВАНИЯ

КОНЦЕПЦИИ  И  ПРИНЦИПЫ

 

 

 

 

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

Базовые концепции и принципы рассмотрены с пяти различных позиций (технологической, авторской, математической, семиотической и реализаторской) и проиллюстрированы примерами из таких языков, как Паскаль, Симула-67, Смолток, Рефал, Ада, Модула-2, Оберон,Оккам$2, Турбо Паскаль, С++ и др.

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

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

Новые подходы применены при изложении известных фактов (пошаговая модификация нормальных алгоритмов Маркова сначала до Рефала, а затем до реляционных языков, сопоставление принципов «сундука» и «чемоданчика» при создании Ады, Модулы-2 и Оберона, развитие концепции наследуемости от модульности до объектной ориентации, систематическое сопоставление концепции параллелизма в Аде и Оккаме-2, и др.).

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

Ил. 5 Библиогр. 64 назв.

 

© Кауфман В.Ш., 1985-2018

Посвящается моим дорогим родителям:

Александре Фоминичне Каревой

Шахно Мордуховичу Кауфману

Эдит Яковлевне Кауфман

 

 

Содержание

Предисловие к сетевому изданию

Предисловие ко второму изданию

Предисловие к первому изданию

ЧАСТЬ 1. СОВРЕМЕННОЕ СОСТОЯНИЕ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ. 6

1. Концептуальная схема языка программирования. 6

1.1. Что такое язык программирования. 6

1.2. Метауровень. 6

1.3. Модель передачи сообщения. 7

1.4.  Классификация недоразумений. 7

1.5. Отступление об абстракции-конкретизации. Понятие модели. 8

1.6. Синтактика, семантика, прагматика. 9

1.7. Зачем могут понадобиться знания о ЯП.. 10

1.8. Принцип моделирования ЯП. 11

1.9. Пять основных позиций рассмотрения ЯП. 12

1.10. Что такое производство программных услуг 12

1.11. Производство программных услуг - основная цель программирования. 14

1.12. Сложность как основная проблема программирования. 15

1.13. Источники сложности. 15

1.14. Два основных средства борьбы со сложностью. Основной критерий качества ЯП. 17

1.15. Язык программирования как знаковая система. 18

1.16. Разновидности программирования. 19

1.17. Понятие о базовом  языке. 20

1.18. Концептуальная схема рассмотрения ЯП. 20

2. Пример современного базового ЯП (модель А) 22

2.1. Общее представление о ЯП Ада. 22

2.2. Пример простой программы на Аде. 23

2.3. Обзор языка Ада. 25

2.4. Пошаговая детализация средствами Ады.. 29

2.5. Замечания о конструктах. 33

2.6. Как пользоваться пакетом управление_сетью.. 34

2.7. Принцип раздельного определения, реализации и использования услуг (принцип РОРИУС) 41

2.8. Принцип защиты абстракций. 41

3. Важнейшие абстракции: данные, операции, связывание. 42

3.1. Принцип единства и относительности трех абстракций. 42

3.2. Связывание. 43

3.3. От связывания к пакету. 44

3.4. Связывание и специализация. 45

3.5. Принцип цельности. 49

4. Данные и типы.. 52

4.1. Классификация данных. 52

4.2. Типы данных. 54

4.3. Регламентированный доступ и типы данных. 62

4.4. Характеристики, связанные с типом. Класс значений, базовый набор операций. 69

4.5. Воплощение концепции уникальности типа. Определение и использование типа в Аде (начало) 70

4.6. Конкретные категории типов. 71

4.7. Типы как объекты высшего порядка. Атрибутные функции. 89

4.8. Родовые (настраиваемые) сегменты.. 90

4.9. Числовые типы (модель числовых расчетов) 92

4.10. Управление операциями. 95

4.11. Управление представлением.. 96

4.12. Классификация данных и система типов Ады.. 99

4.13. Предварительный итог по модели А. 100

5. Раздельная компиляция. 101

5.1. Понятие модуля. 101

5.2. Виды трансляций. 101

5.3.  Раздельная трансляция. 102

5.4. Связывание трансляционных модулей. 102

5.5. Принцип защиты авторского права. 103

6. Асинхронные процессы.. 104

6.1. Основные проблемы.. 104

6.2. Семафоры Дейкстры.. 107

6.3. Сигналы.. 109

6.4. Концепция внешней дисциплины.. 111

6.5. Концепция внутренней дисциплины: мониторы.. 111

6.6. Рандеву. 114

6.7. Проблемы рандеву. 116

6.8. Асимметричное рандеву. 117

6.9. Управление асимметричным рандеву (семантика вспомогательных конструктов) 117

6.10. Реализация семафоров, сигналов и мониторов посредством асимметричного рандеву. 119

6.11. Управление асинхронными процессами в Аде. 122

7. Нотация. 124

7.1. Проблема знака в ЯП. 124

7.2. Определяющая потребность. 124

7.3. Основная абстракция. 125

7.4. Проблема конкретизации эталонного текста. 125

7.5. Стандартизация алфавита. 126

7.6. Основное подмножество алфавита. 127

7.7. Алфавит языка Ада. 127

7.8. Лексемы.. 127

7.9. Лексемы в Аде. 128

8. Исключения. 129

8.1. Основная абстракция. 129

8.2. Определяющие требования. 130

8.3. Аппарат исключений в ЯП. 132

8.4. Дополнительные особенности обработки исключений. 138

9. Библиотека. 142

9.1. Структура библиотеки. 142

9.2. Компилируемый (трансляционный) модуль. 143

9.3. Порядок компиляции и перекомпиляции (создания и модификации программной библиотеки) 143

9.4. Резюме: логическая и физическая структуры программы.. 144

10. Именование и видимость (на примере Ады) 145

10.1. Имя как специфический знак. 145

10.2. Имя и идентификатор. 145

10.3. Проблема видимости. 145

10.4. Аспекты именования. 146

10.5. Основная потребность и определяющие требования. 146

10.6. Конструкты и требования, связанные с именованием.. 147

10.7. Схема идентификации. 148

10.8. Недостатки именования в Аде. 154

11. Обмен с внешней средой. 155

11.1. Специфика обмена. 155

11.2. Назначение и структура аппарата обмена. 158

11.3. Файловая модель обмена в Аде. 159

11.4. Программирование специальных устройств. 166

12. Два альтернативных принципа создания ЯП. 168

12.1. Принцип сундука. 168

12.2. Закон распространения сложности ЯП. 169

12.3. Принцип чемоданчика. 169

12.4. Обзор языка Модула-2. 170

12.5. Пример М-программы.. 171

12.6. Языковая ниша. 177

12.7. Принцип чемоданчика в проектных решениях ЯП Модула-2. 178

12.8. Принцип чайника. 185

12.9. ЯП Оберон. 185

ЧАСТЬ 2. ПЕРСПЕКТИВЫ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ. 192

1. Перспективные модели языка. 192

1.1. Введение. 192

1.2. Операционное программирование - модель фон Неймана (модель Н) 193

1.3. Ситуационное программирование - модель Маркова-Турчина (модель МТ) 195

2. Функциональное программирование (модель Б) 206

2.1. Функциональное программирование в модели МТ. 206

2.2. Функциональное программирование в стиле Бэкуса (модель Б) 213

3. Доказательное программирование (модель Д) 224

3.1. Зачем оно нужно. 224

3.2. Доказательное программирование методом Бэкуса. 225

3.3. Доказательное программирование методом Хоара. 229

4. Реляционное программирование (модель Р) 242

4.1. Предпосылки. 242

4.2. Ключевая идея. 243

4.3. Пример. 244

4.4.  Предопределенные отношения. 248

4.5. Связь с моделями МТ и Б. 249

5. Параллельное программирование в Оккаме-2 (модель О) 252

5.1. Принципы параллелизма в Оккаме. 252

5.2. Первые примеры применения каналов. 253

5.3. Сортировка конвейером фильтров. 255

5.4. Параллельное преобразование координат (умножение вектора на матрицу) 255

5.5. Монитор Хансена-Хоара на Оккаме-2. 259

5.6. Сортировка деревом исполнителей. 260

5.7. Завершение работы коллектива процессов. 264

5.8. Сопоставление концепций параллелизма в Оккаме и в Аде. 265

5.9. Перечень неформальных теорем о параллелизме в Аде и Оккаме. 272

5.10. Единая модель временных расчетов. 273

5.11. Моделирование каналов средствами Ады.. 274

5.12. Отступление о задачных и подпрограммных (процедурных) типах. 276

6.  Наследуемость (к идеалу развития и защиты в ЯП) 283

6.1. Определяющая потребность. 283

6.2. Идеал развиваемости. 283

6.3. Критичность развиваемости. 284

6.4. Аспекты развиваемости. 284

6.5. Идеал наследуемости (основные требования) 286

6.6. Проблема дополнительных атрибутов. 286

6.7.  Развитая наследуемость. 288

6.8. Аспект данных. 289

6.9. Аспект операций. 291

6.10. Концепция наследования в ЯП (краткий обзор) 296

6.11. Преимущества развитой наследуемости. 298

6.12. Наследуемость и гомоморфизм (фрагмент математической позиции) 299

7. Объектно-ориентированное программирование. 302

7.1. Определяющая потребность. 302

7.2. Ключевые идеи объектно-ориентированного программирования. 303

7.3. Пример: обогащение сетей на Турбо Паскале 5.5. 304

7.4. Виртуальные операции. 309

7.5. Критерий Дейкстры.. 316

7.6. Объекты и классы в ЯП Симула-67. 317

7.7. Перспективы, открываемые объектной ориентацией средств программирования. 318

7.8. Свойства объектной ориентации. 321

7.9. Критерий фундаментальности языковых концепций. 321

8. Заключительные замечания. 322

8.1. Реализаторская позиция. 322

8.2. Классификация языков программирования. 328

8.3. Тенденции развития ЯП. 331

8.4. Заключение. 336

Список литературы.. 338

 

Предисловие к сетевому изданию

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

25.10.18 Хельсинки

В.Ш.Кауфман

Предисловие ко второму изданию

К немалому удивлению автора, это книга оказалась востребованной и через 15 лет после своего выхода в свет (насколько известно автору, её используют в МГУ, МАИ и других российских университетах). Это тем более удивительно, что основной её материал подготовлен значительно раньше, примерно в 1985 году.

В Сети также циркулируют (и даже продаются, якобы с разрешения автора ☺) относительно ранние варианты лекций по курсу «Языки программирования», читанных автором в те же годы на факультете ВМиК МГУ.

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

За прошедшие годы многие уважаемые члены программистского сообщества посчитали нужным поддержать уверенность автора в ценности изложенного в книге материала.

Владимир Ильич Головач в своей рецензии в «Мир ПК» одним из первых предсказал ей долгую жизнь. Андрей Андреевич Терехов, один из лучших знатоков компьютерной литературы, также высоко оценил качество книги. Очень хорошо отзывались о ней также Владимир Арнольдович Биллиг, Леонид Федорович Белоус, Сергей Залманович Свердлов, не говоря уже о студентах и преподавателях МГУ. Всем этим людям огромная благодарность за поддержку и стимулирование настоящего интернет-издания.

Немало замечательных членов программисткого сообщества, упоминаемых в книге, многих из которых автор имел удовольствие знать лично и даже обсуждать с ними фрагменты книги или читавшегося в МГУ курса, за прошедшие годы покинули этот мир. Среди них Евгений Андреевич Жоголев, Александр Владимирович Замулин, Игорь Васильевич Поттосин, Святослав Сергеевич Лавров, Эдуард Зиновьевич Любимский, Екатерина Логвиновна Ющенко, Михаил Романович Шура-Бура, Валентин Фёдорович Турчин. Бесконечная им признательность за бесценный вклад в общее дело и светлая память.

 

Переиздание книги, несмотря на имеющиеся запросы, совсем недавно представлялось совершенно нереальным с учетом стопроцентной загрузки автора основной работой. Ведь электронная верстка книги оказалась утраченной в результате непростых пертурбаций бурных 90-х прошлого века. К счастью, мир полон чудес. Одно из них – воскрешение полной электронной версии книги с помощью современных средств сканирования с печатного оригинала и последующего ручного редактирования. Эта огромная работа была выполненна целиком по собственной инициативе Ольгой Львовной Свиридовой-Бондаренко. Как по волшебству, подоспело и предложение Дмитрия Алексеевича Мовчана переиздать книгу в «ДМК Пресс». Автору оставалось только вычитать полученный от Ольги Львовны Word-документ и передать результат в «ДМК Пресс» в надежде помочь людям, желающим глубоко вникнуть в суть проблем, принципов и концепций современного программирования.

 

Удачи, глубокоуважаемый Читатель!

В.Ш.Кауфман

30.08.10 Москва-Хельсинки

Предисловие

 

Эта книга возникла из курса лекций "Языки программирования", читаемого автором в МГУ. Стимулом для написания книги послужи­ло отсутствие доступной литературы, в которой были бы системати­чески изложены, во-первых, ключевые принципы, концепции и по­нятия, составляющие

основу предмета и поэтому претендующие на относительную стабильность, и, во-вторых, перспективные идеи и тенденции, помогающие ориентироваться в огромном и быстро меня­ющемся мире современных языков программирования (ЯП).

Автор столкнулся с немалыми проблемами несмотря на то, что становление современных ЯП происходило, можно сказать, на его глазах. Пришлось пережить и восторги от изобретения первых "язы­ков высокого уровня", быть среди тех, кто увлекался их "усовер­шенствованием" и созданием первых трансляторов, опираясь только на здравый смысл и собственную смекалку, пришлось пережить и надежды на создание "универсального ЯП" объединенными усилия­ми международного программистского братства, и разочарования от бездарной траты сил и средств на бесперспективные начинания.

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

Заметив это обстоятельство, автор уже сознательно стал иногда рассчитывать не только на студенческую аудиторию, но и на более искушенного читателя, позволяя себе намеки и аналогии, подразу­мевающие личный опыт программирования и даже экспертной деятельности в области ЯП. Более того, стало очень трудно отделить то, что известно, признано, устоялось, от того, что удалось только что понять, систематизировать, придумать. В результате жанр книги стал менее определенным, "поплыл" от первоначально задуманного учебного пособия в сторону монографии.

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

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

Книга состоит из двух частей. Первая посвящена основным абст­ракциям, используемым в современных ЯП. В качестве основного языка примеров здесь фигурирует ЯП Ада. Он удобен в этой роли потому, что в той или иной форме содержит ответы практически на все технологические проблемы. Другими словами, Ада служит при­мером "максимального" современного ЯП. "Минимальные" ЯП представлены языками Никлауса Вирта - это Модула-2 и Оберон (образца 1988 г.).

Вторая часть рассказывает о перспективных тенденциях в ЯП. В ней рассмотрены ситуационное, функциональное, доказательное, ре­ляционное, параллельное и объектно-ориентированное программиро­вание. Среди языков-примеров - Рефал, функциональный язык Бэкуса, Оккам-2 для программирования транспьютеров, обьектно-ори­ентированный Турбо Паскаль и др.

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

Так как замысел книги возник восемь лет назад и почти полови­на материала написана еще в 1983-1985 гг., закономерно опасение, не устарела ли книга еще до своего выхода в свет. Конечно, судить об этом читателю, однако автор старался отбирать фундаментальные и, по его мнению, перспективные концепции и принципы, которые по самой своей природе должны быть стабильнее быстро меняющей­ся конъюнктуры.

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

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

Создавать эту книгу помогали многие люди, которые, конечно, не несут какой-либо ответственности за ее недостатки. Автору при­ятно выразить признательность В.К.Мережкову за инициативу и ог­ромную помощь при издании первой части рукописи в НПО "Центрпрограммсистем", профессорам Е.Л.Ющенко, М.Р.Шура-Бу­ре, В.Н.Редько, И.М.Витенбергу, А.А.Красилову, С.СЛаврову, Я.Я.Осису, Е.А.Жоголеву, Н.П.Трифонову, Г.С.Цейтину за поддер­жку и ценные советы, своим коллегам и первым читателям В.Л.Темову, В.Н.Агафонову, В.И.Головачу, А.С.Маркову, Б.Г.Чеблакову, Анд.В.Климову, В.Н.Лукину, И.В.Раковскому за содержательную критику, А.Л.Александрову, И.Н.Зейтленок, И.З.Луговой и особен­но С.И.Рыбину за помощь в подготовке рукописи, своим слушателям и студентам за внимание, терпение и любознательность, своим род­ным за понимание и заботу.

Автор старался не изменить духу преданности сути дела и твор­ческой раскованности, воплощением которых для него остаются рано ушедшие из жизни Андрей Петрович Ершов, успевший прочитать первый вариант рукописи и поддержать настроение автора писать "как пишется", и Адольф Львович Фуксман, который в свое время горячо обсуждал с В.Л.Темовым и автором совместный проект уни­верситетского учебника по программированию.

 

В.Ш.Кауфман

 

ЧАСТЬ 1. СОВРЕМЕННОЕ СОСТОЯНИЕ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ

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

1.1. Что такое язык программирования

 

Для начала дадим экстенсиональное определение ЯП - явно пере­числим те конкретные языки, которые нас заведомо интересуют (их мы уверенно считаем языками программирования). Это Фортран, Симула, Паскаль, Бейсик, Лисп, Форт, Рефал, Ада, Си, Оккам, Оберон. Однако хочется иметь возможность на основе определения предсказывать новые частные случаи, в определении не перечислен­ные. Такое определение должно опираться на существенные свойст­ва выбираемых для изучения языков - оно должно быть интенсио­нальным. Дадим одно из возможных интенсиональных определений ЯП.

 

Язык программирования - это инструмент для планирования по­ведения исполнителя.

 

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

Во-вторых, в нашем определении каждое слово нуждается в уточ­нении. Являются ли "инструментами для планирования поведения исполнителя" должностная инструкция, письменный стол, перего­ворное устройство, правила уличного движения, русский язык?

1.2. Метауровень

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

Чем мы занимались?

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

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

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

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

 

1.3  Модель передачи сообщения

Добиться взаимопонимания бывает очень сложно. Чтобы выде­лить возникающие здесь проблемы, рассмотрим следующую модель передачи сообщения (рис. 1.1).

 

| Семантика | ----- Смысл------------ | Прагматика |

                                    |

                      Отправитель  ->  Сообщение  ->    Адресат

                                    |

            | Синтактика |

Рис. 1.1

 

В этой модели выделены понятия "Отправитель" (автор, генера­тор сообщения), "Адресат" (получатель, читатель, слушатель сооб­щения), собственно "Сообщение" (текст, последовательность зву­ков), "Смысл" сообщения (нечто обозначаемое сообщением в соот­ветствии с правилами, известными и отправителю, и адресату).

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

 

1.4.  Классификация недоразумений

 

С помощью модели на рис. 1.1 займемся классификацией недоразумений, возникающих при попытке установить взаимопонимание.

Автор (отправитель сообщения) может подразумевать одну струк­туру сообщения, а адресат (получатель) - другую, как в классиче­ском королевском указе: "Казнить нельзя помиловать!".  Это синтаксическое недоразумение.

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

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

Например, сообщение лектора о предстоящем коллоквиуме может быть воспринято студентами как призыв не посещать малоинформа­тивные лекции, чтобы иметь время для работы с книгами. Это уже прагматическое недоразумение.

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

Почему же в самом начале речь пошла о взаимопонимании и о стоящих на пути к нему преградах? В основном по двум причинам.

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

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

Особенно бояться синтаксических недоразумений не стоит. Они касаются отдельных неудачных фраз и легко устраняются немедлен­ным вопросом (устным или письменным). В ЯП это тоже не пробле­ма - таких недоразумений там просто не бывает. Дело в том, что создатели ЯП руководствуются принципом однозначности: язык про­граммирования должен быть синтаксически однозначным (т.е. вся­кий правильный текст на ЯП должен иметь единственную допусти­мую структуру).

Итак, сформулирован один из принципов построения ЯП, отли­чающих их, например, от языков естественных. Такого рода общие принципы и концепции нас и будут интересовать в первую очередь.

Семантические недоразумения опаснее. Если, скажем, слово "язык" будет ассоциироваться с субпродуктом, ставшим весьма ре­дким гостем прилавка, то недоразумение может не ограничиться пределами одной фразы. Большой язык, свежий язык, красный язык, зеленый и голубой язык - все это может касаться и говяжьего языка, и ЯП (в конкурсе языковых проектов, ставшем одним из эта­пов создания языка Ада, языки-конкуренты получили условные "цветные" наименования; победил "зеленый" язык).

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

 

1.5. Отступление об абстракции-конкретизации.  Понятие модели

 

Добиваясь взаимопонимания, мы активно пользуемся аппаратом абстракции-конкретизации  (обобщения-специализации).

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

Приводя пример, мы конкретизируем абстрактное понятие, "снабжая" его второстепенными с точки зрения его сущности, но важными в конкретной ситуации деталями. Так, конкретное выпол­нение процедуры происходит при конкретных значениях ее парамет­ров; у конкретного примера ЯП - Фортрана - конкретный синтаксис и конкретная семантика.

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

На "устройство, показывающее время", в известных условиях можно предпочесть смотреть как на "украшение", и с этой точки зрения (в этой его роли) важнее станут форма, цвет, размер, фирма, важнее даже способности правильно показывать время. На процедуру можно смотреть как на "объект, расходующий машинные ресурсы". При такой ее роли совершенно неважно, каков смысл выполняемых в ней действий. На ЯП иногда приходится смотреть как на объект стандартизации, и тогда важно не столько то, каковы именно осо­бенности его семантики и синтаксиса, сколько то, найдется ли доста­точно много заинтересованных в тех или иных его свойствах людей и организаций.

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

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

 

1.6. Синтактика, семантика, прагматика

 

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

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

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

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

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

Итак, о нашей основной цели. Она состоит в том, чтобы поста­раться правильно ориентировать читателя в области ЯП, помочь ему осознать навыки и опыт, приобретенные при самостоятельной работе с конкретными ЯП.

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

К сожалению или к счастью, альтернативы у нас по сути нет. Аб­солютно бесспорные положения касаются, как правило, лишь конк­ретных ЯП. Например, "Один из операторов в языке Паскаль - опе­ратор присваивания. Устроен он так-то. Служит для того-то". В хорошо известном учебнике программирования это положение обобще­но. Сказано так: "Фундаментальным действием в любом алгоритми­ческом языке является присваивание, которое изменяет значение некоторой переменной". И это уже неверно! Сейчас много внимания уделяется так называемому функциональному программированию, аппликативным ЯП, где присваивание не только не "фундаменталь­ное" действие, но его вообще нет!

Значит, в области ЯП нет достаточно общих бесспорных положе­ний? В некотором смысле есть. Чаще не столь бесспорных, сколь за­служивающих изучения. Правда, их общность несколько другого ха­рактера. Примером может служить упоминавшийся принцип одно­значности. Да и приведенная фраза из учебника - вполне бесспорное положение, если считать, что она характеризует определенный класс ЯП, в который не попадает, скажем, язык Лисп - один из самых "заслуженных", распространенных и в то же время перспективных. Итак, даже если ограничиться лишь относительно бесспорными положениями, их все равно нужно отбирать с определенных позиций, с определенной целью. Естественная цель - стремиться принести чита­телю максимальную пользу. Опять мы приходим к "угадыванию" будущих потребностей.

 

1.7. Зачем могут понадобиться знания о ЯП

 

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

Во-вторых, каждый ЯП - это своя философия, свой взгляд на дея­тельность программиста, отражение определенной технологии про­граммирования. Даже представлений об Алголе-60, Фортране и Бей­сике достаточно, чтобы почувствовать, что имеется в виду.

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

Творцы Фортрана (сотрудники фирмы IBM во главе с Джоном Бэкусом) в значительной степени пренебрегли строгостью и изяще­ством языка и со свойственным им в ту пору (1954-1957 гг.) прагма­тизмом уже в первых версиях языка уделили особое внимание вводу-выводу и модульности. Но ни Фортран, ни Алгол не рассчитаны на работу в диалоговом режиме в отличие от Бейсика (созданного в Дартмундском колледже первоначально для обучения студентов).

Таким образом, изучение ЯП дает знание и понимание разнооб­разных подходов к программированию. Это полезно при любой программистской деятельности.

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

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

Все, о чем было сказано до сих пор, касалось интересов потенци­ального пользователя ЯП. Но читатель может оказаться и руководи­телем коллектива, которому требуется оценивать и выбирать ЯП для выполнения конкретного проекта (учитывать, скажем, затраты на освоение этого ЯП или на обмен написанными на нем программны­ми изделиями). Если же он станет творцом ЯП, создателем трансля­тора или руководства для пользователей, то ему понадобятся столь разнообразные знания о ЯП, что их придется извлекать целеустрем­ленным изучением специальной литературы. Можно надеяться дать лишь первоначальный импульс в нужном направлении.

Конечно, предсказать, для чего именно понадобятся приобретен­ные знания, сложно. Могут напрямую и вовсе не понадобиться. Но наверняка пригодится приобретенная обсуждениями, размышления­ми и упражнениями культура работы со сложными объектами при решении сложных задач. В нашем случае это такие задачи, как оценка, использование, разработка и реализация ЯП.

 

1.8. Принцип моделирования ЯП

 

Было бы неправильно ставить нашей целью научить свободному владению конкретными ЯП, пусть даже особо привлекательными или перспективными. Для этого служат специальные учебники, уп­ражнения и, главное, практика.

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

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

 

1.9. Пять основных позиций рассмотрения ЯП

 

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

И сразу вопрос - с чего начать? Легко сказать "систематическо­му". Но ведь системы бывают разные. Часто начинают "снизу" - с основных конструктов, встречающихся почти во всех существующих ЯП. Тогда мы сразу погружаемся в мир переменных, констант, параметров, процедур, циклов и т.п. Такой путь привлекателен хотя бы тем, что им сравнительно легко пойти. Но на этом пути за де­ревьями обычно не видно леса, не удается увидеть ЯП в целом, по­строить его адекватную модель.

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

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

Вторая позиция названа семиотической потому, что ее можно представить себе как позицию человека, знакомого с некоторыми знаковыми системами (русским языком, дорожными знаками, пози­ционными системами счисления) и желающего узнать, чем выделя­ются такие знаковые системы, как ЯП. Следует объяснить ему это в семиотических терминах.

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

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

Математик понимает, что такое математическая модель изучаемого объекта, и желает познакомиться с математическими моделями ЯП. С ним желательно объясняться в математических терминах.

Реализатор обеспечивает возможность пользоваться ЯП как сред­ством практического программирования. Другими словами, он не только создает трансляторы, но и пишет методические руководства, обучающие и контролирующие программы, испытывает трансляторы и т.п.

Уместно подчеркнуть, что с разных позиций мы будем рассматри­вать один и тот же объект. Начнем с технологической позиции. Ус­тановим связь ЯП с производством программных услуг.

 

1.10. Что такое производство программных услуг

 

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

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

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

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

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

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

Людей как исполнителей характеризует прежде всего наличие у них модели ре­ального мира, в достаточной степени согласованной с моделью мира у создателя пла­на. Поэтому в плане для людей можно указывать цели, а не элементарные действия.

 

Ресурс, существенный почти для всех реальных исполнителей, - это время. Важное свойство компьютеров как исполнителей - способ­ность выполнять элементарные действия исключительно быстро (по­рядка микросекунды на действие). Не менее важное свойство компьютера - способность хранить огромные объемы данных (в оперативной памя­ти - мегабайты; на внешней - практически неограниченно).

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

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

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

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

Правомерность абстракции от аппаратуры подчеркивает опреде­ленную искусственность проблемы переноса программ. Если пока в значительной степени безразлично, на чем программировать, то раз­нообразие и несовместимость исполнителей вызваны не объективны­ми, а социальными причинами.

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

Представление пассивного знания ориентировано в первую оче­редь на такой ресурс компьютера, как память, а представление ак­тивного - на процессор. Исторически самыми первыми сферами при­менения компьютеров оказались такие, где главенствовало активное знание - эксплуатировалась способность ЭВМ не столько много знать, сколько быстро считать.

Кстати, и память на первых ЭВМ была очень мала по сравнению со скоростью работы процессора. Всю свою память они могли про­смотреть (или переписать) за десятую долю секунды. Недалеко уш­ли в этом отношении и современные компьютеры (если говорить об оперативной памяти).

Соотношение между объемом оперативной памяти и скоростью процессоров активно влияет на мышление программистов, инжене­ров, пользователей, а также всех связанных с компьютерами людей. Изменение этого соотношения (а его разумно ожидать) способно произвести революцию в практическом программировании (см. далее о модифицированной модели Маркова и функциональном стиле про­граммирования (модель Бэкуса); есть и другие перспективные моде­ли, например, реляционная). Пока же программисты вовсю эконо­мят память, помещая новые значения на место старых, а преподава­тели учат искусству такого "эффективного" программирования. В "эффективных" программах трудно восстановить процесс получения результата, трудно объяснить неожиданный результат. Традицион­ный стиль программирования, обусловленный бедностью ресурсов, затрудняет написание, понимание, проверку и удостоверение пра­вильности программ. Тенденция развития состоит в том, что роли активного и пассивного знаний в производстве программных услуг становятся более симметричными, а забота об эффективности отступает перед заботой о дружественности программы.

 

1.11. Производство программных услуг - основная цель програм­мирования

 

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

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

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

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

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

(О связи науки, искусства, теории и технологии в программиро­вании см. замечательную Тьюринговскую лекцию Дональда Кнута в Communication of the ACM.- 1974.- Vol.12.- P.667-673).

 

1.12. Сложность как основная проблема программирования

 

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

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

В чем же основная причи­на такого положения? Связана ли она с самой природой программи­рования или носит субъективный характер?

В настоящее время крат­кий ответ можно сформулировать так: "сложность - основная про­блема программирования; связана с самой его природой; можно на­деяться на ее понижение для освоенных классов задач".

 

1.13. Источники сложности

 

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

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

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

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

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

Между тем способности человека работать с большим числом свя­занных объектов, как хорошо известно, весьма ограничены. В каче­стве ориентира при оценке этих способностей указывают обычно на так называемое "число Ингве", равное семи (плюс-минус 2). Други­ми словами, человек обычно не в состоянии уверенно работать с объектом, в котором более семи компонент с произвольными взаим­ными связями. До тех пор, пока программирование остается в основ­ном человеческой деятельностью, с указанным ограничением необ­ходимо считаться.

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

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

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

Именно этот источник имелся в виду, когда шла речь об объек­тивном характере присущей программированию сложности. Занима­ясь определенным классом услуг (задач), можно стремиться выде­лить характерный именно для этого класса набор элементарных объ­ектов и операций, построить соответствующий исполнитель (аппа­ратным или программным способом) и программировать на таком более подходящем исполнителе. Фактически это означает создать адекватный выбранному классу услуг ЯП. На практике это самый распространенный способ борьбы со сложностью и одновременно ос­новная причина роста проблемно-ориентированных языков (ПОЯ).

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

Из-за этого самая "мелкая", с точки зрения создателя програм­мы, описка может привести к совершенно непредсказуемым послед­ствиям (широко известен случай, когда из-за одной запятой в про­грамме на Фортране взорвалась космическая ракета, направлявшая­ся на Венеру; пропали усилия, стоившие миллиарды долларов).

Итак, в качестве второго источника сложности в современном программировании следует назвать незнание компьютером реального мира. Лишенный необходимых знаний, компьютер не может не только скорректировать неточно указанные в программе действия, но и проинформировать об отклонениях от направления на цель ра­боты. Традиционное для компьютеров управление посредством ука­зания действий, а не целей требует учета мельчайших нюансов всех обстоятельств, в которых может оказаться исполнитель в процессе предоставления нужной услуги. Это резко увеличивает число объек­тов, с которыми приходится иметь дело при создании программ. От­сюда повышение сложности программирования, увеличение размера программ, понижение их надежности и робастности.

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

 

1.14. Два основных средства борьбы со сложностью. Основной кри­терий качества ЯП

 

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

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

Важнейшим средством борьбы с незнанием реального мира слу­жит аппарат прогнозирования-контроля. Имеются ЯП, в которых этот аппарат практически отсутствует (Апл, Форт, Лисп) или очень слаб (любой ассемблер). Однако именно этот аппарат - основа повы­шения надежности и робастности программ. Последнее не означает, что "дружественные" программы невозможно писать на ЯП со сла­бым прогнозированием-контролем. Просто в этом случае создание подходящего аппарата полностью возлагается на программиста.

Например, в Фортране характерное средство прогнозирования - встроенные (предопределенные) типы данных. Соответствующий контроль предусмотрен семантикой языка, но средств управления таким контролем нет (новые типы данных вводить нельзя). В таких ЯП, как Паскаль или Ада, этот аппарат более развит, а в так назы­ваемых языках искусственного интеллекта он прямо предназначен для представления достаточно полных знаний о мире, целей дея­тельности и контроля действий как самой программы, так и пользо­вателя.

 

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

 

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

На этом оставим пока технологическую позицию и займемся се­миотической.

 

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

 

Продолжим уточнение понятия "язык программирования". Наше новое интенсиональное определение ЯП таково:

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

Итак, не любой "инструмент", а "знаковая система" и не для произвольных "исполнителей", а только для компьютеров. К ограни­чению класса исполнителей в этом определении мы подготовились заранее, а вот о знаковых системах еще подробно не говорили.

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

Вот еще знаковые ситуации (первым укажем знак, вторым - де­нотат): буква и соответствующий звук, дорожный знак ("кирпич") и соответствующее ограничение ("въезд запрещен"), слово и соответ­ствующее ему понятие. Каждый без затруднений пополнит этот спи­сок.

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

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

 

Упражнение. Приведите пример синтаксического и семантического правила из таких знаковых систем, как Фортран, Бейсик, Паскаль, Ассемблер.

 

В общем случае в ЯП знаки - это элементы программ (в том чис­ле полные программы), а денотаты - элементы и свойства поведения исполнителя (атрибуты его поведения), в частности, данные, опера­ции, управление, их структура, их связи и атрибуты. Например, знаку, составленному из шести букв "arctan" (элементу программы на Фортране), использованному в этой программе в подходящем контексте, соответствует в качестве денотата такой элемент поведе­ния исполнителя, как вычисление арктангенса.

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

 

Упражнение. Выпишите подходящие контексты.

 

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

Теперь уточненное определение ЯП как знаковой системы для планирования поведения компьютеров должно быть полностью по­нятным.

 

1.16. Разновидности программирования

 

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

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

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

сборочное - программа составляется из заранее заготовленных модулей (так обычно сейчас работают пакеты прикладных про­грамм);

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

синтезирующее - роль заготовок относительно невелика.

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

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

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

 

1.17. Понятие о базовом  языке

 

Два выделенных источника сложности - семантический разрыв и незнание мира - полезно трактовать как два различных аспекта еди­ного источника: рассогласования моделей проблемной области (ПО) - области услуг, задач, операций у пользователей и исполнителей.

При таком взгляде создаваемая программа выступает как средст­во согласования этих моделей. Чем ближе исходные модели, тем проще программа. При идеальном исходном согласовании программа вырождается в прямое указание на одну из заранее заготовленных услуг (например, "распечатать файл", "взять производную", "вы­дать железнодорожный билет"). Мера рассогласованности моделей положена в основу известной "науки о программах" Холстеда.

Мы уже говорили об исключительном разнообразии моделей даже одного объекта, рассматриваемого с различных точек зрения. Поэто­му невозможно построить исполнитель, непосредственно пригодный для выполнения любой услуги. Однако удается строить специализированные исполнители и ориентировать их на фиксированный класс услуг – ПО. Для управления такими специализированными исполнителями создаются проблемно ориентированные языки (ПОЯ). В качестве хорошо известных примеров укажем язык управления заданиями в операционной системе, язык управления текстовым редактором, язык запросов к базе данных и т.п.

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

Так что безнадежно строить ЯП с моделями, заготовленными "на все случаи жизни". Однако можно попытаться построить ЯП, на базе которого будет удобно (относительно несложно, с приемлемыми затратами) строить модели весьма разнообразных ПО. Такой язык называют базовым языком программирования.

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

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

 

1.18. Концептуальная схема рассмотрения ЯП

 

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

Если бы нас интересовала, например, оценка ЯП с точки зрения легкости их усвоения начинающими программистами, мы предложи­ли бы, конечно, другую схему их рассмотрения, начав с выявления основных целей и проблем обучения, наиболее подходящих средств решения этих проблем и т.д., подобно нашему пути к базовому языку.

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

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

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

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

Исключительная технологическая роль названных средств дает основание уделить им особое внимание в предлагаемой ниже единой концептуальной схеме рассмотрения ЯП. Опишем начальную версию этой схемы. При необходимости она будет корректироваться и уточняться.

Итак, в каждом ЯП нас будет интересовать три аспекта: базис, аппарат развития (просто развитие), аппарат защиты (просто защи­та).

Базис ЯП - это предопределенные (встроенные в ЯП) знаки и их денотаты. В базисе будем выделять, во-первых, элементарные типы данных и элементарные операции (это так называемая скалярная сигнатура) и, во-вторых, структуры данных и операций (структур­ная сигнатура).

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

Некоторые компоненты базиса составляют аппарат развития ЯП, некоторые - аппарат защиты.

Об аппарате развития уже сказано. Добавим лишь, что будем различать развитие вверх - аппарат определения и использования новых абстракций, и развитие вниз - уточнение и переопределение компонент базиса или ранее определенных абстракций. Например, модуль-подпрограмма в Фортране - средство развития вверх. А средств развития вниз в Фортране нет (в отличие от Ады, Си или CDL).

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

Кроме базиса, развития и защиты будем рассматривать иногда другие особенности ЯП и, в частности, особенности эталонного исполнителя ЯП (так называемого языкового процессора) и архитектуру ЯП в целом.   Например, для Фортрана исполнитель последовательный,  для Рефала - циклический, а для Ады - параллельный.

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

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

 

2. Пример современного базового ЯП (модель А)

 

2.1. Общее представление о ЯП Ада

 

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

В соответствии с принципом моделирования ЯП не будем изучать язык Ада полностью, а лишь построим на его основе нашу первую языковую модель - модель А.

Мы исходим из принципа технологичности - всякий языковый конструкт предназначен для удовлетворения технологических по­требностей на определенных этапах жизненного цикла комплексного программного продукта. Этот принцип, с одной стороны, нацеливает на изучение важнейших потребностей в качестве "заказчиков" понятий и конструктов ЯП. С другой стороны, он требует понимать набор потребностей, обслуживаемых каждым понятием и (или) конструктом. Желательно видеть связь этих понятий с общей идеей абстракции-конкретизации. Будем подчеркивать эту связь, когда посчитаем существенной.

Когда говорят о технологических потребностях, всегда имеют в виду более или менее определенную ПО и технологию решения за­дач в этой области, в частности, технологию программирования. По­этому следует рассказать об особенностях той ПО, для которой создавался ЯП Ада.

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

Язык Ада создан в основном в 1975-1980 гг. в результате гранди­озного проекта, предпринятого МО США с целью разработать еди­ный ЯП для так называемых встроенных систем (т.е. систем управ­ления автоматизированными комплексами, работающими в реальном времени). Имелись в виду прежде всего бортовые системы управле­ния военными объектами (кораблями, самолетами, танками, ракета­ми, снарядами и т.п.). Поэтому решения, принятые авторами Ады, не следует считать универсальными. Их нужно воспринимать в кон­тексте особенностей выбранной ПО.

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

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

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

Язык Ада возник в результате международного конкурса языковых проектов, проходившего в 1978-1979 гг. Участники должны были удовлетворить довольно жестким, детально разработанным под эгидой МО США требованиям. Интересно, что все языки, дошедшие до последних туров этого конкурса, были основаны на Паскале. В этой связи Аду можно предварительно охарактеризовать как Паскаль, развитый с учетом перечисленных выше пяти основных требований. При этом авторы (международный коллектив под руководством Жана Ишбиа) пошли в основном по пути расширения Паскаля новыми элементами.  В результате получился существенно более сложный язык.

 

2.2. Пример простой программы на Аде

 

Чтобы создать у читателя первое зрительное впечатление об Аде, дадим пример совсем простой (но полной) программы. Основой этого (и некоторых других наших примеров) послужили программы учебника Янга "Введение в Аду" [2].

Напишем программу, которая вводит последовательность символов со стандартного устройства ввода и на стандартном устройств вывода печатает левые и правые скобки, обнаруженные в этой последовательности.

Вот эта программа:

 

1.   with  т_обмен;

2.   procedure   печать_скобок is

3.   ch : символ ;

4.   begin

5.   т_обмен.дай (ch) ;

6.       while ch /= '.' loop

7.           if ch = '(' or ch = ')' then

8.                т_обмен.возьми (ch) ;

9.           end if ;

10.        т_обмен.дай (ch) ;

11.       end loop  ;

12. end   печать_скобок ;

 

Итак, в программе двенадцать строчек (номера строчек не входят в программу - они нужны, чтобы обсуждать ее свойства). Общий вид программы напоминает текст на Алголе-60 или Паскале (некоторые знакомые ключевые слова, характерная ступенчатая запись и т.п.). Это и не удивительно. Ведь Ада из семейства "поздних алголоидов". Как уже сказано, этот язык - развитие Паскаля, в свою очередь со­зданного (Никлаусом Виртом)  на основе Алгола-60.

Вместе с тем даже в этой простой программе заметны характерные особенности поздних языков индустриального программирования. 

Во-первых, высокоразвитая модульность. Фраза с ключевым сло­вом "with" (в переводе с английского "с" или "совместно с") гово­рит о том, что данную процедуру следует читать, понимать и испол­нять во вполне определенном контексте. Этот контекст задан модулем-ПАКЕТОМ с именем "т_обмен". В нем содержатся определения всех ресурсов, необходимых для ввода-вывода текстов (в частности, процедуры "дай" очередной символ со стандартного устройства ввода и "возьми" очередной символ на стандартное устройство вывода). Внутри программы, использующей такой пакет, обращаться к его ресурсам следует по составным именам (сначала название пакета, а затем через точку - название ресурса) как к полям записи в Паска­ле. При необходимости можно, конечно, ввести сокращенные обоз­начения для часто используемых ресурсов.

Во-вторых, богатый набор типов данных. В строчке 3 находится объявление переменной ch типа "символ". Это один из предопреде­ленных типов Ады.

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

Ни в Алголе-60, ни в Фортране такого символьного типа, равноправного с остальными типами, нет. Один из источников выразительной мощи языка Ада - возможность стро­ить новые типы данных, не предопределенные авторами языка. Та­кая возможность теперь имеется во всех новых ЯП, и мы с ней под­робно познакомимся.

В-третьих, ради надежности повышена избыточность, способству­ющая устранению случайных ошибок. Это и (сколь угодно) длинные названия-идентификаторы, которые можно к тому же составлять из отдельных слов, соединенных одиночным подчеркиванием. Это и строгая скобочная структура текста - каждый управляющий конструкт снабжен специальным "закрывающим" ключевым словом (цикл в строчках с 6 по 11, условный оператор в строчках 7-9, про­цедура в строчках 2-12). Надежности, ясности, четкой структуре и избыточности способствуют и строгие правила ступенчатой записи программ (в Аде она настоятельно рекомендуется в определении языка и отражена в его синтаксисе).

Смысл программы достаточно очевиден. В строчке 5 вводится первый символ обрабатываемой последовательности и помещается в переменную ch. Далее цикл, работающий до тех пор, пока значени­ем переменной ch не станет символ "." ("/=" - это "не равно", "." – это признак конца последовательности обрабатываемых символов). В теле цикла - условный оператор, который посылает на устройство вывода очередной символ, если это открывающая или закрывающая скобка. Затем (строкой 10) вводится в переменную ch очередной символ последовательности и цикл повторяется. Вместе с циклом за­вершается и процедура печать_скобок.

 

2.3. Обзор языка Ада

 

Этот раздел близок по структуре и стилю к разделу 1.4 офици­ального определения языка Ада - национальному стандарту США 1983 г., ставшему в 1986 г. без изменений международным стандар­том ИСО. Рассказывая об этом языке и приводя примеры (из различных источников), будем и впредь опираться на это официальное определение. Без существенных изменений оно принято и в качестве отечественного ГОСТа, имеется его перевод на русский язык, отече­ственные реализации Ады также ориентируются на это определение.

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

Само по себе их обилие в официальном сообщении, к сожалению, также харак­терно. Оно свидетельствует либо о неразвитости науки и практики языкотворчества, либо о фундаментальных свойствах такого социального явления, как ЯП. Поразитель­ный пример лаконичности - определение Никлаусом Виртом языка Модула-2 [5]).

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

Ада-программа состоит из одного или более программных МОДУ­ЛЕЙ (сегментов), которые можно компилировать раздельно (кроме задач).

Ада-модуль - это ПОДПРОГРАММА (определяет действия - час­ти отдельных ПРОЦЕССОВ) или ПАКЕТ (определяет часть контек­ста - совокупность объектов, предназначенных для совместного ис­пользования), или ЗАДАЧА (определяет асинхронный процесс), или РОДОВОЙ модуль (заготовка пакета или подпрограммы с параметрами периода компиляции).

В каждом модуле обычно две части: внешность или СПЕЦИФИ­КАЦИЯ (содержит сведения, видимые из других модулей) и внут­ренность или ТЕЛО (содержит детали реализации, невидимые из других модулей). Разделение спецификации и тела вместе с раздель­ной компиляцией дает возможность проектировать, писать и прове­рять программу как набор относительно самостоятельных (слабо за­висимых) компонент.

Ада-программа пользуется ТРАНСЛЯЦИОННОЙ БИБЛИОТЕ­КОЙ. Поэтому в тексте создаваемого модуля следует указывать на­звания используемых библиотечных модулей.

 

2.3.1. Модули

 

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

Пакет - основной конструкт для определения именованного кон­текста (иногда говорят "логически связанной" совокупности объек­тов). Несколько расплывчатое "логически связанной" подразумевает возможность объединить в пакет все то, что автор пожелает видеть единым модулем, названным подходящим именем. Это может быть сделано потому, что все связанные в пакет объекты, во-первых, предполагается использовать совместно; во-вторых, необходимо или удобно совместно реализовывать; в-третьих, невозможно или неу­добно раздельно определять из-за ограничений на ВИДИМОСТЬ имен. Возможны и иные причины объединения в один пакет опреде­лений отдельных имен. Часть из них может быть при этом скрыта, ЗАЩИЩЕНА от непосредственного использования другими модуля­ми; доступ к таким именам строго регламентирован - только через имена, в спецификации пакета явно предназначенные для внешнего использования.

Задача - основной конструкт для определения асинхронного про­цесса, способного выполняться параллельно с другими процессами. Процессом называется определенным образом идентифицируемая последовательность действий исполнителя, линейно-упорядоченная во времени. В одном модуле-задаче можно определить один асинх­ронный процесс или совокупность аналогичных асинхронных процес­сов (так называемый ЗАДАЧНЫЙ ТИП). Асинхронность можно обеспечи­вать как отдельными процессорами для каждого процесса, так и "прерывистым" выполнением различных процессов на одном процессоре.

 

2.3.2. Объявления и операторы

 

В теле модуля в общем случае две части - ОБЪЯВЛЕНИЯ и ОПЕРАТОРЫ.

Объявления вводят новые знаки (ИМЕНА) и связывают их с де­нотатами (ОБЪЕКТАМИ). Эта связь имени с определенным объек­том (знаковая ситуация) сохраняет силу в пределах ОБЛАСТИ ДЕЙСТВИЯ имени. Таким образом, формально объект - это то, что можно именовать. Вместе с тем авторы языка Ада стремились к то­му, чтобы ада-объектами было удобно представлять содержательные объекты решаемой задачи. Ада-объектами могут быть, в частности, ПОСТОЯННАЯ, ПЕРЕМЕННАЯ, ТИП, ИСКЛЮЧЕНИЕ, ПОД­ПРОГРАММА, ПАКЕТ, ЗАДАЧА и РОДОВОЙ модуль.

Операторы предписывают действия, которые выполняются в по­рядке следования операторов в тексте программы (если только опе­раторы ВЫХОДА из конструкта (exit), ВОЗВРАТА (return), ПЕРЕ­ХОДА по метке (go to) или возникновение исключения (исключи­тельной ситуации)  не заставят продолжить исполнение с другого ме­ста).

Оператор ПРИСВАИВАНИЯ изменяет значение переменной.

ВЫЗОВ ПРОЦЕДУРЫ активизирует исполнение соответствую­щей процедуры после связывания каждого фактического параметра (АРГУМЕНТА) с соответствующим формальным параметром (ПА­РАМЕТРОМ).

УСЛОВНЫЙ (if) и ВЫБИРАЮЩИЙ (case) операторы позволяют выбрать одну из возможных вложенных последовательностей опера­торов в зависимости от значения УПРАВЛЯЮЩЕГО ВЫРАЖЕНИЯ (условия).

Оператор ЦИКЛА - основной конструкт для описания повторяю­щихся действий. Он предписывает повторять указанные в его теле действия до тех пор, пока не будет выполнен оператор выхода, явно указанный в теле цикла, или не станет истинным условие оконча­ния цикла.

Блочный оператор (БЛОК) соединяет последовательность опера­торов с непосредственно предшествующими ей объявлениями в еди­ную ОБЛАСТЬ ЛОКАЛИЗАЦИИ. Объявленные в ней объекты счи­таются ЛОКАЛЬНЫМИ в этой области.

Имеются операторы, обслуживающие взаимодействие асинхрон­ных процессов.

При исполнении модуля могут возникать ошибочные ситуации, в которых нельзя нормально продолжать работу. Например, возможно арифметическое переполнение или попытка получить доступ к ком­поненте массива с несуществующим индексом. Для обработки таких ИСКЛЮЧЕНИЙ (исключительных ситуаций) в конце сегментов можно разместить специальные операторы РЕАКЦИИ на исключе­ние (exception). Имеются и явные операторы ВОЗБУЖДЕНИЯ иск­лючений (raise). Они включают в действие аппарат обработки воз­бужденного исключения.

 

2.3.3. Типы данных

 

Среди объектов языка Ада можно выделить ОБЪЕКТЫ ДАН­НЫХ (т.е. объекты, которым разрешено играть роль данных по от­ношению к каким-либо операциям). Каждый объект данных в Аде характеризуется определенным ТИПОМ. Своеобразие этого языка в значительной степени связано именно с системой типов. Для тех, кто работал только с Фортраном, Алголом и Бейсиком, многое в этой системе окажется совершенно незнакомым. В частности, воз­можность определять новые типы, отражающие особенности решае­мой задачи. Для освоивших Паскаль адовские типы привычнее, но  система адовских типов полнее и строже.

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

Тип ограничивает, во-первых, ОБЛАСТЬ ЗНАЧЕНИЙ объекта; во-вторых, НАБОР ОПЕРАЦИЙ, в которых объекту разрешено фи­гурировать; в-третьих, набор допустимых для него ролей в этих опе­рациях (второй операнд, результат и т.п.).

Имеется четыре категории типов: СКАЛЯРНЫЕ (в том числе ПЕРЕЧИСЛЯЕМЫЕ и ЧИСЛОВЫЕ), СОСТАВНЫЕ (в том числе РЕГУЛЯРНЫЕ (массивы) и КОМБИНИРОВАННЫЕ (записи, струк­туры)), ССЫЛОЧНЫЕ (указатели) и ПРИВАТНЫЕ (закрытые, за­щищенные - их представление для пользователя невидимо).

Скалярные типы. Когда определяют перечисляемый тип, явно указывают перечень лексем, которые и составляют область возмож­ных значений объектов вводимого типа. Такой перечень может быть списком дней недели (пн, вт, ср, чт, пт, сб, вс), списком символов некоторого алфавита ('A','B',...,'Z') и т.п. Перечисляемые типы из­бавляют программиста от необходимости кодировать содержательные объекты целыми числами. Перечисляемые типы BOOLEAN (логиче­ский) и CHARACTER (символьный) считаются ПРЕДОПРЕДЕЛЕН­НЫМИ, т.е. встроенными в язык и действующими без предварительного явного объявления в программе. Набор символов типа CHARACTER соответствует алфавиту ASCII - Американскому стан­дартному коду для обмена информацией.

Числовые типы обеспечивают точные и приближенные вычисле­ния. В точных вычислениях пользуются ЦЕЛЫМИ типами. Область возможных значений для таких типов - конечный диапазон целых чисел. В приближенных вычислениях пользуются либо АБСОЛЮТНЫМИ типами (задается абсолютная допустимая погрешность), ли­бо ОТНОСИТЕЛЬНЫМИ типами (задается относительная погреш­ность). Абсолютная погрешность задается явно и называется ДЕЛЬ­ТОЙ, относительная погрешность вычисляется по заданному допу­стимому количеству значащих цифр в представлении числа. Подра­зумевается, что абсолютные типы будут представлены машинной арифметикой с фиксированной точкой, а относительные - с плаваю­щей. Числовые типы INTEGER (целый), FLOAT (плавающий) и DURATION (временные задержки для управления задачами) счита­ются предопределенными.

Составные типы. Скалярные типы (и перечисляемые, и число­вые) выделяются тем, что объекты этих типов считаются атомарны­ми (не имеющими составляющих). Составные типы в отличие от скалярных позволяют определять структурированные объекты (мас­сивы и записи). Массивы служат значениями регулярных типов - компоненты массивов доступны по индексам. "Регулярность" масси­вов проявляется в том, что все компоненты должны быть одного ти­па. Записи (структуры) служат значениями комбинированных типов - их компоненты могут быть различных типов; компоненты записей доступны по именам-селекторам. Имена компонент одной и той же записи должны быть различны; компоненты называются также ПО­ЛЯМИ записи.

Строение записей одного типа может зависеть от значений выде­ленных полей, называемых ДИСКРИМИНАНТАМИ. Дискриминан­ты играют роль параметров комбинированного типа - задавая набор дискриминантов, выбирают определенный вариант структуры объек­тов этого типа. Поэтому типы с дискриминантами называют также ВАРИАНТНЫМИ типами.

Ссылочные типы. Если структура объектов составных типов (в случае вариантных типов - все варианты такой структуры) фиксиру­ется статически (т.е. до начала выполнения программы), то ссылоч­ные типы позволяют создавать и связывать объекты динамически (при исполнении программы, точнее, при исполнении ГЕНЕРАТО­РОВ). Тем самым появляется возможность динамически создавать сколь угодно сложные конгломераты объектов. Генератор создает объект указанного (статически известного) типа и обеспечивает до­ступ к вновь созданному объекту через переменную соответствующе­го ссылочного типа. Передавая (присваивая) ссылки, можно организовывать произвольные структуры. Важно, что и элементы, и связи в таких динамических структурах можно менять при исполнении программы.

Приватные типы. Доступ к ПРИВАТНЫМ объектам (их называ­ют также абстрактными объектами, а соответствующие типы - абст­рактными типами данных (АТД)) находится под полным контролем автора приватного типа. Такой тип всегда определяется в пакете, который называется ОПРЕДЕЛЯЮЩИМ пакетом для этого типа.

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

Концепция типа в Аде дополнена аппаратом ПОДТИПОВ (они ограничивают область значений, не затрагивая допустимых опера­ций), а также аппаратом ПРОИЗВОДНЫХ типов (они образуются из уже известных типов, наследуя связанные с ними значения и операции).

Поговорим об остальных средствах языка. Посредством УКАЗА­ТЕЛЯ ПРЕДСТАВЛЕНИЯ можно уточнить требования к реализа­ции определенных типов на целевой машине. Например, можно ука­зать, что объекты такого-то типа следует представить заданным ко­личеством битов, что такие-то поля записи должны располагаться с такого-то адреса. Можно указать и другие детали реализации, вплоть до прямой вставки машинных команд. Ясно, что, с одной сто­роны, подробное описание представления мешает переносу програм­мы в другую операционную обстановку. С другой стороны, оно мо­жет оказаться исключительно важным для качества и даже работоспособности программы. Явное указание представления помогает от­делять машинно-независимые части модулей от машинно-зависи­мых. В идеале только указатели представления и потребуется ме­нять при переносе программы.

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

Наконец, имеются средства статической параметризации модулей (действующие до начала исполнения программы, в период компиля­ции) - аппарат РОДОВЫХ модулей. Параметры таких модулей (ро­довые параметры) в отличие от динамических параметров подпрог­рамм и процедур могут быть не только объектами данных некоторо­го типа, но и такими объектами, как типы и подпрограммы (которые в Аде не считаются объектами данных). Так что общие модули, рас­считанные на применение ко всем типам данных определенной кате­гории, в Аде следует оформлять как родовые.

На этом закончим краткий обзор языка.

 

2.4. Пошаговая детализация средствами Ады

 

Рассмотрим следующую задачу.

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

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

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

 

Первый шаг детализации. Уточним постановку задачи в терми­нах языка Ада. Так как речь идет не об алгоритме, а о предоставле­нии пользователю комплекса услуг, в Ада-терминах естественно ото­бразить этот комплекс на совокупность "логически связанных" объ­ектов, в данном случае - связанных по меньшей мере совместным использованием. Другими словами, первое наше решение - создавать ПАКЕТ, а не подпрограмму или задачу. Вспоминая, что разделение спецификации и тела пакета позволит скрыть от пользователей па­кета детали реализации (в частности, представление сети, в полном соответствии с требованиями), получаем еще одно подтверждение, что решение правильное.

Итак, создаем ПАКЕТ. Нужно придумать ему название, выража­ющее назначение предоставляемого комплекса услуг. Попробуем "сеть". Нехорошо. По-видимому, так лучше называть тот объект, который будет моделироваться, и чье представление нужно скрыть в теле нашего пакета. Попробуем "моделирование сети связи". Луч­ше, но слишком конкретно. Хотя в постановке задачи и требованиях речь идет именно о моделировании сети связи, однако специфика связи (кроме самой сети) ни в чем не отражена (нет и речи о пропу­скной способности каналов, классификации сообщений и т.п.), да и специфика моделирования не затронута (никаких моделей отправи­телей, получателей и т.п.). Скорее мы собираемся предоставить лишь комплекс услуг по управлению сетью. Так и назовем пакет:

"управление_сетью".

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

 

Второй шаг детализации. Теперь нужно написать спецификацию пакета, объявив все объекты, с которыми сможет работать пользова­тель:

 

0.   with параметры_сети; use параметры_сети;

1.   package управление_сетью is

2.       type узел is new INTEGER range 1..макс_узлов;

3.            type число_связей is new INTEGER range 0..макс_связей;

4.            type индекс_узла is new INTEGER range 1..макс_связей;

5.            type перечень_связей is array (индекс_узла) of узел;

6.            type связи is

7.           record

8.                           число : число_связей := 0;

9.                           узлы : перечень_связей;

10.         end record ;

11.-- операции над сетью

12.        procedure вставить (X : in узел);

13.        procedure удалить (X : in узел);

14.        procedure связать (X, Y : in узел);

15.-- сведения о текущем состоянии сети

16.        function узел_есть (X : узел) return boolean;

17.        function все_связи (X : узел) return связи;

18. end управление_сетью;

 

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

Но при пошаговой детализации нельзя заранее объявить те име­на, которые понадобятся - они попросту неизвестны. Когда проекти­руют совокупность модулей, это не помеха (порядок модулей несу­ществен). А вот внутри модулей правило последовательного определения мешает пошаговой детализации [особенно внутри пакетов; по­чему?]. Приходится либо применять средства, выходящие за рамки Ады (например, псевдокод), либо записывать пакет "с конца к нача­лу" - этот порядок с учетом правила последовательного определения лучше отражает последовательность появления имен при пошаговой детализации.

 

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

 

Упражнение. Укажите внешний эффект (исходные данные и результаты) хотя бы одного из таких средств.

 

Подсказка. Редактор, располагающий фрагменты Ада-программы в порядке, соот­ветствующем правилу последовательного определения.

 

Упражнение (повышенной сложности). Разработайте проект хотя бы одного та­кого средства; проект комплекса таких средств.

 

Итак, проявим более мелкие шаги проектирования нашего паке­та.

Шаг 2.1 (строка 17). Объявляем функцию с названием "все_связи", формальным параметром с названием "X" (значениям этого па­раметра приписан тип с названием "узел") и результатом, которому приписан тип с названием "связи".

Ниже будем писать короче: функцию "все_связи" с параметром "X" типа "узел" и результатом типа "связи.

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

Шаг 2.2 (строка 16). Нехорошо запрашивать связи узла, не зная, имеется ли он в сети. Поэтому (продолжая предоставлять средства узнавать о состоянии сети) объявляем функцию узел_есть с пара­метром "X" типа узел и результатом логического типа (BOOLEAN).

 

Замечание. Обратите внимание, мы записываем только формальные СПЕЦИФИ­КАЦИИ (заголовки) функций. Содержащихся в них сведений достаточно, чтобы можно было (столь же формально) написать вызов такой функции. Но, во-первых, рано или поздно придется написать ТЕЛО функции (сделаем это в ТЕЛЕ пакета). Во-вторых, нужно как-то сообщить пользователю, что же делает объявляемая функ­ция.

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

 

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

 

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

 

С точки зрения пользовательской документации на программное изделие, ЯП всегда выступает в роли инструмента реализации. Он тем лучше, чем проще объяс­нить пользователю назначение выделенных программных компонент и чем ими удоб­нее и дешевле пользоваться. Назовем соответствующий критерий качества языка кри­терием выделимости.

 

По выделимости Ада превосходит, например, Алгол-60 или Бейсик, так как по­зволяет адекватно оформлять не только компоненты-функции, но и компоненты-дан­ные, и компоненты-задачи, и компоненты более "тонкого" назначения. Другими сло­вами, Ада выигрывает в выделимости потому, что предоставляет более развитые сред­ства абстракции и конкретизации.

 

Упражнение . При чем здесь абстракция-конкретизация?

 

Шаг 2.3 (строки 12-14). Предоставляя средства для изменения се­ти, определяем три процедуры: вставить, удалить и связать (пара­метры у них типа узел).

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

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

Шаг 2.4 (строка 2). Определяем тип "узел". Этот тип уже час­тично нами охарактеризован (где?) - данные типа "узел" могут слу­жить аргументами всех процедур и функций, объявленных в нашем пакете. Другими словами, рассматриваемый тип уже охарактеризован  по фактору применимых операций. Выписывая его явное опреде­ление, мы характеризуем данные этого типа по фактору изменчиво­сти - указываем, что диапазон (range) их возможных значений - це­лые числа от 1 до числа макс_узлов (пока еще не определенного). Одновременно мы относим объявляемый тип к категории целых числовых типов и тем самым завершаем его характеристику по фактору применимых операций (в Аде для целых типов предопределены обычные операции целой арифметики - сложение "+", вычитание "-", умножение "*" и др.).

Шаг 2.5 (строки 6-10). Определяем тип "связи" результата функ­ции все_связи. Замысел в том, чтобы эта функция сообщала число связей указанного узла и перечень связанных с ним узлов. В Алголе-60 или Фортране не могло быть функций, которые в качестве результата выдают составной объект. В Аде можно ввести составной тип, объекты которого состоят либо из однотипных подобъектов - яв­ляются массивами, либо из разнотипных - являются записями. Ре­зультат задуманной нами функции все_связи - пара разнотипных объектов (число и узлы). Другими словами, это запись, первое поле которой называется "число", а второе - "узлы". Тип значений пер­вого поля назван "число_связей", второго - "перечень_связей".

В этом же объявлении указано, что при создании объекта типа "связи" его поле "число" получает начальное значение 0. Это так называемая ИНИЦИАЛИЗАЦИЯ объектов, которой нет, например, в Алголе-60, но для знающих Фортран - дело привычное (вспомните объявление начальных данных DATA).

Итак, на шаге 2.5 снова кое-что определилось, но опять появи­лись новые имена - число_связей и перечень_связей.

Шаг 2.6 (строка 5). Перечень_связей определяем как регулярный тип одномерных массивов, составленных из объектов типа узел, до­ступ к которым - по индексам типа индекс_узла.

Шаг 2.7 (строка 4). Индекс_узла определяем как тип объектов, значения которых лежат в диапазоне целых чисел от 1 до макс_связей (максимального числа связей у узла в сети - оно пока не опреде­лено).

Шаг 2.8 (строка 3). Число_связей определяем как тип объектов, значения которых лежат в диапазоне целых чисел от 0 до макс_связей. Как видите, этот тип похож на предыдущий, но отличается своей ролью и диапазоном значений.

Остались неопределенными только имена макс_узлов и макс_связей. Их неудобно фиксировать в том же модуле - ведь они могут из­меняться в зависимости от потребностей пользователя и наличных ресурсов. Поэтому будем считать, что эти имена определены во внешнем для нашего модуля контексте, а именно в пакете с именем "параметры_сети". Доступ к этому контексту из модуля управление_сетью обеспечивается его нулевой строкой.

Это так называемое УКАЗАНИЕ КОНТЕКСТА. После ключевого слова with в нем перечисляются пакеты, объявления из которых счи­таются видимыми в модуле, непосредственно следующем за таким указанием.

Пакет параметры_сети можно определить, например, так:

 

1.           package параметры_сети is

2.              макс_узлов : constant INTEGER := 100;

3.              макс_связей: constant INTEGER := 8;

4.           end параметры_сети;

 

Тем самым макс_узлов определено в качестве ПОСТОЯННОЙ целого типа со значением 100, а макс_связей - в качестве постоян­ной того же типа со значением 8. Значения постоянных нельзя ме­нять при исполнении программы (вот еще один элемент прогнозиро­вания и контроля в Аде).

 

2.5. Замечания о конструктах

 

Рассмотрим написанные фрагменты программы еще раз. Теперь поговорим о строении, смысле и назначении использованных конст­руктов.

В целом мы написали две СПЕЦИФИКАЦИИ ПАКЕТА. Отличи­тельный признак этого конструкта - ключевое слово package (пакет). Спецификация пакета содержит объявления имен, которые стано­вятся доступными при использовании пакета посредством указания контекста (например, объявления из пакета параметры_сети стано­вятся доступны в пакете управление_сетью, если указать контекст with параметры_сети).

Спецификацию пакета можно оттранслировать и поместить в ТРАНСЛЯЦИОННУЮ БИБЛИОТЕКУ. Получится модуль, пригод­ный для связывания (посредством указаний контекста) с другими (использующими его) модулями в процессе их трансляции и загрузки.

Пакет может состоять из одной спецификации или из специфика­ции и тела. Например, для пакета параметры_сети тело не требуется в отличие от пакета управление_сетью (как Вы думаете, почему?).

Если пакет состоит из двух частей (спецификации и тела), то вы­полнять программу, в которой отсутствует одна из них, нельзя. Од­нако для трансляции использующих модулей достаточно одной только спецификации используемого пакета. Итак, создавать и транслировать спецификации пакетов можно отдельно от их тел, но исполнять - только совместно с телами пакетов. В спецификацию пакета входит совокупность ОБЪЯВЛЕНИЙ.

Так, в каждой из строк 2-3 спецификации пакета параметры_сети находится ОБЪЯВЛЕНИЕ ПОСТОЯННОЙ, точнее, ОБЪЯВЛЕ­НИЕ ЧИСЛА. Это одна из разновидностей ОБЪЯВЛЕНИЯ ОБЪЕК­ТА. Назначение всякого объявления объекта - связать имя с харак­теристиками поведения объекта, названного этим именем. Поэтому обязательными компонентами объявления служат само вводимое имя, ключевые слова, отличающие разновидность объявления и тем самым характеризующие поведение объявляемого объекта в целом, и компоненты-параметры, уточняющие характеристики поведения.

Так, в строке 2 объявляемое имя - макс_узлов, уточняющие па­раметры - имя типа (INTEGER) и константа 100 (изображение це­лого числа). Полное объявление связывает с именем объекта макс_узлов тип INTEGER и константу 100 как характеристику по­ведения объекта. Попросту говоря, имя макс_узлов начинает обозна­чать константу 100 типа INTEGER.

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

 

Вернёмся к спецификации пакета управление_сетью (на стр. 30). В каждой из ее строк 2,3 и 4 мы написали ОБЪЯВЛЕНИЕ ТИПА. В нем всегда указывают, как совокупность значений объявляемого ти­па образуется из совокупности значений ранее известных типов (предопределенных или ранее объявленных). В нашем случае в стро­ке 2 указано, что новый тип "узел" образован из предопределенного типа INTEGER (является типом, ПРОИЗВОДНЫМ от типа INTEGER), причем данные типа "узел" могут обозначать только целые из диапазона от 1 до макс_узлов. В строке 3 и 4 аналогичные сведения сообщаются о типах число_связей и индекс_узла, только здесь указаны другие диапазоны.

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

В строке 5 - объявление типа, но на этот раз не скалярного (как в строках 2-4), а СОСТАВНОГО, точнее РЕГУЛЯРНОГО. Указано, как значения нового типа перечень_связей образуются из значений типов "узел" и индекс_узла. Именно, значения типа перечень_связей - это одномерные (так как указан лишь один диапазон индексов) МАССИВЫ, компонентами которых служат значения типа узел, а доступ к этим компонентам - по индексам типа индекс_узла.

В строках 6-10 - также объявление составного типа, но на этот раз - КОМБИНИРОВАННОГО. Указано, что значениями нового ти­па "связи" могут быть любые ЗАПИСИ с двумя полями. Первое по­ле с именем "число" и допустимыми значениями типа "число_связей" (при создании записи этому полю присваивается начальное значение 0). Второе поле с именем "узлы" типа перечень_связей.

Если в модуле, использующем наш пакет, объявлена переменная, например, X типа "связи" и I типа индекс_узла, то через Х.узлы(I) обозначается значение типа узел, которое служит I-й компонен­той поля "узлы" переменной X.

Строки 11 и 15 - это примечания, не влияющие на смысл модуля. Примечанием считается остаток любой строки, начинающийся с двух минусов.

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

В строках 12-14 - ОБЪЯВЛЕНИЯ ПРОЦЕДУР. В скобках указа­ны имена (названия) формальных параметров, их типы и РЕЖИМ использования (in - только для чтения - ВХОДНЫЕ параметры; out - только для записи - ВЫХОДНЫЕ; in out - и для чтения, и для за­писи - ОБНОВЛЯЕМЫЕ). Режим in напоминает вызов параметров значением в Алголе или Паскале, in out - вызов параметров со спе­цификацией var в Паскале или ссылкой в Фортране, out - точного аналога в этих ЯП не имеет.

В строках 16-17 - ОБЪЯВЛЕНИЯ ФУНКЦИЙ. Отличаются от процедур ключевым словом function (а не procedure) и указанием типа результата (после return). Режим параметров не указывается, потому что для функций всегда подразумевается режим in (все па­раметры функций - только входные, т.е. функции не могут менять значения своих аргументов).

Обратите внимание, в спецификации пакета указаны лишь спе­цификации (заголовки) процедур и функций. В таком случае их те­ла следует поместить в ТЕЛО ПАКЕТА, о котором пойдет речь в следующем разделе.

На этом закончим предварительное знакомство с Ада-конструктами.

 

2.6. Как пользоваться пакетом управление_сетью

 

Пусть нужно построить сеть из пяти узлов (13, 33, 25, 50, 90) и шести дуг (13, 33), (33, 25), (33, 50), (33, 90), (13, 50) и (25, 90). (Нарисуйте такую сеть.)

Это можно сделать следующей процедурой построение_сети:

 

with управление_сетью;

use управление_сетью;

procedure построение_сети is

begin

вставить(ЗЗ); вставить(13);

связать(33, 13); вставить(25);

связать (33,25); вставить (50);

вставить (90); связать (33,50);

связать(33,90); связать(13,50);

связать (25,90);

end построение_сети;

 

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

Как уже сказано, строка с ключевым словом with   называется УКАЗАНИЕМ КОНТЕКСТА (with  clause). Указание контекста де­лает видимыми (доступными по ПОЛНЫМ именам) все услуги, объ­явленные в пакетах, перечисленных вслед за with. Например, к про­цедуре "вставить" можно было бы обратиться так:

 

управление_сетью.вставить(...);

 

а объявить переменную типа "связи" можно так:

 

А : управление_ сетью.связи;

 

Строка с ключевым словом use называется УКАЗАНИЕМ СО­КРАЩЕНИЙ (use clause). Это указание позволяет пользоваться ви­димыми именами, не предваряя их именем пакета. Так мы и посту­пили в процедуре построение_сети. Подчеркнем, что указание со­кращений действует только для уже видимых имен. Его обязательно нужно предварять указанием контекста.

Если нужно напечатать сведения о построенной сети, то это мож­но сделать следующими операторами (будем считать, что предопре­делены процедуры новая_строка (переход на новую строку при печа­ти) и "печать" (целого числа или массива)):

 

новая_строка;

for i in узел loop

if узел_есть (i) then

печать (i);

печать (все_связи (i).узлы);

      end if;

      новая_строка;

end loop;

 

Будет напечатано

13 33 50

25 33 90

33 13 25 50 90

50 33 13

90 33 25

 

Обратите внимание, тип "узел" используется для указания диа­пазона изменения значений переменной цикла. В нашем случае тело цикла выполнится 100 раз.

Третий шаг детализации - тело пакета. До сих пор мы смотрели на наш комплекс услуг с точки зрения потенциального пользовате­ля. Теперь настало время реализовать те услуги, которые мы объя­вили в спецификации пакета. В терминах Ады это означает, что нужно спроектировать ТЕЛО ПАКЕТА управление_сетью. Создавать тело пакета будем также пошаговой детализацией.

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

Есть много вариантов такого представления (таблица, список, пе­ремешанная таблица и т.п.). Выберем представление сети массивом:

сеть : array (узел) of запись_об_узле;

Мы написали ОБЪЯВЛЕНИЕ ОБЪЕКТА. Как всякое объявление объекта, оно связывает имя ("сеть") с характеристиками того объек­та данных, который в дальнейшем будет значением (денотатом) объ­явленного имени. В нашем случае этот объект - одномерный массив с компонентами типа запись_об_узле, доступными по индексам типа узел.

Шаг 3.2. Следует заняться уточнением того, как устроен объект типа запись_об_узле. Естественно считать, что это некоторая струк­тура данных, куда вносятся сведения о том, включен ли узел в сеть и если да, то какие узлы с ним связаны. Объявим тип запись_об_узле.

 

type запись_об_узле is record

включен : BOOLEAN := false;

связан   : связи;

end record;

 

Итак, каждая запись об узле состоит из двух полей. Поле с име­нем "включен" и начальным значением false служит признаком включения узла в сеть, а поле с именем "связан" содержит все свя­зи узла.

Шаг 3.3. Теперь все готово, чтобы заняться операциями над сетью. Начнем с функции узел_есть.

Уточним ее внешний эффект: она должна быть применима к лю­бому объекту типа "узел" и должна выдавать результат true, если узел с таким именем есть в сети, и false в противном случае.

Мы сформулировали ее содержательный эффект. Такого рода сведения о функции узел_есть должны быть в пользовательской до­кументации. Это необходимое для пользователя дополнение к спе­цификации (заголовку функции), указанной в спецификации пакета в строке 18. Но сейчас нас интересует реализация функции. Поэтому следует обеспечить ее содержательный эффект в реализационных терминах, в частности, через представление сети (которое пользова­телю недоступно и даже может оказаться неизвестным). Было бы ес­тественным выдавать в качестве результата просто значение поля "включен" записи об узле. Но для этого на всю остальную реализа­цию пакета необходимо наложить единое требование (если угодно, определить дисциплину работы с этим полем): его значением в лю­бой компоненте массива "сеть" после выполнения любого действия должно быть true, если узел есть в сети, и false в противном случае. При выполнении этого требования необходимый содержательный внешний эффект функции узел_есть обеспечивается следующим объявлением (определением):

 

function узел_есть(Х : узел) return BOOLEAN is

begin

return сеть (X) .включен;

end узел_есть;

 

Обратите внимание, в полном определении функции повторена ее спецификация.

ОПЕРАТОР ВОЗВРАТА (return) завершает исполнение тела функции, доставляя в качестве ее результата значение указанного выражения. В нашем случае это ВЫБОРКА (поля "включен" из за­писи, находящейся в массиве "сеть" по индексу, указываемому значением формального параметра "X").

Шаг 3.4. Займемся реализацией функции все_связи. Ее содер­жательный внешний эффект - проявление связей узла. При соответ­ствующей дисциплине работы с сетью ее реализация могла бы быть такой:

 

function все_связи(Х : узел) return связи is

begin

return сеть (X) .связан;

end все_связи;

 

Вопрос. В чем должна состоять требуемая дисциплина?

 

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

Шаг 3.5. Реализация процедуры "вставить" (с очевидным содер­жательным эффектом) может выглядеть так:

procedure вставить (X : in узел) is

begin

сеть(X) .включен := true;

сеть (X).связан.число := 0;

end вставить;

 

Теперь займемся процедурами "удалить" и "связать". Они чуть сложнее за счет того, что нужно вносить изменения в несколько компонент массива "сеть".

Шаг 3.6. Содержательный эффект процедуры "удалить" очеви­ден: узел с указанным именем должен быть удален из сети и (с уче­том требования поддерживать целостность сети)  все связи, в которых он участвовал, должны быть ликвидированы.

Такого содержательного эффекта можно достичь многими спосо­бами. Здесь естественно учесть то, что пользователи ли­шены возможности непосредственно изменять "сеть" (например, яв­ными присваиваниями этому массиву), они могут к нему добираться только посредством объявленных в спецификации пакета процедур и функций. Наша задача как реализаторов пакета - обеспечить согла­сованный внешний эффект объявленных услуг (при этом внутрен­ний эффект процедур и функций можно варьировать).

Другими словами, действие процедуры "удалить" на массив "сеть" должно быть таким, чтобы функции узел_есть и все_связи выдали результаты, согласованные с содержательным представлени­ем об отсутствии узла в сети. Один вариант реализации - присвоить false соответствующему полю "включен" и подправить поле "свя­зан" во всех узлах, с которыми был связан удаляемый узел. Другой вариант - в этой процедуре поле "связан" не подправлять, но изме­нить реализацию функции все_связи так, чтобы перед выдачей ре­зультата она приводила поле "связан" в соответствие с полем "включен".

Это и есть варианты упоминавшихся выше дисциплин работы с сетью.

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

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

 

Выберем первый вариант реализации.

procedure удалить(X : in узел) is

begin

сеть (X) .включен := false;

for i in 1..сеть(Х).связан.число loop

чистить(Х,сеть(Х) .связан.узлы (i));

end loop;

end;

 

Понадобилась процедура "чистить", которая должна убрать в уз­ле, указанном вторым параметром, связь с узлом, указанным пер­вым параметром.

 

procedure чистить(связь : узел, в_узле : узел) is

begin

for i in 1..сеть(в_узле).связан.число loop

if сеть(в_узле).связан.узлы (i) = связь then

переписать(в_узле, после => i);

          end if;

end loop;

end чистить;

 

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

 

procedure переписать(в_узле : in узел, после : in индекс_узла) is

запись:связи renames сеть (в_узле).связан;

begin

запись.число := запись.число - 1;

for j in после..запись.число loop

запись.узлы(j) := запись.узлы(j+1);

end loop;

end переписать;

 

Здесь мы впервые воспользовались ОБЪЯВЛЕНИЕМ ПЕРЕИМЕ­НОВАНИЯ (renames) чтобы сократить имена и сделать их более наглядными. Этот же прием можно было применять и раньше. Напомним, что о диагностике ошибок мы пока не заботимся (предполагается, что пе­ред применением процедуры "удалить" всегда применяется функция узел_есть чтобы не было попытки удалить несуществующий узел).

"Запись" - это имя объекта типа "связи" (объекта сеть(в_узле).связан), локальное для процедуры "переписать". Общий вид ОБЪЯВЛЕНИЯ ПРОЦЕДУРЫ:

 

<спецификация процедуры>  is

 <локальные объявления>;

 begin

<операторы>;

end процедуры;

 

 

Оборот for j in <диапазон> - это ОБЪЯВЛЕНИЕ УПРАВЛЯЮЩЕЙ ПЕРЕМЕННОЙ ЦИКЛА, область действия которой от объяв­ления до конца цикла. Внутри БАЗИСНОГО ЦИКЛА (от loop до end loop) j считается постоянной. Если диапазон пуст (это бывает, когда его правая граница меньше левой), базисный цикл не выпол­няется ни разу. Иначе он выполняется при всех последовательных значениях j из указанного диапазона, если только выполнение всего оператора цикла не будет досрочно завершено оператором выхода (exit).

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

Шаг 3.7. Содержательный эффект процедуры "связать" также очевиден: она применима к включенным в сеть узлам; после ее при­менения узлы считаются связанными.

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

По-прежнему не будем заботиться о диагностике ошибок, когда связей оказывается слишком много (больше макс_связей). Но если два узла просят связать вторично, то будем такой запрос игнориро­вать. Следует учесть также, что требование связать узел с самим собой вполне законно.

 

 

procedure связать (X, Y: in узел) is

begin

if not есть_связь(Х, Y) then

     установить_связь(Х, Y);

 if X /= Y then

  установить_связь(У, X);

end if;

end if;

end связать;

 

Мы ввели вспомогательную функцию есть_связь с очевидным эф­фектом (возможно, ее полезно и пользователю предоставить) и вспо­могательную процедуру установить_связь, которая призвана вносить изменения в массив "узлы" своего первого аргумента. (Ключевое слово not - это знак отрицания (унарная логическая операция)).

Продолжим детализацию.

 

function есть_связь(Х, Y : узел) return BOOLEAN is

 запись : связи renames сеть(X).связан;

begin

for i in 1..запись.число loop

 if запись.узлы (i) = Y then

return true;

end if;

end loop;

     return false;

end есть_связь;

 

procedure установить_связь(откуда, куда : in узел) is

запись : связи renames сеть(откуда).связан;

begin

запись.число := запись.число+1;

запись.узлы(запись.число) := куда;

end установить_связь;

 

Таким образом, количество связей увеличивается на единицу и в качестве последней связи записывается имя узла "куда".

 

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

 

Подсказка. В переименовании участвуют динамические параметры.

 

 

Итак, все услуги реализованы. Осталось выписать полное тело пакета. Для экономии места и времени позволим себе не выписы­вать объявления процедур и функций полностью, обозначая пропу­ски многоточием.

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

 

 

 

package body управление_сетью is

 type запись_об_узле is

 record

включен : BOOLEAN : = false;

связан: связи;

 end record;

сеть : array (узел) of запись_об_узле;

function узел_есть(Х : узел) return BOOLEAN is

            ……

function все_связи(Х : узел) return связи is

            ……

procedure вставить (X : in узел) is

            ……

procedure переписать(в_узле : in узел, после : in индекс_узла)

            ……

procedure чистить(связь : узел, в_узле:узел)  is

            ……

procedure удалить(Х : in узел) is

            ……

      function есть_связь(Х, Y : узел) return BOOLEAN is

.…..

procedure установить_связь(откуда, куда : in узел) is

……..

procedure связать (X, Y : in узел) is

……..

end управление_сетью;

 

Подчеркнем, что тип запись_об_узле, объект "сеть", процедуры "переписать", "чистить", "установить_связь", функция "есть_связь" недоступны пользователю, так как объявлены в теле, а не в спецификации пакета.

Третий шаг детализации завершен. Осталась прокомментировать полученный результат.

 

2.7. Принцип раздельного определения, реализации и использования услуг (принцип РОРИУС)

 

Итак, мы написали три сегмента: спецификацию пакета управление_сетью, процедуру построение_сети и тело пакета управление_сетью. Важно понимать роли этих сегментов в жизненном цикле программы.

В них воплощен принцип раздельного определения, реализации и использования услуг (РОРИУС). По существу, это рациональное применение абстракции на различных этапах проектирования.

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

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

Проектируя реализацию, отвлекаемся от несущественного (с точки зрения реализации) в определении и использовании.

 

Упражнение. Приведите конкретные примеры деталей, несущественных при определении, реализации и использовании соответственно.

 

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

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

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

 

2.8. Принцип защиты абстракций

 

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

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

 

3. Важнейшие абстракции: данные, операции, связывание

 

3.1. Принцип единства и относительности трех абстракций

 

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

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

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

Первые две роли выделяют и обсуждают чаще. Однако значение связывания никак не меньше. Как будет показано, оно отражает не только разнообразные способы управления, но и разнообразные способы конкретизации в программировании.

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

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

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

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

 

Вопрос. Когда полезна такая спецификация?

 

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

 

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

 

Вопрос. Зачем могут понадобиться такие спецификации?

 

Подсказка. Представьте "живые" данные, отражающие состояние  взаимодействующих процессов. Какие операции потребуются, чтобы их реализовать? Какой должна быть среда, в которую можно перенести такие данные? Конкретный пример – монитор Хоара-Хансена, о котором пойдет речь в главе об асинхронных процессах. Для его реализации требуются операции над сигналами, семафорами или рандеву.

 

 

3.2. Связывание

 

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

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

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

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

Итак, можно говорить о связывании загрузочных модулей для последующего совместного использования. Такая работа выполняется редактором связей. Можно говорить о связывании аргументов подпрограммы с ее телом для последующего совместного выполнения. Такое связывание обеспечивается вызовом и заголовком подпрограммы. Вполне разумно говорить и о связывании отдельных компонент объектной программы в процессе ее трансляции с ЯП. Выполняется оно, естественно, транслятором.

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

По-видимому, все эти примеры хорошо известны. Но общая концепция  связывания способна привести и к совершенно новому понятию, отсутствующему в традиционных ЯП.

 

 

3.3. От связывания к пакету

 

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

Рассмотрим подробнее путь к пакету на конкретном примере.

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

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

Как уже сказано, впервые нужная абстракция была осознана и оформлена соответствующим конструктом в языке Симула-67. Основная идея в том, что совокупность объявлений можно синтаксически оформить (в качестве "класса"), предварив их ключевым словом class и снабдив индивидуальным именем. Так можно получить, например, класс с именем обработка_строк, в котором будут объявлены одномерный массив и процедуры для работы с этим массивом как со строкой символов. Чтобы воспользоваться такими объявлениями (в совокупности!),  достаточно перед началом программы указать в качестве приставки имя нужного класса. Объявления из такого класса считаются выписанными в фиктивном блоке, объемлющем создаваемую программу (т.е. доступны в ней). Например, программу нормального алгоритма достаточно предварить приставкой обработка_строк.

В первом приближении основная идея ПАКЕТА совпадает с идеей класса - это также совокупность объявлений, снабженная именем и пригодная для использования в качестве "приставки". Однако в понятии "пакет" воплощены и другие важнейшие идеи, о которых уже шла речь. Подчеркнем, что к новым понятиям нас привела общая концепция связывания.

 

Вопрос. Чем идея пакета (модуля-контекста) отличается от идеи простого копирования контекста? От идеи макроопределений?

 

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

 

 

3.4. Связывание и специализация

 

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

Рассмотрим операцию "**" возведения основания х в степень n. Если понятно самостоятельное значение связывания, то легко представить себе ситуацию, когда с операцией "**" уже связан один операнд и еще не связан другой. С точки зрения итогового возведения в степень такая ситуация запрещена - еще нельзя совершить запланированный акт поведения (операнды не готовы). Но если понимать связывание как многоэтапный процесс подготовки этого акта, то рассматриваемая ситуация может соответствовать одному из этапов этого процесса. Более того, на аналогичном этапе связывания могут задерживаться целые классы таких процессов. Это повторяющееся следует выделить, обозначить и применить (пользуемся одним из важнейших общих принципов абстрагирования - принципом обозначения повторяющегося). Так получается целый ряд одноместных операций ("1**","2**","3**"...) при фиксированном основании и ряд одноместных операций ("**1","**2","**3"...) при фиксированном показателе степени. Но, например, операцию "**3" можно реализовать просто как х*х*х, что короче, проще и эффективней общей программы для "**".

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

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

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

 

 

3.4.1. Связывание и теория трансляции

 

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

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

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

Введем понятие "универсального специализатора". Вслед за Бэкусом назовем формой функцию высшего порядка, т.е. функцию, аргумент и (или) результат которой также представляет собой некоторую функцию. "Универсальный специализатор" s - это форма, которая по произвольной функции двух переменных F(X,Y) и заданному ее аргументу х0 выдает в качестве результата функцию одного аргумента s(F,x0), такую, что для всех допустимых значений параметра Y справедливо определяющее соотношение

 

(**)      s(F,x0) (Y) = F(x0,Y)   ,

 

так что s(F,x0) - это и есть остаточная функция после связывания первого параметра функции F с аргументом х0.

Покажем, как получить объявленный основной результат. Допустим, что все рассматриваемые функции и формы реализованы подходящими программами. Сохраним для этих программ те же обозначения. Так что s(F,x0) можно теперь считать программой, полученной по исходной программе F с помощью программы s.

 

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

 

Упражнение. Запишите тривиальную остаточную программу на одном из известных вам ЯП.

 

Рассмотрим теперь язык программирования L и его интерпретатор i. С одной стороны, i - это такая программа, что для всякой правильной программы р на языке L и исходных данных d

i(p,d) = r,

где r - результат применения программы р к данным d. Другими словами, программа i реализует семантику языка L - ставит в соответствие программе р результат ее выполнения с данными d. С другой стороны, i - это форма от двух аргументов (а именно так называемая ограниченная аппликация - она применяет свой первый аргумент-функцию р ко второму аргументу d, причем пригодна только для программ из L).

Интерпретатор может быть реализован аппаратно, т.е. быть отдельным устройством, предназначенным для выполнения программ на L. Однако для нас интереснее случай, когда интерпретатор реализован программой. Программа эта написана, конечно, на каком-то языке программирования М. Будем считать, что М отличен от L. Программная реализация интерпретатора интересна именно потому, что в этом случае интерпретатор представлен написанным на языке М текстом-программой, и вполне можно ожидать, что в общем случае из этого текста можно систематическими преобразованиями получать другие программы. Например, программы компилятора и суперкомпилятора, также написанные на языке М.

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

Специализация интерпретатора. Посмотрим, что собой представляет s(i,p), т.е. во что специализатор s превращает интерпретатор i после его связывания с конкретной программой р? (Ведь i - форма от двух аргументов, так что специализатор s к ней применим; при этом в соответствии со смыслом s с i связывается первый аргумент интерпретатора - р, а второй остается свободным параметром). Применяя (**), получаем

s(i,p)(d) = i(p,d) = r.

Обратите внимание, чтобы выписать результат специализатора, нужно "передвинуть" функциональные скобки на позицию вправо и опустить символ специализатора.

Другими словами, s(i,p) - это такая программа р', которая после применения к данным d дает результат г. Следовательно, р' эквивалентна программе р. Но р' написана уже на языке М, а не на L! Следовательно, р' - это перевод программы р на язык М. Итак, связав интерпретатор (написанный на языке М) с исходной программой на языке L, получили ее перевод на М.

Кратко это можно выразить так: специализация интерпретатора по программе дает ее перевод.

Подумайте, что в общем случае можно сказать о качестве полученного перевода -
скорости работы, объеме памяти; а что - о скорости перевода?

 

Специализация специализатора. Итак, при различных i специализатор дает переводы с разных языков. Нетрудно теперь догадаться, что при фиксированном i специализатор s представляет собой компилятор с языка L на язык М. Ведь, как мы видели, в этом случае он по заданной р получает ее перевод р'. Действительно, посмотрим, что такое s(s,i)? Вновь применяя (**), получаем

 

s(s,i)(p) = s(i,p).

 

Но ведь s(i,p) это р', перевод программы р на язык М! Так что s(s,i) (написанный на М) - это компилятор KLM с языка L на язык М.

Кратко выразим это так: специализация специализатора по интерпретатору дает компилятор. Или еще короче: автоспециализация по интерпретатору дает компилятор.

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

Двойная автоспециализация. Специализатор может выступать и в роли суперкомпилятора. Ведь по заданному интерпретатору i (который можно считать описанием языка L), специализатор выдает компилятор с языка L на язык М. Действительно, посмотрим, что такое s(s,s)? Опять применяя (**), получаем

s(s,s)(i) = s(s,i).

 

Но ведь s(s,i) = KLM! Так что s(s,s) - это действительно суперкомпилятор над языком М (в свою очередь написанный на М).

Кратко выразим это так: двойная автоспециализация дает суперкомпилятор.

 

Вопрос. Нельзя ли получить что-либо интересное тройной автоспециализацией?

 

Подсказка. А что если подставлять различные воплощения специализатора s?

 

Три последовательных применения специализатора удобно наглядно выразить следующей серией соотношений:

 

s(s,s)(i)(p)(d) = s(s,i)(p)(d) =s(i,p)(d) = i(p,d) = r.

 

Другими словами, s(s,s) воспринимает описание языка L (т.е. i) и выдает компилятор s(s,i), который, в свою очередь, воспринимает исходную программу р на языке L и выдает ее перевод s(i,p), который уже воспринимает исходные данные d и выдает результат r.

Таким образом, мы убедились, что абстракция связывания (точнее, частичное связывание) позволяет с единых, позиций рассмотреть важнейшие понятия теории трансляции и вывести полезные закономерности. Именно, связав i с р, получили перевод; связав s c i, получили компилятор; связав s с s - суперкомпилятор.

 

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

 

Вопрос. В чем это проявляется?

 

Приведенные выше соотношения называют соотношениями Футамуры-Турчина. Способ их изложения позаимствован у С.А.Романенко.

 

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

 

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

 

Вопрос. Можно ли иными средствами задать семантику ЯП? Предложите свои средства.

 

Написание интерпретаторов на машинных или ранее реализованных языках - хорошо известный, естественный и для многих целей удобный способ реализации ЯП.  Для некоторых из них (Лисп, Апл, Бейсик) - единственный способ полной реализации. Это справедливо для всех языков, в которых программа может меняться в процессе исполнения - только "шаг за шагом" и можно уследить за таким изменением.

 

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

 

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

 

Обратите внимание – приведён пример пользы от рассмотрения языковых концепций (связывания) с математической позиции.

 

С другой стороны, важно понимать, что формальные преобразования специализатора в компилятор и суперкомпилятор не отражают некоторых содержательных аспектов этих понятий.
Обычно компилятор применяют ради повышения скорости работы переведенных программ по сравнению с интерпретацией. Специализатор же в общем случае может выдать остаточную программу, состоящую в сущности из интерпретатора и обращения к нему. В таком случае неоткуда ждать выигрыша
в скорости. При попытках "оптимизировать" такую программу за счет раскрытия циклов и т.п. она может стать непомерно длинной. Аналогичные соображения касаются и суперкомпилятора. Тем не менее в указанном направлении получены обнадеживающие результаты для частных видов специализаторов [4,5].

 

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

 

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

 

3.5. Принцип цельности

 

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

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

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

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

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

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

 

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

 

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

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

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

 

Упражнение. Проверьте это утверждение на нашем примере с сетями. Обратите
внимание на возможность вводить содержательные понятия, не слишком беспокоясь пока о возможности их реализации средствами ЯП.

 

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

 

Упражнение. Приведите примеры нарушения этого требования в известных вам ЯП.

 

Подсказка. В Паскале функции вырабатывают только скалярный результат – нет прямого средства сопоставить "емкой" функции согласованную по "емкости"  структуру данных. Например, нельзя определить функцию все_связи. Уже упоминалось отсутствие развитых средств связывания с контекстом.

 

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

 

Упражнение. Приведите примеры нарушения этого требования в известных вам ЯП.

 

Подсказка. Проще всего это сделать по отношению к связыванию - в  традиционных ЯП этот аппарат развит слабо. Легко ли, например, на Паскале определить ПОЯ, в котором возможно раздельно определять заголовки и тела процедур, а связывать их но мере необходимости?

 

Итак, несмотря на свой весьма абстрактный характер (скорее, благодаря ему), принцип цельности, обнаруживает "точки роста" ЯП, намечает тенденции их развития: в частности, от языков (готовых) программ к языкам собственно программирования, позволяющим с исчерпывающей полнотой управлять связыванием компонент программы [3].

 

3.5.1. Принцип цельности и нормальные алгоритмы

 

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

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

 

Вопрос.  В чем это проявляется?

 

Таким образом, качество ЯП не определяется критерием цельности самим по себе. Влияние этого критерия на оценку качества ЯП зависит от того, какие технологические потребности признаются определяющими.

 

Известный тезис нормализации утверждает, что всякий алгоритм можно заменить
эквивалентным нормальным алгоритмом. Но важно хорошо понимать смысл слова
"можно" в этом тезисе. Можно заменить, если принять абстракцию потенциальной
осуществимости, отвлечься от таких “несущественных деталей”, как необходимые для этого ресурсы. Любой, кто писал нормальные алгоритмы, прекрасно понимает, что ни одной реальной программы непосредственно в исходной модели Маркова нельзя даже написать - она практически наверняка будет неправильной и отладить ее будет невозможно в обозримое время (даже если предположить сколь угодно высокую скорость выполнения самих марковских подстановок). Ведь все выделяемые при программировании абстракции, как данных, так операций и связывания, нужно подразумевать или хранить вне программы. Поэтому единственный разумный путь к практической осуществимости программирования на языке нормальных алгоритмов – моделировать на этом языке другой, более совершенный в технологическом отношении язык.

 

3.5.2. Принцип цельности и Ада. Критерий цельности

 

   Как видно на примере Ады, в более современных ЯП принцип согласования абстракций (как между собой, так и с важнейшими технологическими потребностями) осознан и учтен в гораздо большей степени. Взглянем на шаги с 3.1 по 3.7 на стр 36-39 с точки зрения потребности согласовывать абстракции.

Выделив на шаге 3.1 операционную абстракцию – функцию все_связи, мы немедленно ощутили потребность обозначить классы возможных аргументов и результатов этой функции, не занимаясь их детальной проработкой. Если бы приходилось работать на Фортране, Алголе-60 или Бейсике, сделать это оказалось бы невозможным - непосредственно подходящих предопределенных типов данных в этих ЯП нет, а возможность строить новые типы также отсутствует. Скорее всего пришлось бы нарушить естественный порядок детализации и сначала придумать способ представлять "связи" некоторым массивом, а затем учесть, что в этих ЯП функции не вырабатывают результаты-массивы (только скаляры), и представить нужную абстракцию не функцией, а процедурой. Важно понимать, что такого рода отклонения запутывают логику программы, провоцируют ошибки, затрудняют отладку и т.п.

Лучшее, что можно сделать в этом случае, - выйти за рамки используемого ЯП и фиксировать шаги детализации на частично формализованном псевдокоде. О применении такого псевдокода в структурном подходе к программированию на классических ЯП можно прочитать, например, в [6].

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

Обратите внимание, мы ввели не названия отдельных объектов данных (только так и можно в классических ЯП), а именно названия целых классов (типов) обрабатываемых объектов.

Определения типов мы также имели возможность вводить по шагам, вполне аналогично тому, как в классических ЯП вводят операционные абстракции, выделяя нужные процедуры. На шаге 3.1 обозначили новый тип "связи"; на шаге 3.5 уточнили его строение, но потребовались названия типов число_связей и перечень_связей. На шагах 3.6 и 3.7 уточнили строение этих типов, но остались неопределенными макс_узлов и макс_связей. Наконец, уточнили их характеристики и даже значения.

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

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

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

 

Упражнение. Пользуясь критерием цельности, оцените другие известные ЯП.

 

4. Данные и типы

 

4.1. Классификация данных

 

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

Данными обычно считают любые обрабатываемые объекты независимо от их внутренней природы. Одна и та же категория объектов в одном ЯП может выступать в роли данных, а в другом это может быть запрещено. Так, процедуры могут быть данными в Паскале и Алголе-68 (их можно передавать в качестве значений, присваивать компонентам других объектов), но не в Аде.

Данные различаются по многим признакам.

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

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

Во-вторых, данные различаются по своему внутреннему строению, структуре, характеру связей своих составляющих. Например, массивы, таблицы, списки, очереди. С этой точки зрения важен способ доступа к составляющим данных. Классификация данных по способу доступа к составляющим обычно имеется в виду, когда говорят о структурах данных. Классификация данных по их структуре есть в том или ином варианте почти во всех ЯП. В современных ЯП чаще всего выделяются массивы и записи. И то, и другое можно считать частным случаем таблиц, которые служат ключевой структурой, например, в МАСОНе [10] и его последующих модификациях.

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

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

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

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

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

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

Например, если данное представляет собой число, символ, указатель, задачу, очередь, стек, то в каждом из этих случаев к нему применим определенный набор операций, у него имеется определенный набор атрибутов, характерных для данных именно этого класса, и т.п. Чтобы не было путаницы с первым фактором классификации (по содержательным ролям), подчеркнем, что переменная для хранения числа апельсинов может отличаться от переменной для хранения числа яблок по содержательной роли, но не отличаться по применимым операциям. А вот объекты типов "узел" и индекс_узла различаются по применимым операциям (по каким?).

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

В заключение подчеркнем, что указанные факторы ортогональны. Классификация не претендует на полноту, но позволит ориентироваться, в частности, в системе управления данными в Аде. Дополнительные факторы классификации предложены, например, в языке Том [7].

 

4.2. Типы данных

 

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

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

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

Таким образом, излагаемые ниже принципы построения системы типов в Аде нужно воспринимать как интересный и в целом дееспособный вариант классификации обрабатываемых данных, но отнюдь не как окончательное единственно верное решение. Элегантная теория типов предложена А.В.Замулиным и воплощена в ЯП Атлант [8,9].

 

4.2.1. Динамические, статические и относительно статические ЯП

 

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

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

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

Например, конкретное значение переменной – динамическое свойство. Связь формального параметра с конкретным фактическим в результате вызова процедуры - динамическая связь. Размер конкретного массива с переменными границами - динамическое свойство.

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

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

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

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

Неограниченный динамизм присущ не только практически всем машинным языкам, но и многим ЯП достаточно высокого уровня. Эта концепция в разной степени воплощена в таких динамических ЯП, как Бейсик, Алл, Лисп, отечественных ИНФ и Эль-76 [10,11]. Идеология и следствия динамизма заслуживают отдельного изучения.

Другая крайняя позиция выражена в стремлении затруднить программисту всякое изменение характеристик денотатов. Вводя знак, нужно объявить характеристики денотата, а использование знака должно соответствовать объявленным характеристикам. Конечно, "неограниченной" статики в программировании добиться невозможно (почему?). Так что всегда разрешается менять, например, значения объявленных переменных.

Зато остальные характеристики в таких статических ЯП изменить трудно. Обычно стремятся к статике ради надежности программ (за счет дополнительной избыточности, при обязательном объявлении характеристик возникает возможность дополнительного контроля) и скорости объектных программ (больше связываний можно выполнить при трансляции и не тратить на это времени в период исполнения).

Вместе с тем сама по себе идея объявления характеристик (прогнозирования поведения) и контроля за их инвариантностью требует создания, истолкования и реализации соответствующего языкового аппарата. Поэтому статические ЯП, как правило, сложнее динамических, их описания объемнее, реализации тяжеловеснее. К тому же надежды на положительный эффект от статики далеко не всегда оправдываются. Тем не менее среди массовых языков индустриального программирования преобладают статические. Раньше это частично можно было объяснить трудностями эффективной реализации дина­мических ЯП. Сейчас на первое место выходит фактор надежности, и с этой точки зрения "старые" статические ЯП оказываются "недостаточно статическими" - аппарат прогнозирования и контроля у них связан скорее с требуемым распределением памяти, чем с други­ми характеристиками поведения, существенными для обеспечения надежности (содержательными ролями, изменчивостью значений, применимыми операциями и т.п.). С этой точки зрения интересен анализ возможностей динамических ЯП, в частности Эль-76, содер­жащийся в [11].

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

 

4.2.2. Система типов как знаковая система

 

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

Крайняя позиция - полное или почти полное отсутствие таких средств. Пример -  нормальные алгоритмы Маркова и другие модели, которые мы еще рассмотрим, а также Лисп, Форт, Апл, где нельзя охарактеризовать данные ни по одному из семи факторов. Конечно, эти факторы существенны независимо от применяемого ЯП. Просто при проектировании программы на ЯП без специальных средств классификации данных программист вольно или невольно использу­ет для характеристики данных внеязыковые средства (держит "в уме", отражает в проектной документации и т.п.).

Кое-что из такой "внешней" классификации находит воплощение и в программе. Но если находит, то часто в такой форме, что про­граммиста не могут проконтролировать не только компьютер, но и коллеги-программисты (да и он сам делает это, как правило, с тру­дом).

Указанная крайняя позиция игнорирует потребность прогнозиро­вания-контроля. Она характерна для ранних машинных ЯП и в чис­том виде в современном программировании не встречается.

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

Допустим, что эта тенденция осознана. Возникает следующая за­дача - какие ключевые концепции должны быть положены в основу средств описания данных? Если угодно, как построить "язык в язы­ке", знаковую систему для характеристики данных в ЯП?

 

Первый вариант: перечни атрибутов. По-видимому, первое, что приходит в голову, - ввести свои средства для характеристики каж­дого фактора и сопровождать каждый объект перечнем характери­стик.

Примерно так сделано в ПЛ/1. Объявляя переменную, в этом ЯП можно перечислить ее характеристики (так называемые "атрибуты") по многим факторам (основание системы счисления, способ пред­ставления, способ выделения памяти, структура и способ доступа к компонентам, потребуется ли печатать значения и т.п.).

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

Во-первых, довольно утомительно задавать длинные перечни ат­рибутов при объявлении данных. Из-за этого в ПЛ/1 придуманы да­же специальные "правила умолчания" атрибутов (это одна из самых неудачных и опасных с точки зрения надежности концепция ПЛ/1, к тому же этих правил много, они сложны, так что запомнить их невозможно).

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

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

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

 

Второй вариант: структурная совместимость типов. Воспользуем­ся принципом обозначения повторяющегося. Заметили, что часто приходится иметь дело с перечнями атрибутов? Значит, нужно обоз­начить повторяющийся перечень некоторым именем и упоминать это имя вместо самого перечня. Следовательно, нужен языковый конст­рукт для объявления таких имен.

Естественно считать, что имя обозначает не только сам перечень атрибутов, но и класс данных, обладающих объявленными характе­ристиками. Мы пришли к понятию типа данных как класса объек­тов-данных, обладающих известными атрибутами. А искомый языко­вой конструкт - это объявление типа данных. Его назначение - свя­зывать объявляемое имя с указанным перечнем атрибутов данных. Подчеркнем, что основой классификации данных остается перечень характеристик-атрибутов, а имя типа лишь обозначает соответству­ющий перечень.

Теперь, чтобы характеризовать данное, не нужно умалчивать ат­рибуты, как в ПЛ/1, можно коротко и ясно обозначать их одним именем. Кажется, совсем просто, но это шаг от ПЛ/1 к Алголу-68, в котором очень близкая к изложенной концепция типа данных.

С проблемой прогнозирования мы справились, а как с проблемой контроля? По-прежнему нужно сравнивать перечни атрибутов. Дру­гими словами, здесь мы никак не продвинулись. Как справиться с проблемой? Ведь для контроля поведения данных (например, конт­роля совместимости аргументов с параметрами) недостаточно знать имена типов: если имена совпадают, все ясно, а вот когда не совпа­дают, нужно проверять совместимость характеристик типов.

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

 

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

 

Третий вариант: именная совместимость типов. Поистине бле­стящее решение состоит в том, чтобы полностью избавиться от про­блемы структурной совместимости, "чуть-чуть" подправив концеп­цию типа, сделав центральным понятием не атрибуты, а имя типа. При этом типы с разными именами считаются разными и в общем случае несовместимыми (если программист не ввел соответствующих операций преобразования типов). Другими словами, забота о содер­жательной совместимости характеристик объектов полностью пере­кладывается на программиста, определяющего типы (что вполне ес­тественно). Если теперь потребовать, чтобы каждый объект данных был связан ровно с одним типом, то и прогнозировать, и проверять - одно удовольствие! Нужно сказать, что объект будет вести себя так-то, обозначаем стиль (характер) его поведения именем типа из име­ющегося набора типов. Нет подходящего - объявляем новый. Нужно проверить совместимость, сравниваем имена типов. Просто, ясно и быстро.

Но наше "чуть-чуть" - в историческом плане шаг от Алгола-68 с его структурной совместимостью типов к Аде с ее именной совмести­мостью через Паскаль, где именная совместимость принята для всех типов, кроме диапазонов (для них действует структурная совмести­мость).

 

4.2.3. Строгая типизация и уникальность типа

 

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

1.   Каждому объекту данных сопоставлен один и только один тип.

2.   Каждому типу сопоставлено одно и только одно имя (явное или неявное). Типы с неявными именами называются анонимными, и все считаются различными.

3.   При объявлении каждой операции должны быть явно указа­ны (специфицированы) имена типов формальных параметров (и результата, если он есть).

4.   Различные типы априорно считаются несовместимыми по присваиванию и любым другим операциям.

Очень близкая концепция в литературе часто называется строгой типизацией [26], а ЯП с такой концепцией типа - строго типизиро­ванными. От уникальности типа строгая типизация отличается воз­можным ослаблением четвертой аксиомы. Поэтому мы ввели специ­альный термин "уникальность типа" для "совсем строгой" типиза­ции.

 

4.2.4. Критичные проблемы, связанные с типами

 

Остался маленький вопрос. Прогнозировать - легко, проверять - легко. А легко ли программировать? Как увязать несовместимость типов, обозначенных разными именами, с естественным желанием иметь операции, применимые к объектам разных типов (полиморф­ные операции). Ведь если связать с формальным параметром опера­ции некоторый тип, то к объектам других типов эта операция ока­жется неприменимой.

Назовем отмеченную проблему проблемой полиморфизма опера­ций. Напомним, что полиморфизм операций широко распространен в математике, жизни, программировании. Операция "+" применима к целым числам, вещественным числам, матрицам, комплексным числам, метрам, секундам и т.п.

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

Каким должен быть единственный (уникальный) тип такого объ­екта? Если он отражает сразу много ролей, то не придем ли мы к отсутствию контроля (ведь пока буфер работает как очередь, к нему нужно запретить обращаться как к стеку и наоборот)? Если же толь­ко одну роль, то как ему попасть в другую (ведь разные типы апри­орно несовместимы)? Назовем выделенную проблему янус-проблемой (объект ведет себя, как двуликий Янус, даже "многоликий").

 

4.2.5. Критичные потребности и критичные языковые проблемы

 

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

Допустим, что к его услугам тип "целый", тип "комплексный", тип "матрица". Ему нужно предоставить пользователю возможность складывать объекты любого из названных типов. Хороший стиль программирования требует ввести операционную абстракцию, позво­ляющую пользователю игнорировать несущественные детали (в дан­ном случае - особенности каждого из трех типов данных) и действо­вать независимо от них. Попросту говоря, нужно ввести единую (по­лиморфную) операцию сложения. Если такой возможности ЯП не предоставит, то у программиста может оказаться единственный разумный вариант - избегать пользоваться таким ЯП. Аналогичным ситуациям нет числа. Например, есть возможность рассчитать по­требности взвода, роты, батальона, полка. Требуется ввести абстрак­цию - "рассчитать потребности подразделения" и т.п.

Мы пришли к понятию критичной технологической потребности. Технологическая потребность называется критичной для ЯП в неко­торой ПО, если язык не может выжить в этой ПО без средств удовлетворения этой потребности. Проблему удовлетворения кри­тичной потребности назовем критичной языковой проблемой.

 

 

 

4.2.6. Проблема полиморфизма

 

Допустим, что критичность проблемы полиморфизма для Ады осознана. Как же ее решать?

По-видимому, самым естественным было бы ввести "объединяющий" тип (например, "операнд_сложения" или "боевое_подразделение"), частными случаями которого были бы исходные типы. И оп­ределить нужную операцию для объединяющего типа. Но что значит "частными случаями"? Ведь в соответствии с концепцией уникаль­ности каждый объект принадлежит только одному типу. Если это "взвод", то не  "боевое_подразделение"! Так что в чистом виде эта идея не проходит.

 

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

 

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

 

type Т - взвод  is new боевое_подразделение (вид => взвод);

 

Теперь "взвод" - это уже знакомый нам ПРОИЗВОДНЫЙ тип с РОДИТЕЛЬСКИМ типом боевое_подразделение. К его объектам применимы все операции, применимые к боевому_подразделению. Но можно теперь объявить новые операции, применимые только к "взводам", т.е. к "боевым_подразделениям", в поле "вид" которых - значение "взвод". Итак, это решение проблемы полиморфизма "сверху вниз", т.е. нужно заранее предусмотреть частные случаи нужного типа.

Основной вариант решения проблемы полиморфизма, предлагае­мый Адой, это так называемое ПЕРЕКРЫТИЕ операций. Идея со­стоит в том, что связь между вызовом операции и ее объявлением устанавливается не по одному только имени операции, а по так на­зываемому "профилю" (имени операции с учетом типов операндов, типа ре­зультата и даже имен формальных параметров (если вызов по клю­чу)). Другими словами, идея перекрытия в том, что денотат знака определяется не по самому знаку, а с привлечением его ограничен­ного контекста.

Например, в одной и той же области действия можно объявить две функции:

 

function потребности (подразделение:взвод) return расчет;

function потребности (подразделение:рота) return расчет;

 

Для каждой функции нужно написать свое тело. Если теперь объ­явить объекты

А:взвод;

В:рота;

 

то вызов потребности(А) будет означать выполнение тела первой функции, а вызов потребности(В) - второй. Для пользователя же "видна"  единственная  операционная  абстракция   "потребности", применимая к объектам и типа "взвод", и типа "рота", т.е. поли­морфная функция.

 

Упражнение. Укажите дополнительный контекст знака функции в приведенном примере.

 

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

 

Вопрос.  Как это сделать?

 

4.2.7. Янус-проблема

 

Вспомним, как возникла янус-проблема. Мы пришли к концеп­ции уникальности, желая упростить контроль. И потеряли возмож­ность иметь объекты, играющие одновременно разные роли.

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

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

Еще сложней ситуация, когда классификация не иерархическая. Человек - одновременно сотрудник лаборатории, отдела, института и т.п.; жилец в квартире, доме, микрорайоне и т.п.; подписчик газеты, муж, брат, сват, любитель бега и т.п. Если нужно написать на Аде пакет моделирование_человека, то как уложиться в концепцию уни­кальности типа?

Напомним, что проблема возникла именно потому, что мы хотим прогнозировать и контролировать различные роли объектов. Если иг­норировать проблему прогнозирования-контроля, то исчезнет и янус-проблема. Как в Алголе-60 - везде массивы целых, и представ­ляй их себе в любой роли (ведь эти роли - вне программы!).

 

Полного и изящного решения янус-проблемы Ада не предлагает - этого пока нет ни в одном ЯП. Ближе всего к идеалу - объектно-ориентированные ЯП.

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

Эти средства - ПРОИЗВОДНЫЕ ТИПЫ + ПРЕОБРАЗОВАНИЯ типов + понятие ОБЪЕКТА ДАННЫХ.

 

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

Если, например, определен тип "млекопитающие", то его произ­водным может стать тип "хищные", его производным тип "кошки", его производными типы "сибирские_кошки" и "сиамские_кошки". При этом все уменьшается совокупность допустимых значений (в "хищные" не попадают "коровы", в "кошки" - "собаки", в "сибирские_кошки" - "львы") и добавляются операции и свойства (млеко­питающие - операция "кормить молоком"; хищные - "съедать живо­тное"; кошки - "влезать на дерево"; сибирские кошки - "иметь пушистый хвост"), причем всегда сохраняются операции и свойства всех родительских типов, начиная с так называемого БАЗОВОГО ТИПА, не имеющего родительского типа.

Итак, производные типы решают проблему полиморфизма "свер­ху вниз" - это одновременно частное решение янус-проблемы.

 

Вопросы. Почему это решение названо решением "сверху вниз"? Сравните с предложенным ранее решением "снизу вверх". Почему это лишь частное решение янус-проблемы?

 

Подсказка. Для полиморфизма: не полиморфная операция "надстраивается" над независимыми типами, а типы заранее строятся "сверху вниз" так, что операция над объектами "старшего" типа оказывается применимой и к объектам "младшего". Для янус-проблемы: существенно, что при объявлении типа "млекопитающие" нужно знать об атрибутах потенциальных производных типов, иначе негде спрятать "пуши­стый хвост” - подробнее об этом в разделе о наследовании с критикой Ады; вспомните также о пакете модель_человека - там не спасет иерархия типов.