/
Author: Себеста Р.
Tags: компьютерные технологии программирование языки программирования
ISBN: 5-8459-0192-8
Year: 2001
Text
ОСНОВНЫЕ КОНЦЕПЦИИ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ ПЯТОЕ ИЗДАНИЕ ВИЛЬЯМС РОБЕРТ У. СЕБЕСТА ‘ л*'’ -‘ . • Л
ОСНОВНЫЕ КОНЦЕПЦИИ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ Пятое издание РОБЕРТ У. СЕБЕСТА, Университет штата Колорадо в Колорадо-Спрингс Этот бестселлер, теперь в пятом издании, содержит широкое и глубокое обсуждение концепций языков программирования. Как и в предыдущих изданиях, автор описывает основные концепции языков программирования, освещая вопросы, связанные с разработкой различных языковых конструкций, на примере проектных решений, принятых в нескольких широко распространенных языках, и критически сравнивая различные возможные варианты их реализации. В книге описаны наиболее широко используемые методы описания синтаксиса и общепринятые подходы к описанию семантики языков программирования. В ходе изложения автор обсуждает также методы реализации языков программирования и проблемы, связанные с этим. ИЗМЕНЕНИЯ, СДЕЛАННЫЕ В ПЯТОМ ИЗДАНИИ • Освещается поддержка объектно-ориентированного программирования, параллельное выполнение программ и обработка исключительных ситуаций в языке Java™. • Дается более глубокое описание объектно-ориентированного программирования - расширенное описание объектно-ориентированного програмирования в данной книге появляется раньше, чем это делалось в предыдущих изданиях, одновременно с обсуждением императивных языков, не являющихся объектно-ориентированными. • Излагается более обширный материал, посвященный семантике, включая доказательство правильности готовых программ с использованием аксиоматической семантики. ОБ АВТОРЕ Роберт У. Себеста - профессор и декан факультета компьютерных наук в Университете штата Колорадо, Колорадо-Спрингс. Профессор Себеста получил степень доктора философии в области компьютерных наук в Университете штата Пенсильвания и преподает компьютерные науки более 25 лет. Его научные интересы лежат в области языков программирования, разработки компиляторов, а также методов и инструментов тестирования программного обеспечения. Профессор Себеста - автор нескольких книг, посвященных, в частности, структурному программированию на языке ассемблеров компьютеров PDP-11 и миникомпьютеров VAX. Он является членом Ассоциации по вычислительной технике (АСМ - Association for Computing Machinery) и Компьютерного общества Американского института инженеров электротехники и электроники (IEEE Computer Society) Издательский дом “Вильямс"
ПЯТОЕ ИЗД/ЖИЕ Основные концепции языков программирования
Основные концепции языков программирования Роберт У. Себеста Университет штата Колорадо в Колорадо-Спрингс Издательский дом “Вильямс” Москва ♦ Санкт-Петербург ♦ Киев 2001
ЬБК 32.973.26-018.2.75 С28 УДК 681.3.07 Издательский дом “Вильямс” Перевод с английского канд.физ.-мат.наук Д А. Илюшина. А.В. Назаренко Под редакцией канд.физ.-мат.нау к Д А Илюшина По общим вопросам обращайтесь в Издательский дом “Вильямс*’ по адрес}: info^u illiamspublishing.com. http://wuw.uilliamspublishing.com Себеста, Роберт, У. С28 Основные концепции языков программирования, 5-е изд. : Пер. с англ. — М.: Издательский дом “Вильямс". 2001. — 672 с.: ил. — Парал. тит. англ. ISBN 5-8459-0192-8 (рус.) Книга, ставшая бестселлером в США. посвящена всестороннему обсуждению основных концепций языков программирования. Автор описывает фундаменталь- ные понятия программирования на примере вопросов разработки различных языко- вых конструкций, подвергая критическому анализу их реализацию в широком спектре таких языков программирования, как FORTRAN, PASCAL, PL/1, С, C++, Ada. Java. Smalltalk, Eiffel и LISP. Материал книги охватывает все парадигмы программирования, начиная с функциональной и заканчивая объектно-ориенти- рованной. и. несомненно, представляет интерес как учебник по современным методологиям. ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Addison-Wesley Publishing Company, Inc Authorized translation from the English language edition published by Addison-Wesley Publishing Company, Inc . Copyright C 2002 All rights resen cd No part of this book may be reproduced or transmitted in any form or by any means, elec- tronic or mechanical, including photocopying, recording or by any information storage retrieval system, without per- mission from the Publisher Russian language edition published by Williams Publishing House according to the Agreement with R&l Enterprises International. Copyright £ 2001 ISBN 5-8459-0192-8 (рус.) С Издательский дом “Вильямс”. 2001 ISBN 0-201-75295-6 (англ.) C Addison-Wesley Publishing Company. Inc.. 2002
WIFTH EDITION Ж Concepts of Programming Languages Robert W. Sebesta University of Colorado, Colorado Springs ADDISON WESLEY Вззтс'* • Sa~ Zra~z sco • Чел vor< • -.o^con • Toronto • Sydney • ~z<,z • S r';aocre • Vaor-a • Mexico City • Munich • Paris • Cape "ел- • -e*; <c^g • Vontrea
Оглавление ПРЕДИСЛОВИЕ 18 ГЛАВА 1. ВВОДНЫЕ ЗАМЕЧАНИЯ 23 ГЛАВА 2. ОБЗОР ОСНОВНЫХ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ 57 ГЛАВА 3. ОПИСАНИЕ СИНТАКСИСА И СЕМАНТИКИ 123 ГЛАВА 4. ИМЕНА, СВЯЗЫВАНИЕ, ПРОВЕРКА ТИПОВ И ОБЛАСТИ ВИДИМОСТИ 173 ГЛАВА 5. ТИПЫ ДАННЫХ 213 ГЛАВА 6. ВЫРАЖЕНИЯ И ОПЕРАТОРЫ ПРИСВАИВАНИЯ 275 ГЛАВА 7. СТРУКТУРЫ УПРАВЛЕНИЯ НА УРОВНЕ ОПЕРАТОРОВ 303 ГЛАВА 8. ПОДПРОГРАММЫ 345 ГЛАВА 9. РЕАЛИЗАЦИЯ ПОДПРОГРАММ 393 ГЛАВА 10. АБСТРАКТНЫЕ ТИПЫ ДАННЫХ 429 ГЛАВА 11. ПОДДЕРЖКА ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПРОГРАММИРОВАНИЯ 451 ГЛАВА 12. ПАРАЛЛЕЛЬНОСТЬ 503 ГЛАВА 13. ОБРАБОТКА ИСКЛЮЧИТЕЛЬНЫХ СИТУАЦИЙ 545 ГЛАВА 14. ФУНКЦИОНАЛЬНЫЕ ЯЗЫКИ ПРОГРАММИРОВАНИЯ 579 ГЛАВА 15. ЯЗЫКИ ЛОГИЧЕСКОГО ПРОГРАММИРОВАНИЯ 615 ЛИТЕРАТУРА 649 ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ 661
2.3.3. Обзор языка FORTRAN I 65 2.3.4. Обзор языка FORTRAN II 66 2.3.5. Языки FORTRAN IV. FORTRAN 77 и FORTRAN 90 66 2.3.6. Оценка 67 2.4. Функциональное программирование: язык LISP 69 2.4.1. Истоки работ в области искусственного интеллекта и обработка списков 69 2.4.2. Процесс разработки языка LISP 70 2.4.3. Обзор языка 71 2.4.4. Оценка 72 2.4.5. Два потомка языка LISP 73 2.4.6. Родственные языки 74 2.5. Первый шаг к совершенствованию: язык ALGOL 60 74 2.5.1. Историческая ретроспектива 74 2.5.2. Начальная стадия разработки 75 2.5.3. Обзор языка ALGOL 58 76 2.5.4. Принятие отчета о языке ALGOL 58 76 2.5.5. Процесс разработки языка ALGOL 60 77 2.5.6. Обзор языка ALGOL 60 78 2.5.7. Оценка языка ALGOL 60 78 2.6. Компьютеризация коммерческих записей: язык COBOL 80 2.6.1. Исторические предпосылки 80 2.6.2. Язык FLOW-MATIC 81 2.6.3. Процесс разработки языка COBOL 81 2.6.4. Оценка 82 2.7. Начало разделения времени: язык BASIC 85 2.7.1. Процесс разработки 85 2.7.2. Обзор языка 86 2.7.3. Оценка 86 2.8. Все для всех: язык Р1У1 88 2.8.1. Исторические предпосылки 88 2.8.2. Процесс разработки 89 2.8.3. Обзор языка 89 2.8.4. Оценка 90 2.9. Два ранних динамических языка: APL и SNOBOL 91 2.9.1. Истоки и характеристики языка APL 92 2.9.2. Происхождение и характеристики языка SNOBOL 92 2.10. Возникновение абстракции данных: язык SIMULA 67 92 2.10.1. Процесс разработки 92 2.10.2. Обзор языка 93 2.11. Ортогональная структура: язык ALGOL 68 94 2.11.1. Процесс разработки 94 2.11.2. Обзор языка 94 2.11.3. Оценка 95 2.12. Несколько важных наследников семейства языков ALGOL 95 Содержание 7
2.12.1. Преднамеренная простота: язык Pascal 96 2.12.2. Машинно-независимый язык: С 98 2.12.3. Другие потомки языка ALGOL 100 2.13. Программирование, основанное на логике: язык Prolog 102 2.13.1. Процесс разработки 102 2.13.2. Обзор языка 102 2.13.3. Оценка 103 2.14. Величайший проект в истории: язык Ada 103 2.14.1. Историческая ретроспектива 104 2.14.2. Процесс разработки 104 2.14.3. Обзор языка 106 2.14.4. Оценка 106 2.14.5. Язык Ada 95 107 2.15. Объектно-ориентированное программирование: язык Smalltalk 109 2.15.1. Процесс разработки 109 2.15.2. Обзор языка 110 2.15.3. Оценка 111 2.16. Объединение императивных и объектно-ориентированных свойств: язык C++ 112 2.16.1. Процесс разработки 112 2.16.2. Обзор языка 113 2.16.3. Оценка 114 2.16.4. Родственный язык: Eiffel 114 2.17. Программирование в World Wide Web: язык Java 115 2.17.1. Процесс разработки 115 2.17.2. Обзор языка 116 2.17.3. Оценка 117 ГЛАВА 3. ОПИСАНИЕ СИНТАКСИСА И СЕМАНТИКИ 123 3.1. Введение 124 3.2. Общая задача описания синтаксиса 125 3.2.1. Устройства распознавания языков 126 3.2.2. Генераторы языков 126 3.3. Формальные методы описания синтаксиса 127 3.3.1. Форма Бэкуса-Наура и контекстно-свободные грамматики 127 3.3.2. Расширенная форма БНФ 138 3.3.3. Синтаксические графы 139 3.3.4. Грамматики и устройства распознавания языков 140 3.4. Рекурсивный нисходящий синтаксический анализ 141 3.5. Атрибутивные грамматики 143 3.5.1. Статическая семантика 144 3.5.2. Основные понятия 144 3.5.3. Определение атрибутивных грамматик 144 3.5.4. Внутренние атрибуты 145 3.5.5. Примеры атрибутивных грамматик 146 8 Содержание
3.5.6. Вычисление значений атрибутов 148 3.5.7. Оценка 149 3.6. Описание смысла программ: динамическая семантика 150 3.6.1. Операционная семантика 150 3.6.2. Аксиоматическая семантика 152 3.6.3. Денотационная семантика 162 ГЛАВА 4. ИМЕНА, СВЯЗЫВАНИЕ, ПРОВЕРКА ТИПОВ И ОБЛАСТИ ВИДИМОСТИ 173 4.1. Введение 174 4.2. Имена 174 4.2.1. Вопросы структуры 175 4.2.2. Виды имен 175 4.2.3. Специальные слова 176 4.3. Переменные 177 4.3.1. Имя 177 4.3.2. Адрес 177 4.3.3. Тип 179 4.3.4. Значение 179 4.4. Концепция связывания 179 4.4.1. Связывание атрибутов с переменными 180 4.4.2. Связывание типов 180 4.4.3. Связывание переменных с ячейками памяти и время их жизни 184 4.5. Проверка типов 187 4.6. Строгая типизация 188 4.7. Совместимость типов 189 4.8. Область видимости 192 4.8.1. Статическая область видимости 193 4.8.2. Блоки 195 4.8.3. Оценка статического обзора данных 196 4.8.4. Динамические области видимости 198 4.8.5. Оценка динамического обзора данных 199 4.9. Область видимости переменных и время их жизни 200 4.10. Среды ссылок 201 4.11. Именованные константы 203 4.12. Инициализация переменных 204 ГЛАВА 5. ТИПЫ ДАННЫХ 213 5.1. Введение 214 5.2. Элементарные типы данных 215 5.2.1. Числовые типы 216 5.2.2. Булевские типы 218 5.2.3. Символьные типы 218 5.3. Символьные строки 219 Содержание 9
5.3.1. Вопросы разработки 219 5.3.2. Строки и действия над ними 219 5.3.3. Варианты длины строк 221 5.3.4. Оценка 222 5.3.5. Реализация символьных строк 222 5.4. Порядковые типы, определяемые пользователем 224 5.4.1. Перечислимые типы 224 5.4.2. Ограниченные типы 226 5.4.3. Реализация порядковых типов, определяемых пользователем 227 5.5. Массивы 227 5.5.1. Вопросы разработки 228 5.5.2. Массивы и индексы 228 5.5.3. Связывания индексов и категории массивов 229 5 5.4. Количество индексов массива 232 5.5.5 Инициализация массива 232 5.5.6. Операции над массивами 233 5.5.7. Сечения 235 5.5.8. Оценка 236 5.5.9. Реализация типов массивов 236 5.6. Ассоциативные массивы 241 5.6.1. Структура и операции 241 5.6.2. Реализация ассоциативных массивов 242 5.7. Записи 242 5.7.1. Описания записей 242 5.7.2. Ссылки на поля записи 243 5.7.3. Операции над записями 245 5.7.4. Опенка 245 5.7.5. Реализация записей 246 5.8. Объединения 246 5.8.1. Вопросы разработки 247 5.8.2. Свободные объединения 247 5.8.3. Размеченные объединения языка ALGOL 68 247 5.8.4. Типы объединения в языке Pascal 248 5.8.5. Объединения в языке Ada 250 5.8.6. Оценка 252 5.8.7. Реализация объединений 252 5.9. Множества 253 5.9.1. Множества в языках Pascal и Modula-2 253 5.9.2. Оценка 254 5.9.3. Реализация множественных типов данных 255 5.10. Указатели 255 5.10.1. Вопросы разработки 2 ;6 5.10.2. Операции над указателями 2^6 5.10.3. Проблемы, возникающие при использовании указателей 2 ;8 5.10.4. Указатели в языке Pascal 259 5.10.5. Указатели в языке Ada 259 10 Содержание
5.10.6. Указатели в языках С и С— 260 5.10.7. Указатели в языке FORTRAN 90 261 5.10.8. Ссылки 262 5.10.9. Оценка 263 5.10.10. Реализация ссылок и хказателей 263 ГЛАВА 6. ВЫРАЖЕНИЯ И ОПЕРАТОРЫ ПРИСВАИВАНИЯ 275 6.1. Введение 276 6.2. Арифметические выражения 277 6.2.1. Порядок вычисления операторов 277 6.2.2. Порядок вычисления операндов 282 6.3. Перегруженные операторы 284 6.4. Преобразования типов 286 6.4.1. Приведение типов в выражениях 287 6.4.2. Явное преобразование типов 288 6.4.3. Ошибки в выражениях 289 6.5. Выражения отношений и бхлевские выражения 289 6.5.1. Выражения отношения 289 6.5.2. Булевские выражения 290 6.6. Сокращенное вычисление 291 6.7. Операторы присваивания 293 6.7.1. Простые присваивания 293 6.7.2. Множественные целевые объекты 294 6.7.3. Условные целевые объекты 294 6.7.4. Составные операторы присваивания 294 6.7.5. Унарные операторы присваивания 294 6.7.6. Присваивание как выражение 296 6.8. Смешанные присваивания 297 ГЛАВА 7. СТРУКТУРЫ УПРАВЛЕНИЯ НА УРОВНЕ ОПЕРАТОРОВ 303 7.1. Введение 304 7.2. Составные операторы 305 7.3. Операторы ветвления 306 7.3.1. Двухвариантные операторы ветвления 306 7.3.2. Конструкции многовариантного ветвления 311 7.4. Операторы цикла 318 7.4.1. Циклы со счет чиком 318 7.4.2. Логически управляемые циклы 326 7.4.3. Циклы с механизмами управления, размешенными пользователем 328 ’ 7.4.4. Циклы, основанные на структурах данных 330 7.5. Безусловный переход 331 7.5.1. Проблемы безусловного перехода 331 7.5.2. Виды меток 332 7.5.3. Ограничения переходов 333 Содержание 11
7^6. Защищенные команды 334 7.7. Выводы 338 ГЛАВА 8. ПОДПРОГРАММЫ 345 8.1. Введение 346 8.2. Основы подпрограмм 346 8.2.1. Общие свойства подпрограмм 346 8.2.2. Основные определения 347 8.2.3. Параметры 348 8.2.4. Процедуры и функции 350 8.3. Вопросы разработки подпрограмм 351 8.4. Среды локальных ссылок 353 8.5. Методы передачи параметров 354 8.5.1. Семантические модели передачи параметров 354 8.5.2. Модели реализации передачи параметров 355 8.5.3. Методы передачи параметров в основных языках про грам м ирован ия 360 8.5.4. Проверка типов параметров 362 8.5.5. Методы реализации передачи параметров 363 8.5.6. Многомерные массивы в качестве параметров 366 8.5.7. Вопросы разработки 368 8.5.8. Примеры передачи параметров 369 8.6. Параметры, являющиеся именами подпрограмм 373 8.7. Перегруженные подпрограммы 375 8.8. Настраиваемые подпрограммы 376 8.8.1. Настраиваемые подпрограммы в языке Ada 377 8.8.2. Настраиваемые подпрограммы в языке C++ 379 8.9. Раздельная и независимая компиляция 380 8.10. Вопросы разработки функций 382 8.10.1. Побочные эффекты функций 382 8.10.2. Типы возвращаемых значений 382 8.11. Дост\п к нелокальным средам 383 8.11.1. Блоки COMMON языка FORTRAN 383 8.11.2. Внешние объявления и модули 384 8.12. Перегруженные операторы, определяемые пользователем 385 8.13. Сопрограммы 386 ГЛАВА 9. РЕАЛИЗАЦИЯ ПОДПРОГРАММ 393 9.1. Общая семантика вызовов и возвратов 394 9.2. Реализация подпрограмм на языке FORTRAN 77 394 9.3. Реализация подпрограмм на языках, подобных языку ALGOL 397 9.3.1. Более сложные записи активации „ 397 9.3.2. Пример без рекурсии и нелокальных ссылок 400 9.3.3. Рекурсия 402 12 Содержание
9.3.4. Механизмы реализации нелокальных ссылок 405 9.4. Блоки 416 9.5. Реализация методов динамического обзора данных 418 9.5.1. Глубокий доступ 419 9.5.2. Теневой доступ 421 9.6. Реализация параметров, являющихся именами подпрограмм 422 9.6.1. Статические цепочки 422 9.6.2. Индикаторы 423 9.6.3. Ошибочное повторное обращение к среде ссылок 423 ГЛАВА 10. АБСТРАКТНЫЕ ТИПЫ ДАННЫХ 429 10.1. Понятие абстракции 430 10.2. Инкапсуляция 431 Ю.З. Введение в абстракцию данных 432 Ю.3.1. Число с плавающей точкой как абстрактный тип данных 433 Ю.3.2. Абстрактные типы данных, определяемые пользователем 433 Ю.З.З. Пример 434 Ю.4. Вопросы разработки типов 435 Ю.5. Примеры абстракции данных в разных языках 436 10.5.1. Классы в языке SIMULA 67 436 10.5.2. Абстрактные типы данных в языке Ada 437 10.5.3. Абстрактные типы данных в языке C++ 441 10.6. Параметризованные абстрактные типы данных 446 10.6.1. Язык Ada 446 10.6.2. Язык C++ 447 ГЛАВА 11. ПОДДЕРЖКА ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПРОГРАММИРОВАНИЯ 451 11.1. Введение 452 11.2. Объектно-ориентированное программирование 452 11.2.1. Введение 452 11.2.2. Наследование 453 11.2.3. Полиморфизм и динамическое связывание 455 11.2.4. Вычисления в объектно-ориентированных языках 456 11.3. Вопросы разработки объектно-ориентированных языков 457 11.3.1. Исключительность объектов 457 11.3.2. Являются ли подклассы подтипами 457 11.3.3. Реализация и наследование интерфейса 458 11.3.4. Проверка типов и полиморфизм 459 11.3.5. Одиночное и множественное наследование 459 11.3.6. Размещение в памяти и удаление из памяти объектов 460 11.3.7. Динамическое и статическое связывание 461 11.4. Обзор языка Smalltalk 461 11.4.1. Общие характеристики 461 11.4.2. Среда языка Smalltalk 462 Содержание 13
12.1.2 . Разновидности параллельности 507 12.1.3 . Почему нужно изучать параллельность 508 12.2. Введение в параллельность на уровне подпрограмм 508 12.2.1. Основные понятия 508 12.2.2. Разработка языков для поддержки параллельности 512 12.2.3. Вопросы разработки языков программирования 512 12.3. Семафоры 512 12.3.1. Введение 512 12.3.2. Синхронизация взаимодействия 513 12.3.3. Синхронизация конкуренции 515 12.3.4. Оценка 517 12.4. Мониторы 517 12.4.1. Введение 517 12.4.2. Синхронизация взаимодействия 518 12.4.3. Синхронизация конкуренции 519 12.4.4. Оценка 521 12.5. Передача сообщений 521 12.5.1. Введение 522 12.5.2. Концепция синхронной передачи сообщений 522 12.5.3. Модель передачи сообщения в языке Ada 95 523 12.5.4. Синхронизация взаимодействия 527 12.5.5. Синхронизация конкуренции 528 12.5.6. Завершение задачи 529 12.5.7. Приоритеты 530 12.5.8. Бинарные семафоры 530 12.5.9. Оценка 531 12.6. Параллельность в языке Ada 95 531 12.6.1. Защищенные объекты 531 12.6.2. Асинхронные сообщения 532 12.7. Потоки языка Java 533 12.7.1. Класс Thread 534 12.7.2. Приоритеты 535 12.7.3. Синхронизация взаимодействия 535 12.7.4. Синхронизация конкуренции 536 12.7.5. Оценка 539 12.8. Параллельность на уровне операторов 539 12.8.1. Язык High-Performance FORTRAN 539 ГЛАВА 13. ОБРАБОТКА ИСКЛЮЧИТЕЛЬНЫХ СИТУАЦИЙ 546 13.1. Введение в обработку исключительных ситуаций 547 13.1.1. Основные понятия 548 13.1.2. Вопросы разработки 550 13.1.3. Исторический обзор 553 13.2. Обработка исключительных ситуаций в языке PL/1 553 13.2.1. Обработчики исключительных ситуаций 554 Содержание 15
12.1.2 . Разновидности параллельности 507 12.1.3 . Почему нужно изучать параллельность 508 12.2. Введение в параллельность на уровне подпрограмм 508 12.2.1. Основные понятия 508 12.2.2. Разработка языков для поддержки параллельности 512 12.2.3. Вопросы разработки языков программирования 512 12.3. Семафоры 512 12.3.1. Введение 512 12.3.2. Синхронизация взаимодействия 513 12.3.3. Синхронизация конкуренции 515 12.3.4. Оценка 517 12.4. Мониторы 517 12.4.1. Введение 517 12.4.2. Синхронизация взаимодействия 518 12.4.3. Синхронизация конкуренции 519 12.4.4. Оценка 521 12.5. Передача сообщений 521 12.5.1. Введение 522 12.5.2. Концепция синхронной передачи сообщений 522 12.5.3. Модель передачи сообщения в языке Ada 95 523 12.5.4. Синхронизация взаимодействия 527 12.5.5. Синхронизация конкуренции 528 12.5.6. Завершение задачи 529 12.5.7. Приоритеты 530 12.5.8. Бинарные семафоры 530 12.5.9. Оценка 531 12.6. Параллельность в языке Ada 95 531 12.6.1. Защищенные объекты 531 12.6.2. Асинхронные сообщения 532 12.7. Потоки языка Java 533 12.7.1. Класс Thread 534 12.7.2. Приоритеты 535 12.7.3. Синхронизация взаимодействия 535 12.7.4. Синхронизация конкуренции 536 12.7.5. Оценка ’ 539 12.8. Параллельность на уровне операторов 539 12.8.1. Язык High-Performance FORTRAN 539 ГЛАВА 13. ОБРАБОТКА ИСКЛЮЧИТЕЛЬНЫХ СИТУАЦИЙ 546 13.1. Введение в обработку исключительных ситуаций 547 13.1.1. Основные понятия 548 13.1.2. Вопросы разработки 550 13.1.3. Исторический обзор 553 13.2. Обработка исключительных ситуаций в языке PL/1 553 13.2.1. Обработчики исключительных ситуаций 554 Содержание 15
13.2.2. Связывание исключительных ситуаций с обработчиками 554 13.2.3. Продолжение 554 13.2.4. Другие проектные решения 555 13.2.5. Пример 556 13.2.6. Оценка 557 13.3. Обработка исключительных ситуаций в языке Ada 558 13.3.1. Обработчики исключительных ситуаций 558 13.3.2. Связывание исключительных ситуаций с обработчиками 559 13.3.3. Продолжение 560 13.3.4. Другие проектные решения 561 13.3.5. Пример 563 13.3.6. Оценка 564 13.4. Обработка исключительных ситуаций в языке C++ 564 13.4.1. Обработчики исключительных ситуаций 564 13.4.2. Связывание исключительных ситуаций с обработчиками 565 13.4.3. Продолжение выполнения программы 566 13.4.4. Другие проектные решения 566 13.4.5. Пример 567 13.4.6. Оценка 568 13.5. Обработка исключительных ситуаций в языке Java 568 13.5.1. Классы исключительных ситуаций 568 13.5.2. Обработчики исключительных ситуаций 569 13.5.3. Связывание исключительных ситуаций с обработчиками 569 13.5.4. Продолжение выполнения программы 570 13.5.5. Другие проектные решения 571 13.5.6. Пример 572 13.5.7. Оператор finally 573 13.5.8. Оценка 574 ГЛАВА 14. ФУНКЦИОНАЛЬНЫЕ ЯЗЫКИ ПРОГРАММИРОВАНИЯ 580 14.1. Введение 581 14.2. Математические функции 582 14.2.1. Простые функции 582 14.2.2. Функциональные формы 583 14.3. Основы функциональных языков программирования 584 14.4. Первый язык функционального программирования — LISP 585 14.4.1. Типы и структуры данных 586 14.4.2. Первый интерпретатор языка LISP 586 14.5. Введение в язык Scheme 588 14.5.1. Происхождение языка Scheme 589 14.5.2. Элементарные функции 589 14.5.3. Функции для построения функций 594 14.5.4. Поток управления 595 14.5.5. Пример функции на языке Scheme 597 14.5.6. Функциональные формы 601 16 Содержание
14.5.7. Функции для создания кода 602 14.5.8. Императивные свойства языка Scheme 603 14.6. Язык COMMON LISP 603 14.7. Язык ML 605 14.8. Язык Haskell 607 14.9. Применение функциональных языков 610 14.10. Сравнение функциональных и императивных языков 611 ГЛАВА 15. ЯЗЫКИ ЛОГИЧЕСКОГО ПРОГРАММИРОВАНИЯ 616 15.1. Введение 617 15.2. Краткое введение в исчисление предикатов 617 15.2.1. Высказывания 618 15.2.2. Дизъюнктивные формы 620 15.3. Исчисление предикатов и доказательство теорем 620 15.4. Обзор логического программирования 623 15.5. Происхождение языка Prolog 624 15.6. Основные элементы языка Prolog 625 15.6.1. Термы 625 15.6.2. Факты 626 15.6.3. Правила 627 15.6.4. Цель 628 15.6.5. Процесс логического вывода в языке Prolog 628 15.6.6. Простая арифметика 631 15.6.7. Списковые структуры 634 15.7. Недостатки языка Prolog 638 15.7.1. Управление порядком выполнения резолюции 639 15.7.2. Предположение о закрытом мире 641 15.7.3. Проблема логического отрицания 642 15.7.4. Внутренние ограничения 643 15.8. Применение логического программирования 644 15.8.1. Системы управления реляционными базами данных 644 15.8.2. Экспертные системы 645 15.8.3. Системы обработки естественных языков 646 15.8.4. Образование 646 15.9. Выводы 646 ЛИТЕРАТУРА 650 ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ 661 Содержание 17
П ре дис ловие Цели. общая структура и подход к изложению материала в пятом издании ‘'Концепций языков программирования*’ остались теми же, что и в четырех предыдущих изданиях. Основ- ная цель книги — предоставить читателю инструменты, необходимые для критической оцен- ки существующих и будущих языков и конструкций программирования. Дополнительной це- лью является подготовка читателя к изучению методов разработки и создания компиляторов. Книга должна также дать ответы на бесчисленные вопросы, коюрые мот возникнуть у ниппеля, шакомого исключительно с языками программирования высокого уровня. Напри- мер. почем\ С}шес1В\ет так мною я тыков программирования9 Как и для чею они создава- лись9 Насколько они похожи? Чем отличаются? Появления каких языков можно ожидать в б\ I . тем ? И почем} мы просто не можем продолжать исполыовать языки, которые уже есть? (Чшесгвхет два подхода к организации структуры книги, посвященной концепциям языков npoiраммирования. вертикальный и юризонтальный. При горизонтальном подходе каждый вы- бранный я тык аналишруезся на определенном уровне. Если используется вертикальный подход, тс- ибщие концепции и конструкции языков программирования описываются в определенной пос тедовательности. Для каждой конструкции рассматриваются вопросы ее реализации, разби- раются примеры программ, написанных на различных языках. Оба подхода имеют свои досто- инства Дтя того чтобы точно описать индивидуальные концепции каждою языка, нужно сосре- доточить на них внимание и рассмотреть их влияние на программирование и эволюцию языков программирования Однако хронологический анализ развития языков неизбежно влечет за со- бой изучение специфики языков, а также их происхождения и развития. Более того, на реализа- цию определенной функции в отдельном языке всегда оказывают влияние другие характеристи- ки пого языка. В связи с этим в книге было решено при изложении большей части материала использовать вертикальный подход, а там. где это удобно, применять горизонтальный подход. В этой книге описаны основные концепции языков программирования, вопросы, связан- ные с разработкой разнообразных языковых конструкций, а также проанализированы различ- ные подходы к реализации этих конструкций в наиболее распространенных языках програм- мирования. У казанный подход требует изучения набора тесно связанных между собой тем. Для обсуж- дения языков и языковых конструкций жизненно необходимы описательные инструменты. На- ми подробно рассмотрены самые эффективные и широко распространенные методы синтакси- ческого описания, а также введены наиболее общие методы описания семантики языков про- граммирования. Для того чтобы понять причины выбора конкретных Проектных решений в су шествующих языках программирования, следует изучать исторический контекст и конкрет- ные нужды, обусловившие этот выбор. Поскольку сложность реализации языка зачастую значи- тельно влияет на ею разработку в целом, в книге обсуждаются различные методы и проблемы реализации языков программирования. Ниже приводится краткое содержание глав пятого издания книги. Глава 1 начинается с объяснения, зачем нужно изучать языки программирования. Затем обсуждаются критерии их оценки. Мы отдаем себе отчет в том, что вводить такие критерии рискованно, но. тем не менее, принципы оценки необходимы в любом серьезном исследова- нии проблем разработки языков программирования Кроме того, в этой главе рассматривают- ся основные факторы, влияющие на разработку языков программирования, общепринятые соглашения, используемые при их разработке, а также основные подходы к реализации. При рассмотрении эволюции большинства важнейших языков, обсуждаемых в книге, в главе 2 используется горизонтальный подход. Несмотря на то что ни один из языков не опи- 18 Предисловие
сывается полностью. указываются исток», цели и значение каждсгс из них Эт<" историче- ский обзор ценен, так как он подготавливает базу для понимания теоретических и практиче- ских основ разработки современных языков Он также обосновывает дальнейшее изучение вопросов, связанных с разработкой языков и их оценкой Поскольку материал остальной час- ти книги никак не зависит от материала главы 2. без нарушения целостности изложения эта глава может быть полностью пропущена. В главе 3 рассматриваются основные методы формального описания синтаксиса языков программирования: форма EBNF (Extended Backus-Naur Form — расширенная форма Бэкхса- Наура) и синтаксические графы Затем следует описание атрисутнсй грамматики, играющей очень важную роль при разработке ко\<пилятэра. Далее анализир>ется трудная задала описа- ния семантики, даются основы трех наиболее распространенных методов описания операци- онной, аксиоматической и денотационной семантики В главах 4-13 использован вертикальный подход для подробного описания разработки основных конструкций императивных языков программирования В каждом случае представ- лено и оценено несколько альтернативных проектных решений В частности, в главе 4 рас- смотрены многие свойства переменных, а в главе 5 — более сложные типы данных, глава 6 посвящена выражениях! и операторах! присваивания: управляющие операторы описаны в гла- ве 7: подпрограммы и их реализация разобраны в главах 8 и 9: способы абстракции данных изучаются в главе 10: особенности языков, поддерживающих объектно-ориентированное программирование (наследование и динамическое связывание».— в главе 11: параллельно выполняемые программные модули описываются в главе 12: и. наконец, обработка исключи- тельных ситуаций описывается в главе 13. Мы решили использовать вертикальный подход, поскольку при горизонтальнох! подходе приходится описывать и оценивать детали некоторой конкретной конструкции в разных частях книги одновременно, что создает определенное не- удобство. Например, обсуждение в одной и той же главе различных методов обеспечения па- раллельности позволяет нах! точно сравнить и оценить эти методы. В двух последних главах (главы 14 и 15) описываются две важнейшие парадигмы про- граммирования: функциональная и логическая. Обе парадигмы вначале рассматриваются с точки зрения методологии программирования, а затех! приводится краткое введение в кон- кретный язык. В частности, глава 14 начинается с обсуждения простых математических функций, функ- циональных форм и языков функционального программирования Затехг представлено введе- ние в язык Scheme с описанием некоторых его основных функций, специальных форм, функ- циональных форм и приведено несколько примеров простых функций, написанных на этох! языке. Для иллюстрации разновидностей языков функционального программирования кратко описаны языки COMMON LISP, ML и Haskell. Завершается глава сравнением функциональ- ных и императивных языков программирования. Темой главы 15 является логическое программирование и языки логического программиро- вания. Глава начинается с введения в исчисление предикатов и объяснения их использования для доказательства теорем. Затех! следует обзор логического программирования. Основная часть главы посвящена языку Prolog, в частности, резолюции и унификации, а также содержит не- сколько примеров програмх! с подробными описаниями их работы. Изменения, внесенные в пятое издание Пятое издание данной книги подверглось значительной переработке и исправлению по сравнению с предыдущими. Большинство изменений было вызвано растущих! влиянием объ- ектно-ориентированной парадигмы программирования. Важнейшие из сделанных изменений описаны в следующих абзацах. Предисловие 19
Нарушая традицию, поддерживаемую первыми изданиями данной книги и большинством других книг, посвященных языкам программирования, в пятое издание не входит глава, посвя- щенная объектно-ориентированным языкам программирования. Книга содержит главу, посвя- щенную языковой поддержке объектно-ориентированного программирования, в частности, в ней рассматриваются наследование и динамическое связывание. Эта глава была переставлена на более подходящее для нее место — сразу после главы, посвященной абстракции данных, и стала главой 11. Глава была существенно увеличена за счет всестороннего обсуждения вопросов раз- работки объектно-ориентированных языков программирования, что создало основу для описа- ния и оценки различных языковых конструкций, обеспечивающих наследование и динамическое связывание. Две основные причины, обусловившие выбор этого нового подхода, заключаются в сле- дующем: во-первых, в настоящее время появилось огромное количество объектно- ориентированного программного обеспечения, создаваемого на языках, подобных импера- тивным языкам последних четырех десятилетий. Выражения, операторы присваивания, структура данных и управляющие структуры этих языков очень похожи на соответствующие структуры языков С и Pascal. Следовательно, нет причин отдельно обсуждать указанные свойства этих языков. Согласно нашему определению императивного языка языки C++, Ada 95 и Java являются императивными. Мы рассматриваем поддержку объектно- ориентированного программирования данными языками как начало следующей стадии разви- тия императивных языков программирования. В то время как парадигма развития объектно- ориентированного программного обеспечения значительно отличается от процедурной пара- дигмы. различия между языками, относящимися к данным двум подходам, не столь значи- тельны. В то же время различия между такими языками, поддерживающими информационно- ориентированное программирование, как Ada 83. и языками, поддерживающими объектно- ориентированное программирование, менее заметны. Второй причиной изменения подхода к обсуждению объектно-ориентированных языков программирования является то, что эта па- радигма уже не такая новая и экспериментальная, какой была несколько лет назад. В совре- менных разработках программного обеспечения этот подход доминирует, и используемые для этой цели языки программирования являются самыми распространенными на данный момент языками. Следовательно, в книге не стоит обсуждать объектно-ориентированные языковые функции в одной главе, к тому же расположенной в конце книги, — подход, до сих пор используемый по отношению к языкам логического программирования. Эту информацию легко можно подать в большинстве глав книги, что и было сделано в пятом издании. Кроме указанных, были внесены еще некоторые изменения. Появление языка Java и быст- рый рост его популярности потребовал дополнительного увеличения материала книги за счет описания некоторых его интересных свойств. В частности, обсуждение поддержки объектно- ориентированного программирования в языке Java было добавлено в главу 11, возможность параллельного функционирования — в главу 12, а обработка исключительных ситуаций— в главу 13. Кроме того, в начальные главы книги добавилось обсуждение еще нескольких его функций. Поскольку язык Miranda запатентован, в то время как язык Haskell общедоступен, обсуж- дение языка Miranda было удалено из главы 14 и оставлено только описание языка Haskell. В пятом издании вновь был расширен раздел главы 3, посвященный описанию аксиома- тической и денотационной семантики. Из-за введения в книгу описания новых языков и новых свойств старых языков обсужде- ние старых языков было сокращено. Например, был удален материал о сопрограммах языка Modula-2 и его поддержке абстрактных типов данных. Текущее состояние развивающихся языков программирования отражают другие много- численные изменения, незначительные по объему. 20 Предисловие
Указания преподавателю При преподавании языков программирования на младших курсах университета штата Ко- гало в Колорадо-Спрингс (University of Colorado at Colorado Springs) данная книга исполь- . ется следующим образом: главы 1 и 3. как правило, рассматриваются подробно. На главу 2. -е содержащую трудной для восприятия технической информации, отводится совсем немно- лекционного времени. Тем не менее, студенты находят эту главу интересной и полезной. <ак указывалось ранее, глава2 может быть просто пропущена, поскольку она не содержит “зтериала. необходимого для понимания последующих глав. Главы 4-8 и глава 10 должны относительно легко восприниматься студентами, имеюши- большой опыт работы с языками Pascal. С. С— или Ada. Главы 9. 11. 12 и 13 более слож- и требу ют обстоятельных лекций. Главы 14 и 15 являются совершенно новыми для большинства студентов младших курсов. ~ги объяснении этих глав идеальным было бы наличие языковых процессоров для языков <cheme и Prolog. Кроме того, в эти главы включено достаточно материала для того, чтобы студенты могли повозиться с простыми программами. Студенты, вероятно, не смогут усвоить последние две главы во всех деталях. Однако ас- пиранты могут сразу приступать к обсуждению неимперативных языков программирования, "гопустив части начальных глав, относящиеся к императивным языкам. Приложения В книгу включены два важных и полезных приложения. Электронный учебник с реше- чиями упражнений можно получить у торгового представителя издательства “Addison-Wesley эublishing". Также доступен набор диапозитивов к лекциям (по одному на каждую из первых ’ ? глав), имеющий формат файлов источника программы Microsoft Powerpoint. Автор создал и\ за последние несколько лет чтения курса лекций по предлагаемой книге. Файлы в формате -рограммы Powerpoint можно получить с помощью анонимного ftp-доступа на узле ::р.aw.com в каталоге /cseng'authors/sebastaconcepts4e. Подробнее об этом и других приложе- ниях вы можете узнать из файлов README или .message, находящихся в указанном каталоге. Доступность языкового процессора Процессоры для работы с некоторыми из обсуждаемых языков программирования и ин- формацию об этих языках вы можете найти на следующих Web-узлах: язык Java — узел http://java.sun.com язык Haskell — узел http://haskell.org язык Scheme — узел http://www-swiss.ai.mit/ftpdir scheme-7.4/ Обновленные версии можно найти на домашней странице данной книги: http://www.awl.com/cseng/titles/0-201-38596-l/ Благодарности Качество этой книги было значительно улучшено благодаря большому количеству предло- жений. уточнений и комментариев ее рецензентов. Рецензентами первых четырех изданий книги были Вики Алан (Vicki Allan), Генри Бауэр (Henry Bauer), Питер Брауэр (Peter Brouwer). Пао- шенг Чанг (Paosheng Chang). Джон Креншоу (John Crenshaw), Барбара Энн Грим (Barbara Ann Griem), Мери Лоу Хааг (Mary Lou Haag). Джон Мауни (Jon Mauney). Роберт Мак-Коард (Robert Предисловие 21
McCoard). Майкл Д* Мерфи (Michel G. Murphs). Эндрю Олдройд (Andrew Oldroyd), Джеффри Поляк (Jeffers Popsack). Стивен Рапкин (Steven Rapkin), Гамильтон Ричард (Hamilton Richard), losi Сейджер (Tom Sager). Джозеф Шелл (Joseph Schell) и Мэри Луиза Соффа (Mary Louise Soffa). Пятое издание рецензировали* Мэри Jos Хааг (Mars Lou Haag). университет штата Колорадо в Колорадо-Спрингс (Lniseisity of Colorado at Colorado Springs). Хайкью Ko (Hiksoo Koh), университет Lamar University: Ьрюс Максим (Bruce Maxim), университет штата Мичиган в Дебоне (University of Michigan at Dearborn): Л. Эчдрю Олдройд (L. Andrew Oldrosd). Вашингтонский университет (Washington I msersity). Ребекка Парсонс (Rebecca Parsons), университет Центральной Флориды (University of C entral Г londa): Дон Багерг (Don Bagert). Технический университет штата Техас (Texas Technical Unis ersits). Мейл Суарез-Ривас (Maite Suarez-Risas). редактор. Молли Тейлор (Molly Taylor), помощ- ник редактора, и Пат Юнабан (Pat Unubun). технический редактор, заслужили мою призна- те -ьносп. за их усилия, направленные на быстрый выход пятого издания книги, а также за иомошь в улучшении этого издания по сравнению с предыдущим. В заключение автор благодарит своих детей -- - Джейка (Jake.) и Дарси (Darcie) за их тер- не..ивое отношение к оюутствию отца, проводившего бесконечные часы в работе над пятым и зданием ланкой кнш и. Об авторе Рооерт Собес га (Robert Sebesta) является ассоциированным профессором и деканом фа- культета компьютерных наук университета штата Колорадо в Колорадо-Спрингс. Профессор (А-беста по 1учил степень бакалавра по прикладной математике в университет штата Колора- до в Ьолдере (University о! Colorado in Boulder), а степени Mai истра компьютерных наук и л<»кюра философии— в Госутарственнохт университете штата Пенсильвания (Pennsylvania Slate I msersity i. Теорию вычис штельных систем он преподает уже свыше 25 лет. К области ею профессионазьных интересов относятся проектирование и оценка языков программиро- вания. разработка компиляторов, а также методы и инструменты тестирования протраммного обеспечения. Автор является h.tohosi Ассоциации по вычислительной технике (АСМ — Association for Computing Machinery) и Компьютерного ootueciea Института инженеров по электротехнике и электронике (IEEE— Institute of Electrical and Electronics Engineers Computer Society i. 22 Предисловие
1.1. Для чего нужно изучать концепции языков программирования 1.2. Области применения программирования 1.3. Критерии оценки языков программирования 1.4. Факторы, влияющие на разработку языка 1.5. Категории языков 1.6. Компромиссы при разработке языка 1.7. Методы реализации 1.8. Среды программирования Конрад Цузе (Konrad Zuse) С 1936 по 1944 год Конрад Цузе спроектировал в Германии серию электромеханических компьюте- ров В 1945 году он разработал алгоритмический язык програм- мирования Plankalkul, который никогда не был реализован, причем полное описание этого языка до 1972 года даже не пуб линовалось Вводные замечания 23
Прежде чем начать описание концепций языков программирования, сделаем не- которые вводные замечания. Во-первых, обсудим причины, по которым и сту- денты. и профессиональные разработчики программного обеспечения должны изучать общие концепции, связанные с разработкой и оценкой языков программирования. Эта информация приводится специально для тех, кто полагает, что программисту достаточно знать один-два рабочих языка программирования. Во-вторых, рассмотрим области при- менения программирования. Затем, поскольку в книге оцениваются различные языковые свойства, представим перечень критериев, по которым принимаются решения. Далее об- суждаются два основных фактора, влияющих на структуру языка: архитектура машины и методологии разработки программ. Затем рассмотрим некоторые альтернативы, изуче- ние которых необходимо при разработке языка. Кроме того, в главе содержится обзор наиболее общих подходов к реализации языков программирования. В заключение приводится несколько примеров сред программирова- ния и обсуждается их влияние на производство программного обеспечения. 1.1. Для чего нужно изучать концепции языков программирования Студенты часто интересуются, какие выгоды они получат от изучения концепций языков программирования. Помимо всего прочего, в компьютерных науках существует множество других тем. заслуживающих серьезного изучения. Ниже приводится полный, как мы надеемся, перечень потенциальных выгод изучения языковых концепций. Больше возможностей для выражения идей. Широко распространенным явля- ется мнение, что на глубину наших мыслей влияет выразительная сила языка, на котором мы эти мысли выражаем. Люди, плохо понимающие естественный язык, не могут выражать сложные мысли, особенно на уровне абстракций. Другими словами, людям сложно осмыслить структуры, которые они не могут описать уст- но или письменно. С подобными проблемами сталкиваются и программисты при разработке программного обеспечения. Язык, на котором они программируют, налагает ограничения на виды управляющих структур, структур данных и абст- ракций. которые они могут использовать; следовательно, число форм алгоритмов, которые могут создать программисты, также ограничено. Осознанав разнообразие свойств языков программирования, можно решить по- добные проблемы. Изучение новых языковых конструкций помогает программи- стам повысить свое мастерство. Можно возразить, что изучение возможностей других языков не поможет про- граммисту. вынужденному использовать язык, в котором подобные возможности отсутствуют. Этот аргумент, однако, нас не останавливает, поскольку довольно часто языковые средства одного языка могут быть воспроизведены в других язы- ках. непосредственно их не поддерживающих. Например, после изучения таких функций обработки строк, предусмотренных в языке FORTRAN 90 (ANSI. 1992), как функция поиска подстрок INDEX, програм- мист. использующий язык Pascal (Ledgard. 1984), естественным образом перейдет к созданию подпрограмм, выполняющих те же операции. Сказанное справедливо и для многих других более сложных конструкций, обсуждаемых в данной книге. 24 Глава!. Вводные замечания
Изучение концепций языков программирования позволяет разобраться в полезных языковых свойствах и поощряет программистов к их использованию. Тот факт, что многие свойства могут быть воспроизведены в разных языках, не уменьшает значения разработки языков, обладающих наилучшим набором свойств. Всегда лучше использовать свойство, уже встроенное в язык, чем его ме- нее элегантный и более громоздкий аналог в языке, изначально это свойство не поддерживающем. Более обоснованный выбор подходящего языка. Многие профессиональные про- граммисты имеют лишь формальное образование в области компьютерных наук, а дальнейший опыт они получали самостоятельно или с помощью заочного обучения. Людей часто обучают одному-двум языкам, непосредственно имеющим отношение к текущей работе организации. Некоторые программисты прошли формальное обу- чение в далеком прошлом. Изучавшиеся ими языки уже давно не используются, а многие свойства, доступные в современных языках программирования, известны не очень широко. В результате, многие программисты продолжают использовать при- вычный язык, даже если он совсем не подходит для работы над новым проектом. Если бы они знали другие языки программирования, в частности их особые свойст- ва, то выбор рабочего языка был бы более осознанным. Повышаются способности к изучению новых языков. Программирование — мо- лодая дисциплина. Методологии, средства разработки программного обеспечения и языки программирования все еще находятся в состоянии развития. Это делает раз- работку программного обеспечения захватывающим занятием, подразумевающим необходимость непрерывного обучения. Процесс изучения новых языков програм- мирования может быть длительным и трудным, особенно для программиста. >ве- ренно себя чувствующего только с одним-двумя языками и никогда не изучавшего концепций языков программирования в целом. Один раз досконально разобравшись в основных концепциях языков программирования, вы поймете, как эти концепции реализованы в структуре изучаемого вами языка программирования. Например, программистам, понимающим концепцию абстракции данных, намного легче разобраться в структуре абстрактных типов данных языка Java (Gosling et al., 1996), чем людям, не знакомым с этим понятием. Это относится и к естествен- ным языкам. Чем лучше вы знаете грамматику вашего родного языка, тем легче вам дастся изучение второго языка. Более того, положительным побочным эффек- том изучения второго языка станет лучшее понимание первого. И последнее, знание лексики и основных концепций языков программирования для практикующих программистов необходимо, поскольку только так они смогут читать и понимать справочники по языкам программирования и литературу, по- священную языкам и компиляторам. Углубляется понимание важности реализации. При изучении концепций язы- ков программирования необходимо затронуть вопросы, связанные с реализацией языков. Разобравшись в этих вопросах, вы поймете, почему язык разработан именно таким образом. В свою очередь, это позволит более рационально исполь- зовать язык. Программист может значительно повысить свое мастерство, если научится правильно выбирать конструкции языков программирования и оценивать последствия своего выбора. 1.1. Для чего нужно изучать концепции языков программирования 25
Некоюрые виды ошибок могут наГпи и \ciранить только программисты, знающие с«'<нвс1с1в\1ощие детали реализации языка. Кроме того, понимание проблем, свя- злнне’Ч с реализацией языка. позволяет отчетливо представлять, каким образом компькмер Во1по.тияе1 различные языковые конструкции. В свою очередь, это по- зво.1Яс1 ючнее оненизь относительную эффективность альтернативных конструк- ции. ко!ирые moist быть выбраны при разрабо1ке программы. Например, про- ; р: 1мисты. не разбирающийся в реализации рекурсии, не знают что рекурсивные • Г И’мы. лак правило медленнее зквивален )ны\ теративных. »• знаются uO кзибности к разработке новых языков Возможная необходи- мое. ь ли щния нового языка ciyдешу може! показаться далекой. Однако экспер- ты вр-‘\!л gi времени разрабатываю! различные язым! программирования. Напри- мер. болbnhinc।во сисим lipoiр immhuiо обеспечения требуют определенного взаимодействия с пользовалелем. .\о«я бы ввода данных и команд. В простых си- туациях вводится несколько значении данных, а выходные данные выдаются в тривиальном формате. С другой стороны, пользователь может быть вынужден проходить по нескольким уровням меню и вводить большое количество разнооб- разных команд, как при работе с текстовым процессором. Создание интерфейса пользователя в подобных системах — сложная проблема Форма этого интерфейса создается разработчиком системы, а крп1епии его оценки очень похожи на крите- рии. используемые при оценке структуры языка программирования. Следователь- но. критический разбор языков программирования поможет при конструировании таких сложных систем, ботее того, он позволит пользователям изучить и оценить подобные программные продукты. Повсеместное распространение вынисаитеанной техники Ответить на во- прос. зачем нужно изучать концепции языков программирования, поможет общий обзор вычислительной техники Обычно можно определить, почему конкретный язык программирования стал популярным, но при этом далеко не очевидно, по крайней мере в ретроспективе, что самый популярный язык — самый лучший. Иногда причиной широкого распространения языка становится недостаточное знание пользователями общих концепций языков программирования. Например, в начале 1960-х годов многие полагали, что будет лучше заменить язык FORTRAN языком ALGOL 60 (Backus et al.. 1962). поскольку, помимо всего прочего, последний элегантнее и содержит более эффективные управляющие структуры. Подоб- ное предложение не поддержала часть программистов и менеджеров, руководивших разработками программного обеспечения, которые не вполне понимали концепции языка ALGOL 60. Им казалось, что его описание трудно даже просто прочитать (так оно и было), а понять — еше сложнее Они не приняли во внимание выгод блочной структуры, рекурсии и великолепно организованных управляющих операторов, поэтому они и не увидели преимуществ языка ALGOL 60 перед языкам FORTRAN. Разумеется, как показано в главе 2. на отказ от использования языка ALGOL 60 по- влияли многие факторы, но неосведомленность пользователей компьютеров также сыг- рала свою роль. Вообще, если люди, выбирающие язык программирования, будут полнее информиро- ваны. то можно надеяться, что лучшие языки быстрее вытеснят худшие. 26 Глава!. Вводные замечания
1.2. Области применения программирования Компьютеры применяются во множестве различных облас.еи >ч::о~^.?ю'тся как для управления атомными электростанциями, так и для хранения зи”исси о лнч&л чеко- вых книжках. Из-за такого разнообразия использования компьчэтерсь разработка языков программирования также ведется в различных направлениях В этом газдетс мы кратко рассмотрим несколько областей применения компьютеров и укажем ислоль?.юшиеся в этих областях языки программирования 1.2.1. Научные приложения Первые цифровые компьютеры, появившиеся в 1940-\ ^с-стч'овх-’чь и фак- гически изобретались для научных целей Обычно научные приложение иощют дело с простыми структурами данных и значительным количеством ар?фме'гичесч;:\ вычисле- ний, выполняемых над числами с плавающей точкой. Наиболее част? у потребляемыми структурами данных являются массивы и матрицы, из управляющих структур чаще дру- гих используются циклы со счетчиком и условные операторы Языки грогрзммирсвания высокого уровня, созданные для применения в научных исследованиях, разрабатывались именно для удовлетворения таких потребностей. Конкурентом этих языков быт язык ас- семблера. поэтому главной была эффективность Первым языков созданным для науч- ных приложений, был язык FORTRAN Язык ALGOL 60 и большинство е;с поюмков также предназначались для подобных целей, хотя при этом они могли ислольх-вазься и в других родственных областях Однако для научных приложений особо важна ^ффимив- ность. а ни один из созданных языков не оказался лучше языка FORTRAN 1.2.2. Коммерческие приложения Использование компьютеров в области коммерции началось в -ч? )-\ гсдах Для этой цели были разработаны специальные компьютеры и языки программирования Первым удачным языком высокого уровня для коммерческих целей бы; СОВО! 1 XN4!. ’). появившийся в I960 году и по сегодняшний день являющийся в этой области < ,чч>:м ши- роко используемым языком Деловые языки программирования отличаются ’‘W.o - стями генерации подробных отчетов, точными способами описания и. хранения сс^и”:- ных чисел и символьных данных, а также возможностью определять арифмеч*1 сечи действия с десятичными числами С появлением микрокомпьютеров возникли новые способы использование теров в деловой сфере, особенно в малом бизнесе. Для коммерческих целей бы разра- ботаны два специальных инструмента, широко используемых сейчас в малых нимгыотс- рах: системы крупноформатных электронных таблиц и системы баз данных Кроме языка COBOL, существует лишь несколько языков программирования. ^иен- тированных на коммерческую сферу применения. В книге эти языки не рассме. ривают- ся, за исключением главы 2. в которой излагается история развития языка СоК 1.2.3. Искусственный интеллект Искусственный интеллект (ИИ)— обширная область применения компьютеров от- личаюшаяся использованием символьных, а не числовых вычислений Под символьными вычислениями подразумевается манипулирование не с числами, а с символами Кроме 1.2. Области применения программирования 27
ioio. символьные вычисления удобнее производить с использованием связных списков idii 1ых. а не массивов. Символьные вычисления иногда требуют большей гибкости, чем г ие области программирования. Например, в некоторых приложениях искусственного ишеллскта очень удобно иметь возможность создавать и реализовывать фрагменты про- I рам мы в процессе ее выполнения. Первым широко используемым языком программирования, созданным для примене- ние в области искусственного интеллекта, был язык функционального программирова- ния LISP (McCarthx et al.. 1965). появившийся в 1959 году. Большинство программ в этой области были написаны именно на языке LISP или на одном из родственных языков. Од- нако в начале 1970-х годов возник альтернативный подход к решению этих задач — язык логического программирования Prolog (Clocksin and Mellish, 1997). Диалект языка LISP и '.i названием Scheme и язык Prolog рассматриваются соответственно в главах 14 и 15. 1.2.4. Системное программирование Операционные системы и все инструменты программной поддержки компьютерных систем называются системным программным обеспечением (system software). Оно ис- пользуется практически повсеместно и. следовательно, должно эффективно функциони- ровать. В связи с этим языки программирования, применяемые в этой области, должны обеспечивать быстрое выполнение программ. Более того, они должны иметь низкоуров- невые свойства, позволяющие писать программы, осуществляющие взаимодействие с внешними устройствами. В 1960-70-х годах такие производители компьютеров, как корпорация IBM, компа- ния Digital и компания Burroughs (теперь UNISYS), разработали для системного про- граммного обеспечения специальные машинно-ориентированные языки высокого уров- ня Для универсальных вычислительных машин корпорации IBM таким языком является PL S (диалект языка PL 1): для компьютеров компании Digital— это язык BLISS, нахо- дящийся всего на один уровень выше языка ассемблера; компания Burroughs в тех же це- лях использовала язык Extended ALGOL. Операционная система UNIX практически полностью написана на языке С (ANSI, bS9j. что позволяет относительно просто переносить ее на различные машины. Некото- рые характерные черты языка С способствуют его использованию в системном програм- мировании. Он низкоуровневый, эффективно функционирует и не обременяет пользова- теля больших! количеством ограничений. Люди, занимающиеся системным программи- рованием. обычно великолепные программисты и не верят в то, что подобные О1раничения им нужны. Тем не менее, некоторые считают язык С слишком опасным для использования в больших и важных системах программного обеспечения. 1.2.5. Языки подготовки сценариев Языки подготовки сценариев развивались постепенно в течение последних 25 лет. Про- I раммы на этих языках представляют собой исполняемые файлы, называемые сценария- ми. которые содержат перечень команд. Первым из таких языков был язык sh (сокр. от англ, shell — оболочка), первоначально состоявший из небольшого набора команд, интер- претируемых как вызовы системных подпрограмм, выполнявших служебные функции, на- пример. управление файлами и их простую фильтрацию. Затем к этой основе были добав- лены переменные, операторы потока управления, функции и многие другие возможности, и в результате получился завершенный язык программирования. Один из наиболее мощных и 28 Глава!. Вводные замечания
широко известных языков подготовки сценариев — язык ksr. (Bolsky and Кот. 1995). раз- работанный Дэвидом Корном (Da\ id Кот) из компании Bell Laboratory Еще один язык подготовки сценариев, awk (Aho et al.. 1988). был разработан сотруд- никами компании Bell Laboratory Алом Ахо (Al Aho). Питером Уайнбергером (Peter Wienberger). Брайаном Керниганом (Brian Kemighan). Вначале этот язык предназначался для генерации отчетов, но позже стал универсальным. Расширяемый язык подготовки сценариев tel был разработан в университете штата Калифорния в Беркли (University of California at Berkeley) Джоном Остераутом (John Ousterhout) (Ousterhout. 1994). Сейчас язык tel объединен с языком позволяющим создавать приложения в системе X Window. Разработанный Ларри Уоллом (Larry Wall) язык Perl сначала представлял со- бой комбинацию языков sr. и (Wall aet al.. 1996). После этого он получил значи- тельное развитие и сейчас является мощным, .хотя и несколько примитивным, языком программирования. Этот язык часто продолжают называть языком подготовки сценари- ев, но мы предпочитаем считать его странным, но вполне завершенным языком програм- мирования. С появлением W’orld Wide Web популярность языка Perl резко возросла, в основном благодаря тому, что он является практически идеальным языком для програм- мирования интерфейса CGI (Common Gateway Interface — общий шлюзовой интерфейс). Вообще говоря, языки подготовки сценариев внесли не очень большой вклад в разви- тие более традиционных языков программирования. Однако язык Perl имеет несколько интересных свойств, которые будут описаны далее в книге. 1.2.6. Специализированные языки программирования За последние 40 лет появилось множество специализированных языков программи- рования. В их число входит язык RPG. используемый для генерации деловых отчетов, язык APT, созданный для управления программируемыми устройствами, и язык GPSS. разработанный для моделирования систем. Специализированные языки программирова- ния не будут рассматриваться в книге, в основном из-за их узкой направленности, а так- же из-за того, что их трудно сравнивать с другими языками программирования. 1.3. Критерии оценки языков программирования Как указывалось ранее, цель книги — тщательно изучить концепции, лежащие в основе различных конструкций и возможностей языков программирования. Мы также оценим эти концепции, обратив особое внимание на то. как они влияют на разработку и эксплуатацию программного обеспечения. Для этого нам необходимы критерии оценки. Очевидно, пере- чень таких критериев неизбежно будет спорным, поскольку практически невозможно найти хотя бы двух специалистов по вычислительной технике, имеющих общую точку зрения на относительную ценность того или иного свойства языка. Несмотря на это. большинство ученых согласится с важностью критериев, предлагаемых ниже. Некоторые из характеристик, воздействующих на наиболее важные из этих критериев, указаны в таблице 1.1, а собственно критерии рассматриваются в следующих разделах. 1.3. Критерии оценки языков программирования 29
Таблица 1.1. Критерии оценки языков программирования и влияющие на них характеристики Критерии \о; актсристика Читабельность Легкость создания Надежность Простота Ортогональность • • • Управляющие структуры • • • I ииы и структуры данных • • • Синтаксическая структура Потдержка абстракции • • Выразительность • • Проверка типов • Отработка исключительных • ситуаций Ограниченное совмещение имен • 1.3.1. Читабельность Одним из важнейших критериев оценки языка программирования является легкость тения и понимания программ, написанных на нем. До 1970-х годов разработка про- граммного обеспечения в основном сводилась к созданию кода. Однако в 1970-х годах была создана концепция жизненного цикла программного обеспечения (Booch, 1987). Значение собственно кодирования команд снизилось, а основной частью жизненного цикла программного обеспечения стали считать его эксплуатацию, особенно с точки зрения стоимости. Поскольку легкость эксплуатации в основном определяется читабель- ностью программ, то читабельность стала важной характеристикой качества программ и языков программирования. Читабельность необходимо рассматривать только в контексте определенной при- кладной области. Если, к пример}, программа, описывающая вычисления, была написана на языке, для этого не предназначенного, то она может оказаться неестественной и запу- г энной. так что читать ее будет крайне трудно. В следующих подразделах описываются характеристики, способствующие улучше- нию читабельности языка программирования 1.3.1.1. Простота На читабельность языка программирования сильное влияние оказывает его простота. Прежде всего, язык, содержащий большое количество элементарных конструкций, изу- чить сложнее, чем язык, в котором их меньше. Программисты, вынужденные использо- вать большой язык, стремятся изучить лишь некоторое его подмножество и пренебречь остальными свойствами Такой способ изучения иногда оправдывают тем. что количест- во конструкций в языке слишком велико, однако этот аргумент неверен. Проблемы при чтении программы возникают всякий раз. когда автор и читатель программы изучали разные подмножества языка. Второй характеристикой языка программирования, снижающей читабельность про- грамм. является множественность свойств, т.е. наличие нескольких способов совершения какого-либо действия. Например, пользователь программирующий на языке С, может прибавить единицу кислому числу четырьмя различными способами: 30 Глава!. Вводные замечания
count = count ч 1 count +•= 1 count + + + + count Значения двух последних выражении несколько отличаются друг от друга и с.т шанс- кий остальных выражений, однако применение всех четырех приведет к одинаковым ре- зультатам. Все эти варианты рассматриваются в главе 6. Третьей потенциальной проблемой является перегрузка операторов, т.с. наличие у символа, обозначающего оператор, нескольких значений. Это свойство полезно однако может ухудшить читабельность программы, особенно если пользователи имеют право создавать собственные перегруженные операторы и делают это без разумных основании Например, вполне допустимо использовать оператор как для целочисленных «итера- ций. так и для операций с плавающей точкой. Такая перегрузка фактически упрощает язык, уменьшая число операторов. Предположим, однако, что программист определил оператор “*'* с двумя операндами, представляющими собой одномерные массивы. имея в виду суммирование всех элементов обоих массивов. Поскольку обычно сложение векто- ров имеет совершенно иной смысл, это собьет с толку как автора, так и чигатетей про- граммы. Еще большую путаницу может вызвать программа, в которой пользователь оп- ределит оператор “ + " с двумя векторными операндами, имея в виду вычисление разно- сти между первыми элементами этих векторов. Тема перегрузки операторов освещается далее в главе 6. Упрощение языка программирования может, конечно, зайти слишком далеко На- пример, изучая следующий раздел, вы можете заметить, что форма и смысл большинства операторов языка ассемблера являются эталонами простоты. Однако эта чрезмерная простота снижает читабельность программ, написанных на языке ассемблера. Из-за от- сутствия более сложных управляющих операторов эти программы имеют менее ясную структуру. Кроме того, вследствие простоты операторов их количество в программе, на- писанной на языке ассемблера, намного превышает количество операторов в эквива- лентной программе, написанной на языке высокого уровня Те же самые аргументы можно привести и при рассмотрении менее экстремального случая: языков высокого уровня с неадекватными управляющими операторами и структурами данных. 1.3.1.2. Ортогональность Ортогональность языка программирования означает, что управляющие опер,поры и структуры данных языка можно выразить с помощью относительно небольшою количе- ства элементарных конструкций (primitive constructs), пользуясь ограниченным числом способов. Более того, любая возможная комбинация этих элементарных конструкций разрешена и имеет смысл. Рассмотрим, например, типы данных. Предположим, чю язык содержит четыре элементарных типа данных: целый (integer), число с плавающей точкой (float), число с двойной точностью (double) и символьный (character), а также тва типа операторов: массив (array) и указатель (pointer) (poi nter). Если операторы обоих типов могут применяться к самим себе и к четырем элементарным типам данных, то можно определить большое количество структур данных. Однако, если указатели не moixt ука- зывать на массивы, многие из этих возможностей реализовать не удастся. Смысл термина “ортогональность языка программирования’* не зависит оз контекста, в котором это свойство проявляется в программе. (Название ‘‘ортогональность’ про- изошло от математического понятия ортогональных независимых векторов.) Ортою- 1.3. Критерии оценки языков программирования 31
нальность следует из симметрии отношений между элементарными элементами. Указа- 1 е in могут хранить адрес любой переменной или структуры данных. Отсутствие ортого- на п.ности ведет к появлению исключений из правил языков программирования. Использование ортогональности как структурной концепции мы можем проиллюст- рировать. сравнив один из аспектов языков ассемблера мейнфреймов корпорации IBM и серии суперминикомпьютеров компании VAX. Рассмотрим простую ситуацию: сложе- ние двух 32-битных целых чисел, хранящихся либо в памяти, либо в регистрах, и замена одной из этих величин суммой. В мейнфреймах корпорации IBM для этой цели исполь- зукхся две команды, имеющие следующую форму: Regl, memory__cell .'-.г Regl, Reg2 Здесь Regl и Reg2 представляют собой регистры, a memory_cell — ячейку памя- ти. Эти команды имеют следующую семантику: ?eagl «—содержимое(Regl) + содержимое(memory_cell) reagl «—содержимое(Regl) + содержимое(Reg2) Команда сложения 32-битных целых чисел в компьютерах компании VAX выглядит так: ADDL operand_l, operand_2 Эта команда имеет следующую семантику: грегапо_2 «—содержимое(operand_l) + содержимое(operand_2) В этом случае второй операнд может быть как регистром, так и ячейкой памяти. Структура команд компьютеров VAX ортогональна в том смысле, что отдельная ко- манда в качестве операнда может использовать как регистры, так и содержимое ячейки памяти. Существуют два способа определения операторов, которые затем можно комби- нировать всеми возможными способами. Структура, использующаяся в компьютерах IMB. не ортогональна, поскольку из четырех возможных комбинаций операндов разре- шены только две. а еще две требуют дополнительных команд А и AR. Следовательно, стру ктура мейнфреймов корпорации IBM более ограничена и, значит, сложнее в исполь- зовании. Вы не можете, например, сложить две величины и сохранить сумму в ячейке памяти. Более того, изучение структуры мейнфреймов корпорации IBM осложняется ог- раничениями и дополнительными командами. Ортогональность тесно связана с простотой: чем более ортогональной является структура языка, тем меньше исключений из правил. Меньше исключений— значит, язык более систематичен, его легче изучать, а программы — легче читать и писать. Лю- бой человек, глубоко изучавший английский язык, может подтвердить, как трудно за- помнить его многочисленные исключения из правил (например, употребление буквы i перед буквой е, кроме случая, когда эта комбинация следует за буквой с). В качестве примера недостатка ортогональности в языках высокого уровня, прояв- ляющегося в виде исключений, рассмотрим следующие правила языка С. Несмотря на то что этот язык содержит две разновидности структур данных: массивы и записи (под об- щим названием struct), записи могут возвращаться функциями, а массивы — нет. Членом структуры может быть переменная любого типа, за исключением переменной, имеющей тип void, и структуры того же типа. Элемент массива может иметь любой тип. за исключением типа void, кроме того, они не могут быть функциями. Параметры 32 Глава!. Вводные замечания
передаются по значению, если они не являются массивами. Если параметр представляет собой массив, то он передается по ссылке (поскольку имя массива без индексов интер- претируется в программах на языке С как адрес первого элемента массива). Простейшее выражение, содержащее операцию сложения, например а + b обычно означает извлечение значений величин а и b из памяти и их сложение. Однако, ес- ли переменная а окажется указателем, то извлеченное значение переменной о может изме- ниться до того, как будет выполнена операция сложения. Если, к примеру, переменная а указывает на величину, размером в два байта, то перед сложением значение переменной b умножается на 2. Тип переменной а, являющийся левым контекстом выражения + b вызывает изменение значения переменной b до сложения этой величины со значением переменной а. Излишняя ортогональность также может стать источником проблем. Вероятно, са- мым ортогональным языком программирования является ALGOL 68 (van Wijngaarden, 1969). Каждая языковая конструкция этого языка имеет некий тип. и на эти типы не на- ложено никаких ограничений. Кроме того, большинство конструкций вычисляют неко- торые значения. Такая свобода комбинирования порождает крайне сложные конструк- ции. Например, поскольку результатом всех вычислений является адрес ячейки памяти, то в левой части оператора присваивания могут находиться как условный оператор, так и объявления и другие операторы. Такая крайняя форма ортогональности приводит к из- лишней сложности. Более того, поскольку языки программирования требуют большого количества элементарных конструкций, высокая степень ортогональности приводит к неконтролируемому росту количества возможных комбинаций. Таким образом, даже ес- ли эти комбинации просты, их общее число приводит к усложнению языка. Следовательно, простота языка отчасти является результатом комбинирования отно- сительно небольшого числа элементарных конструкций и ограниченного применения концепции ортогональности. Некоторые полагают, что языки функционального программирования предлагают хо- рошее соотношение простоты и ортогональности. В таких языках функционального про- граммирования, как LISP, вычисления преимущественно производятся посредством применения функций к заданным параметрам. Напротив, в таких императивных языках программирования, как С, Pascal и Java, вычисления, как правило, определяются пере- менными и операторами присваивания. Языки функционального программирования по- тенциально наиболее просты, поскольку они могут выполнять все что угодно с помощью простой конструкции, а именно: вызова функции, который может легко комбинироваться с другими вызовами функций. Вследствие этой элегантности некоторые исследователи отдают предпочтение языкам функционального программирования как основной альтер- нативе таким сложным языкам нефункционального программирования, как О+ (Ellis and Stroustrup, 1990). Однако следует заметить, что широкому распространению языков функционального программирования мешают другие факторы, например, их низкая эф- фективность. 1.3. Критерии оценки языков программирования 33
1.3.1.3. Управляющие операторы Революция в структурном программировании, произошедшая в 1970-х годах, отчасти была вызвана плохой читабельностью языков, разработанных в 1950-60-х годах, в кото- рых ощущался недостаток управляющих операторов. В частности, широко распростра- ненным стало мнение, что беспорядочное использование операторов безусловного пере- хода goto значительно снижает читабельность программы. Программа, которую можно прочесть от начала до конца, значительно проще для понимания, чем программа, для прослеживания хода выполнения которой читателю требуется переходить от одного опе- ратора к другому, находящемуся в любом месте программы. Однако в некоторых языках использование операторов goto для возврата назад просто необходимо; например, эти операторы нужны для построения циклов WHILE в языке FORTRAN 77. Улучшить чита- бельность программы могут следующие ограничения, налагаемые на использование опе- раторов безусловного перехода to. Операторы безусловного перехода должны предшествовать точке, в которую пе- редается управление, за исключением случая, когда они используются для созда- ния циклов. Точки перехода никогда не должны находиться слишком далеко. Количество операторов безусловного перехода должно быть ограничено. Использовавшимся в начале 1970-х годов версиям языка BASIC и FORTRAN не дос- тавало управляющих операторов, которые налагали бы строгие ограничения на исполь- зование операторов безусловного перехода, поэтому писать удобочитаемые программы на этих языках было трудно. Однако большинство языков программирования, появив- шихся позднее, имело достаточное количество управляющих операторов, так что потреб- ность в операторе безусловного перехода практически отпала. Следовательно, управ- ляющие операторы языка уже не так влияют на его читабельность, как это было раньше. 1.3.1.4. Типы и структуры данных Существенный вклад в улучшение читабельности программы вносят адекватные средства определения типов и структур данных. Предположим, что в качестве индикато- ра в некотором языке используется числовой тип данных, поскольку булевского типа данных в этом языке нет. В таком случае, например, присваивание sum_is_too_big = 1 будет непонятным, в то время как в языке, имеющем булевский тип данных, мы можем записать выражение sum_is_too_big = true Смысл этого выражения абсолютно понятен. Аналогично, тип данных, называемый за- писью. обеспечивает более читабельный способ хранения информации о сотрудниках, чем набор сходных массивов, в каждом из которых хранится отдельный элемент данных, как это бывает, если в языке не предусмотрена поддержка записей. В языке FORTRAN 77, на- пример, информация о сотрудниках может храниться в следующих массивах: CHARACTER (LEN = 30) NAME (100) INTEGER AGE (100), EMPLOYEE_NUMBER (100) REAL SALARY (100) 34 Глава!. Вводные замечания
Таким образом, информация о каждом сотруднике представлена элементами этих че- тырех массивов, имеющих одинаковые индексы. 1.3.1.5. Анализ синтаксической структуры Синтаксис, или форма, элементов языка программирования оказывает значительное влияние на читабельность программы. Ниже перечислены три элемента синтаксической структуры языка, воздействующей на читабельность программы. Формы идентификаторов. Ограничение длины идентификаторов до очень ма- леньких размеров уменьшает читабельность. Если размер идентификатора не мо- жет превышать шести символов, как в языке FORTRAN 77. то часто просто не- возможно использовать осмысленные названия переменных. Еще большей край- ностью является созданный Национальным институтом стандартизации США (ANSI — American National Standards Institute) язык BASIC (ANSI, 1978b). в кото- ром идентификатор может состоять только из одной буквы или сочетания, со- стоящего из буквы и следующей за ней цифры. К вопросу о формах идентификаторов мы еще вернемся в главе 4. Специальные слова. На внешний вид программы и. следовательно, на ее чита- бельность значительно влияет форма специальных слов языка (например, begin, end и for). Особенно важен, главным образом в управляющих структурах, спо- соб образования составных операторов, или групп операторов. В некоторых язы- ках для формирования групп используются согласованные пары специальных слов или символов. В языке Pascal, например, для всех управляющих структур, кроме оператора repeat, используется пара специальных слов begin-end. причем даже там, где она может быть пропущена (свидетельство недостатка ортогональ- ности в языке Pascal). В языке С с той же целью используются фигурные скобки. В обоих языках недостатком является единообразное завершение группы опера- торов. поскольку довольно трудно определить, какая именно группа замыкается очередным оператором end или символом }. В языках FORTRAN 90 и Ada эта проблема не так остра, поскольку для каждого типа группы операторов использу- ется отдельный синтаксис замыкания. В языке Ada, например, для замыкания кон- струкции ветвления используется оператор end if, а для замыкания цикла — оператор end loop. Это — пример противоречия между простотой языка Pascal, приводящей к малому количество зарезервированных слов, и лучшей читабельно- стью языка Ada, обеспеченной многочисленными зарезервированными словами. Другим важным вопросом является использование специальных слов языка в ка- честве имен переменных в программе. Если такая возможность есть, то программа может оказаться очень запутанной. Например, в языке FORTRAN 90 такие специ- альные слова, как DO и END. можно использовать в качестве имен переменных, так что их появление в программе может не означать ничего особенного. Форма и значение. Облегчить чтение программы могут также операторы, само появление которых хотя бы отчасти указывает на их цели. Семантика, или смысл, может следовать непосредственно из синтаксиса, или формы. В некоторых случа- ях этот принцип нарушается двумя языковыми конструкциями, идентичными или похожими по форме, но различными по смыслу, который может зависеть от кон- текста. В языке С. например, значение зарезервированного слова static зависит 1.3. Критерии оценки языков программирования 35
от контекста, в котором оно появляется. Если оно использовано при определении некоторой переменной внутри функции, это означает, что данная переменная соз- дается во время компиляции. Если слово static используется при определении переменной вне всех функций, это означает, что она является видимой только в файле, содержащем данное описание. Следовательно, эта переменная не может экспортироваться из данного файла. Одна из основных жалоб на команды оболочки операционной системы UNIX (Kemighan and Pike, 1984) связана с тем, что названия этих команд не всегда связаны с их предназначением. Например, название используемой в системе UNIX команды grep может быть расшифровано только при наличии предварительных знаний или должной смекалки, а также опыта работы с текстовым редактором ed системы UNIX. Для начи- нающих пользователей системы UNIX появление указанной команды ни о чем не гово- рит. (В текстовом редакторе ed команда /стандартное выражение/ выполняет поиск подстроки, совпадающей с указанным стандартным выражением. Если предварить эту команду атрибутом д, то ее выполнение станет глобальным, определяя весь редактируе- мый файл как область поиска. Употребление атрибута р после указанной команды при- ведет к печати строки, содержащей искомое выражение. Таким образом, команда g/стандартное выражение/р, которая сокращается до grep (по первым буквам анг- лийских слов, образующих эту команду), печатает все строки файла, содержащие под- строку, совпадающую с искомым стандартным выражением.) 1.3.2. Легкость создания программ Легкость создания программ характеризует удобство языка для создания программ в выбранной области. Легкость создания программ в большинстве случаев определяется те- ми же характеристиками, что и читабельность. Это объясняется тем, что в процессе созда- ния программы автор вынужден часто перечитывать уже написанные части программы. Так же, как и читабельность, легкость создания программ должна рассматриваться в контексте конкретной области применения языка. Просто неразумно сравнивать легкость использования двух языков для решения одной задачи, когда один из языков был разра- ботан именно для этого, а другой — нет. Например, легкость создания программ на язы- ках COBOL (ANSI, 1985) и APL (Gilman and Rose, 1976) совершенно различается при обработке двумерных структур, поскольку язык APL подходит для этой цели идеально. А создавать сложные финансовые отчеты намного легче на языке COBOL, поскольку он разрабатывался специально для этой цели, а язык APL — нет. В следующих подразделах описаны важнейшие факторы, влияющие на легкость ис- пользования языка. Т.3.2.1. Простого и ортогональность Если язык содержит много разнообразных конструкций, то некоторые программисты могут просто не знать каждую из них. Это приводит к неправильному использованию одних свойств и игнорированию других. В результате программисты пишут менее изящ- ные и менее эффективные программы, чем могли бы. Как отметил в 1973 году Хоар (Ноаге), некоторые неизвестные свойства могут быть использованы случайно, что при- водит к странным результатам. Следовательно, использовать небольшое количество эле- ментарных конструкций и согласованные между собой правила их комбинирования (т.е. ортогональность) намного лучше, чем применять большое количество примитивов. 36 Глава!. Вводные замечания
В этом случае программист может решить сложную проблему, изучив лишь небольшой набор элементарных конструкции. С другой стороны, слишком ортогональная структура усложняет использование языка. Если можно использовать любую комбинацию элементарных конструкций, то ошибки, до- пущенные при создании программы, могут просто остаться незамеченными. Это приведет к логическим противоречиям в программе, которые не сможет выявить компилятор. 1.З.2.2. Поддержка абстракции Кратко говоря, абстракция— это возможность определять, а затем использовать сложные структуры или операции, игнорируя при этом многие детали. Абстракция — это ключевая концепция разработки современных языков программирования. Следовательно, высокая степень абстракции, допускаемая языком программирования и его выражениями, значительно облегчает его использование. Языки программирования поддерживают две различные категории абстракции: абстракцию процессов и абстракцию данных. Простым примером абстракции процесса может служить подпрограмма, реализую- щая алгоритм сортировки, которая несколько раз вызывается в разных местах главной программы. Если бы этой подпрограммы не было, то команды, выполняющие сортиров- ку, следовало бы внедрять везде, где это нужно, что очень сложно и утомительно. Кроме того, в этом случае детали алгоритма сортировки загромоздили бы основную программу, делая неясным ее общий смысл. В качестве примера абстракции данных можно рассмотреть двоичное дерево, храня- щее в своих узлах целочисленные данные. На языке FORTRAN 77 такое дерево обычно реализуется в виде трех параллельных целочисленных массивов, в которых два целых числа используются в качестве индексов для идентификации положения данных на дере- ве. На языках C++ и Java эти деревья можно реализовать, описав узел дерева в форме простого класса, содержащего два указателя и целое число. Такое описание является бо- лее естественным, поэтому написать программу, использующую двоичное дерево, на языке, поддерживающем такую абстракцию, легче, чем на языке FORTRAN 77. Это оз- начает только то, что следует выбирать язык, наиболее близкий к расматриваемой пред- метной области. Очевидно, что полная поддержка абстракции значительно влияет на легкость исполь- зования языка. 1.3.2.3. Выразительность Выразительность языка может относиться к нескольким различным характеристикам. В языке, подобном языку APL. она означает наличие очень мощных операторов, позво- ляющих производить большое количество вычислений с помощью очень маленькой про- граммы. В более широком смысле выразительность позволяет производить вычисления более удобными и менее громоздкими способами. Например, в языке С запись count++ удобнее и короче записи count = count + 1. Аналогично, булевские операторы and и then в языке Ada помогают описать сокращенное вычисление булевских выражений. В языке Pascal циклы со счетчиком проще создавать с помощью оператора for. чем с помощью оператора while. Очевидно, что все перечисленные возможности облегчают использование языка. 1.3. Критерии оценки языков программирования 37
1.3.3. Надежность Программа называется надежной, если она соответствует своему предназначению в любых условиях. В следующих подразделах описаны языковые свойства, имеющие зна- чительное влияние на надежность программ, написанных на данном языке. 1.3.3.1. Проверка типов Проверка типов — это обычная проверка совместимости типов в программе, осуще- ствляемая при компиляции либо при выполнении программы. Проверка типов является важным фактором надежности языка. В целях экономии рекомендуется проводить про- верку в процессе компиляции. Более того, чем раньше в программе будет обнаружена ошибка, тем дешевле обойдется ее устранение. Структура языка Ada требует проверки типов практически всех переменных и выражений в процессе компиляции, если только пользователь не дал явного указания заблокировать проверку типов. Такой подход фак- тически устраняет ошибки типов при выполнении программы, написанной на языке Ada. Более подробно типы и проверка типов описаны в главах 4 и 5. Одним из примеров того, как отказ от проверки типов при компиляции или при вы- полнении может привести к многочисленным ошибкам в программе, служит использо- вание параметров подпрограмм в первоначальной версии языка С (Kemighan and Ritchie, 1978). В этом языке совпадение типов фактического и формального параметров при вы- зове функции не проверялось. Переменная типа int могла использоваться в качестве действительного параметра при вызове функции, ожидавшей формального параметра, имевшего тип float, и ни компилятор, ни система поддержки выполнения программ не могли выявить это несоответствие. Это, естественно, приводило к проблемам, источник которых было трудно определить. (Для решения таких проблем в операционную систему UNIX была включена сервисная программа lint, выполнявшая проверку типов в про- граммах, написанных на языке С.) Подробнее о подпрограммах и методах передачи па- раметров рассказывается в главе 8. В языке Pascal диапазон изменения индексов массива является частью типа перемен- ной, представляющей собой массив. Следовательно, проверка диапазона изменения ин- дексов является частью проверки типов, хотя она должна производиться во время вы- полнения программы. Поскольку в языке Pascal контролируется большинство типов, то в нем выполняется также проверка диапазона изменения индексов. Такая проверка крайне важна для обеспечения надежности программы, поскольку индексы, выходящие за пре- делы допустимого диапазона, часто создают проблемы, проявляющиеся намного позже того момента, когда действительно произошло нарушение. Аналогичная проверка диапа- зона индексов проводится также в языках Ada и Java. 1.3.3.2. Обработка исключительных ситуаций Обработка исключительных ситуаций позволяет перехватывать ошибки и другие необычные ситуации во время выполнения программы, принимать соответствующие меры, а затем продолжать работу. Это значительно повышает надежность программ. Языки Ada, C++ и Java позволяют обрабатывать исключительные ситуации, однако во многих широко используемых языках, таких как С или FORTRAN, эти возможности практически отсутст- вуют. Более подробно обработка исключительных ситуаций рассмотрена в главе 13. 38 Глава 1. Вводные замечания
1.3.3.3. Совмещение имен Совмещение имен — это наличие нескольких разных имен у одной и той же ячейки памяти. В наше время широко распространено мнение, что совмещение имен — опасная функция языка программирования. Большинство языков программирования допускают не- которые виды совмещения имен — например, в языке С указатели и члены объединения могут ссылаться на одну и ту же переменную. В обоих случаях две различные переменные в программе могут ссылаться на одну и ту же ячейку памяти. Некоторые разновидности со- вмещения имен, как описывается в главах 4 и 8, могут запрещаться структурой языка. В некоторых языках совмещение имен используется для компенсации недостатков языковых средств, поддерживающих абстракцию данных. В других языках совмещение имен значительно ограничивается для повышения надежности. Т.3.3.4. Легкость чтения и использования На надежность программы влияет как легкость ее чтения, так и легкость ее создания. В программе, написанной на языке, не поддерживающем естественные способы выражения требуемых алгоритмов, обязательно будут использоваться неестественные методы, а такие методы наверняка не будут корректно работать во всех возможных ситуациях. Чем легче написать программу, тем выше вероятность того, что она будет правильно работать. Читабельность влияет на надежность программы и на стадии создания, и на стадии ее эксплуатации. Программу, которую сложно прочесть, одинаково трудно будет писать и редактировать. 1.3.4. Стоимость Суммарная стоимость языка программирования зависит от многих его характеристик. Во-первых, в стоимость входят затраты на обучение программистов данному языку. Эта сумма зависит от простоты и ортогональности языка, а также от опыта программи- стов. Как правило, более мощные языки изучать труднее. Во-вторых, в сумму входит стоимость создания программ на данном языке. Она зави- сит от легкости использования языка, зависящей, в свою очередь, от того, насколько он подходит для конкретного приложения. Первые попытки разработать и реализовать язы- ки высокого уровня были вызваны желанием уменьшить затраты на создание программ- ного обеспечения. Стоимость подготовки программистов и создания программ может быть значительно уменьшена за счет выбора хорошей среды программирования. Подробнее среды про- граммирования рассматриваются в разделе 1.7 данной главы. Третьим фактором, влияющим на стоимость языка, являются затраты на компиляцию программ. Основным препятствием для использования языка Ada на первых порах была именно чрезмерно высокая стоимость работы его компиляторов первого поколения. С появлением более совершенных компиляторов языка Ada острота этой проблемы была снижена. Четвертым фактором является стоимость выполнения программы, написанной на данном языке. Она значительно зависит от структуры языка. Язык, требующий много- численных проверок типов во время выполнения программы, будет препятствовать бы- строй работе программы независимо от качества компилятора. Эффективность выполне- ния программ была основной целью при разработках ранних языков программирования, однако сейчас ее считают менее важной. 1.3. Критерии оценки языков программирования 39
Между стоимостью компиляции и скоростью выполнения программы может быть достигнут простой компромисс. Совокупность способов, которые компилятор может ис- пользовать для уменьшения размера и/или увеличения скорости выполнения программы, называется оптимизацией. Если оптимизация применяется в небольших объемах или иг- норируется вообще, то компиляцию можно выполнить намного быстрее, чем в случае, когда для создания оптимизированной программы прилагаются значительные усилия. В свою очередь, дополнительная работа компилятора, направленная на оптимизацию ко- да, ускорит выполнение программы. Выбор между этими двумя альтернативами опреде- ляется средой, в которой используется компилятор. В лаборатории для начинающих сту- дентов-программистов, затрачивающих большую часть времени на компиляцию про- грамм, время выполнения которых незначительно (их программы невелики и должны правильно отработать только один раз), следует осуществлять незначительную оптими- зацию кода или не применять ее совсем. В производственной среде, где завершенные программы выполняются многократно, денег на оптимизацию жалеть не стоит. Пятым фактором, влияющим на стоимость языка, являются затраты на систему реа- лизации языка. Одна из причин быстрой популярности языка Java — бесплатное распро- странение систем компиляции/интерпретации вскоре после разработки первой версии языка. Язык, система реализации которого либо слишком дорога, либо работает только на дорогостоящем аппаратном обеспечении, имеет меньше шансов получить широкое распространение. Шестой фактор — стоимость низкой надежности. Если программное обеспечение бу- дет давать сбои в таких жизненно важных системах, как атомные электростанции или рентгеновские аппараты, то стоимость окажется очень высокой. Сбои в некритических системах также могут быть дорогими, если несовершенные системы программного обес- печения приведут к срыву потенциальных сделок или проигрышу судебных процессов. Последней мы рассмотрим стоимость эксплуатации Программ, в которую входят вне- сение исправлений и осуществление модификаций с целью добавления новых возможно- стей. Стоимость сопровождения программного обеспечения зависит от многих характе- ристик языка, но, в первую очередь, от его читабельности. Поскольку поддержка зачас- тую производится не авторами программного обеспечения, то плохая читабельность может крайне усложнить эту задачу. Важность удобства эксплуатации программного обеспечения трудно переоценить. Исследователями были получены оценки, свидетельствующие о том, что затраты на экс- плуатацию больших систем программного обеспечения, существующих относительно долго, могут быть в 2-4 раза больше затрат на их разработку (Sommerville, 1992). Из перечисленных факторов, влияющих на стоимость языка, три являются основны- ми: разработка программы, ее сопровождение и надежность. Поскольку указанные фак- торы зависят от легкости создания и использования программы, то эти два критерия оценки также очень важны. Разумеется, существует множество других критериев оценки языков программирова- ния. Одним из них, например, является мобильность, определяющая легкость, с которой программу можно переносить из одной реализации в другую. На мобильность значи- тельно влияет уровень стандартизации языка. Некоторые языки, например BASIC, со- всем не стандартизированы, что затрудняет перенос программ, написанных на этих язы- ках. из одной реализации в другую. Стандартизация — долговременный и сложный про- цесс. Комитет начал работу по выработке унифицированной версии языка C++ в 1989 году. К концу 1998 года этот процесс еще не был завершен. 40 Глава!. Вводные замечания
Еще два критерия оценки языка — универсальность (применимость к широкому кру- гу задач) и четкость (полнота и точность официального описания языка). Большинство критериев, в частности читабельность, легкость создания и надежность, не являются ни строго определенными, ни точно измеримыми. Тем не менее, эти кон- цепции полезны и дают ценную возможность выявить суть структуры и оценки языков программирования. Последнее замечание: критерии разработки языка имеют разный вес при рассмотрении с разных точек зрения. Разработчиков систем реализации языков в основном интересует сложность реализации конструкций и свойств языка. Пользователей языка в первую оче- редь волнует легкость создания, а во вторую— читабельность. Разработчики языков про- граммирования придают особое значение элегантности и возможности широкого исполь- зования языка. Кроме того, указанные характеристики иногда противоречат одна другой. 1.4. Факторы, влияющие на разработку языка Кроме характеристик, описанных в разделе 1.3, существует еще несколько факторов, влияющих на разработку языка программирования. Важнейшие из них— архитектура компьютера и методологии программирования. 1.4.1. Архитектура компьютера Базовая архитектура компьютера оказывает решающее влияние на разработку языка. Большинство популярных языков последних 35 лет разрабатывались на основе сложив- шейся архитектуры компьютера, названной по имени одного из ее авторов — Джона фон Неймана (John von Neumann)— неймановской архитектурой. Эти языки программирова- ния называются императивными. В компьютере Неймана данные и программы хранят- ся в одной и той же памяти. Центральный процессор (CPU — central processor unit), вы- полняющий команды, от памяти отделен. Следовательно, команды и данные должны на- правляться, или передаваться, процессору из памяти. Результаты операций, выполнен- ных процессором, должны возвращаться в память. На неймановской архитектуре ком- пьютера основаны практически все цифровые вычислительные машины, созданные по- сле 1940-х годов. Общая структура компьютера Неймана показана на рис. 1.1. Главными элементами императивных языков программирования, основанных на неймановской архитектуре компьютера, являются переменные, которые моделируют ячейки памяти; операторы присваивания, основанные на операции пересылки данных; а также итеративная форма повторений, являющаяся наиболее эффективным методом в этой архитектуре. Операнды выражений передаются из памяти в процессор, а результат вычисления выражения возвращается в ячейку памяти, представляемую левой частью оператора присваивания. Поскольку команды хранятся в соседних ячейках памяти, то итерации на компьютере Неймана выполняются быстро. Эта эффективность не способ- ствует использованию рекурсий для повторения операций, хотя применение рекурсии часто более естественно. 1.4. Факторы, влияющие на разработку языка 41
Устройства ввода-вывода Центральный процессор Рис. 1.1. Архитектура компьютера по Нейману Как указывалось ранее, в функциональных, или прикладных, языках программирования вычисления в основном производятся посредством применения функций к заданным пара- метрам. Программирование на функциональном языке может осуществляться без тех видов переменных, которые используются в императивных языках программирования, без опера- торов присваивания и без итераций. Хотя многие ученые в области компьютерных наук приводят миллионы доводов в пользу таких функциональных языков программирования, как LISP, не похоже, что эти языки заменят императивные языки программирования, по крайней мере, до тех пор, пока не будет разработан компьютер с архитектурой, отличной от неймановской, эффективно выполняющий программы, написанные на функциональных языках программирования. Среди людей, недовольных этим фактом, выделяется Джон Бэ- кус. главный разработчик исходной версии языка FORTRAN (Backus, 1978). Машины с параллельной архитектурой, появившиеся за последние 15 лет, дают на- дежду, что программы, написанные на функциональных языках, будут выполняться бы- стрее, но этого совершенно недостаточно для того, чтобы сделать их конкурентоспособ- ными по сравнению с программами, написанными на императивных языках программи- рования. Действительно, несмотря на то что существуют элегантные способы исполь- зования параллельной архитектуры для выполнения программ на функциональных язы- ках программирования, большинство машин с параллельной архитектурой используется для выполнения программ, написанных на императивных языках, в частности программ, написанных на диалектах языка FORTRAN. 1.4.2. Методологии программирования В конце 1960-х-начале 1970-х годов началось интенсивное изучение структурного программирования и его применения для разработки программного обеспечения и язы- ков программирования. Важной причиной этих исследований было смещение основных статей расходов с аппаратного на программное обеспечение, произошедшее вследствие 42 Глава!. Вводные замечания
уменьшения стоимости микросхем и увеличения стоимости программ. Увеличение про- изводительности работы программистов было относительно небольшим. Кроме того, с помощью компьютеров решались все большие и сложные задачи. Программы писались не только для простого решения систем уравнений, моделирующих траектории спутни- ков, как в начале 1960-х годов, но и для таких задач, как управление большими нефтепе- регонными заводами и всемирная система бронирования авиабилетов. Новые методологии разработки программного обеспечения, возникшие в результате исследований, проведенных в 1970-х годах, были названы нисходящим проектированием и пошаговым уточнением. Основными обнаруженными недостатками языков програм- мирования были неполная проверка типов и нехватка управляющих операторов (приводившая к широкому использованию операторов goto). В конце 1970-х годов началось смещение центра внимания от процедурно-ориен- тированных к информационно-ориентированным методологиям разработки программ. Проще говоря, информационно-ориентированные методы придавали особое значение структуре данных, сосредоточившись на использовании абстрактных типов данных для решения проблем. Для того чтобы эффективно использовать абстракцию данных при разработке про- граммного обеспечения, нужно было обеспечить ее поддержку языками программирова- ния. Первым языком, предназначенным для ограниченной поддержки абстракции дан- ных, был язык SIMULA 67 (Birtwistle et al., 1973), хотя это и не принесло ему популярно- сти. Преимущества абстракции данных начали осознаваться только в начале 1970-х годов. Тем не менее, большинство языков, созданных с конца 1970-х годов, имеют эту возможность. Подробно абстракция данных рассматривается в главе 10. Последним этапом эволюции информационно-ориентированного программного обеспечения, начавшимся в начале 1980-х, стало возникновение объектно-ориенти- рованной методологии разработки языков. Объектно-ориентированная методология со- стоит из трех основных частей: абстракции данных, инкапсулирующей в одном объекте данные вместе с методами их обработки и скрывающей доступ к данным, а также насле- дования и динамического связывания. Наследование — это мощная концепция, значи- тельно усилившая возможность многократно использовать существующее программное обеспечение и. соответственно, резко повысившая производительность труда при разра- ботке программ. Наследование— это важный фактор увеличения популярности объект- но-ориентированных языков программирования. Более гибко использовать наследование позволяет динамическое связывание, осуществляемое в ходе выполнения программы. Объектно-ориентированное программирование развивалось одновременно с языком Smalltalk (Goldberg and Robson, 1983), поддерживающим его концепцию. Несмотря на то что язык Smalltalk не получил такого распространения, как некоторые другие языки, под- держка объектно-ориентированного программирования теперь включена в популярнейшие императивные языки программирования, к которым относятся языки Ada 95 (AARM, 1995), Java и C++. Концепции объектно-ориентированного программирования также нашли свое применение в функциональном программировании— в языке CLOS (Bobrow, 1988) и в логическом программировании— в языке Prolog++. Языковая поддержка объектно- ориентированного программирования подробно рассматривается в главе 11. Процедурно-ориентированное программирование в некотором смысле является противоположностью информационно-ориентированного программирования. Методы последнего сейчас преобладают в разработке программного обеспечения, но и методы процедурно-ориентированного программирования еще не забыты. Наоборот, в последние 1.4. Факторы, влияющие на разработку языка 43
годы значительная часть исследований ведется именно в области процедурно-ориен- тированного программирования, особенно это относится к области параллельного выполнения программ. В результате этих научно-исследовательских работ возникла необходимость разработать языковые средства, позволяющие создавать параллельные программные модули и управлять ими. Такими свойствами обладают языки Ada и Java. Таким образом, развитие программирования снова требует от языков новых возможностей. Более подробно вопросы, связанные с параллельностью, рассматриваются в главе 12. 1.5. Категории языков Часто языки программирования делят на четыре типа: императивные, функциональ- ные, логические и объектно-ориентированные. Мы уже обсуждали характеристики им- перативных и функциональных языков программирования. Кроме того, выше описано, как наиболее популярные объектно-ориентированные языки программирования про- изошли от императивных. Несмотря на то что парадигма разработки объектно-ориен- тированного программного обеспечения значительно отличается от процедурно- ориентированной парадигмы, обычно используемой в императивных языках программи- рования, вполне возможно расширить возможности императивных языков за счет объ- ектно-ориентированных свойств. Например, выражения, операторы присваивания и управляющие операторы в языках С и Java практически идентичны. (С другой стороны, массивы, подпрограммы и семантика этих языков различаются значительно.) Логический язык программирования — пример языка, основанного на продукцион- ных правилах. В императивном языке программирования алгоритмы описываются очень подробно с обязательным указанием порядка выполнения команд или операторов. В языке, основанном на продукционных правилах, не существует определенного порядка применения правил, и система реализации языка должна сама выбрать нужный порядок выполнения команд, который приведет к желаемому результат}'. Такой подход к разра- ботке программного обеспечения радикально отличается от подходов, используемых в остальных трех группах языков программирования и, безусловно, требует абсолютно иных языков. Самый популярный язык логического программирования Prolog и собст- венно логическое программирование описываются в главе 15. С языками программирования иногда путают такие языки разметки, как HTML. Од- нако эти языки не описывают вычислений— они всего лишь задают общий вид доку- мента. Тем не менее, к языкам разметки применимы многие методы разработки и крите- рии оценки, описываемые в данной главе. Помимо всего прочего, вполне очевидна важ- ность читабельности и легкости создания кодов разметки. 1.6. Компромиссы при разработке языка Описанные в разделе 1.3 критерии оценки языков программирования создают основу для разработки языка. К сожалению, эта основа внутренне противоречива. В своей про- ницательной статье, посвященной вопросам разработки языков программирования, Хоар отметил: “Существует так много важных, но противоречивых критериев, что их согласо- вание между собой и удовлетворение является основной задачей проектирования” (Ноаге, 1973). 44 Глава!. Вводные замечания
Примером пары конфликтующих между собой критериев могут служить надежность и стоимость выполнения программы. Например, в языке Ada выполняется проверка всех ссылок на элементы массива, для того чтобы гарантировать, что индекс или индексы на- ходятся в допустимых пределах. Это значительно повышает стоимость выполнения про- грамм. написанных на языке Ada. содержащих большое количество ссылок на элементы массивов. В языке С проверка пределов изменения индексов не предусмотрена, так что программы на языке С выполняются быстрее, чем семантически эквивалентные про- граммы на языке Ada. хотя надежность выше у последних. Разработчики языка Ada по- жертвовали эффективностью выполнения в пользу надежности. В качестве другого примера конфликтующих между собой критериев, прямо приво- дящих к поиску компромиссов при разработке, рассмотрим язык APL. Этот язык содер- жит мощный набор операторов для действий с массивами. Из-за большого количества этих операторов для их представления в язык APL пришлось включить значительное число новых символов. Кроме того, многие операторы языка APL могут использоваться в отдельных длинных и сложных выражениях. Такая высокая степень выразительности языка позволяет легко писать программы, содержащие большое количество операций с массивами. Действительно, огромное количество вычислений может быть описано в очень компактной программе. Недостатком является плохая читабельность программ, написанных на языке APL. Компактное и лаконичное выражение с математической точ- ки зрения, конечно, красиво, но для всех, кроме автора, создает сложности при чтении. Известный автор Даниель Мак-Кракен (Daniel McCracken) однажды отметил, что чтение и анализ программы из четырех строк, написанной на языке APL. заняли у него четыре часа (McCracken, 1970). Таким образом, разработчики языка APL пожертвовали чита- бельностью в пользу легкости создания программ. Противоречия между гибкостью и безопасностью обычны для языков программирова- ния. Записи в языке Pascal разрешают ячейке памяти в разное время содержать переменные разных типов. Например, ячейка может содержать либо указатель, либо целое число. Таким образом, с помещенным в такую ячейку указателем можно выполнять действия, опреде- ленные для целых величин. Это создает лазейку в проверке типов, позволяющую програм- ме выполнять арифметические действия с указателями, что иногда бывает удобным. Одна- ко следует заметить, что бесконтрольно использовать ячейки памяти опасно. Существует множество примеров конфликтов между критериями разработки (и оцен- ки) языка; некоторые из них очевидны, другие— едва различимы. Отсюда следует, что решить задачу выбора конструкций и свойств языка невозможно без компромиссов. 1.7. Методы реализации Как указано в разделе 1.4.1, двумя основными компонентами компьютера являются его оперативная память и процессор. Оперативная память используется для хранения программ и данных. Процессор — это совокупность электронных схем, обеспечивающих реализацию набора элементарных операций, или машинных команд, т.е. арифметических и логических операций. В большинстве компьютеров некоторые из этих команд, иногда называемых макрокомандами, реализуются с помощью набора низкоуровневых команд, называемых микрокомандами. Поскольку микрокоманды всегда скрыты от программи- ста, то их обсуждение, как правило, не включается в обсуждение программного обеспе- чения. По этой причине микрокоманды мы далее обсуждать не будем. 1.7. Методы реализации 45
Набор макрокоманд компьютера называется его машинным языком. При отсутствии остального программного обеспечения большая часть аппаратного обеспечения компью- тера “понимает” только его машинный язык. Теоретически можно спроектировать и соз- дать компьютер, использующий некий язык высокого уровня в качестве машинного, но это было бы очень сложно и дорого. Более того, он был бы крайне негибким, поскольку такой компьютер сложно (хотя и можно) использовать совместно с другими языками вы- сокого уровня. Более практичное решение — аппаратно реализовать язык очень низкого уровня, что обеспечит выполнение наиболее распространенных элементарных операций и потребует системного программного обеспечения для взаимодействия с программами, написанными на языках высокого уровня. Система реализации языка не может рассматриваться как единственное программное обеспечение компьютера. Кроме нее нужен также большой набор программ, поддержи- вающих языковые конструкции более высокого уровня, чем конструкции машинного языка. Такой набор программ называется операционной системой. Языковые конструк- ции, поддерживаемые операционной системой, осуществляют управление системными ресурсами, операциями ввода-вывода, системой управления файлами, редакторами тек- ста и/или программ, а также обеспечивают выполнение большого количества других не- обходимых функций. Поскольку системам реализации языка требуется большое количе- ство возможностей операционной системы, они взаимодействуют с ней. а не с процессо- ром (на машинном языке). Уровни операционной системы и систем реализации языков программирования нахо- дятся выше уровня машинного языка. Эти системы можно представить в виде виртуаль- ных компьютеров, обеспечивающих взаимодействие с пользователем, находящимся на более высоком уровне. Например, операционная система и компилятор языка С создают виртуальный компьютер языка С. С другим компилятором машина может стать другим виртуальным компьютером. Большинство вычислительных систем создает несколько различных виртуальных компьютеров. Пользовательские программы образуют следую- щий уровень, находящийся над уровнем виртуальных компьютеров. Многоуровневое представление компьютера показано на рис. 1.2. Системы реализации первых языков программирования высокого уровня были созда- ны в конце 1950-х годов и считались одними из сложнейших систем программного обес- печения того времени. В 1960-х годах были проведены интенсивные научно- исследовательские работы, направленные на анализ и формализацию процесса конст- руирования этих реализаций языков высокого уровня. Наибольшие успехи были достиг- нуты в области синтаксического анализа, главным образом из-за того, что эта часть про- цесса реализации является приложением части теории автоматов и теории формальных языков, которые в то время были хорошо изучены. 46 Глава!. Вводные замечания
Виртуальный Ada Рис. 1.2. Многоуровневый интерфейс виртуальных компьютеров, создавае- мый стандартной вычислительной системой 1.7.1. Компиляция Языки программирования могут реализовываться одним из трех методов. Например, программа может переводиться на машинный язык и выполняться непосредственно ком- пьютером. Такая реализация называется компиляцией. Преимущество этого метода — очень быстрое выполнение программы после завершения процесса трансляции. Боль- шинство промышленных реализаций таких языков, как С, COBOL и Ada, выполняются с помощью компилятора. Язык, который компилятор переводит в машинный, называется исходным языком. Процесс компиляции протекает в несколько этапов, важнейшие из которых показаны на рис. 1.3. 1.7. Методы реализации 47
Результаты (необязательно) Рис. 1.3. Процесс компиляции Лексический анализатор объединяет символы исходной программы в лексические единицы. Лексические единицы программы — это идентификаторы, служебные слова, операторы и знаки пунктуации. Поскольку комментарии исходной программы компиля- тором не используются, то лексический анализатор их игнорирует. Синтаксический анализатор получает от лексического анализатора лексические еди- ницы и использует их для создания иерархических структур, называемых деревьями син- таксического анализа. Эти деревья представляют собой синтаксическую структуру про- 48 Глава!. Вводные замечания
граммы. Однако в большинстве случаев никакой структуры дерева синтаксического ана- лиза не создается, а просто генерируется и используется информация, необходимая для его создания. К лексическим единицам и деревьям синтаксического анализа мы еще вер- немся в главе 3. Генератор промежуточных команд создает программы на особом языке, промежуточ- ном между языком исходной программы и машинным языком. (Отметим, что термины “язык" и “код” — часто взаимозаменяемы.) Промежуточные языки зачастую очень похожи на языки ассемблера и иногда действительно ими являются. В остальных случаях промежу- точный код имеет более высокий уровень, чем язык ассемблера. Неотъемлемой частью ге- нератора промежуточного кода является семантический анализатор, выполняющий поиск тех ошибок, выявление которых в процессе синтаксического разбора затруднительно либо невозможно, например, ошибок, связанных с несовместимостью типов. Простейшие примеры синтаксического анализа и генерации промежуточного кода рассмотрены в главе 3. Оптимизация, улучшающая программы путем уменьшения их размеров или ускоре- ния их выполнения, или и того, и другого одновременно, часто является дополнительной частью процесса компиляции. В действительности некоторые компиляторы вообще не способны выполнять сколько-нибудь существенную оптимизацию. Такие компиляторы могут использоваться в ситуации, когда скорость компиляции программы значительно важнее скорости выполнения оттранслированной программы. В качестве примера такой ситуации можно привести вычислительную лабораторию для начинающих программи- стов. Однако в большинстве коммерческих и промышленных ситуаций скорость выпол- нения значительно важнее скорости компиляции, так что оптимизация желательна. По- скольку многие разновидности оптимизации не могут выполняться на машинных языках, то для выполнения основной оптимизации используется промежуточный язык. Генератор кода переводит оптимизированную программу с промежуточного языка на машинный. Таблица символов используется в качестве базы данных для процесса компиляции. Изначально в этой таблице находится информация о типах и атрибутах всех определен- ных пользователем имен, встречающихся в программе. Эту' информацию в таблицу сим- волов помещают лексические и синтаксические анализаторы, а используют семантиче- ский анализатор и генератор кода. Как указывалось выше, хотя машинный код, генерируемый компилятором, может выполняться непосредственно аппаратным обеспечением, почти всегда требуется, чтобы он выполнялся вместе с какими-то еще программами. Для выполнения большинства пользовательских программ необходимы также программы операционной системы. К числу наиболее часто используемых из них относятся программы ввода-вывода. Ком- пьютер вызывает требуемые системные программы при необходимости. Прежде чем созданная компилятором программа на машинном языке будет выполнена, требуется найти и связать с программой пользователя нужную программу операционной системы. Операция связывания соединяет программу пользователя и системную программу между собой, помещая адрес входа в системную программу на место ее вызова из программы пользователя. Пользовательский и системный код, взятые вместе, часто называют загру- зочным модулем. Процесс сборки системных программ и связывания их с программой пользователя называется редактированием связей и загрузкой (loading), или просто ре- дактированием связей (linking). Эти действия выполняет системная программа под на- званием редактор связей (linker). 1.7. Методы реализации 49
Пользовательские программы часто должны связываться не только с системными программами, но и с предварительно откомпилированными пользовательскими про- граммами. находящимися в библиотеках. Таким образом, редактор связей может связы- вать данную программу не только с системными программами, но и с другими програм- мами пользователя. Выполнение программы на машинном коде на компьютере с неймановской архитек- турой происходит в ходе процесса, называемого циклом выборки-исполнения коман- ды. Как отмечалось в разделе 1.4.1. программы находятся в памяти, но выполняются процессором. Каждая команда, подлежащая выполнению, должна пересылаться из памя- ти в процессор. Адрес следующей команды, подлежащей выполнению, хранится в реги- стре. называемом счетчиком программы. Цикл выборки-исполнения команды можно просто описать с помощью следующего алгоритма: хнхциализировать счетчик программы повторять постоянно выбрать команду, указываемую счетчиком программы увеличить счетчик для указания на следующую команду расшифровать команду выполнить команду конец повторения Этап “расшифровки команды” описанного выше процесса подразумевает изучение команды, чтобы определить, какое действие она обозначает. Выполнение программы за- вершается при достижении команды остановки, хотя такая команда в реальных компью- терах выполняется очень редко. Вместо этого, операционная система передает управле- ние программе на время ее выполнения, а после завершения программы управление вновь возвращается операционной системе. Отметим, что в вычислительных системах, в памяти которых может одновременно находиться несколько программ, этот процесс зна- чительно сложнее. Поскольку время выполнения команды, подлежащей выполнению, часто меньше времени, затрачиваемого на ее передачу процессору, то скорость работы компьютера определяется скоростью соединения между процессором и памятью компьютера. Это со- единение называется неймановским узким местом и является основным фактором, ог- раничивающим скорость работы машин с неймановской архитектурой. Именно наличие неймановского узкого места стало одной из основных причин исследований и разработок в области параллельных компьютеров. 1.7.2. Чистая интерпретация При использовании диаметрально противоположных методов реализации программа может интерпретироваться с помощью другой программы, называемой интерпретато- ром. вообще без трансляции. Интерпретатор программно моделирует работу машины, цикл выборки-исполнения которой работает с командами на языках высокого уровня, а не с машинными командами. Разумеется, такое программное моделирование создает виртуальную вычислительную машину, реализующую язык. Достоинством этого подхода, получившего название чистой интерпретации, или просто интерпретации, является легкость реализации многих операций отладки на уров- не исходной программы, поскольку все сообщения об ошибках, возникающих в процессе выполнения, могут ссылаться на модули исходной программы. Например, при обнару- 50 Глава!. Вводные замечания
женин индекса массива, находящегося вне допустимого диапазона значений, в сообще- нии об ошибке будет указана строка исходной программы и имя массива. С другой сто- роны, у этого метода имеется серьезный недостаток— выполнение интерпретируемых программ от 10 до 100 раз медленней, чем компилируемых. Главным источником замед- ления является расшифровка операторов языка высокого уровня, значительно более сложных, чем команды на машинном языке (хотя они могут записываться и меньшим числом команд, чем эквивалентные программы на машинном языке). Следовательно, уз- ким местом чистого интерпретатора является не соединение между памятью и процессо- ром, а расшифровка операторов. Еще одним недостатком чистой интерпретации является тот факт, что для нее часто требуется больше памяти. Кроме исходной программы в процессе интерпретации долж- на участвовать еще и таблица символов. Более того, исходная программа может хранить- ся в памяти в виде, ориентированном на легкий доступ и модификацию программы, а не на компактный размер. Процесс интерпретации довольно сложен для программ, написанных на сложных языках, посколь- ку смысл каждого выражения и оператора должен определяться непосредственно во время выполнения исходной программы. Чистая интерпретация приме- няется для языков с более простой структурой. На- пример, системами с чистой интерпретацией иногда реализуются языки APL и LISP. Большинство ко- манд операционных систем, содержащихся, напри- мер, в сценариях оболочек операционной системы UNIX или .ВАТ-файлах системы DOS, также реали- зуются посредством чистой интерпретации. Более сложные языки, такие как FORTRAN и С. редко реа- лизуются с помощью чистых интерпретаторов. Графически процесс чистой интерпретации представлен на рис. 1.4. 1.7.3. Смешанные системы реализации Некоторые системы реализаций языков представляют собой компромиссный вариант между компилятором и чистой интерпретацией: они транслируют программу с языка вы- сокого уровня на промежуточный язык, разработанный для обеспечения более легкой интерпретации. Этот метод работает быстрее, чем чистая интерпретация, поскольку вы- ражения исходного языка расшифровываются только один раз, и называется смешанной системой реализации (hybrid implementation system). Процесс, используемый в смешанной системе реализации, показан на рис. 1.5. В нем вместо трансляции промежуточных команд в машинные коды происходит их интерпре- тация. Примером языка, реализованного с помощью описанной системы, может служить язык Perl. Этот язык происходит от интерпретируемых языков программирования sb и awk, однако он является частично компилируемым, что позволяет обнаруживать ошибки до начала интерпретации и упрощать ее. 1.7. Методы реализации 51
Изначально все реализации языка Java были смешанными. Его промежуточная форма, названная байтовым кодом, обеспечивает переносимость языка на любую машину, содер- жащею интерпретатор байтовых кодов и соответствующую систему поддержки исполнения программ. Все вместе это называется Java Virtual Machine ("виртуальная вычислительная машина на языке Java**)* Сейчас появились системы, транслирующие байтовые коды языка Java в машинные коды для ускорения их выполнения. Однако аплеты языка Java всегда за- грежаются с Web-сервера только в форме байтового кода. Иногда разработчик системы реализации языка может создавать и компилирующую, и интерпретирующую системы реализации. В таких случаях интерпретатор используется для разработки и отладки программы. После отладки программ они компилируются для ускорения их выполнения. Результаты Рис. 1.5. Смешанные системы реализации 52 Глава!. Вводные замечания
1.8. Среды программирования Среда программирования — это совокупность инструментов, используемых при разра- ботке программного обеспечения. Этот набор обычно состоит только из файловой систе- мы, текстового редактора, редактора связей и компилятора. Дополнительно он может включать большое количество инструментальных комплексов с единообразным интерфей- сом пользователя. В этом случае разработка и эксплуатация программного обеспечения значительно улучшается. Следовательно, характеристики языка программирования — это не только мера возможностей, предоставляемых системой для разработки программного обеспечения. Ниже кратко представлены некоторые среды программирования. Старейшей средой программирования считается UNIX — машинно-независимая опе- рационная система с разделением времени, начавшая распространяться в середине 1970-х годов. Она предоставляет многочисленные мощные инструментальные средства, предна- значенные для производства программного обеспечения и эксплуатации разнообразных языков. В прошлом основным недостатком системы UNIX было отсутствие единообраз- ного интерфейса ее инструментов. Это затрудняло ее изучение и использование. Однако сейчас работа с системой UNIX часто осуществляется с помощью графического интер- фейса, устанавливаемого поверх нее. Во многих случаях этим интерфейсом является среда программирования Common Desktop Environment (CDE). На IBM-совместимых микрокомпьютерах используется среда Borland C++. содержащая встроенный компилятор, текстовый редактор, отладчик и файловую систему, причем дос- туп к ним осуществляется с помощью графического интерфейса. Одним из удобных свойств данной среды программирования является то. что при обнаружении компилятором синтаксической ошибки происходит переключение на текстовый редактор, причем курсор помешается в то место исходной программы, где была обнаружена ошибка. Язык и интегрированная среда программирования Smalltalk продуманнее, сложнее и мощнее, чем среда Borland C++. Система Smalltalk первой стала использовать оконный интерфейс и мышь для обеспечения единообразного доступа ко всем инструментам. К обсуждению этого языка мы еще вернемся в главе 11. Последнюю стадию развития сред разработки программного обеспечения представ- ляет собой среда Microsoft Visual C++. Это объемный и продуманный набор инструмен- тов разработки программного обеспечения, доступных посредством оконного интерфей- са. Эта система, а также подобные ей Visual BASIC. Delphi и Java Development Kit ком- пании Sun Microsystems предлагают легкий способ создания графических интерфейсов для программ пользователя. Не вызывает сомнений, что для большинства разработок программного обеспечения, по крайней мере в ближайшем будущем, будут использоваться мощные среды програм- мирования. Это. очевидно, приведет к увеличению производства программного обеспе- чения и. возможно, к улучшению его качества. 1.8. Среды программирования 53
Р е з ю м е Изучение языков программирования полезно по многим причинам: оно позволяет ис* пользовать более разнообразные конструкции при создании программ, более осмыслен- но выбирать языки программирования, подходящие для разрабатываемых проектов, а также облегчает изучение новых языков программирования. Компьютеры применяются для решения задач, возникающих в различных областях. Разработка и оценка отдельного языка программирования в значительной мере опреде- ляется областью его применения. В число важнейших критериев оценки языков программирования входят читабель- ность. легкость создания программ, надежность и общая стоимость. Эти критерии и бу- дут тем базисом, на котором мы построим наше дальнейшее изучение и оценку различ- ных свойств языков программирования. Основное влияние на разработку языка оказывает вычислительная архитектура и ме- тодологии разработки программного обеспечения. Разработка языка программирования— это искусство проектирования, в котором должен быть достигнут компромисс между огромным количеством свойств, конструкций и возможностей. Основными методами реализации языка программирования являются компиляция, чистая интерпретация и смешанная реализация. Среды программирования стали важной частью систем разработки программного обеспечения, в которых языки программирования являются лишь одним из многих ком- поненюв. Вопросы 1. Почему программисту полезно иметь представление о разработке языков про- граммирования. даже если сам он никогда не будет разрабатывать ни один язык про1раммирования? 2. Какие выгоды могут принести знания свойств языков программирования всему компьютерному сообществу? 3. Какой язык программирования последние 35 лет занимает ведущее место в науч- ных вычислениях? 4. Какой язык программирования последние 35 лет занимает ведущее место в ком- мерческих приложениях? 5. Какой язык программирования последние 35 лет занимает ведущее место в созда- нии искусственного интеллекта? 6. На каком языке написана операционная система UNIX? 7. Что плохого, если в языке слишком много функций? 8. Как может перегрузка операторов понизить читабельность программы? 9. Приведите пример недостатка ортогональности в языке С. 10. В каком языке ортогональность используется в качестве основного критерия раз- работки0 54 Глава!. Вводные замечания
11. Какой основной управляющий оператор используется для создания более сложных управляющих операторов в языках, их не имеющих? 12. Какая трудность возникает при чтении программ, написанных на языках, исполь- зующих одно и то же замыкающее служебное слово для разных типов управляю- щих структур? 13. Какая конструкция языка программирования обеспечивает абстракцию процесса? 14. Что означает надежность программы? 15. Почему важна проверка типов параметров подпрограммы? 16. Что такое “совмещение имен"? 17. Что такое “обработка исключительных ситуаций”? 18. Как влияет читабельность языка на легкость создания программ на нем? 19. Как стоимость компилятора для данного языка связана с его структурой? 20. Что за последние 40 лет оказало наибольшее влияние на разработку языков про- граммирования? 21. Как называется категория языков программирования, структура которых обуслов- лена неймановской архитектурой компьютера? 22. Какие два недостатка языков программирования были обнаружены в результате проведения научно-исследовательских работ в 1970-х годах? 23. Назовите три основных свойства объектно-ориентированных языков программи- рования. 24. Какой язык первым стал поддерживать три основных свойства объектно-ориенти- рованного программирования? 25. Назовите два критерия разработки языка, непосредственно конфликтующих между собой. 26. Назовите три общих метода реализации языка программирования. 27. Что обеспечивает более быстрое выполнение программы— компилятор или чис- тая интерпретация? 28. Какова роль таблицы символов в компиляторе? 29. Что делает редактор связи? 30. В чем состоит важность неймановского узкого места? 31. Какие преимущества предлагает реализация языка методом чистой интерпретации? 32. Какие недостатки имеет операционная система UNIX при использовании ее в каче- стве среды разработки программного обеспечения? Вопросы 55
У Л р О- г.< Л : >8 •’ '- миммииивпмммммимт111П“Т'^*^ы^г^т1!У1тпт1т,гтг1Г'П| 1. Верите ли вы, что на наши мыслительные способности влияет наш язык? Аргумен- тируйте ваше мнение. 2. Какие свойства конкретных языков программирования вы не понимаете? 3. Какие аргументы вы можете привести в пользу создания общего языка для всех областей программирования? 4. Какие аргументы вы можете привести против создания общего языка для всех об- ластей программирования? 5. Назовите и обоснуйте другие критерии оценки языков программирования (в до- полнение к перечисленным в данной главе). 6. Какие, по вашему мнению, обычные операторы языков программирования являют- ся наиболее трудными при чтении программы? 7. В языке Modula-2 оператор END используется для замыкания всех управляющих структур. Назовите достоинства и недостатки такого проектного решения. 8. Некоторые языки, особенно С и Java, различают прописные и строчные буквы в именах идентификаторов. Какие плюсы и минусы имеет такое проектное решение? 9. Объясните различные факторы, влияющие на стоимость языка программирования. 10. Какие аргументы можно привести в пользу создания эффективных программ даже на относительно недорогом аппаратном обеспечении? 11. Опишите несколько компромиссов между эффективностью и безопасностью в из- вестных вам языках программирования. 12. Какими, по вашему мнению, основными свойствами должен обладать совершен- ный язык программирования? 13. Как был реализован первый изученный вами язык высокого уровня: посредством чистой интерпретации, смешанной реализации или компилятора? (Для ответа на этот вопрос может потребоваться дополнительный анализ.) 14. Опишите достоинства и недостатки используемых вами сред программирования. 15. Как операторы объявления типов простых переменных влияют на читабельность языка, с учетом того, что в некоторых языках они не требуются. 16. Оцените известный вам язык программирования, использовав критерии, указанные в данной главе. 17. В языке Pascal точка с запятой используется для разделения выражений, а в языке С — для их завершения. Какой из способов, по вашему мнению, естественнее и менее вероятно вызовет синтаксическую ошибку? Аргументируйте ваш ответ. 18. Некоторые языки, например Pascal и С, используют разделители для ограничения комментариев с двух сторон. Другие языки, такие как FORTRAN и Ada, использу- ют пару символов для указания на начало и конец комментария. Взвесьте достоин- ства и недостатки обоих подходов в соответствии с приведенными в данной главе критериями. 56 Глава!. Вводные замечания
Джон Бэкус (John Backus) Нанятый корпорацией IBM Джон Бэкус в начале 1950-х годов разработал систему псевдокодов Speedcoding для компьютеров IBM 701. В пери- од с 1954 по 1957 год он воз- главлял команду разработчи- ков языка FORTRAN — родо- Е - j 2.1. Язык PlankalkOI Конрада Цузе 2.2. Минимальное программирование на аппаратном уровне* псевдокоды 2.3. Компьютер IBM 704 и язык FORTRAN 2.4. Функциональное программирование: язык LISP 2.5. Первый шаг к совершенствованию язык ALGOL 60 2.6. Компьютеризация коммерческих записей, язык COBOL 2.7. Начало разделения времени: язык BASIC 2.8. Все для всех: язык PL/I 2.9. Два ранних динамических языка: APL и SNOBOL 2.10. Возникновение абстракции данных: язык SIMULA 67 2.11. Ортогональная структура: язык ALGOL 68 2.12. Несколько важных наследников семейства начальника практически всех императивных языков про- граммирования С 1958 по 1960 годы Бэкус участвовал в разработке языка ALGOL. 2.13. 2.14. языков ALGOL Программирование, основанное на логике: язык Prolog Величайший проект в истории: язык Ada 2.15. Объектно-ориентированное программирование: язык Smalltalk 2.16. Объединение императивных и объектно- ориентированных свойств: язык C++ 2.17. Программирование в World Wide Web: язык Java Обзор основных языков программирования 57
В этой главе хронологически прослеживается развитие некоторых языков про- граммирования, исследуется среда, в которой каждый из них разрабатывался, особое внимание обращается на вклад языка в компьютерные науки и причины, привед- шие к его возникновению. Глава не содержит полного описания языков, рассматривают- ся только новые свойства, которыми они отличаются друг от друга. Особый интерес вы- зывают свойства, оказавшие наибольшее влияние на языки программирования, разрабо- танные позднее, или на компьютерные науки в целом. В данную главу не включено подробное обсуждение какого-либо языкового свойства или концепции; эти вопросы будут рассмотрены в следующих главах. Мы считаем, что крат- кого неформального объяснения свойств будет достаточно для нашего обзора истории развития этих языков. Выбор языков, обсуждаемых в этой главе, был субъективным, и многие читатели с сожалением отметят отсутствие одного или нескольких излюбленных языков. Тем не менее, для того чтобы этот исторический обзор не выходил за разумные пределы, было необходимо исключить из рассмотрения несколько языков, высоко ценимых некоторыми программистами. Наш выбор основывался на оценке важности каждого языка для разви- тия языков программирования и компьютерных наук в целом. В этой главе также рас- сматриваются некоторые другие языки, упоминающиеся далее в книге. В главе содержатся листинги 11 полностью завершенных примеров программ, все они написаны на различных языках. Ни одна из этих программ в главе не описывается; они приводятся просто для иллюстрации внешнего вида программы на конкретном язы- ке. Читатели, знакомые с любым из распространенных императивных языков, смогут прочесть и понять большинство команд этих программ, за исключением программ, на- писанных на языках LISP, COBOL и Smalltalk. Пример программы на языке LISP рас- сматривается в главе 14; на языке Smalltalk— в главе И. Программы на языках FORTRAN, ALGOL 60, PL/1, BASIC, Pascal, C, Ada и Java решают одну и ту же задачу. Генеалогическое древо развития языков программирования высокого уровня, рас- сматриваемых в этой главе, приведено на рис. 2.1. 2.1. Язык Plankalkiil Конрада Цузе Первый из обсуждаемых в этой главе языков в некоторых отношениях крайне необы- чен. Во-первых, он никогда не был реализован. Более того, хотя он был разработан в 1945 году, его описание не публиковалось вплоть до 1972 года. Из-за всеобщего игнори- рования языка Plankalkiil некоторые его возможности не использовались в других языках программирования в течение 15 лет после его разработки. 2.1.1. Исторические предпосылки Между 1936 и 1945 годами немецкий ученый Конрад Цузе (Konrad Zuse) на основе электромеханических реле создал ряд сложных и изощренных компьютеров. К 1945 году война привела к разрушению всех этих компьютеров, за исключением одной из послед- них моделей — компьютера Z4. Цузе отправился в удаленное баварское селение Хин- терштейн (Hinterstein), а остальные члены его исследовательской группы разъехались в разные стороны. 58 Глава 2. Обзор основных языков программирования
1957 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 Рис. 2.1. Генеалогия распространенных языков программирования высокого уровня Работая самостоятельно. Цузе направил свои усилия на разработку языка для выра- жения вычислений, продолжив проект, начатый им в 1943 году с целью написания док- торской диссертации. Новый язык он назвал Plankalkiil. что означает ’’программное ис- числение”. В объемной рукописи, датированной 1945 годом, но не п\отковавшейся до 1972 года (Zuse, 1972), Цузе дал определение языка Plankalkiil и использовал этот язык для написания алгоритмов решения множества задач. 2.1.2. Обзор языка Язык Plankalkiil был необыкновенно полным и обладал несколькими очень перспек- тивными свойствами в области структур данных. Простейшим типом данных в языке Plankalkiil был отдельный бит. На основе этого типа создавались типы для представления целых чисел и чисел с плавающей точкой. Для представления чисел с плавающей точкой использовались дополнительный код числа в двоичной системе и схема “скрытого бита”, в настоящее время применяемая для того, чтобы не хранить в памяти старший значащий бит в нормализованной мантиссе числа. 2.1. Язык Plankalkul Конрада Цузе 59
Кроме этих обычных скалярных типов, язык Plankalkiil содержал массивы и записи. При работе с записями для включения в них других записей в качестве элементов допус- калось использование рекурсии. Хотя язык и не содержал явного оператора goto, он имел оператор цикла, подобный оператору for языка Pascal. В языке также имелась команда Fin, верхний индекс кото- рой означал выход из внутреннего цикла на указанное число вложенных итерационных циклов или к началу нового итерационного цикла. Язык Plankalkiil содержал оператор ветвления, не содержащий оператора else. Интереснейшей особенностью программ Конрада Цузе было включение математиче- ских выражений, показывающих отношения между переменными в программе. Эти вы- ражения устанавливали, что именно в процессе выполнения должно быть истинным в тех точках программы, где находились данные выражения. Сами эти выражения очень по- хожи на утверждения, используемые в современном языке программирования Eiffel (Meyer. 1992) и аксиоматической семантике, рассматриваемой в главе 3. Рукопись Цузе содержала программы, которые были значительно сложнее программ, написанных до 1945 года. В ней были приведены программы сортировки массивов це- лых чисел: проверки связности заданного графа; программы, выполняющие действия с целыми числами и с числами с плавающей точкой, в том числе извлечение квадратного корня; программы, которые производили синтаксический анализ логических формул, со- держащих скобки и операторы с шестью различными уровнями приоритета. Вероятно, самыми примечательными были принадлежащие Цузе 49 страниц алгоритмов игры в шахматы (игры, в которой сам Цузе специалистом не был). Если бы в начале 1950-х годов какой-нибудь специалист по компьютерным наукам обнаружил созданное Цузе описание языка Plankalkiil. то единственным аспектом, кото- рый бы затруднил реализацию этого языка в описанном виде, была бы форма записи. Каждый оператор состоял из двух-трех строк команд. Первая строка была очень похожа на операторы современных языков программирования. Вторая строка, которая была не- обязательной. содержала список индексов массивов, упоминающихся в первой строке. Интересно отметить, что тот же метод указания индексов использовался Чарльзом Бэб- биджем (Charles Babbage) в программах, написанных им для его “аналитической маши- ны" в середине XIX столетия. Последняя строка каждого выражения на языке Plankalkiil содержала названия типов переменных, упоминаемых в первой строке. На первый взгляд, подобная запись выглядит устрашающе. Для иллюстрации описанной формы записи ниже приводится пример оператора, при- сваивающего значения выражения А (4 ) +1 переменной А (5). Строка, помеченная бук- вой V. содержит индексы, а строка, помеченная буквой S, — типы данных. Запись 1. п в данном примере означает целое число, состоящее из п битов: I А + 1 => А V । 4 5 S I l.n 1.п Сейчас мы можем только гадать, по какому пути пошло бы развитие языков про- граммирования и компьютеров, если бы работа Конрада Цузе стала бы широко извест- ной в 1945 или даже в 1950 году. Также интересно предположить, как отличалась бы сделанная Цузе работа, если бы она выполнялась в мирное время в коллективе ученых, а не в фактической изоляции в Германии 1945 года. 60 Глава 2. Обзор основных языков программирования
2.2. Минимальное программирование на аппаратном уровне: псевдокоды Компьютеры, появившиеся в конце 1940-х и начале 1950-х годов, были далеко не та- кими удобными, как современные. Кроме того что они были очень медленными, нена- дежными, дорогими и имели крайне малый объем памяти, программировать на них было очень сложно из-за недостатка программной поддержки. Не существовало языков программирования высокого уровня или. по крайней мере, языков ассемблера, поэтому программирование выполнялось в машинных кодах, что было кропотливой и подверженной ошибкам работой. Среди возникавших проблем можно назвать использование целочисленных кодов для указания команд. Например, команда ADD (“прибавить”) должна была определяться кодом 14. а не напрашивающим- ся текстовым обозначением, даже состоящим из одного символа. Это значительно ус- ложняло чтение программы. Более существенной проблемой являлась абсолютная адре- сация, усложнявшая модификацию программ. Предположим, что у нас в памяти имеется программа в машинных кодах. Многие команды такой программы ссылаются на другие ячейки памяти вне программы, обычно на справочные данные, или указывают цели ко- манд ветвления. Введение команды не в конец программы, а в произвольное место внут- ри нее, сводит на нет правильность ссылок на адреса, находящиеся за точкой ввода но- вой команды, поскольку для предоставления места новой команде эти адреса должны соответствующим образом увеличиться. Для того чтобы введение новой команды не на- рушало структуры уже существующей программы, программист должен найти и изме- нить все команды, ссылающиеся на адреса, находящиеся за дополнительно введенной командой. Подобная проблема возникает и при удалении команды. Правда, в этом случае в машинных языках часто используется команда “холостая операция*’, которой можно заменить удаленную команду, не нарушив при этом общей структуры программы. Эти стандартные проблемы, существующие во всех машинных языках, и были основ- ной причиной изобретения ассемблеров и языков ассемблера. Кроме того, в то время за- дачи, связанные с программированием, были в основном вычислительными. Для их ре- шения требовалось выполнять арифметические операции над числами с плавающей точ- кой, а также операции индексации для более удобного использования массивов. Тем не менее, ни одна из указанных возможностей не была включена в архитектуру компьюте- ров конца 1940-х и начала 1950-х годов. Для устранения этих недостатков, естественно, нужно было разрабатывать языки более высокого уровня. 2.2.1. Язык Short Code Первый из этих новых языков, названный Short Code, был разработан Джоном Мочли (John Mauchly) в 1949 году для компьютера BIN АС. Позднее язык Short Code был пере- несен на компьютер UN1VAC I и в течение нескольких лет был одним из основных средств программирования этих машин. Несмотря на то что об исходном языке Short Code известно очень мало (полное его описание никогда не публиковалось), сохранилось руководство по программированию для версии UN1VAC I (Remington-Rand. 1952). Ра- зумным будет предположить, что эти две версии были очень похожи. Слова языка UN1VAC 1 состояли из 72 бит. сгруппированных в 12 шестибитовых байт. Язык Short Code состоял из закодированных версий математических выражений, которые затем предстояло вычислять. Команды представляли собой двухбайтовые вели- 2.2. Минимальное программирование на аппаратном уровне: псевдокоды 61
чины, и большинство выражений умещались в одном слове. Вот несколько примеров команд языка Short Code: 03 = 04 / 06 модуль 07 + 08 пауза 09 ( In (п+2)-я степень 2п корень (п+2)-й степени 4п если <= п 58 печать и табуляция Переменные, или ячейки памяти, именовались двухбайтовыми кодами, так как ячейки памяти должны были использоваться как константы. Например, величины Х0 и Y0 могли быть переменными. Оператор ХС = SQRT(ABS(Y0)) можно было закодировать в слово 00 Х0 03 20 06 Y0. Заглавный код 00 использо- вался в качестве дополнения для заполнения слова. Интересно, что команды умножения не с> шествовало; на эту операцию указывало простое размещение двух операторов ря- дом. как это принято записывать в алгебре. Язык Short Code не был переведен в машинные коды; он был реализован с помощью чистого интерпретатора. Со временем подобный процесс был назван автоматическим программированием. Он сильно упростил процесс программирования, правда, за счет времени, затрачиваемого на выполнение программы. Интерпретация языка Short Code была приблизительно в 50 раз медленнее, чем интерпретация машинных кодов. 2.2.2. Система Speedcoding В это время в других местах были разработаны императивные системы, которые расширяли машинный язык, дополняя его действиями над числами с плавающей точкой. Примером такой системы может служить система Speedcoding, разработанная Джоном Бэкусом для компьютера IBM 701 (Бэкус, 1954). Интерпретатор системы Speedcoding эффективно преобразовывал 701-й компьютер в виртуальную трехадресную вычисли- тельную машину, выполняющую действия над числами с плавающей точкой. Система включала в себя псевдокоманды, выполняющие четыре арифметические операции над числами с плавающей точкой, а также такие операции, как извлечение квадратного кор- ня. вычисление синуса, арктангенса, экспоненты и логарифма. Условные и безусловные переходы и преобразования ввода-вывода также являлись частью виртуальной архитек- туры. Для того чтобы представить себе ограниченность подобной системы, примите во внимание то, что после загрузки интерпретатора оставалось всего 700 слов доступной оперативной памяти, и команда сложения двух чисел требовала 4,2 миллисекунды на ее выполнение. С другой стороны, система Speedcoding содержала такую новую возмож- ность. как автоматическое увеличение индексных регистров. В аппаратном обеспечении эта возможность не реализовывалась вплоть до появления в 1962 году компьютеров UN1VAC1107. Благодаря наличию таких свойств умножение матриц в системе Speedcoding можно было выполнить, затратив всего 12 команд. Бэкус утверждал, что за- дача. на программирование которой в машинных кодах уйдет две недели, при использо- вании системы Speedcoding может быть решена за несколько часов. 62 Глава 2. Обзор основных языков программирования
2.2.3. Система "компиляции" UNIVAC Между 1951 и 1953 годами команда, возглавляемая Грейс Хоппер (Grace Hopper), создала на компьютере UNIVAC серию “компилирующих" систем, названных А-0. А-1 и А-2, в основе которых лежали машинные коды, реализующие псевдокоды, аналогично тому, как язык ассемблера реализует макросы. Исходная программа, написанная на псевдокоде для этих “компиляторов", была еще слишком примитивной, хотя это уже бы- ло значительным усовершенствованием по сравнению с машинным кодом, поскольку сделало исходные программы короче. Независимо от этой команды в 1952 год} подоб- ный подход был предложен Уилксом (Wilkes). 2.2.4. Смежная работа Практически в то же время были разработаны и другие средства облегчения про- граммирования. В Кембриджском университете Дэвид Дж. Уилер (David J. Wheeler) раз- работал метод использования блоков перемещаемых адресов для частичного решения задачи абсолютной адресации (Wheeler. 1950). а позднее Морис В. У ил кис (Maurice V. Wilkes) (также в Кембридже) осуществил эту идею при разработке програм- мы ассемблера, которая могла бы объединять избранные подпрограммы и распределять память (Wilkes et al., 1951. 1957). Это было действительно важное и фундаментальное достижение. Кроме того, мы должны упомянуть, что языки ассемблера, совершенно отличавшиеся от описанных псевдокодов, хотя и эволюционировали на протяжении начала 1950-х го- дов, но на разработку языков высокого уровня не оказали существенного влияния. 2.3. Компьютер IBM 704 и язык FORTRAN Безусловно, одно из величайших достижений в вычислительной технике обязано сво- им появлением внедрению в 1954 году компьютера IBM 704. поскольку возможности именно этой машины подтолкнули к разработке языка FORTRAN. Можно утверждать, что если бы не корпорация IBM со своим компьютером IBM 704 и языком FORTRAN, то очень скоро появилась бы какая-нибудь другая организация, имеющая подобный компь- ютер и соответствующий язык высокого уровня. Тем не менее, первой была вес же кор- порация IBM, которая не только предвидела такое развитие событий, но и нашла ресур- сы для того, чтобы взяться за это дело. 2.3.1. Историческая ретроспектива Одной из основных причин того, что императивные системы с конца 1940-х до сере- дины 1950-х годов считались вполне приемлемыми, было отсутствие на существ} ющих компьютерах аппаратных средств, выполняющих действия над числами с плавающей точкой. Подобные действия должны были моделироваться на программном уровне, что значительно увеличивало время их выполнения. Из-за того, что на программною обра- ботку чисел с плавающей точкой уходило очень большое время работы процессора, за- траты на интерпретацию и моделирование индексной адресации были относительно не- существенными. Пока действия над числами с плавающей точкой выполнялись на про- граммном уровне, расходы на интерпретацию были допустимыми. Впрочем, многие программисты того времени никогда не использовали системы интерпретации, отдавая 2.3. Компьютер IBM 704 и язык FORTRAN 63
предпочтение эффективному ручному программированию на машинных кодах. Сообще- ние о появлении системы IBM 704. имеющей на аппаратном уровне как индексную адре- сацию. так и команды для работы с плавающей точкой, возвестило о конце эры интер- претации (по крайней мере, в области научных вычислений). Несмотря на то что язык FORTRAN считается первым компилируемым языком высо- кою уровня, вопрос о том. кто действительно заслуживает чести быть названным созда- телем первого подобного языка, все еще остается открытым. Кнут (Knuth) и Пардо (Pardo) (1977) приписывают эту честь Алику Гленни (Alick Е. Glennie) за создание ком- пилятора Autocode для компьютера Mark I в Манчестере. Гленни разработал этот компи- лятор в британском Форт-Холстеде (Fort Halstead), принадлежащем Королевскому науч- но-исследовательскому обществу Вооруженных Сил (Royal Armaments Research Establishment). В рабоче.м состоянии компилятор был с сентября 1952 года. Но Джон Бэ- к\с (Wexelblat. 1981) придерживается мнения, что созданный Гленни компилятор Autocode был настолько низкоуровневым и машинно-ориентированным, что его нельзя считать в полной мере системой компиляции. Бэкус считает, что первыми авторами ком- пилятора нужно считать Ленинга (Laning) и Цирлера (Zierler) из Массачусетского техно- логического института (MIT — Massachusetts Institute of Technology). Система Ленинга и Цирлера (Laning and Zierler, 1954) была первой алгебраической системой трансляции, созданной для реализации. Под словом “алгебраическая” мы под- разумеваем то. что эта система транслировала арифметические выражения, используя вызовы математических функций, а также содержала индексированные ссылки на пере- менные. Реализована эта система была на принадлежащем институту7 М1Т компьютере Whirlwind, который летом 1952 года существовал в форме экспериментального прототи- па. а к маю 1953 года принял форму, более пригодную для использования. Для кодиро- вания каждой формулы или выражения, встречающихся в программе, описываемый транслятор генерировал вызов подпрограммы. Исходный язык был легкочитаемым, и единственными действительно машинными командами были команды ветвления. Не- смотря на то что работа по созданию этого транслятора предшествовала работе по соз- данию языка FORTRAN, она так и не вышла за рамки Массачусетского технологическо- го института. Несмотря на эти ранние работы, первым компилируемым языком высокого уровня, получившим широкое распространение, стал язык FORTRAN, хронология развития ко- торого приводится в следующих подразделах. 2.3.2. Процесс разработки Планы по разработке языка FORTRAN существовали еще до извещения о создании системы IBM 704 в мае 1954 года. Работавшие в корпорации IBM Джон Бэкус и его группа в ноябре 1954 года опубликовали отчет, озаглавленный “The IBM Mathematical FORmula TRANslating System: FORTRAN" (IBM, 1954). В этом документе описывается первая версия языка FORTRAN, которую мы называем FORTRAN 0, и которая предше- ствовала его реализации. В работе смело утверждалось, что язык FORTRAN так же эф- фективен. как программы, закодированные вручную, и программировать на нем так же просто, как в системе интерпретируемых псевдокодов. Еще одной вспышкой оптимизма в данном документе было утверждение, что язык FORTRAN будет устранять ошибки программирования и содержать систему отладки. На основе этих предпосылок в первом 64 Глава 2. Обзор основных языков программирования
компиляторе языка FORTRAN предусматривалась лишь незначительная проверка нали- чия синтаксических ошибок. Язык FORTRAN разрабатывался в следующей обстановке. 1. Компьютеры все еше были небольшими, медленными и относительно ненадеж- ными. 2. Использовались компьютеры в основном для научных расчетов. 3. Не существовало эффективных способов программирования компьютеров. 4. Из-за относительно высокой стоимости компьютеров по сравнению с оплатой ра- боты программистов основной задачей первых компиляторов языка FORTRAN была высокая скорость выполнения сгенерированного объектного кода. В такой среде и формировались характеристики ранних версий языка FORTRAN. 2.3.3. Обзор языка FORTRAN I Язык FORTRAN 0 видоизменялся на протяжении всего периода реализации, начав- шегося в январе 1955 года и завершившегося выходом компилятора в апреле 1957 года. Реализованный язык, который мы называем FORTRAN 1, описан в первом руководстве пользователя “Programmer's Reference Manual”, опубликованном в октябре 1956 года (IBM, 1956). В языке FORTRAN I содержалось форматирование ввода-вывода, имена переменных, которые не должны были превышать шести символов (в FORTRAN 0 — двух), определяемые пользователем подпрограммы, которые, правда, не могли компили- роваться раздельно, условный оператор IF и цикл DO. Язык FORTRAN 0 содержал логический оператор IF, оперировавший с булевскими выражениями, в которых операторы отношений записывались в их алгебраической фор- ме,— например, использовался знак “>’* для обозначения отношения “больше". По- скольку алфавит компьютера IBM 704 не содержал таких символов, как “>", то в даль- нейшем от этих операторов отношения нужно было отказаться. Машина содержала трех- вариантную команду ветвления, основанную на сравнении величины из ячейки памяти с величиной в регистре, поэтому исходный логический оператор IF был заменен арифме- тическим, имевшим форму: IF (арифметическое выражение) Nl, N2, N3 Здесь с помощью Nl, N2 и N3 обозначены метки операторов. Если значение выражения оказывалось отрицательным, то выполнялся переход на метку N1. если оно равнялось нулю— на метку N2, а если было положительным— на метку N3. Этот оператор и по сегодняшний день является частью языка FORTRAN. Оператор цикла языка FORTRAN 1 имел следующую структуру: DO N1 переменная = первое_значение, последнее_значение Здесь N1 — метка последнего оператора цикла, а первым считался оператор, следующий за оператором DO. Помимо оператора IF. компьютер IBM 704 имел отдельную команду для реализации оператора DO. Поскольку эта команда была создана для циклов с последующей провер- кой условия, оператор DO языка FORTRAN 1 также был сконструирован подобным обра- зом. Цикл с предварительной проверкой условия мог быть реализован на компьютере IBM 704. но это потребовало бы введения дополнительной машинной команды, что вви- 2.3. Компьютер IBM 704 и язык FORTRAN 65
ду ориентированности структуры языка FORTRAN на максимальную эффективность и стало причиной отказа от его реализации. Все управляющие операторы языка FORTRAN I основывались на командах компью- тера IBM 704. Сейчас уже не ясно, была ли форма управляющих структур языка FORTRAN I навязана конструкторами компьютера IBM 704, или же разработчики языка FORTRAN I предложили использовать такую форму команд в компьютере IBM 704. В языке FORTRAN I не существовало операторов, определяющих типы данных. Счита- лось. что переменные, начинавшиеся с букв I, J, К, L, И и N, принадлежат к типу целых чи- сел, тогда как все остальные подразумевались числами с плавающей точкой. Такая услов- ность при выборе букв связана с тем, что тогда целые числа использовались преимущест- венно в качестве индексов, а ученые, как правило, использовали для этой цели буквы /,у и к. Еще три буквы были щедро добавлены разработчиками языка FORTRAN. Наиболее дерзким заявлением группы разработчиков языка FORTRAN в процессе ра- боты над языком было то, что машинные коды, порождаемые компилятором, будут поч- ти столь же эффективными, что и написанные вручную. Эти заявления более, чем что- либо другое, способствовали скептическому отношению со стороны потенциальных пользователей и препятствовали возникновению значительного интереса к языку до его фактического выпуска. Тем не менее, всех удивило то, что создатели языка FORTRAN практически достигли поставленной цели с точки зрения эффективности. Большая часть 18 человеко-лет напряженной работы по созданию первого компилятора была посвящена оптимизации, но результаты были в высшей степени эффективными. Первый успех языка FORTRAN отражен в обзоре, датированном апрелем 1958 года. К этому времени около половины команд, написанных для компьютеров IBM 704, были реализованы в языке FORTRAN, и это несмотря на то, что годом ранее отношение к языку со стороны вычислительного сообщества было крайне скептическим. 2.3.4. Обзор языка FORTRAN II Компилятор FORTRAN II начал распространяться весной 1958 года. Он исправил множество ошибок системы компиляции языка FORTRAN I, а также добавил в язык не- сколько существенных свойств, важнейшим из которых была независимая компиляция подпрограмм. При отсутствии такого свойства любое изменение требовало перекомпи- ляции всей программы. Отсутствие в языке FORTRAN 1 возможности независимой ком- пиляции вместе с низкой надежностью компьютера IBM 704 ограничивало объем про- граммы, который не должен был превышать 300-400 строк (Wexelblat, 1981). Програм- мы. имевшие больший объем, имели мало шансов завершить компиляцию до сбоя в работе машины. Возможность включения в программу подпрограмм, предварительно откомпилированных на машинных языках, значительно сокращала процесс компиляции. 2.3.5. Языки FORTRAN IV, FORTRAN 77 и FORTRAN 90 Язык FORTRAN III был разработан, но так никогда и не получил широкого распро- странения. Язык FORTRAN IV, напротив, стал одним из самых используемых в то время языков программирования. Развитие этого языка происходило в период с 1960 по 1962 год. и он оставался стандартной версией языка FORTRAN вплоть до выхода в 1978 году сообщения о выпуске языка FORTRAN 77 (ANSI. 1978а). По многим параметрам язык FORTRAN IV был значительно лучше языка FORTRAN 11. Среди важнейших его ново- 66 Глава 2. Обзор основных языков программирования
введений было явное объявление типов переменных, логический оператор IF. а также возможность передавать одни подпрограммы другим в качестве их параметров. Язык FORTRAN 77 поддерживал большинство свойств языка FORTRAN IV. кроме то- го, в него были добавлены обработка символьных строк, логические операторы управления циклами, а также возможность использования оператора ELSE в структуре выбора IF. Название FORTRAN 90 используется для обозначения позднейшей версии языка FORTRAN (ANSI, 1992). Язык FORTRAN 90 разительно отличается от языка FORTRAN 77. Наиболее значительные изменения кратко описываются ниже. Встроен набор функций, выполняющий действия с массивами. В их число входят: DOTPRODUCT, MATMUL, TRANSPOSE. MAXVAL. MINVAL, PRODUCT и SUM, о назначении которых можно догадаться по названиям (“скалярное произведение", “умножение матриц", “транспонирование", “максимальная величина", “минимальная величина", “произведение" и “сумма"). Это только некоторые из наиболее полезных доступных функций. Если массивы были объявлены как ALLOCATABLE, то они могут по команде динами- чески размещаться в памяти и удаляться из нее. Последняя возможность является ради- кальным изменением по сравнению с предыдущими версиями языка FORTRAN, имев- шими только статические данные. Была введена форма записей, названных производны- ми типами. Частью языка FORTRAN 90 стали также указатели. Были добавлены новые управляющие операторы: оператор многовариантного ветв- ления CASE, оператор EXIT — для преждевременного выхода и оператор CYCLE — для передачи управления на начало цикла без выхода из цикла. Подпрограммы могут быть рекурсивными, а также иметь необязательные и ключевые параметры. Была добавлена возможность использования модулей, аналогичных пакетам языка Ada. Модули могут содержать объявления данных и подпрограммы, каждую из которых для управления внешним доступом можно описать с помощью спецификаторов PRIVATE либо PUBLIC. В описании языка FORTRAN 90 появилась новая концепция, позволявшая удалять свойства языка, присутствовавшие в ранних версиях. Хотя язык FORTRAN 90 и обладал всеми возможностями языка FORTRAN 77, было составлено два списка свойств, кото- рые могли быть ликвидированы в следующей (после языка FORTRAN 90) версии языка FORTRAN. В этом списке были указаны такие операторы, как арифметический оператор IF и назначенный оператор GOTO. Список свойств, не рекомендуемых к употреблению, включал свойства, от которых планировалось отказаться во второй версии, которая по- следует за языком FORTRAN 90. В этом списке находились операторы COMMON, EQUIVALENCE, вычисляемый оператор GOTO, а также операторные функции. 2.3.6. Оценка Команда разработчиков первоначального варианта языка FORTRAN полагала, что разработка языка — только необходимая прелюдия перед насущной задачей разработки транслятора. Более того, им никогда не приходило на ум, что язык FORTRAN мог бы использоваться на компьютерах производства других корпораций, а не корпорации IBM. Действительно, они были вынуждены продумывать построение компиляторов языка FORTRAN для других машин, создаваемых корпорацией IBM, только потому, что пре- емник компьютера IBM 704, компьютер IBM 709, был выпушен до выпуска компилятора языка FORTRAN, предназначенного для компьютера IBM 701. Тот факт, что язык 2.3. Компьютер IBM 704 и язык FORTRAN 67
FORTRAN был реально использован на компьютерах, а все последующие языки про- граммирования своим появлением в той или иной степени обязаны языку FORTRAN, яв- ляется действительно впечатляющим в свете скромных притязаний его разработчиков. Одним из свойств языка FORTRAN 1 и всех его последующих версий, за исключени- ем языка FORTRAN 90. позволявшим создание высоко оптимизированных компилято- ров, было то, что типы и ячейки памяти для всех переменных фиксировались до выпол- нения программы. В процессе выполнения программы новые переменные не вводились, и распределение памяти не производилось. Таким образом, гибкость программирования была принесена в жертву простоте и эффективности. Подобное решение исключило воз- можность создания рекурсивных подпрограмм и затруднило реализацию структур дан- ных, которые могли бы динамически увеличиваться или изменять вид. Разумеется, про- граммы, создаваемые во время разработки ранних версий языка FORTRAN, были вычис- лительными по своей природе и простыми по сравнению с современными програм- мными проектами. Следовательно, жертва была не так уж и велика. Трудно переоценить глобальный успех языка FORTRAN: он действительно разитель- но и навечно изменил способ использования компьютеров. Это, разумеется, произошло в значительной мере благодаря тому, что он оказался первым широко используемым язы- ком высокого уровня. По сравнению с концепциями и языками, разработанными позже, ранние версии языка FORTRAN значительно проигрывают, как и следовало ожидать. Все-таки выпущенному Фордом в 1910 году автомобилю “Model Т” далеко до созданно- го в 1998 году автомобиля “Ford Mustang’. Тем не менее, несмотря на недостаточность языка FORTRAN, движущая сила вложенных в него огромных инвестиций, помимо дру- гих факторов, вывела его в число самых используемых языков высокого уровня. Алан Перлис (Alan Perlis), один из разработчиков языка ALGOL 60, в 1978 году ска- зал: “Язык FORTRAN — это lingua franca (смешанный язык из элементов романских, греческого и восточных языков, служащий для общения в восточном Средиземноморье; также* язык с широкой сферой употребления, жаргон. — Прим.перев.) компьютерного мира. Это язык улиц, в лучшем, а не вульгарном смысле этого слова. Он выжил и будет жить, поскольку он был создан для того, чтобы быть замечательно полезной частью жизненно важной коммерческой деятельности’’ (Wexelblat, 1981). Ниже следует пример программы на языке FORTRAN 90. С ПРИМЕР ПРОГРАММЫ НА ЯЗЫКЕ FORTRAN 90 С ВВОД: ЦЕЛОЕ ЧИСЛО LIST_LEN, МЕНЬШЕ 100, ЗА КОТОРЫМ С СЛЕДУЕТ НАБОР ЦЕЛЫХ ЧИСЕЛ, В КОЛИЧЕСТВЕ С LIST_LEN С ВЫВОД: КОЛИЧЕСТВО ВВЕДЕННЫХ ВЕЛИЧИН, КОТОРЫЕ БОЛЬШЕ ИХ СРЕДНЕГО С АРИФМЕТИЧЕСКОГО INTEGER INTLIST(99) INTEGER LIST_LEN, COUNTER, SUM, AVERAGE, RESULT RESULT = 0 SUM = 0 READ *, LIST_LEN IF ((LIST—LEN .GT. 0) .AND. (LIST_LEN .LT. 100)) THEN С СЧИТЫВАНИЕ ВХОДНЫХ ВЕЛИЧИН В МАССИВ И ВЫЧИСЛЕНИЕ ИХ СУММЫ DO 10 COUNTER = 1, LIST_LEN READ *, INTLIST(COUNTER) SUM = SUM + INTLIST(COUNTER) 68 Глава 2. Обзор основных языков программирования
10 CONTINUE С ВЫЧИСЛЕНИЕ СРЕДНЕГО AVERAGE = SUN / LISU_LEN С ПОДСЧЕТ ЧИСЛА ВЕЛИЧИН, КОТОРЫЕ БОЛЬТЕ СРЕДНЕГО DO 20 COUNTEP = 1, LIST TEN if (iNTLEST'.counter: 7gt. average: then RESULT = RESULT -1 END IF 20 CONTINUE С ВЫВОД РЕЗУЛЬТАТОВ PRINT -,’ЧИСЛО ВЕЛ;-ЧИН,КОТОРЫЕ БОЛЬТЕ СРЕДНЕГО,РАВНО', *RESULT ELSE PRINT *, ’ОШИБКА. — ВВЕДЕНА НЕВЕРНАЯ ДЛИНА. СЛИСКА' END IF STOP END 2.4. Функциональное программирование: язык LISP Первый язык функционального программирования был изобретен для поддержки языковых средств обработки списков, потребность в которых возникла с появлением первых работ в области искусственного интеллекта. 2.4.1. Истоки работ в области искусственного интеллекта и обработка списков Интерес к искусственному интеллекту' начал появляться в середине 1950-х годов в различных областях. Потребность в нем ощущалась частично в лингвистике, частично в философии, а частично— в математике. Лингвисты сосредоточились на обработке тек- стов на естественном языке. Психологов интересовало моделирование способностей че- ловека к запоминанию и воспроизведению воспоминаний, а также другие фундаменталь- ные процессы человеческого мышления. Математики изучали механизм протекания та- ких процессов размышления, как доказательство теорем. Все эти исследования пришли к одному выводу: должны быть созданы некоторые методы, которые позволили бы ком- пьютерам обрабатывать символьные данные, хранящиеся в виде связных списков. Отме- тим также, что в то время практически все вычисления производились с числовыми дан- ными, хранящимися в виде массивов. Концепция обработки списков была разработана Алленом Ньюелом (Allen Newell). Дж. Шоу (J. С. Shaw) и Гербертом Саймоном (Herbert Simon), Впервые она была опубли- кована в классической работе, описывавшей одну' из первых программ искусственного интеллекта, программу Logical Theorist, а также язык, на котором она могла быть реали- зована (Newell and Simon. 1956). Язык, названный 1PL-I (Information Processing Language — язык обработки информации), никогда не был внедрен. Следующая версия этого языка IPL-1I была реализована на компьютере Johnniac корпорации Rand Corporation. Развитие языка IPL продолжалось до 1960 года, в котором было опублико- вано описание языка IPL-V (Newell and Tonge. 1960). Широкому распространению язы- ков семейства 1PL мешал их низкий уровень. Для гипотетического компьютера, реал изо- 2.4. Функциональное программирование: язык LISP 69
ванного посредством интерпретаторов с включенными в него командами обработки спи- сков. эти языки фактически были языками ассемблера. Первая их реализация была вы- полнена на ничем не примечательной машине Johnniac, что также не очень способство- вало распространению языков семейства 1PL. Вкладом языков семейства 1PL в развитие языков программирования были их спи- сковые структуры и наглядная демонстрация того, что обработка списков вполне воз- можна и полезна. Корпорация IBM заинтересовалась искусственным интеллектом в середине 1950-х го- дов и в качестве демонстрационной области выбрала доказательство теорем. В то время полным ходом шла разработка языка FORTRAN. Высокая стоимость компилятора языка FORTRAN I убедила корпорацию IBM в том, что обработку списков проще присоединить к языку FORTRAN, чем выделять ее в отдельный язык. Так и возник язык FLTP (FORTRAN List Processing Language — язык обработки списков на базе языка FORTRAN), который был разработан и реализован в качестве расширения языка FORTRAN. Язык FLPL исполь- зовался для доказательства теорем в области планиметрии, которая тогда рассматривалась как простейшая область для механического доказательства теорем. 2.4.2. Процесс разработки языка USP Летом 1958 года отделом информационных исследований корпорации IBM (IBM Information Research Department) был нанят Джон Мак-Карти (John McCarthy) из инсти- тута MIT. Его целью на это лето было исследование символьных вычислений и разра- ботка конструктивных требований для проведения подобных вычислений. В качестве пробной проблемной области он выбрал дифференцирование алгебраических выраже- ний. При изучении этой области появился набор требований к языку. Среди них были методы управления выполнением математических функций: рекурсия и условные выра- жения. Язык FORTRAN 1, единственный существующий на то время язык высокого уровня, ни одной из этих функций не имел. Другие требования возникли при исследовании символьного дифференцирования и заключались в необходимости наличия связных списков, динамически размещаемых в памяти, и некоторого способа неявного удаления списков из памяти, использование кото- рых в программе уже не предполагалось. Мак-Карти просто не мог позволить явным опе- раторам освобождения памяти загромождать его изящный алгоритм дифференцирования. Поскольку язык FLPL не поддерживал рекурсию, условные выражения, динамическое выделение памяти или неявное ее освобождение, Мак-Карти стало ясно, что нужен но- вый язык. Когда осенью 1958 года Мак-Карти вернулся в институт М1Т, он вместе с Марвином Мински (Marvin Minsky) основал проект, посвященный искусственному интеллекту, MIT Al Project, финансировавшийся Исследовательской электротехнической лабораторией (Research Laboratory for Electronics). Первой важной работой нового проекта было созда- ние системы обработки списков. Ее планировалось использовать для первоначальной реализации предложенной Мак-Карти программы, названной Advice Taker. Это прило- жение стало стимулом к развитию языка обработки списков LISP. Первую версию этого языка иногда называют чистым языком LISP, поскольку он представляет собой язык функционального программирования в чистом виде. Эволюция чистого языка LISP опи- сывается в следующем разделе. 70 Глава 2. Обзор основных языков программирования
2.4.3. Обзор языка 2.4.3.1. Структуры данных В чистом языке LISP существовало только два типа структур данных: атомы и спи- ски. Атомы могли быть либо символами, принимавшими форму идентификаторов, либо числовыми константами. Естественно использовать для хранения символьной информа- ции связные списки, что и было сделано в языке IPL-II. Подобные структуры позволяют вставки и удаления в любом месте (операции, которые тогда считались неотъемлемой частью обработки списков). Правда, в конце концов было обнаружено, что подобное в языке LISP требуется очень редко. Списки определялись путем разграничения составляющих их элементов круглыми скобками. Простые списки, в которых элементы разделены на атомы, имеют следующую форму: (А В С D) Структуры со вложенными списками также определялись круглыми скобками. Например, список (А (В С) D (Е (F G) ) ) состоит из четырех элементов. Первый — это атом А, второй — подсписок (В С), тре- тий— атом D, а четвертый— подсписок (Е (F G)), содержащий в качестве второго элемента подсписок (F G). Списки, как правило, представляются в виде односвязных списковых структур, каждый узел которых содержит два указателя и представляет собой элемент списка. Первый указа- тель узла, соответствующего атому, указывает на представление атома, т.е. на его символь- ное или числовое значение. Первый указатель узла, соответствующего элементу подсписка, указывает на первый узел подсписка. В обоих случаях второй указатель узла указывает на следующий элемент списка. Ссылаются на список, указывая его первый элемент. Внутренние представления двух списков, упомянутых ранее, изображены на рис. 2.2. Отметим, что элементы списка показаны горизонтально. Последний элемент списка не имеет последующего элемента, так что он связан с нулевым указателем NIL. С помощью подобной же структуры показаны подсписки. 2.4.3.2. Процессы в функциональном программировании Язык LISP создавался как язык функционального программирования. Все вычисления в функциональной программе производятся путем применения функций к аргументам. Ни операторы присваивания, ни переменные, с избытком имеющиеся в императивных языках программирования, не нужны в программах, написанных на языках функцио- нального программирования. Более того, итеративные процессы могут определяться с помощью рекурсивных вызовов функций, что делает ненужными циклы. Эти основные концепции функционального программирования отличают его от программирования на императивных языках. 2.4. Функциональное программирование: язык LISP 71
В с D G Рис. 2.2. Внутреннее представление двух списков в языке LISP 2.4.3.3. Синтаксис языка USP Язык LISP значительно отличается от императивных языков как из-за того, что он явля- ется функциональным языком, так и из-за того, что программы, написанные на этом языке, выглядят совершенно иначе, чем программы, написанные на таких языках, как FORTRAN или С. Синтаксис языка С, например, является сложной смесью английского и алгебры, то- гда как синтаксис языка LIST — эталон простоты. Рассмотрим снова список (А В С D) Если интерпретировать его в терминах данных, то это — список из четырех элемен- тов. Если же рассматривать его как программу, то это значит, что функция А применяет- ся к трем параметрам: В, С и D. 2.4.4. Оценка Язык LISP всецело доминировал в сфере искусственного интеллекта четверть столетия, и все еще остается самым распространенным языком в этой области. Устранено большин- ство причин, создавших языку LISP репутацию неэффективного. Скомпилированы многие современные реализации, и результирующие программы выполняются значительно быст- рее, чем интерпретация их исходного кода. Помимо успехов в области искусственного ин- теллекта, язык LISP оказался пионером функционального программирования, доказавшего свою жизнеспособность в качестве области исследования языков программирования. Как указывалось в главе 1, многие исследователи языков программирования полагают, что функциональное программирование значительно лучше подходит для разработки про- граммного обеспечения, чем использование императивных языков. На протяжении 1970-х и в начале 1980-х годов было разработано и использовано множество различных диалектов языка LISP. Это привело к знакомой проблеме перено- 72 Глава 2. Обзор основных языков программирования
симости. Для исправления возникшей ситуации была создана стандартная версия языка, названная COMMON LISP (Steele. 1984). Подробно язык Scheme (диалект языка LISP) и функциональное программирование в целом рассмотрены в главе 14. Ниже следует пример программы на языке LISP. ; Пример функции языка LSIP ; Следующая программа определяет предикативную функцию языка ; LISP, принимающую в качестве аргументов два списка и ; возвращающую значение TRUE, если оба списка идентичны, и ; значение NIL (FALSE) в противном случае ; (DEFUN equal_lists (lisl lis2) (COND ((ATOM lisl) (EQ lisl lis2)) ((ATOM lis2) NIL) ((equal_list (CAR lisl) (CAR lis2)) (equal_list (CDR lisl) (CDR lis2))) (T NIL) ) ) 2.4.5. Два потомка языка LISP В наше время широко используются два диалекта языка LISP— язык COMMON LISP и язык Scheme. Оба кратко рассмотрены в следующих подразделах. 2.4.5.1. Язык Scheme Язык Scheme был разработан в институте MIT в середине 1970-х годов (Sussman and Steele, 1975). Для него характерен небольшой размер, использование исключительно статического обзора данных (рассматриваемого в главе 4). а также обработка функций как объектов первого класса. В таком качестве функции языка Scheme могут быть значе- ниями выражений и элементами в списках, присваиваться переменным, передаваться в качестве параметров и возвращаться в качестве результата применения функции к своим аргументам. В ранних версиях языка LISP всех этих возможностей не было, также в них не использовался статический обзор данных. Как небольшой язык с простыми синтаксисом и семантикой язык Scheme вполне приго- ден для таких учебных приложений, как курсы функционального программирования и об- щие вводные курсы программирования. Подробнее язык Scheme описывается в главе 14. 2.4.5.2. Язык COMMON LISP Язык COMMON LISP (Steele, 1984) был создан в попытке объединить в одном языке свойства нескольких диалектов языка LISP, разработанных в начале 1980-х годов, в том числе и языка Scheme. Будучи такой комбинацией, язык COMMON LISP представляет собой большую и сложную структуру. Тем не менее, его основой является чистый язык LISP, так что его синтаксис, основные функции и принципиальная природа происходят от этого языка. Признав гибкость, обеспечиваемую динамическим обзором данных, и простоту, пре- доставляемую статическим обзором, разработчики языка COMMON LISP использовали оба способа. По умолчанию используется статический обзор переменных, но если объя- вить переменную как special, ее область видимости становится динамической. 2.4. Функциональное программирование: язык LISP 73
Язык COMMON LISP содержит большое число типов и структур данных, среди кото- рых записи, массивы, комплексные числа и строки символов. Также в нем есть форма пакетов, используемая для сборки в единое целое набора функций и данных, которая обеспечивает, таким образом, управление доступом. К языку COMMON LISP мы еще вернемся в главе 14. 2.4.6. Родственные языки Язык ML (Metalanguage) изначально был создан в 1980-х годах Робином Милнером (Robin Milner) в Эдинбургском университете как метаязык для системы верификации программ, названной Logic for Computable Function (LCF) (Milner at al., 1990). Главным образом, язык ML — это язык функционального программирования, но он поддерживает также императивное программирование. В языке ML функции являются более общими, чем в императивных языках: они передаются между подпрограммами как параметры, они также могут быть полиморфными, что означает возможность получения параметров разных типов при разных вызовах. В отличие от языков LISP и Scheme, тип каждой пе- ременной и каждого выражения языка ML может определяться в процессе компиляции. Типы связываются с объектами, а не с именами. Типы выражений логически выводятся из контекста выражений, как это показано в главе 6. В отличие от языков LISP и Scheme, язык ML не использует функциональный синтак- сис со скобками, возникший вместе с лямбда-выражениями. Синтаксис языка ML скорее похож на синтаксис таких императивных языков, как Pascal и С. Язык Miranda был разработан Дэвидом Тернером (David Turner) в Университете Кента в Кентербери, Великобритания (University of Kent in Canterbury), в начале 1980-х (Turner, 1986). Этот язык частично основан на языках ML, SASL и KRC. Язык Haskell (Hudak and Fasel, 1992) в значительной мере основан на языке Miranda. Подобно языку Miranda, он яв- ляется чисто функциональным языком, не содержащим ни переменных, ни операторов присваивания. Еще одной отличительной чертой языка Haskel) является использование “ленивого” вычисления. Никакое выражение не вычисляется до тех пор, пока не потребует- ся его значение, что приводит к нескольким неожиданным возможностям языка. Языки ML и Haskell кратко описываются в главе 14. 2.5. Первый шаг к совершенствованию: язык ALGOL 60 Язык ALGOL 60 оказал значительное влияние на последующие языки программирова- ния и, как следствие, занимает центральное место в любом историческом обзоре языков. 2.5.1. Историческая ретроспектива Язык ALGOL 60— результат попытки создания универсального языка. На конец 1954 года в обращении уже более года находилась алгебраическая система Ленинга и Цирлера, и был опубликован первый отчет по языку FORTRAN. Реально язык FORTRAN был создан в 1957 году, к тому же году были разработаны еще несколько языков высоко- го уровня. Самыми известными из них были язык IT, разработанный в Карнеги-Тех (Camegy Tech) Аланом Перлисом (Alan Perlis), и два языка для компьютеров UNIVAC: MATH-MATIC и UNICODE. Рост количества языков программирования затруднял связи 74 Глава 2. Обзор основных языков программирования
между пользователями. Более того, все новые языки возникали на основе отдельных ар- хитектур, некоторые создавались для компьютеров UNIVAC. а некоторые — для машин корпорации IBM серии 700. В ответ на такой количественный рост языков несколько ос- новных компьютерных групп в Соединенных Штатах, в том числе SHARE (научная группа корпорации IBM) и USE (UNIVAC Scientific Exchange — промышленная группа обмена научными разработками). 10 мая 1957 года подали прошение в Ассоциацию по вычислительной технике (АСМ — Association for Computing Machinery) с просьбой сформировать комитет для изучения ситуации и создания универсального языка про- граммирования. Хотя кандидатом на такое звание мог быть язык FORTRAN, но истори- чески так не произошло, поскольку в то время он находился в исключительном владении корпорации IBM. Ранее, в 1955 году. Общество прикладной математики и механики (GAMM) также сформировало комитет по разработке единого универсального машинно-независимого ал- горитмического языка для использования на всех типах компьютеров. Желание иметь та- кой язык частично было вызвано европейской боязнью господства корпорации IBM. Тем не менее, появившиеся к концу 1957 года в Соединенных Штатах языки высокого уровня убедили подкомиссию общества GAMM в том. что их работа должна быть расширена для включения американских разработок, и письмо с соответствующим предложением было направлено в ассоциацию АСМ. После того как в апреле 1958 года Фриц Бауэр (Fritz Bauer) из общества GAMM представил ассоциации АСМ формальный план, указанные две группы официально согласились принять участие в проекте по разработке нового языка. 2.5.2. Начальная стадия разработки Общество GAMM и ассоциация АСМ решили, что совместная конструкторская рабо- та должна вестись на собрании, на которое каждая группа пошлет четырех представите- лей. Эта встреча, состоявшаяся в период с 27 мая по 1 июня 1958 года в Цюрихе, нача- лась с формулирования следующих целей нового языка. Синтаксис языка должен быть максимально близок к общепринятым математиче- ским обозначениям, и программы, написанные на нем. должны быть читабельны- ми сами по себе с минимальными дополнительными пояснениями. Язык должен использоваться для описания вычислительных процессов в публика- циях. Программы на новом языке должны механически транслироваться на машинные языки. Первая цель означает возможность использования языка для научного программиро- вания, которое в то время было основной сферой приложения вычислительной техники. Вторая цель была чем-то совершенно новым в компьютерном бизнесе. Необходимость последней цели для любого языка программирования очевидна. В зависимости от точки зрения встреча в Цюрихе породила либо важные результаты, либо бесконечные споры. В действительности, справедливо и то, и другое. Сама по себе встреча привела к многочисленным компромиссам как между отдельными лицами, так и между двумя сторонами Атлантики. В некоторых случаях компромиссы были не так уж и существенны в сравнении с глобальной целью, если они не касались различных сфер влияния. Одним из примеров может служить вопрос использования запятой (европейский метод) или точки (американский метод) в десятичной записи числа. 2.5. Первый шаг к совершенствованию: язык ALGOL 60 75
2.5.3. Обзор языка ALGOL 58 Язык, разработанный на встрече в Цюрихе, был назван IAL (International Algorithmic Language— интернациональный алгоритмический язык). В процессе конструкторских работ было предложено название ALGOL (ALGOrithmic Language— алгоритмический язык), но это название было отклонено, поскольку оно не отражало интернационального состава комитета. Впрочем, в течение следующего года название все-таки было измене- но на ALGOL, а сам язык получил впоследствии имя ALGOL 58. По многим параметрам язык ALGOL 58 был наследником языка FORTRAN, что вполне естественно. В нем обобщались многие свойства языка FORTRAN, а также вво- дились некоторые новые конструкции и концепции. Одни обобщения делались с целью создания языка, не привязанного к отдельной машине, другие — для увеличения гибко- сти и мощности языка. В результате получилась редкая комбинация простоты и изы- сканности. В языке ALGOL 58 была формализована концепция типов данных, хотя явного объ- явления требовали только переменные, не являющиеся числами с плавающей точкой. В нем была реализована идея составных операторов, которые затем были включены в большинство языков. Среди прочих были обобщены следующие свойства языка FORTRAN: разрешено использование идентификаторов произвольной длины (в отличие от имеющегося в языке FORTRAN ограничения на шесть символов): разрешено исполь- зование массивов любой размерности (в отличие от имеющегося в языке FORTRAN ог- раничения на три измерения); нижний предел массива мог определяться программистом (в то время как в языке FORTRAN он всегда имел значение 1); разрешены вложенные операторы ветвления (чего не было в языке FORTRAN). В языке ALGOL 58 была использована несколько необычная форма оператора при- сваивания. В языке PlankalkUl Цузе использовал следующую форму оператора присваи- вания: выражение => величина Хотя во время обсуждаемых событий описание языка Plankalkiil и не было еще опуб- ликовано. некоторые европейские члены комитета по созданию языка ALGOL 58 были знакомы с этим языком. Комитет позаимствовал из языка PlankalkUl форму оператора присваивания, но из соображений ограниченного использования символов знак ‘‘больше” был заменен двоеточием. Затем, в основном из-за настойчивых требований американцев, полный оператор принял вид выражение := величина Европейцы предпочитали обратную форму. 2.5.4. Принятие отчета о языке ALGOL 58 Публикация в декабре 1958 года отчета о языке ALGOL 58 (Perlis and Samelson) была встречена с большим энтузиазмом. В Соединенных Штатах новый язык представлялся скорее набором идей в области структуры языка программирования, чем универсальным языком программирования. Действительно, отчет о языке ALGOL создавал впечатление не законченного продукта, а чернового документа, выставленного на международное об- суждение. Тем не менее, этот отчет лег в основу трех основных работ по разработке и реализации языков программирования. В Университете штата Мичиган был создан язык 76 Глава 2. Обзор основных языков программирования
MAD (Arden et al.. 1961). Американской военно-морской электронной группой (U. S. Naval Electronics Group) был создан язык NEL1AC (Huskey et al.. 1963). Корпорация no разработке систем System Development Corporation разработала и реализовала язык JOVIAL (Shaw. 1963). Название последнего языка— аббревиатура от “Jules’ Own Version of the International Algebraic Language" (Собственная версия интернационального алгебраического языка, разработанная Жюлем), а сам он является единственной версией языка, основанного на языке ALGOL 58 и получившего широкое распространение (Жюлем был Жюль Шварц (Jules 1. Schwartz), один из разработчиков языка JOVIAL). Язык JOVIAL стал широко используемым, поскольку он четверть столетия был офици- альным языком научных разработок ВВС США. Остальная часть вычислительного сообщества Соединенных Штатов не была так бла- госклонна к новому языку. На первых порах корпорация IBM и ее главная научная груп- па SHARE, похоже, приняли язык ALGOL 58. Корпорация IBM начала его реализацию вскоре после публикации отчета, и группа SHARE сформировала подкомитет SHARE IAL для изучения этого языка. Позднее подкомитет предложил ассоциации АСМ стан- дартизировать язык ALGOL 58, и таким образом корпорация IBM реализовала его на всех компьютерах 700-й серии. Тем не менее, энтузиазм вскоре спал. Весной 1959 года и корпорация IBM, и группа SHARE, имевшие уже опыт работы с языком FORTRAN, ре- шили, что с них достаточно проблем и расходов, чтобы начинать работу над новым язы- ком. В середине 1959 года и корпорация IBM, и группа SHARE, вложившие немалые средства в язык FORTRAN, решили поддержать именно этот проект и сделать его язы- ком научных разработок для 700-й серии машин корпорации IBM. отказавшись, таким образом, от языка ALGOL 58. 2.5.5. Процесс разработки языка ALGOL 60 Ha протяжении 1959 года язык ALGOL 58 горячо обсуждался как в Европе, так и Со- единенных Штатах. В европейском издании ALGOL Bulletin и в Communications of the АСМ было опубликовано большое количество предложений, касающихся изменений и дополнений к этому языку. В 1959 году на Интернациональной конференции по обра- ботке информации (International Conference on Information Processing) была представлена работа Цюрихского комитета, в которой Бэкус предложим новую форму для описания синтаксиса языков программирования, позднее названную формой Бэкуса-Наура (BNF — Backus-Naur form). Подробно она описана в главе 3. В январе 1960 года состоялась второе собрание комитета по разработке языка ALGOL, на этот раз в Париже. Это собрание должно было обсудить 80 предложений, официально представленных на рассмотрение. При этом в разработку языка ALGOL был глубоко вовлечен Питер Наур (Peter Naur) из Дании, несмотря на то. что он не являлся членом Цюрихской группы. Именно Наур создал и издавал ALGOL Bulletin. Наур долгое время изучал работу Бэкуса. вводившую понятие формы BNF. и пришел к заключению, что эту форму можно использовать для формального описания результатов собрания 1960 года. После внесения в форму BNF нескольких относительно небольших Изменений Наур создал с ее помощью описание вновь предложенного языка и передал его комитету 1960 года в начале совещания. 2.5. Первый шаг к совершенствованию: язык ALGOL 60 77
2.5.6. Обзор языка ALGOL 60 Хотя собрание 1960 года длилось всего шесть дней, изменения, внесенные в язык ALGOL 58, были значительными. Среди важнейших новых разработок были следующие. Была введена концепция блочной структуры. Это позволило программистам лока- лизовать части программ, вводя новые среды данных, или области видимости. Появилась возможность передавать параметры подпрограммам двумя новыми способами: передача по значению и передача по имени. Появилась возможность создания рекурсивных процедур. Описание языка ALGOL 58 не совсем ясно освещало этот вопрос. Отметим, что. хотя для импера- тивных языков подобная рекурсия и была новой, язык LISP в 1959 году уже со- держал рекурсивные функции. Появились автоматические (stack-dynamic) массивы. Автоматическим массивом на- зывается такой массив, для которого диапазон или диапазоны изменения индексов определяются значениями переменных. Размер массива устанавливается во время его размещения в памяти. Это происходит в процессе выполнения программы при объявлении массива. Автоматические массивы подробно описаны в главе 5. Были предложены, но не приняты еще несколько свойств, которые могли бы значи- тельно повлиять или на успех, или на провал данного языка. Важнейшими среди них бы- ли операторы ввода-вывода с форматированием, которые не были приняты, поскольку считалось, что они будут значительно машинно-зависимыми. Отчет о языке ALGOL 60 был опубликован в мае 1960 года (Naur, 1960). В описании языка все еще оставалось много неопределенностей, и на апрель 1962 года в Риме была назначено третье собрание комитета, на рассмотрение которого и передавались все во- просы. На этом собрании группа занималась только разрешением затруднений, никаких новых дополнений в язык не вносилось. Результаты собрания были опубликованы под названием “Revised Report on the Algorithmic Language ALGOL 60” (“Переработанный отчет об алгоритмическом языке ALGOL 60”) (Backus at al., 1962). 2.5.7. Оценка языка ALGOL 60 Если судить по одним показателям, то язык ALGOL 60 имел ошеломительный успех, если по другим — был крупнейшим провалом. Успехом, проявившимся практически не- медленно, было то, что он стал единственным официальным средством представления алгоритмов в научной литературе и свыше 20 лет оставался таковым. Каждый импера- тивный язык программирования, созданный после 1960 года, что-то позаимствовал у языка ALGOL 60. Большинство из них прямо или косвенно являются потомками языка ALGOL 60; в качестве примеров можно назвать языки PL/1, SIMULA 67, ALGOL 67, ALGOL 68, С, Pascal, Ada, C++ и Java. В процессе создания языков ALGOL 58/ALGOL 60 многое было сделано впервые. Впервые интернациональная группа попыталась разработать язык программирования. Язык ALGOL был первым машинно-независимым языком. Он также был первым языком с формально описанным синтаксисом. Это успешное использование формы BNF поло- жило начало нескольким важным отраслям теории вычислительных систем: формаль- ным языкам, теории синтаксического анализа и теории разработки компиляторов. В ито- ге, структура языка ALGOL 60 повлияла на машинную архитектуру. Одним из самых 78 Глава 2. Обзор основных языков программирования
выдающихся примеров этого является тот факт, что расширенная версия этого языка ис- пользовалась в качестве системного языка в серии универсальных вычислительных ма- шин компании Burrough: В5000. В6000 и В7000. которые были сконструированы с аппа- ратно-реализованным стеком для эффективной реализации блочной структуры и рекур- сивных процедур языка. Рассмотрим обратную сторону медали: в Соединенных Штатах язык ALGOL 60 нико- гда не получил широкого распространения или хотя бы значительного использования. Да- же в Европе он так и не стал доминирующим языком. Причин его неприятия множество. Во-первых, некоторые свойства языка ALGOL 60 стали слишком гибкими; они затрудняли понимание и делали его реализацию неэффективной. Лучшим примером этого служит ме- тод передачи подпрограммам параметров по имени, подробно описываемый в главе 8. Сложности реализации языка ALGOL 60 сформулировал Рутишаузер (Rutishauser. 1967), сказавший, что очень немногие (если таковые вообще есть) реализации содержат язык ALGOL 60 полностью. Отсутствие в языке операторов ввода-вывода было еще одной существенной причи- ной его неприятия. Зависимый от реализации ввод и вывод затруднял перенос программ на другие компьютеры. Один из важнейших связанных с языком ALGOL 60 вкладов в компьютерные нау- ки — форма BNF — также не способствовал широкому распространению языка. Хотя сейчас эта форма рассматривается как простое и элегантное средство описания синтак- сической структуры, в I960 году она казалась странной и сложной. В заключение отметим, что. хотя и существовало множество других проблем, закре- пившийся в среде пользователей язык FORTRAN, а также отсутствие поддержки со сто- роны корпорации IBM, были, вероятно, решающими факторами провала языка ALGOL 60 как языка широкого применения. Работа над языком ALGOL 60 в действительности никогда не завершалась в том смысле, что неясности и неопределенности всегда были частью описания этого языка (Knuth, 1967). Ниже следует пример программы на языке ALGOL 60. comment Пример программы на языке ALGOL 60 Ввод: Целое число listlen, меньше 100, за которым следует набор целых чисел в количестве listlen Вывод: Количество введенных величин, которые больше их среднего арифметического; begin integer array intlist [1:99]; integer listlen, counter, sum, average, result; sum := 0; result := 0; readint (listlen); if (listlen > 0) a (listlen < 100) then begin comment Считывание входных величин в массив и вычисление суммы; for counter := 1 step 1 until listlen do begin readint (intlist[counter]); 2.5. Первый шаг к совершенствованию: язык ALGOL 60 79
sum := sum + intlist[counter] end; comment Вычисление среднего арифметического; average := sum / listlen; comment Вычисление количества входных величин, которые больше ::х среднего; for counter := 1 step 1 until listlen do if intlist[counter] > average then result := result + 1; comment Вывод результатов; printstring("Число величин, которые больше их среднего:); printint (result) end else printstring (’’Ошибка — введена неверная длина списка”); end Происхождение языка ALGOL 60 представлено на рис. 2.3. FORTRAN 1(1957) ALGOL 58 (1958) • ALGOL 60 (1960) Рис. 2.3. Генеалогия языка ALGOL 60 2.6. Компьютеризация коммерческих записей: язык COBOL История языка COBOL действительно необычна. Хотя использовался он больше, чем какой бы то ни было язык программирования, его влияние на структуру последующих языков было очень незначительным, за исключением языка PL/1. Возможно, он и по се- годняшний день является самым широко используемым языком программирования, хотя трудно сказать, так это или нет. Вероятно, важнейшей причиной слабого влияния языка COBOL можно назвать то. что очень немногие после его появления пытались разрабо- тать новый язык для коммерческих приложений. Последнее может быть следствием того, что возможности языка COBOL вполне удовлетворяют потребности этой области. Дру- гой причиной малого влияния можно назвать то. что значительная часть роста коммер- ческих вычислений последних 15 лет приходится на малый бизнес. В этой области раз- рабатывается очень мало программного обеспечения. Вместо этого большая часть ис- пользуемого программного обеспечения заказывается в виде готовых пакетов для различных коммерческих приложений. 2.6.1. Исторические предпосылки Возникновение языка COBOL в чем-то схоже с возникновением языка ALGOL 60 в том смысле, что этот язык тоже разрабатывался группой людей, собиравшихся на отно- сительно короткий промежуток времени. В 1959 году положение в сфере коммерческих вычислений было подобно состоянию в области научных расчетов, существовавшему 80 Глава 2. Обзор основных языков программирования
несколькими годами ранее во время разработки языка FORTRAN. В 1957 году был реа- лизован компилируемый язык для коммерческих приложений FLOW-MATIC, но он при- надлежал одному производителю, компании UNIVAC. и был разработан для компьюте- ров именно этой компании. Другой язык. AIMACO. использовался ВВС США. но он был разновидностью языка FLOW-MATIC, незначительно отличавшейся от оригинала. Кор- порация IBM разработала язык программирования для коммерческих приложений COMTRAN (COMmercial TRANsIator— коммерческий транслятор), но этот язык не реа- лизован до сих пор. Еще несколько конструкторских работ по созданию языков тогда только намечались. 2.6.2. Язык FLOW-MATIC Происхождение языка FLOW-MATIC стоит хотя бы краткого обсуждения, поскольку этот язык был прародителем языка COBOL. В декабре 1953 года Грейс Хоппер из ком- пании Remington-Rand UNIVAC высказала предположение, оказавшееся пророческим. Она утверждала, что “математические программы следует писать с помощью математи- ческих обозначений, программы обработки данных следует писать на английском" (Wexelblat, 1981). К сожалению, в 1953 году было невозможно убедить непрограммистов в том. что можно создать компьютер, понимающий английские слова. Только в 1955 го- ду подобное предложение получило шансы на финансирование со стороны руководства компании UNIVAC, но даже тогда потребовался пробный объект для принятия оконча- тельного решения. Частью рекламы был запуск небольшой программы, вначале исполь- зовавшей английские ключевые слова, затем французские, а после этого — немецкие. Руководство компании UNIVAC признало демонстрацию убедительной, что и стало ре- шающим фактором при принятии ими плана Хоппер. 2.6.3. Процесс разработки языка COBOL Первое официальное собрание, посвященное универсальному языку для коммерче- ских приложений, финансируемое Министерством обороны США. состоялось в Пента- гоне 28 и 29 мая 1959 года (точно через год после собрания в Цюрихе, посвященного разработке языка ALGOL). Единодушно было выработано мнение, что новый язык, по- лучивший название CBL (Common Business Language— общий коммерческий язык), должен иметь следующие основные характеристики. Большинство согласилось, что нужно максимально использовать английский язык, хотя некоторые и выступали за бо- лее широкое использование математических обозначений. Использование языка должно было быть легким, даже за счет потерн мощности, чтобы расширить круг тех. кто может программировать на компьютере. Кроме того, предполагалось, что менеджеры смогут читать программы, если использовать в языке программирования английский язык. На- конец, проект не должен был чрезмерно ограничиваться проблемами его реализации. Поскольку была проведена большая подготовительная работа, создавать универсаль- ный язык следовало немедленно. Кроме того, компании RCA и Sylvania уже работали над собственными языками коммерческих приложений. При таких обстаятельствах было решено провести краткое изучение существующих языков программирования, для чего был сформирован комитет Short Range Committee. Ранее были приняты решения о разделении выражений языка на две категории — описания данных и выполняемых операций — ио размещении выражений этих двух ка- тегорий в разных частях программ. Один из самых жарких споров в комитете Short 2.6. Компьютеризация коммерческих записей: язык COBOL 81
Range Committee велся вокруг введения индексов. Многие члены комитета доказывали, что индексы — слишком сложное понятие для математически малообразованных людей, занимающихся обработкой данных. Подобные аргументы приводились и при обсужде- нии возможности включения математических выражений. В окончательном отчете коми- тета, завершенном к декабрю 1959 года, описывался язык, позднее названный языком COBOL 60. Спецификации языка COBOL 60, опубликованные правительственной типографией Goverment Printing Office в апреле 1960 года (Department of Defense, 1960), были описаны как “исходные". Исправленные версии были опубликованы в 1961 и 1962 годах (Department of Defense, 1961, 1962). В 1968 году язык был стандартизирован Националь- ным институтом стандартизации США (ANSI). Следующие две переработанные версии языка были стандартизированы тем же институтом в 1974 и 1985 годах. В наше время язык продолжает эволюционировать. 2.6.4. Оценка Язык COBOL положил начало большому количеству новых концепций, некоторые из которых в конечном итоге появились и в других языках. Например, использованный в языке COBOL глагол DEFINE (“определить”) был первой высокоуровневой языковой конструкцией для макросов. Более важный факт: иерархические структуры данных, впервые появившиеся в языке PlankalkUl, были реализованы в языке COBOL. Эти струк- туры были включены практически во все императивные языки, разработанные с тех пор. Кроме того, язык COBOL был первым языком, в котором имена стали действительно осмысленными, поскольку в этом языке допускалось использование как длинных имен (до 30 символов), так и символов соединения слов (тире). Вообще, раздел данных — сильная сторона структуры языка COBOL, тогда как раз- дел процедур — относительно слабая. В разделе данных подробно определяется каждая переменная, в том числе количество десятичных разрядов и связанное с ним положение точки в десятичной записи. Записи файлов описывались на таком же детальном уровне, как строки, выводимые на принтер, что сделало язык COBOL идеальным для печати бух- галтерских отчетов. Вероятно, самой слабой стороной раздела процедур было недоста- точное количество функций. Отметим, что до стандарта 1974 года все версии языка COBOL не допускали подпрограмм с параметрами. Наш последний комментарий относительно языка COBOL: это был первый язык про- граммирования, использование которого поддерживалось Министерством обороны США. Эта поддержка возникла после его первоначальной разработки, поскольку язык COBOL не был создан специально для Министерства обороны. Несмотря на свое качест- во, язык COBOL, вероятно, не смог бы выжить без этой поддержки. Недостаточная про- изводительность его первых компиляторов просто сделала бы его использование слиш- ком дорогим. В конечном итоге, разумеется, люди больше узнали о разработке компиля- торов, да и компьютеры стали быстрее, дешевле и имеют больше памяти. Все эти факторы в совокупности привели язык COBOL к успеху как внутри, так и вне Министер- ства обороны. Его появление привело к электронной механизации бухгалтерских дел, что по любым меркам было революцией. Ниже следует пример программы на языке COBOL. Эта программа считывает файл BAL-FWD-FILE, содержащий инвентаризационную информацию о некотором наборе изделий. Среди прочего, каждая запись по изделию содержит количество имеющихся из- 82 Глава 2. Обзор основных языков программирования
делий (BAL-ON-HAND) и момент возобновления заказа на изделие (BAL-REORDER- POINT), т.е. момент, когда число изделий, требующих доставки, превышает количество число имеющихся изделий. Программа порождает файл REORDER-LISTING, содержа- щий список изделий, требующий повторного заказа. IDENTIFICATION DIVISION. PROGRAM-ID. PRODUCE-REORDER-LISTING. ENVIRONMENT DIVISION. CONFIGURATION SECTION. SOURCE-COMPUTER. DEC-VAX. OBJECT-COMPUTER. DEC-VAX. INPUT-OUTPUT SECTION. FILE-CONTROL. SELECT BAL-FDW-FILE ASSIGN TO READER. SELECT REORDER-LISTING ASSIGN TO LOCAL-PRINTER. DATA DIVISION. FILE SECTION. FD BAL-FDW-FILE LABEL RECORDS ARE STANDARD RECORD CONTAINS 80 CHARACTERS. 01 BAL-FDW-CARD. 02 BAL-ITEM-NO PICTURE IS 9(5) . 02 BAL-ITEM-DESC PICTURE IS X(20) . 02 FILLER PICTURE IS X(5) . 02 BAL-UNIT-PRICE PICTURE IS 999V99 02 BAL-REORDER-POINT PICTURE IS 9(5) . 02 BAL-ON-HAND PICTURE IS 9(5) . 02 BAL-ON-ORDER PICTURE IS 9(5) . 02 FILLER PICTURE IS X(30). FD REORDER-LISTING LABEL RECORD ARE STANDARD RECORD CONTAINS 132 CHARACTERS. 01 REORDER-LINE. 02 RL-ITEM-NO PICTURE IS Z(5). 02 FILLER PICTURE IS X(5). 02 RL-ITEM-DESC PICTURE IS X(20). 02 FILLER .PICTURE IS X(5) . 02 RL-UNIT-PRICE PICTURE IS ZZZ.99 02 FILLER PICTURE IS X(5). 02 RL-AVAILABLE-STOCK PICTURE IS Z(5). 02 FILLER PICTURE IS X(5). 02 RL-REORDER-POINT PICTURE IS Z(5). 02 FILLER PICTURE IS X(71). WORKING-STORAGE SECTION. 01 SWITCHES. 02 CARD-EOF-SWITCH PICTURE IS X. 2.6. Компьютеризация коммерческих записей: язык COBOL 83
лЭР.К—г It.LDS. и 2 AVAILABLE-STOCK PICTURE IS 9(5). OPEN INPUT BAL-FDW-FILE. OPEL' OUTPUT REORDER-LISTING. MO’rE “И” TO CARD-EOF-SWITCH. PERFORM 3 CO-PRODUCE-REORDER-LINE UNTIL CARD-EOF-SWITCH IS EQUAL TO “Y”. CLOSE BAL—DW-FILE. CLOSE REORDER-LISTING. STOP RUN. : , C-PR0DUCE-RE3RDER-LINE. PERFORM 2CO-READ-INVENTORY-RECORD. IF CARD-EOF-SWITCH IS NOT EQUAL TO “Y“ PERFORM 120-CALCULATE-AVAILABLE-STOCK IF AVAILABLE-STOCK IS LESS THAN BAL-REORDER-POINT PERFORM 130-PRINT-REORDER-LINE. :IC-READ-INVENTORY-RECORD. READ BAL-FDW-FILE RECORD AT END MOVE "Y" TO CARD-EOF-SWITCH. L2 0-CALCULAT E-AVAILAB LE-STOCK. ADD BAL-ON-HAND BAL-ON-ORDER GIVING AVAILABLE-STOCK. PRINT-REORDER-LINE. MOVE SPACE MOVE BAL-ITEM-NO MOVE BAL-IТЕМ-DESC MOVE BAL-UNIT-PRICE MOVE AVAILABLE-STOCK MOVE BAL-REORDER-POINT WRITE REORDER-LINE. TO REORDER-LINE. TO RL-ITEM-NO. TO RL-ITEM-DESC. TO RL-UNIT-PRICE. TO RL-AVAILABLE-STOCK TO RL-REORDER-POINT. Происхождение языка COBOL представлено на рис. 2.4 • FLOW-MATIC (1957) • COBOL (1960) Рис. 2.4. Генеаюгия языка COBOL 84 Глава 2. Обзор основных языков программированы
2.7. Начало разделения времени: язык BASIC Еше одним примером широко распространенного языка программирования, не полу- чившего значительного признания, является язык BASIC. Подобно языку COBOL, он в значительной степени игнорировался специалистами по компьютерным наукам. Так же. как и язык COBOL, в своих ранних версиях язык BASIC был элегантен и содержал толь- ко ограниченный набор управляющих операторов Язык BASIC был очень популярным языком программирования на микрокомпьюте- рах в конце 1970-х и начале 1980-х годов. Это было следствием его основных характери- стик. Он легок для изучения начинающими, особенно теми, кто не связан с наукой, его небольшие диалекты могут быть реализованы на компьютерах с очень малым количест- вом памяти. Возрождение языка BASIC произошло в начале 1990-х годов с выходом языка Visual BASIC (Microsoft. 1991). 2.7.1. Процесс разработки Язык BASIC (Beginner's All-purpose Symbolic Instruction Code — универсальный сим- вольный набор команд для начинающих) был разработан в Дартмутском колледже (Dartmouth College) (теперь Дартмутском университете (Dartmouth University)) в Нью- Гемпшире двумя математиками. Джоном Кемени (John Kemeny) и Томасом Курцом (Thomas Kurtz), которые в начале 1960-х годов были связаны с производством компиля- торов для различных диалектов языков FORTRAN и ALGOL 60. Их студенты, изучавшие естественные науки, испытывали трудности в работе с указанными языками. Однако Дартмут был преимущественно гуманитарным учреждением, где студенты, изучавшие естественные и технические науки, составляли приблизительно 15 процентов. Весной 1963 года было решено разработать новый язык, предназначенный главным об- разом для студентов, изучавших гуманитарные науки. В качестве метода доступа к ком- пьютеру этот новый язык должен был использовать терминалы. К языку были предъяв- лены следующие требования. 1. Он должен быть легким для изучения и использования студентами, не изучающи- ми естественные науки. 2. Он должен быть приятным и дружественным. 3. Язык должен обеспечивать быстрый обмен информацией для выполнения домаш- ней работы. 4. Он должен позволять как свободный, так и конфиденциальный доступ. 5. Время пользователя должно быть важнее времени работы компьютера. Последняя цель была действительно революционной концепцией. Она опиралась, по крайней мере частично, на убежденность в том. что со временем компьютеры станут значительно дешевле, что. в общем-то, и произошло. Комбинация второй, третьей и четвертой целей породила концепцию разделения вре- мени в языке BASIC. В начале 1960-х достичь поставленных целей можно было только с помощью одновременного индивидуального доступа многих пользователей к компьюте- ру посредством терминалов. Летом 1963 года Кемени начал работу над компилятором для первой версии языка BASIC, используя для этого удаленный доступ к компьютеру GE 225. Разработка и про- 2.7. Начало разделения времени: язык BASIC 85
граммирование операционной системы для языка BASIC начались осенью 1963 года. В 4 часа утра 1 мая 1964 года была напечатана и запушена первая программа с использо- ванием языка с разделением времени BASIC. В июне количество терминалов системы возросло до 11, а к осени — до 20. 2.7.2. Обзор языка Исходная версия языка BASIC была очень небольшой и, как ни странно, не была ин- терактивной: не существовало средств получения входных данных с терминала. Про- граммы печатались, компилировались и запускались пакетно-ориентированным спосо- бом. Исходный язык BASIC имел только 14 различных типов операторов и один тип данных — числа с плавающей точкой. Поскольку считалось, что только некоторые из пользователей, на которых был ориентирован язык, будут принимать во внимание разли- чия между целым типом и типом с плавающей точкой, этот тип был назван “числа”. Во- обще, это был очень ограниченный язык, хотя и довольно простой для изучения. 2.7.3. Оценка Важнейшим аспектом исходного языка BASIC было его первенство в использовании метода удаленного доступа к компьютеру посредством терминала. В то время терминалы только-только стали доступны. До этого большинство программ вводились в компьютер с помощью перфокарт либо бумажной ленты. Большинство конструкций языка BASIC произошли от языка FORTRAN при некото- ром влиянии синтаксиса языка ALGOL 60. Позднее он развивался различными путями при незначительных усилиях по его стандартизации или совсем без них. Институт ANSI выпустил минимальный стандарт для языка BASIC (Minimal BASIC) (ANSI, 1978b), представлявший только необходимый минимум свойств языка. Фактически исходный язык BASIC был очень похож на язык Minimal BASIC. Хотя это и может показаться неожиданным, корпорация Digital Equipment Corporation использовала детально разработанную версию языка BASIC, названную языком BASIC- PLUS, с целью написания в 1970-х годах значительной части их крупнейшей операцион- ной системы RSTS для миникомпьютеров PDP-11. Среди прочего язык BASIC критиковался за плохую структуру программ, написанных на нем. Согласно нашему критерию оценки язык действительно стоит очень немногого. Очевидно, ранние версии языка не были и не должны были быть средством для написа- ния серьезных программ мало-мальски значительного размера. Последующие версии, правда, были более приспособлены для подобных задач. Наиболее вероятной причиной успеха языка BASIC можно назвать легкость его изу- чения и реализации даже на очень маленьких компьютерах. Существуют две современные версии языка BASIC, не получившие, правда, широко- го распространения: язык QuickBASIC (Bradley, 1989) и язык Visual BASIC. Оба они за- пускаются на персональных компьютерах. Язык Visual BASIC основан на языке QuickBASIC, но он был создан для разработки систем программного обеспечения, ис- пользующих оконный интерфейс пользователя. Язык Visual BASIC также мог использо- ваться в качестве языка программирования для написания сценариев интерфеса компью- терной графики (CGI — Computer Graphics Interface). Ниже следует пример программы на языке QuickBASIC. 86 Глава 2. Обзор основных языков программирования
REK .р^.мер программы на языке QuickBASIC REM Ввод: Целое число listlen, меньше 100, за которым REM следует набор целых чисел в количестве REM listlen REM Вывод: Количество введенных величин, которые больше их REM среднего арифметического DIM intlist(99) result = 0 sum = 0 INPUT listlen IF listlen > 0 AND listlen < 100 THEN REM Считывание входных величин в массив и вычисление суммы FOR counter = 1 ТО listlen INPUT intlist(counter) sum - sum + intlist(counter) NEXT counter REM Вычисление среднего арифметического average = sum / listlen REM Вычисление количества входных величин, которые больше их REM среднего FOR counter = 1 ТО listlen IF intlist(counter) > average THEN result = result + 1 NEXT counter REM Вывод результатов; PRINT "Число величин, которые больше их среднего, равно"; result ELSE PRINT "Ошибка — введена неверная длина списка" END IF END Происхождение языка BASIC представлено на рис. 2.5. Л ALGOL 60 (1960) FORTRAN IV (1962) / ^^^*VbASIC(1964) ° QuickBASIC (1988) и Visual BASIC (1990) Рис. 2.5. Генеалогия языка BASIC 2.7. Начало разделения времени: язык BASIC 87
2.8. Все для всех: язык PL/I Язык PL'I представляет собой первую масштабную попытку разработки языка, кото- рый бы использовался в широком спектре областей. Все предшествовавшие и большин- < то последующих языков программирования концентрировались на какой-то одной об- мети применения (наука, искусственный интеллект или коммерция). 2.8.1. Исторические предпосылки Подобно языку FORTRAN язык PL/I разрабатывался как продукт корпорации IBM. В начале 1960-х годах промышленные пользователи компьютеров сформировали два от- ельных лагеря. С точки зрения корпорации IBM научные программисты могли исполь- ничать либо универсальный компьютер 7090, либо компьютер с ограниченными возмож- ностями 1620. которые выпускались указанной корпорацией. Эта группа в значительной <чепени использовала тип данных с плавающей точкой и массивы. Основным языком в их работе был язык FORTRAN, хотя параллельно рассматривались и некоторые языки ассемблера. Эти пользователи имели собственную группу SHARE и поддерживали сла- бые связи с программистами, работавшими над коммерческими приложениями. Для коммерческих приложений использовались большие или малые компьютеры, IBM 7080 и IBM 1401. Для этих приложений требовались типы данных для десятичных чисел и символьных строк, а также детально разработанные и эффективные свойства ввода-вывода. Данная группа пользователей использовала язык COBOL, хотя в начале 1чоЗ года переход с языков ассемблера на язык COBOL был далек от завершения. Эта каичория пользователей также имела свою собственную группу пользователей GUIDE и изредка налаживала контакты с научными пользователями. В начале 1963 года проектировщики корпорации IBM почувствовали, что ситуация начинает меняться. Указанные группы двигались навстречу друг другу по путям, кото- рые неизбежно должны были привести к возникновению проблем. Ученые начали соби- рагь большие файлы данных, требующих обработки. Для этого нужны были изощренные и эффективные средства ввода-вывода. Люди же, занимавшиеся коммерческими прило- жениями. для построения систем управления информацией начали использовать регрес- сионный анализ, что требовало чисел с плавающей точкой и массивов. Стало казаться, чю внедрение компьютерной техники вскоре потребует двух отдельных компьютеров, поддерживающих два значительно отличающихся языка программирования. Эти предчувствия вполне естественно привели к возникновению концепции разра- бо|ки единого универсального компьютера, который бы мог выполнять как действия над числами с плавающей точкой, так и над над десятичными числами, и, следовательно, оыл бы применим и в нахке. и в коммерции. Так зародилась концепция серии компьюте- ров IBM System/360. Наряду с этой концепцией возникла идея о языке программирова- ния. который был бы применим и для коммерческих, и для научных приложений. В зна- чи!ельной степени в язык были включены возможности системного программирования и обработки списков. Следовательно, новый язык должен был заменить языки FORTRAN, COBOL и LISP, а в области системных приложений — еще и язык ассемблера. 88 Глава 2. Обзор основных языков программирования
2.8.2. Процесс разработки Разработка началась с образования в октябре 1963 года корпорацией IBM и группой SHARE комитета Advanced Language Development Committee of the SHARE FORTRAN Project. Этот новый комитет быстро собрался и сформировал подкомитет 3<->3 Committee, названный так из-за наличия в нем трех человек со стороны корпорации IBM и трех со стороны группы SHARE. Для разработки языка комитет 3<н>3 Committee соби- рался на три-четыре дня через неделю. Как и при работе комитета Short Range Committee, разрабатывавшего язык COBOL, завершение исходной разработки была запланировано на удивительно близкую дату По- видимому, независимо от целей разработки языка в начале 1960-х широко распростра- ненно мнение, что это задание может быть выполнено за три месяца. Первую версию языка PL/I, позже названную FORTRAN VI. предполагалось завершить к декабрю, что составляло менее трех месяцев со дня основания комитета. Комитет, приведя в оправда- ние две различные причины задержки, передвинул срок окончания работ сначала на ян- варь, а затем и на февраль 1964 года. Исходная концепция разработки состояла в том. что новый язык должен был быть расширенной версией языка FORTRAN IV. совместимой с ним. но это намерение было быстро отброшено, как и название FORTRAN VI. Вплоть до 1965 года этот язык был из- вестен как NPL, что являлось аббревиатурой названия New Programming Language (новый язык программирования). Первый опубликованный отчет о языке NPL был пре- доставлен на собрании группы SHARE в марте 1964 года. Более полное описание после- довало в апреле, а работоспособная версия была опубликована в декабре 1964 года (IBM. 1964) группой разработчиков компилятора из выбранной для реализации лаборатории IBM Hursley Laboratory в Великобритании. В 1965 году чтобы избежать путаницы с аб- бревиатурой Национальной физической лаборатории (NPL— National Physical Laboratory) в Великобритании, название языка было изменено на PL I. Если бы компиля- тор разрабатывался за пределами Соединенного Королевства, название, вероятно, оста- лось бы прежним. 2.8.3. Обзор языка Возможно, лучшим описанием языка PL/I. состоящим из одного предложения, будет следующее: он содержит то. что считалось лучшими частями языка ALGOL 60 (рекурсия и блочная структура), языка FORTRAN IV (раздельная компиляция со связью посредст- вом глобальных данных) и языка COBOL 60 (структуры данных, средства ввода-вывода и генерации отчетов), а также несколько новых конструкций, каким-то образом соеди- ненных в единое целое. Мы не будем пытаться даже в конспективной форме описать все свойства этого языка или хотя бы его наиболее противоречивые конструкции. Вместо этого мы кратко назовем несколько вкладов, сделанных этим языком в фонд знаний в области языков программирования. Язык PL/1 был первым языком, имевшим следующие возможности. Программы могли порождать параллельно выполняемые задачи. Хотя сама идея была хороша, но ее реализация в языке PL/I была скверной. Появилась возможность выявления и обработки 23 видов различных исключи- тельных ситуаций или ошибок в процессе выполнения. 2.8. Все для всех: язык PL/I 89
Процедуры могли использоваться рекурсивно, но для более эффективного програм- мирования нерекурсивных процедур эта возможность могла быть заблокирована. В качестве типа данных были включены указатели. Появилась возможность обращаться к пересекающимся разделам массивов. На- пример, к третьей строке матрицы можно было обращаться как к вектору. 2.8.4. Оценка Любая оценка языка PL/I должна начинаться с упоминания о претенциозности работ по его созданию. Оглядываясь назад, кажется наивным полагать, что такое количество конструкций можно было успешно объединить вместе. Тем не менее, это суждение сле- дует смягчить признанием того, что опыт разработки языков в то время был очень не- большим. Вообще говоря, структура языка PL/I была основана на допущении, что любая полезная конструкция, которая может быть реализована, должна включаться в язык, при этом вопросу, как большое количество функций сможет ужиться вместе, уделялось мало внимания. Эдсгер Дийкстра (Edsger Dijkstra) в своей лекции на церемонии вручения премии Тьюринга (Turing Award Lecture (Dijkstra, 1972)) подверг сильнейшей критике сложность языка PL/I: “Я совершенно не вижу, как мы можем охватить умом наши уве- личивающиеся в размере программы, в то время как из-за своей явной переусложненно- сти язык программирования — наш основной инструмент, прошу отметить! — уже вы- ходит из-под нашего интеллектуального контроля”. Помимо сложности, возникшей из-за большого размера, язык PL/I страдал от боль- шого количества конструкций, которые сейчас считаются неудачно разработанными. Среди них можно отметить указатели, обработку исключительных ситуаций, параллель- ность, хотя мы должны обратить внимание и на то, что в каждой из указанных конструк- ций было что-то новое. С точки зрения использования язык PL/I должен считаться, по крайней мере частич- но, успешным. В 1970-х годах он в значительной мере использовался как в коммерции, так и в науке. В это же время он широко использовался в качестве учебного средства, главным образом в несколько сокращенных формах, таких как язык PL/C (Cornell, 1977) и язык PL/CS (Conway and Constable, 1976). Ниже следует пример программы на языке PL/I. /* ПРИМЕР ПРОГРАММЫ НА ЯЗЫКЕ PL/I ВВОД: ЦЕЛОЕ ЧИСЛО LISTLEN, МЕНЬШЕ 100, ЗА КОТОРЫМ СЛЕДУЕТ НАБОР ЦЕЛЫХ ЧИСЕЛ В КОЛИЧЕСТВЕ LISTLEN ВЫВОД: КОЛИЧЕСТВО ВВЕДЕННЫХ ВЕЛИЧИН, КОТОРЫЕ БОЛЬШЕ ИХ СРЕДНЕГО АРИФМЕТИЧЕСКОГО */ PLIEX: PROCEDURE OPTIONS (MAIN); DECLARE INTLIST (1:99) FIXED. DECLARE (LISTLEN, COUNTER, SUM, AVERAGE, RESULT) FIXED; SUM = 0; RESULT = 0; GET LIST(LISTLEN); IF (LISTLEN > 0) & (LISTLEN < 100) THEN DO; /* СЧИТЫВАНИЕ ВХОДНЫХ ДАННЫХ В МАССИВ И ВЫЧИСЛЕНИЕ ИХ СУММЫ */ DO COUNTER = 1 ТО LISTLEN; 90 Глава 2. Обзор основных языков программирования
GET LIST (INTLIST (COUNTER)); SUM = SUM + INTLIST (COUNTER); END; /* ВЫЧИСЛЕНИЕ СРЕДНЕГО */ AVERAGE = SUM / LISTLEN; /* ПОДСЧЕТ ЧИСЛА ВЕЛИЧИН, КОТОРЫЕ БОЛЬШЕ СРЕДНЕГО */ DO COUNTER = 1 ТО LISTLEN; IF INTLIST (COUNTER) > AVERAGE THEN RESULT = RESULT -rl; END; /* ВЫВОД РЕЗУЛЬТАТОВ */ PUT SKIP LIST (’ЧИСЛО ВЕЛИЧИН, КОТОРЫЕ БОЛЬШЕ СРЕДНЕГО:'); PUT LIST (RESULT); END; ELSE PUT SKIP LIST (’ОШИБКА - ВВЕДЕНА НЕВЕРНАЯ ДЛИНА СПИСКА’); END PLIEX Происхождение языка PL/I представлено на рис. 2.6. Рис. 2.6. Генеалогия языка PL/I 2.9. Два ранних динамических языка: APL и SNOBOL Поскольку языки, рассматриваемые в данном разделе, значительно отличаются от язы- ков, рассматривавшихся ранее, то и сама структура раздела отличается от структуры пре- дыдущих разделов. Ни язык APL, ни язык SNOBOL не основывались ни на одном из пред- шествовавших языков, они также не оказали существенного влияния на последующие язы- ки (хотя язык J и основан на языке APL, а язык ICON (Griswold and Griswold, 1983)— на языке SNOBOL). И язык APL, и язык SNOBOL используются нами для иллюстрации и противопоставления некоторых их свойств соответствующим свойствам более традицион- ных языков. Именно по этой причине они кратко описываются в данном разделе. По возникновению и по предназначению языки APL и COBOL значительно различа- ются. Тем не менее, их объединяют две общие характеристики: динамическая проверка типов и динамическое распределение памяти. По существу, переменные в обоих языках не имеют типов. Переменная получает тип при присвоении ей значения, при этом она получает тип присваиваемой величины. Память выделяется переменной только при при- своении ей значения, поскольку заранее определить требуемое количество памяти не- возможно. 2.9. Два ранних динамических языка: APL и SNOBOL 91
2.9.1. Истоки и характеристики языка APL Язык APL (Polinka and Pakin. 1975) был разработан около 1960 года Кеннетом Айвер- соном (Kenneth Е. Iverson) из корпорации IBM. Изначально он разрабатывался не для реализации в качестве языка программирования, а в качестве средства описания архитек- туры компьютера. Впервые язык APL был описан в книге, давшей ему имя, “A Programming Language” (Iverson. 1962). Первая его реализация была создана корпора- цией IBM в середине 1960-х годов Язык APL содержит большое количество мощных операторов, что создавало пробле- мы при его реализации. Первой областью использования языка APL были печатные тер- миналы корпорации IBM. В этих терминалах содержались специальные печатающие ша- ры. обеспечивавшие печать разрозненных символов, требующихся языку. Большое ко- личество операторов языка APL позволяет манипулировать с массивами как со скалярными величинами. Например, транспонирование любой матрицы выполняется с помощью одного оператора. Большой набор операторов обеспечивает значительную вы- разительность. одновременно затрудняя чтение программ, написанных на языке APL. Многие считали, что лучшим использованием языка APL было бы “одноразовое” про- граммирование. Хотя программы можно писать очень быстро, от них пришлось бы из- бавляться после использования, поскольку модифицировать их было бы затруднительно. Язык APL был популярен около 30 лет и на данный момент все еше используется, хо- тя и не очень широко. Более того, на протяжении своей жизни он не претерпел сколько- нибудь существенных изменений. 2.9.2. Происхождение и характеристики языка SNOBOL Язык SNOBOL (произносится как “сноубол ') (Criswold et al., 1971) был разработан в начале 1960-х годов тремя со (рудниками компании Bell Laboratories: Д.Фарбером (D.J.Farber). Р.Грисволдом (R.E.Griswold) и Ф.Поленски (F.Polensky) (Farber at al., 1964). Он был создан специально для обработки текстов. Ядро языка SNOBOL — это набор мощных операций для сопоставления образцов строк. Одним из ранних применений языка SNOBOL было создание текстовых редакторов. Поскольку динамическая природа языка SNOBOL делала его медленнее других языков, то в наше время он нечасто используется с подобной целью. Тем не менее, язык SNOBOL все еще жив и поддерживается языками, используе- мыми для разнообразных задач обработки текстов в многочисленных приложениях. 2.10. Возникновение абстракции данных: язык SIMULA 67 Хотя язык SIMULA 67 так и не получил широкого распространения и слабо повлиял на вычислительную технику и программирование того времени, некоторые из введенных в нем концепций делают его важным. 2.10.1. Процесс разработки Язык SIMULA I был разработан двумя норвежцами. Кристеном Нигаардом (Krysten Nygaard) и Оле-Джоханом Далом (Ole-Johan Dahl), между 1962 и 1964 годами в Норвеж- ском вычислительном центре (Norwegian Computing Center, NCC). В первую очередь их 92 Глава 2. Обзор основных языков программирования
интересовало использование компьютеров для моделирования, хотя они также работали и в области исследования операций. Язык SIMULA I был разработан исключительно для моделирования систем и впервые был реализован в конце 1964 года на компьютере UNIVAC 1107. Сразу после завершения реализации языка SIMULA I Нигаард и Дал начали работ} над расширением языка с целью введения в него абсолютно новых свойств и изменения некоторых уже существующих конструкций для того, чтобы сделать язык полезным для более универсальных приложений. В результате этой работы появился язык SIMULA 67. проект которого был впервые публично представлен в марте 1967 года (Dahl and Nygaard, 1967). Мы ограничимся рассмотрением только языка SIMULA 67. хотя в языке SIMULA 1 также встречается несколько интересных свойств языка SIMULA 67. 2.10.2. Обзор языка Язык SIMULA 67 представляет собой расширенную версию языка ALGOL 60 с его блочной структурой и структурой управляющих операторов. Основным недостатком языка ALGOL 60 (да и других языков того времени) при использовании для моделирования были их подпрограммы. Для моделирования требовались подпрограммы, позволяющие переза- пускать их с места, на котором их выполнение было ранее прервано. Подпрограммы с та- ким типом управления называются сопрограммами, поскольку и вызывающая и вызывае- мая подпрограммы в некотором смысле равноправны, а не состоят в жестких иерархиче- ских отношениях, обычных для императивных языков программирования. Для поддержки сопрограмм в языке SIMULA 67 была разработана конструкция клас- са. Это было важное усовершенствование, поскольку оно положило начало нашим поня- тиям об абстракции данных. Основополагающая идея класса состоит в том. что структу- ра данных и процедуры, обрабатывающие ее. объединяются вместе. Более того, описа- ние класса— это только шаблон структуры данных, и в таком качестве описание класса отличается от его экземпляра, так что программа может создавать и использовать любое количество экземпляров отдельного класса. Экземпляры класса могут содержать локаль- ные данные, и программы, выполняемые во время создания экземпляра, которые могут инициализировать некоторые структуры данных, принадлежащие экземплярам класса. Более подробно классы и экземпляры классов представлены в главе 10. Интересно отметить, что важная концепция абстракции данных не была выявлена и приписана к конструкции класса вплоть до 1972 года, когда Хоар (Ноаге) (1972) распознал эту связь Происхождение языка SIMULA 67 показано на рис. 2.7. о ALGOL 60 (1960) о SIMULA 1(1964) о SIMULA 67 (1967) Рис. 2.7. Генеалогия языка SIMULA 6" 2.10. Возникновение абстракции данных: язык SIMULA 67 93
2.11. Ортогональная структура: язык ALGOL 68 Язык ALGOL 68 был источником нескольких новых идей в области структуры языка, некоторые из которых впоследствии переняли другие языки. Мы рассматриваем этот язык именно по этой причине, несмотря но то, что он так и не получил широкого рас- пространения ни в Европе, ни в Соединенных Штатах. Поскольку некоторые конструк- ции языка ALGOL 68 описаны в дальнейших главах, в этом разделе мы остановимся только на его самом важном вкладе в компьютерные науки. 2.11.1. Процесс разработки Развитие семейства языков ALGOL не закончилось с выходом в 1962 году перерабо- танного отчета, хотя результаты следующего этапа разработки языка были опубликова- ны только через шесть лет. Получившийся в результате язык ALGOL 68 (van Wijngaarden et al., 1969) разительно отличался от своего предшественника. Одним из интереснейших нововведений языка ALGOL 68 является его основной структурный критерий— ортогональность. Вернемся к нашему обсуждению ортого- нальности в главе 1. Использование в языке ALGOL 68 ортогональности приводит к не- скольким новым свойствам, два из которых описаны в следующем разделе. 2.11.2. Обзбр языка Одним из важных результатов ортогональности языка ALGOL 68 было введение оп- ределяемых пользователем типов данных. Ранние языки, например FORTRAN, содержа- ли только несколько основных структур данных. Язык PL/1 обладал большим количест- вом структур данных, что затрудняло его изучение и реализацию, но он, очевидно, не мог обеспечить соответствующую структуру данных для каждой области применения. ALGOL 68 предоставлял несколько элементарных типов и структур и позволял пользо- вателю комбинировать эти основные элементы для создания большого количества различ- ных структур. Возможность определять новые типы данных была с некоторыми уточне- ниями включена во все основные императивные языки программирования, разработанные позднее. Типы данных, определяемые пользователем, полезны из-за того, что они позволя- ют пользователю создавать абстракции данных, которые максимально соответствуют кон- кретным задачам. Все аспекты типизации данных рассматриваются в главе 5. Еще одним новшеством в области типов данных было использование в языке ALGOL 68 динамических массивов, которые в главе 4 описываются с точки зрения ди- намической памяти. Динамический массив— это массив, в котором при объявлении не устанавливаются предельные значения индексов. Распределение памяти, требуемой ди- намическому массиву, происходит при присваивании ему значения. В языке ALGOL 68 динамические массивы называются flex. Например, объявление flex [1:0] int list показывает, что переменная list — это динамический массив целых чисел с одним ин- дексом, нижнее значение которого равно 1, распределение памяти при этом не происхо- дит. Присвоение переменной множества значений list := (3, 5, 6, 2) приводит к выделению переменной list памяти, достаточной для хранения четырех це- лых чисел, при этом значения индексов массива меняют на реальные значения [1:4]. 94 Глава 2. Обзор основных языков программирования
2.11.3. Оценка Язык ALGOL 68 содержит значительное количество новых свойств. Его использование ортогональности, утрированное, по мнению некоторых, было, тем не менее, революцион- ным. Многие свойства, введенные языком ALGOL 68. стали частью последующих языков. Впрочем, язык ALGOL 68 повторил одну из ошибок языка ALGOL 60. что значительно повлияло на отсутствие широкого признания языка. Язык был описан с использованием элегантного и лаконичного, но к тому же неизвестного, метаязыка. Прежде чем приступить к чтению описания языка (van Wijngaarden et al.. 1969). следовало изучить новый метаязык, названный грамматикой Вийнгаардена. Чтобы больше усложнить условия изучения, разра- ботчики языка придумали набор слов для объяснения языка и его грамматики. Например, ключевые слова были названы indicant, извлечение подстроки — trimming, а процесс выполнения процедуры — coercion of deproceduring, ко- торый мог быть meek, firm или каким-либо еще. Естественным будет противопоставить структуры языков PL/1 и ALGOL 68. Удобство написания на языке ALGOL 68 достигалось за счет принципа ортогональности: несколь- ко основных концепций и неограниченное использование механизмов комбинирования. Удобству использования языка PL/1 способствовало большое число фиксированью кон- струкций. Язык ALGOL 68 расширил элегантную простоту' языка ALGOL 60. тогда как язык PL/1 просто объединил свойства нескольких языков. Разумеется, необходимо пом- нить, что целью языка PL/I было создание единого инструмента для широкого класса за- дач; а язык ALGOL 68 был рассчитан только на научные приложения. Язык PL/I был признан более широко, чем язык ALGOL 68. в значительной степени благодаря поддержке со стороны корпорации IBM и проблемам, связанным с понимани- ем и реализацией языка ALGOL 68. Реализация была сложной задачей для обоих языков, но язык PL/1 имел ресурсы корпорации IBM, использованные для создания компилятора, а язык ALGOL 68 подобного покровителя не имел. Происхождение языка ALGOL 68 показано на рис. 2.8. f ALGOL60 (I960) * ALGOL 68 (1968) ' Рис. 2.8. Генеалогия языка ALGOL 68 2.12. Несколько важных наследников семейства языков ALGOL Все императивные языки, в том числе и такие императивно-объектно-ориентированные, как Java и C++, разработанные после 1960 года, чем-то в своей структуре обязаны языку ALGOL 60 и/или ALGOL 68. В этом разделе рассмотрены некоторые из них. В разделе опушено обсуждение языка Ada, которое будет проведено в разделе 2.14, C++, который бу- дет рассмотрен в разделе 2.16, и Java, который описан в разделе 2.17. 2.12. Несколько важных наследников семейства языков ALGOL 95
2.12.1. Преднамеренная простота: язык Pascal 2.12,1,1, Исторические предпосылки Никлаус Вирт (Niklaus Wirth) был членом группы International Federation of Information Processing (1FIP) Working Group 2.1 (рабочая группа 2.1 Международной фе- дерации по обработке информации), которая в середине 1960-х годах была создана для продолжения разработки языка ALGOL. В августе 1965 года Вирт и Хоар внесли свой вклад в эту работу, представив группе довольно скромный план, касающийся дополне- ний и изменений в языке ALGOL 60 (Wirth and Hoare, 1966). Большинство членов груп- пы отклонили его как слишком незначительный. Вместо него была разработана более сложная редакция, которая, в конечном итоге, и стала языком ALGOL 68. Вирт с не- большой группой разработчиков считал, что отчет о языке ALGOL 68 не должен публи- коваться. обосновывая свое мнение как сложностью самого языка программирования, так и сложностью метаязыка, использованного для его описания. Эта точка зрения позже была отчасти подкреплена тем, что документация по языку ALGOL 68, а следовательно, и сам язык, действительно считались в компьютерном мире слишком трудными. Предложенная Виртом и Хоаром версия языка ALGOL 60 была названа языком ALGOL-W. Она была реализована в Стенфордском университете и изначально использо- валась в качестве учебного средства, правда только в небольшом количестве университе- тов. Основными особенностями языка ALGOL-W были метод передачи параметров по значению и результату, а также оператор case для многовариантного ветвления. Метод передачи параметров по значению результата является альтернативой используемому в языке ALGOL 60 методу передачи параметров по имени. Оба метода рассмотрены в гла- ве 8. Оператор case описан в главе 7. Следующим значительным проектом Вирта, также основанным на языке ALGOL 60, был его крупнейший успех — язык Pascal. Исходное описание языка Pascal было опуб- ликовано в 1971 году (Wirth, 1971). В процессе реализации эта версия была несколько 1вменена и описана в 1973 году (Wirth, 1973). Отметим, что свойства, часто приписы- ваемые языку Pascal, фактически произошли от более ранних языков. Например, опреде- ляемые пользователем типы данных впервые были введены в языке ALGOL 68, оператор case— в языке ALGOL-W, а записи в языке Pascal подобны структурированным пере- менным в языках COBOL и PL/I. 2.12.1.2. Оценка Язык Pascal имеет большое влияние на обучение программированию. В 1970 году большинство студентов, изучавших естественные, технические и компьютерные науки, знакомились с программированием с помощью языка FORTRAN, хотя в некоторых уни- верситетах для этой цели использовались язык PL/1, языки, основанные на языке PL/I, и язык ALGOL-W. С середины 1970-х самым широко используемым для обучения языком стал Pascal. Произошедшее было довольно естественно, поскольку язык Pascal специаль- но создавался для обучения программированию, хотя его популярность было невозмож- но предсказать. Поскольку язык Pascal создавался как язык для обучения, в нем отсутствовали некото- рые свойства, необходимые для различных видов приложений. Лучшим примером этого служит отсутствие возможности написания подпрограммы, которая бы принимала в каче- стве параметра массив переменной длины. Другим примером является отсутствие какой бы 96 Глава 2. Обзор основных языков программирования
то ни было раздельной компиляции. Эти различия естественным образом привели к воз- никновению многочисленных нестандартных диалектов, таких как язык Turbo Pascal. Популярность языка Pascal, как в сфере обучения программированию, так и в других областях, главным образом основана на его замечательной комбинации простоты и выра- зительности. Хотя, как мы увидим из последующих глав, для языка Pascal характерна неко- торая ненадежность, по сравнению с другими языками (особенно с языками FORTRAN и С), он все же является относительно надежным языком. С середины 1990-х годов популяр- ность языка Pascal стала падать как в промышленности, так и в университетах. Ниже следует пример программы на языке Pascal. {Пример программы на языке Pascal Ввод: Целое число listlen, меньше 100, за которым следует набор целых чисел в количестве listlen Вывод: Количество введенных величин, которые больше их среднего арифметического} program pasex (input, output); type intlisttype = array [1..99] of integer; var intlist : intlisttype; listlen, counter, sum, average, result : integer; begin result := 0; sum := 0; readln (listlen); if ((listlen > 0) and (listlen < 100)) then begin {Считывание входных величин в массив и вычисление суммы} for counter := 1 to listlen do begin readln (intlist[counter]); sum := sum + intlist[counter] end; {Вычисление среднего арифметического} average := sum / listlen; {Вычисление количества входных величин, которые больше их среднего} for counter := 1 to listlen do if (intlist[counter] > average) then result := result + 1; {Вывод результатов} writein(’Число величин, которые больше их среднего:’, result) end {относится к условию if ((listlen > 0 ...)) then ...} else writein (’Ошибка - введена неверная длина списка’); end. Происхождение языка Pascal представлено на рис. 2.9. 2.12. Несколько важных наследников семейства языков ALGOL 97
ALGOL 60 (1960) ALGOLW(1966)< \ \ ALGOL 68 (1968) ’ Pascal (1971) Puc. 2.9. Генеалогия языка Pascal 2.12.2. Машинно-независимый язык: С Подобно языку Pascal, язык С немногое добавил в ранее известные языковые свойст- ва, но получил очень широкое распространение. Несмотря на то что изначально язык С создавался для системного программирования, он также нашел применение и в большом количестве других областей. 2.12.2.1. Исторические предпосылки Среди предков языка С известны языки CPL, BCPL, В и ALGOL 68. Язык CPL был разработан в Кембриджском университете в начале 1960-х годов. Язык BCPL— простой системный язык, разработанный в 1967 году Мартином Ричардсом (Richards, 1969). Первая работа над операционной системой UNIX была выполнена в конце 1960-х го- дов Кеном Томпсоном (Ken Thompson) из компании Bell Laboratories. Первая ее версия была написана на языке ассемблера. Первым высокоуровневым языком, реализованным под операционной системой UNIX, был язык В, основанный на языке BCPL. Язык В был разработан и реализован Томпсоном в 1970 году. Ни язык BCPL, ни язык В не содержат типов данных, что довольно странно для язы- ков высокого уровня, хотя уровень обоих языков значительно ниже, чем у таких языков, как Pascal. То, что язык не содержит типов данных, означает, что все данные рассматри- ваются как машинные слова, которые, хотя и крайне просты, но приводят ко многим сложностям и ненадежности. Например, существует проблема определения арифметики, в которой должно вычисляться выражение: целочисленной или с плавающей точкой. В одной реализации языка BCPL перед операндами с плавающей точкой указывались точ- ки. Операнды, которым не предшествовали точки, рассматривались как целочисленные. Альтернативным вариантом могло бы быть использование различных символов для опе- раций с плавающей точкой. Эта проблема, а также несколько других, привели к разработке на основе языка В но- вого языка, предусматривавшего типы данных. Изначально названный NB, позже пере- именованный в С, этот новый язык был разработан и реализован в 1972 году Деннисом Ритчи (Dennis Ritchie) из компании Bell Laboratories (Kemighan and Ritchie, 1978). Язык ALGOL 68 оказал на язык С значительное влияние, как через язык BCPL, так и непо- средственно. Это можно заметить по его операторам for и switch, операторам при- сваивания и его обращению с указателями. 98 Глава 2. Обзор основных языков программирования
Единственным “стандартом’' для языка С в первые 15 лет его существования была книга Кернигана и Ритчи (1978). Со временем язык медленно эволюционировал при участии раз- личных разработчиков, вводящих дополнительные свойства. В 1989 году институт ANSI выпустил официальное описание языка С (ANSI, 1989), в котором содержалось большое количество свойств, уже включенных в язык разработчиками средств его реализации. В середине и конце 1980-х годов была разработана новая версия языка С, названная языком C++ (Ellis and Stroustrup, 1990). Его история и некоторые из наиболее значитель- ных свойств описаны в разделе 2.16. Подробнее о поддержке языком C++ абстракции данных написано в главе 10. Поддержка языком C++ объектно-ориентированного про- граммирования рассмотрена в главе 11. 2.12.2.2. Оценка Язык С обладает достаточным количеством управляющих операторов и возможно- стей структуризации данных, чтобы он мог использоваться в различных прикладных об- ластях. Кроме того, он содержит богатый набор операторов, обеспечивающих высокую степень выразительности. Одной из важнейших причин и любви, и неприязни к языку С является отсутствие полной проверки типов. Например, можно написать функции, для параметров которых не производится проверка типов. Люди, которым язык С нравится, высоко ценят его гиб- кость; те же, кто его недолюбливает, находят его слишком ненадежным. Главной причи- ной значительного роста его популярности в 1980-х годах является то, что он представ- ляет собой часть широко используемой операционной системы UNIX. Эта привязка к системе UNIX обеспечивает недорогой (часто просто бесплатный вместе с системой UNIX) и достаточно хороший компилятор, доступный программистам на большом коли- честве типов компьютеров. Ниже следует пример программы на языке С. /* Пример программы на языке С Ввод: Целое число listlen, меньше 100, за которым следует набор целых чисел в количестве listlen Вывод: Количество введенных величин, которые больше их среднего арифметического */ void main () { int intlist[98], listlen, counter, sum, average, result; sum = 0; result = 0; scant ("%d", &listlen); if ((listlen > 0) && (listlen < 100)) { /* Считывание входных величин в массив и вычисление суммы */ for (counter = 0; counter < listlen; counter++) { scant( "%d”, &intlist[counter]); sum = sum + intlist[counter]; ) /* Вычисление среднего арифметического */ average = sum / listlen; /* Вычисление количества входных величин, которые больше их среднего */ for (counter = 0; counter < listlen; counter++) if (intlist[counter] > average) result++; 2.12. Несколько важных наследников семейства языков ALGOL 99
/* Вывод результатов */ printf("Число величин, которые больше их среднего:%d\n", result); } else printf ("Ошибка - введена неверная длина списка\п"); ) Происхождение языка С представлено на рис. 2.10. 2.12.3. Другие потомки языка ALGOL В этом разделе кратко описываются истоки и характеристики других потомков языка ALGOL, которые будут упоминаться далее в книге. 2.12.3.1. Язык Modula-2 После языка Pascal Никлаус Вирт разработал язык Modula, появившийся в результате его экспериментов с параллельностью (Wirth, 1976). Для языка Modula никогда не вы- пускался компилятор, и его разработка была прекращена вскоре после его публикации. Тем не менее, сам Вирт от разработки языка не отказался. Он сместил акцент на созда- ние языка, который должен был стать единым языком новой компьютерной системы, позже названной Lilith. В то время как компьютер сам по себе коммерческого успеха не имел, этот успех имел язык Modula-2 (Wirth, 1985). 100 Глава 2. Обзор основных языков программирования
Основными характерными свойствами языка Modula-2, структура которого основы- валась на языках Pascal и Modula. были модули, обеспечивающие поддержку абстракт- ных типов данных, использование процедур как типов, низкоуровневые средства сис- темного программирования и сопрограммы, а также некоторые синтаксические свойства, которые были улучшены по сравнению с соответствующими свойствами языка Pascal. Язык Modula-2 стал широко использоваться в конце 1980-х годах и в начале 1990-х годах в качестве учебного языка в университетах. Также он использовался, по крайней мере несколько лет, в различных промышленных отраслях. 2.12.3.2. Язык Modula-З Язык Modula-З был совместно разработан Исследовательским центром корпорации Digital Equipment (Systems Research Center of Digital Equipment Corporation) в Пало-Альто и Исследовательским центром компании Olivetti (Olivetti Research Center) в Менло-Парк в конце 1980-х годах (Cardelli et al., 1989). Он основан на языках Modula-2, Mesa (Mitchell et а)., 1979), Cedar (Lampson, 1983) и Modula-2+ (Rovner, 1986). К языку Modula-2 были добавлены классы и объекты для поддержки объектно-ориентированного программирования, обработка исключительных ситуаций, “сборка мусора” и параллель- ность. Несмотря на то что это мощный и хорошо сконструированный язык, надежды на его широкое распространение довольно мало. Хотя этот язык и финансируется компани- ей Digital, пользовательская база языка Modula-2, на которой мог бы начать расти язык Modula-З, уже практически исчезла. 2.12.3.3. Язык Oberon Язык Oberon, частично основанный на языке Modula-2, является последним языком, созданным Никлаусом Виртом. Последняя версия языка Oberon названа Oberon-2 (M6ssenbock. 1993). Страстное увлечение Вирта простотой в языках программирования ошеломляюще откровенно проявилось в проекте языка Oberon. Хотя с целью получения языка Oberon к языку Modula-2 были добавлены некоторые свойства, значительно большее количество свойств было удалено, что сделало язык Oberon абсолютной противоположностью язы- ков C++ и Ada 95, если говорить об этом с точки зрения сложности и размера. Основным свойством, добавленным к языку Modula-2, было расширение типов, под- держивающих объектно-ориентированное программирование. В числе удаленных свойств находятся: вариантные записи, непрозрачные типы, перечислимые типы, подти- пы, тип CARDINAL, нецелочисленные индексы массивов, оператор with, оператор for. Поразительно, но язык Oberon значительно меньше и проще своего предшественника. 2.12.3.4. Язык Delphi Язык Delphi, подобно языку C++, является смешанным языком, созданным путем введения в существующий императивный язык, помимо других свойств, объектно- ориентированной поддержки. Язык Delphi происходит от языка Pascal. Большое количе- ство различий между языками C++ и Delphi является результатом различий между поро- дившими их языками и сопутствующими им культурами программирования. Поскольку язык С мощный, но потенциально ненадежный, такое же определение применимо и к языку C++, по крайней мере это касается проверки диапазона изменения индексов мас- сива, арифметики указателей и многочисленных приведений типов. Аналогично тому, как язык Pascal элегантнее и надежнее языка С, язык Delphi элегантнее и надежнее языка 2.12. Несколько важных наследников семейства языков ALGOL 101
С-+. Язык Delphi также менее сложен, чем язык C++. Например, Delphi не позволяет ис- пользовать определяемою пользователем перегрузку операторов, настариваемые подпро- граммы и параметризованные классы, в то время как все это являются частью языка C++. Язык Delphi, подобно языку Visual C++, предоставляет программистам графический интерфейс пользователя (GUI — Graphical User Interface) и простые способы создания GUI-интерфейсов для приложений, написанных на языке Delphi. Язык Delphi был разра- ботан, финансировался и продавался компанией Borland, той же компанией, что создала язык Turbo Pascal. 2.13. Программирование, основанное на логике: язык Prolog Говоря просто, логическое программирование— это использование формальной ло- гической записи для сообщения компьютеру вычислительных процессов. В современных языках логического программирования в качестве формы записи используется исчисле- ние предикатов. Программирование на языках логического программирования является непроцедур- ным. Программы на таких языках не устанавливают точно, как должен вычисляться ре- зультат, а только описывают его форму. Для этого языкам логического программирова- ния необходимы четкие средства предоставления компьютеру как соответствующей ин- формации, так и процесса логического вывода. Исчисление предикатов обеспечивает компьютер основной формой передачи информации, а метод доказательства, названный резолюцией и изначально разработанный Робинсоном (Robinson, 1965), предоставляет способы логического вывода. 2.13.1. Процесс разработки В начале 1970-х годов Ален Колмерье (Alain Colmerauer) и Филипп Руссель (Phillippe Roussel) из группы по разработке искусственного интеллекта (Artificial Intelligence Group) Марсельского университета (University of Aix-Marseille) вместе с Робертом Ко- вальски из отдела искусственного интеллекта (Department of Artificial Intelligence) Эдин бургского университета (University of Edinburgh) разработали базовую структуру язык< Prolog. Основными компонентами языка Prolog были метод задания высказываний ис- числения предикатов и реализация ограниченной формы резолюции. Исчисление преди катов и резолюция описываются в главе 15. Первый интерпретатор языка Prolog бы; разработан в 1972 году в Марселе. Реализованная версия языка была описана Русселем (Roussel, 1975). Само название языка произошло от сокращения словосочетания “programming /ogic”. 2.13.2. Обзор языка Программы языка Prolog состоят из набора утверждений. Видов утверждений в языке Prolog немного, правда, все они могут оказаться сложными. Одним из обычных применений языка Prolog является его использование в качестве базы данных со встроенной логикой. Эта область применения предоставляет простую основу для обсуждения языка Prolog. 102 Глава 2. Обзор основных языков программирования
База данных программы языка Prolog состоит из двух типов утверждений: фактов и правил. Примерами фактов могут служить следующие выражения: mother (joanne, jake). father (vern, joanne) . Они утверждают, что объект joanne приходится матерью (mother) объекту jake, а объект vern — отцом (father) объекту' j oanne. Рассмотрим теперь пример правила: grandparent(X, Z) parent(X, Y}t parent (Y, Z) . Оно утверждает, что для конкретных значений переменных X, Y и Z из факта, что объект X является родителем объекта Y. а объект Y — родителем объекта Z, можно сде- лать вывод о том, что объект X приходится объекту' Z дедушкой или бабушкой (grandparent). База данных языка Prolog может интерактивно запрашиваться целевыми утвержде- ниями, например: father (bob, darcie). В нем спрашивается, является ли объект bob отцом (father) объекта darcie. При получении такого запроса (или цели) система Prolog использует резолюцию, чтобы опреде- лить истинность данного утверждения. Если система сможет заключить, что утверждение . верно, она выдаст ответ “истина”. В противном случае, будет выдан ответ “ложь”. 2.13.3. Оценка Существует относительно небольшая группа специалистов по компьютерным наукам, которые верят, что логическое программирование представляет собой наилучший способ избежать применения императивных языков программирования, а также решить огром- ную проблему производства большого количества надежного программного обеспече- ния, необходимого на сегодняшний день. До настоящего времени, правда, существуют две основные причины, почему логическое программирование не становится широко ис- пользуемым. Во-первых, доказано, что, как и другие неимперативные подходы, логиче- ское программирование крайне неэффективно. Во-вторых, оно зарекомендовало себя в качестве эффективного метода только в нескольких относительно небольших приклад- ных областях: некоторых видах систем управления базами данных и некоторых областях искусственного интеллекта. Логическое программирование и язык Prolog подробно рассмотрены в главе 15. 2.14. Величайший проект в истории: язык Ada Язык Ada— результат самого обширного и самого дорогостоящего из когда-либо предпринимавшихся проектов по.созданию языка программирования. Язык Ada разраба- тывался для Министерства обороны США, так что состояние существовавшей вычисли- тельной среды способствовало определению его формы. 2.14. Величайший проект в истории: язык Ada 103
2.14.1. Историческая ретроспектива к 1974 году около половины областей применения компьютеров в Министерстве обороны приходилось на встроенные системы. Последний термин означает, что аппарат- ное обеспечение такой системы встраивалось в устройство, управляемое или обслужи- ваемое этой системой. При этом быстро росла стоимость программного обеспечения, в основном из-за увеличения сложности систем. В проектах Министерства обороны ис- пользовались более 450 различных языков программирования, ни один из которых Ми- нистерством стандартизован не был. Каждый подрядчик мог определить новый, отли- чающийся от предыдущих, язык программирования. Из-за такого количества языков по- вторное использование прикладного программного обеспечения было очень редким. Более того, не было инструментальных средств разработки программного обеспечения (поскольку они обычно зависели от конкретного языка) Большинство языков не подхо- дили для использования в области встроенных систем. Поэтому в 1974 году армия, флот и ВВС независимо друг от друга предложили разработать язык высокого уровня для встроенных систем. 2.14.2. Процесс разработки Обратив внимание на это широко распространенное мнение, директор Отдела иссле- дований и проектирования Министерства обороны (Defense Research and Engineering) Малкольм Кури (Malcolm Currie) в январе 1975 года сформировал группу HOLWG (High- Order Language Working Group— группа по разработке первоочередного языка), кото- рою изначально возглавил подполковник ВВС Уильям Уайтекер (William Whitaker). В группе HOLWG имелись представители всех военных служб и служб связи с Великобри- танией, Францией и Западной Германией. Исходным заданием группы было следующее. Определить требования к новому высокоуровневому языку, заказанному Мини- стерством обороны. Оценить существующие языки и определить их жизнеспособность. Предложить принятие или реализацию минимального набора языков программи- рования. В апреле 1975 года группа выпустила список требований к новому языку, названный Strawman document (Department of Defense, 1975a). (Здесь и далее фамилии в названиях документов отмечают этапы реализации плана создания нового языка программирова- ния: Strawman— “соломенный человек”, Woodman— “деревянный”, Tinman — “оловянный”, Ironman — “железный”, Steelman — “стальной” и Stoneman — “каменный". — Прим. перев.) Этот документ был распространен по родам войск, феде- ральным агентствам, избранным промышленным и образовательным структурам и заин- тересованным сторонам в Европе. Вслед за указанным документом в августе 1975 года последовал Woodman document (Department of Defense, 1975b), а в январе 1976 года— Tinman document (Department of Defense, 1976). В последнем рассматривался полный набор требований к языку, а также необходимые его характеристики. Основным автором всех этих документов был Дэвид Фишер (David Fisher) из Института анализа обороноспособности (Institute for Defense Analysis). Группа, принимавшая участие в проекте, была значительной и насчитывала более 200 человек, представляющих более 40 организаций помимо Министерства оборо- 104 Глава 2. Обзор основных языков программирования
ны. В январе 1977 года требования документа Tinman document были заменены требова- ниями документа Ironman requirements document (Department of Defense. 1977). который имел практически идентичное содержание, но несколько отличался форматом. В апреле 1977 года в качестве основы для неограниченных условий подряда был ис- пользован документ Ironman document, затем этот документ был предан огласке, сделав, таким образом, язык Ada первым языком, созданным на основе конкурсного контракта. В июне 1977 года четверо из заявленных подрядчиков— компании Softech. SRI International. Cii Honeywell/Bull и Intermetrics — были выбраны для перехода, независимо и параллельно, к фазе 1 разработки языка. Все четыре из предложенных в результате проектов были основаны на языке Pascal. Когда в феврале 1978 года шестимесячная фаза 1 была завершена, ее результаты оце- нили 400 добровольцев из 80 экспертных команд, разбросанных по всему миру. В ре- зультате этой оценки два финалиста— компании Intermetrics и Cii Honeywell/Bull — бы- ли выбраны для продолжения разработки в фазе 2. В июне 1978 года была выпущена следующая итерация документа требований — Steelman document (Department of Defense. 1978). В конце фазы 2 была выполнена еще одна двухмесячная опенка и в мае 1979 года по- бедителем была признана структура языка, предложенная компанией Cii Honeywell'Bull. Любопытно отметить, что победителем оказался единственный иностранный подрядчик из четырех финалистов. Руководителем французской команды разработчиков Cii Honeywell/Bull была тогда Джин Ичбиа (Jean Ichbiah). Весной 1979 года Джек Купер (Jack Cooper) из Командования материальным обеспе- чением ВМФ США (Navy Materiel Command) предложил для нового языка название Ada. которое позднее и было принято. Августа Ада Байрон (1815-1851). графиня Лавлейс (Augusta Ada Bayron. countess of Lovelace), математик и дочь поэта лорда Байрона, обще- признанна в качестве первого программиста в мировой истории. Она работала с Чарль- зом Бэббиджем на его механических компьютерах, названных “Разностными и аналити- ческими машинами" (Difference and Analytical Engines), создавая программы для отдель- ных вычислительных алгоритмов. Фаза 3 разработки языка Ada началась после выбора проекта-победителя. Его струк- тура и логическое обоснование были опубликованы ассоциацией АСМ в сообщении "SIG PLAN Notices" (ACM, 1979) и распространены для изучения более чем 10 000 чи- тателям. Открытое испытание и конференция по оценке языка состоялись в октябре 1979 года в Бостоне при участии более 100 организаций из Соединенных Штатов и Европы. К ноябрю из 15 стран было получено более 500 сообщений, касавшихся нового языка. Среди этих сообщений гораздо чаше встречались предложения о незначительных изме- нениях, чем о радикальной переработке или полном неприятии языка. Основываясь на этих сообщениях, в феврале 1980 года была выпущена следующая версия требований к спецификации — документ Stoneman document (Department of Defense. 1980a). Пересмотренная и исправленная версия структуры языка была завершена к июлю 1980 года и принята как M1L-STD 1815, стандартное справочное руководство по языку Ada (“Ada Language Reference Manual”). Число 1815 было выбрано как год рождения Августы Ады Лавлейс. Другая переработанная версия справочника “Ada Language Reference Manual" вышла в свет в июле 1982 года. В 1983 году институт ANSI стандар- тизировал язык Ada. “Окончательная" официальная версия языка вышла в 1983 году (Goos and Hartmanis, 1983). После этого разработка языка Ada была заморожена по край- ней мере на 5 лет. 2.14. Величайший проект в истории: язык Ada 105
2.14.3. Обзор языка В данном разделе кратко рассматриваются четыре основных свойства языка Ada. По- скольку этот язык используется во многих примерах далее в книге, то остальные свойст- ва мы будем описывать по ходу дела. Пакеты языка Ada представляют собой средство выделения в отдельные модули объ- ектов данных, спецификации типов данных и процедур, что, в свою очередь, обеспечива- ет поддержку использования абстракции данных в структуре программы, как это описа- но в главе 10. Язык Ada содержит обширные средства обработки исключительных ситуаций, что позволяет программисту получать управление после обнаружения одной из многочис- ленных исключительных ситуаций или ошибок времени выполнения программы. Под- робнее об обработке исключительных ситуаций рассказывается в главе 13. Блоки программ в языке Ada могут быть настраиваемыми. Например, можно напи- сать процедуру сортировки, использующую неуточненный тип сортируемых данных. До использования такой настраиваемой процедуры ей должно быть приписано значение оп- ределенного типа. Для этого оператор вызывает создание компилятором разновидности процедуры, использующей данный тип. Наличие таких настраиваемых компонентов уве- личивает количество блоков программ, которые могут быть использованы повторно, не требуя дублирования со стороны программистов. Настраиваемые блоки рассмазривают- ся в главах 8 и 10. Язык Ada также обеспечивает параллельное выполнение особых программных бло- ков, названных заданиями, используя для этого механизм рандеву. Последнее название обозначает метод взаимодействия и синхронизации параллельных процессов в языке Ada. Параллельность подробно рассмотрена в главе 12. 2.14.4. Оценка Важнейшими аспектами разработки языка Ada считаются следующие. Поскольку разработка языка проводилась на конкурентной основе, не было огра- ничено количество участников. Язык Ada воплощает большинство концепций разработки программного обеспе- чения и разработки языков программирования, существовавших на конец 1970-х годов. Хотя можно подвергать сомнению фактические методы, использованные для внедрения этих свойств, а также разумность включения такого большого ко- личества свойств, большинство согласится, что эти свойства полезны. Разработка компилятора для языка Ada была сложнейшей задачей, несмотря на то, что большинство людей не догадываются об этом. Только в 1985 году, почти че- рез четыре года после завершения разработки языка, начали появляться действи- тельно практичные компиляторы языка Ada. Наиболее серьезной критике в первые годы существования языка Ada подвергались его объемность и сложность. В частности Хоар утверждал, что этот язык не должен ис- пользоваться в областях, где основным требованием является надежность (Ноаге, 1981), хотя для применения именно в подобных областях язык и создавался. Однако другие люди превозносили его как вершину достижений в области разработки языков програм- мирования. 106 Глава 2. Обзор основных языков программирования
Ниже следует пример программы на языке Ada. — Пример программы на языке Ada - - Ввод: Целое число listlen, меньше 100, за которым следует набор целых чисел в количестве listlen — Вывод: Количество введенных величин, которые больше их среднего арифметического with TEXT-IO; use TEXT_IO; procedure ADA_EX is package INT_IO is new INTEGER_IO (INTEGER); use INT-IO type INT_LIST_TYPE is array (1..99) of INTEGER; INT_LIST : INT-LIST-TYPE; LIST__LEN, SUM, AVERAGE, RESULT : INTEGER; begin RESULT := 0; SUM := 0; GET (LIST—LEN); if (LIST—LEN > 0) and (LIST_LEN < 100) then - - Считывание входных величин в массив и вычисление суммы for COUNTER := 1 .. LIST_LEN loop GET (INT—LIST(COUNTER)); SUM := SUM + INT_LIST(COUNTER); end loop; — Вычисление среднего арифметического AVERAGE := SUM / LIST_LEN; — Вычисление количества входных величин, которые больше их - - среднего for COUNTER := 1 .. LIST_LEN loop if INT—LIST(COUNTER) > AVERAGE then RESULT := RESULT + 1; end if; end loop; - - Вывод результатов PUT (’’Число величин, которые больше их среднего, равно”); PUT (RESULT); NEW-LINE; else PIT—LINE (’’Ошибка — введена неверная длина списка”); end if; end ADA—EX; 2Л4.5. Язык Ada 95 Работа по исправлению языка Ada началась в 1988 году с создания учреждением Ada Joint Program Office проекта Ada 9X. Проект Ada 9X имел три фазы: определение требо- ваний к переработанному языку, реальная разработка описания переработанного языка и переход к его использованию. Все требования были опубликованы Министерством обо- роны (Department of Defense, 1990). 2.14. Величайший проект в истории: язык Ada 107
Требования к обновленному языку7 сосредоточились на четырех областях: создание интерфейсов (особенно графических интерфейсов пользователя), поддержка объектно- ориентированного программирования, более гибкие библиотеки, а также усовершенст- вование механизмов управления совместно используемыми данными. Два из важнейших новых свойств исправленной версии языка Ada. названной Ada 95. кратко описаны в следующих абзацах. Вообще, в остальной части книги мы будем ис- пользовать название Ada 83 для обозначения исходной версии языка, а название Ada 95 (действительно употреблявшееся)— для его более поздней версии. При обсуждении свойств, общих для обоих языков, мы будем использовать название Ada. Стандарт языка Ada 95 был определен в справочном руководстве AARM (1995). Существующий в языке Ada 83 механизм образования типов в языке Ada 95 был рас- ширен. что позволяет создавать новые компоненты, образованные из меченых базовых ти- пов. Эго обеспечивало наследование— ключевую составную часть объектно-ориентиро- ванных языков программирования. Динамическое связывание вызовов подпрограмм с их определениями выполнялось с помощью координации, которая основывалась на значении признака производных типов посредством межклассовых типов. Это свойство обеспечива- ет полиморфизм, еще одно основное свойство объектно-ориентированного программиро- вания. Все указанные свойства языка Ada 95 рассмотрены в главе 11. Механизм рандеву языка Ada 83 обеспечивал только неуклюжие средства распреде- ления данных между параллельными процессами. Для управления доступом к совместно используемым данным следовало ввести новое задание. Защищенные объекты языка Ada 95 предложили этому привлекательную альтернативу. Совместно используемые данные выделялись в синтаксическую структуру, управляющую всем доступом к данным с помошью рандеву или вызова подпрограммы. Новые свойства языка Ada 95 — парал- лельность и совместное использование данных — подробно рассмотрены в главе 12. Широко распространено мнение, что популярность языка Ada 95 будет падать, по- скольку его использование в военных системах программного обеспечения Министерст- ву обороны США уже не требуется. Хотя, разумеется, существуют и другие причины, препятствующие росту популярности языка Ada. Происхождение языка Ada представлено на рис. 2.11. f Pascal (1971) Ada 83 (1983) Ada 95 (1995) Рис. 2.11. Генеалогия языка Ada 108 Глава 2. Обзор основных языков программирования
2.15. Объектно-ориентированное программирование: язык Smalltalk Как уже обсуждалось в главе 1, объектно-ориентированное программирование со- держит в качестве одной из трех основных характеристик абстракцию данных, осталь- ными двумя являются наследование и динамическое связывание. В ограниченной форме наследование появилось в языке SIMULA 67, в котором мож- но было определять иерархию классов. Наследование обеспечивало эффективный метод повторного использования программ. Существующая в объектно-ориентированном программировании концепция управле- ния модулями приближенно выражается идеей, происходящей от языка SIMULA 67 и за- ключающейся в том, что программы моделируют реальный мир. Поскольку большая часть реального мира населена объектами, моделирование такого мира должно включать в себя моделирование объектов. По сути, язык, основанный на концепциях моделирова- ния реального мира, нуждается только во введении в него модели объектов, которые мо- гут отправлять и получать сообщения, а также реагировать на получаемые сообщения. Объектно-ориентированное программирование решает задачи с помощью идентифи- кации объектов, принадлежащих реальному миру, и действий, которые необходимо вы- полнить над этими объектами, с последующим моделированием этих объектов, их про- цессов и требуемых связей между объектами. Абстрактные типы данных, динамическое связывание и наследование— вот концепции, которые делают решение объектно- ориентированной задачи не только возможным, но также удобным и эффективным. 2.15.1. Процесс разработки Концепции, приведшие к разработке языка Smalltalk, были впервые сформулированы в докторской диссертации Алана Кэя (Alan Kay), выполненной в конце 1960-х годов в Университете штата Юта (University of Utah) (Kay, 1969). Кэй на удивление точно пред- видел будущую доступность мощных настольных компьютеров. Напомним, что первые микрокомпьютерные системы поступили в продажу лишь в середине 1970-х годов и имели очень отдаленное отношение к машинам, представляемым Каем, которые должны были выполнять миллион или более операций в секунду' и содержать несколько мегабай- тов памяти. Подобные машины в виде рабочих станций стали широко доступны только в начале 1980-х годов. Кэй полагал, что настольные компьютеры смогут использоваться непрограммистами, и, следовательно, им понадобятся очень мощные средства интерфейса. Компьютеры конца 1960-х годов были в основном пакетно-ориентированными и использовались ис- ключительно профессиональными программистами и учеными. Для использования не- программистами, как установил Кэй, компьютер должен иметь высокую степень инте- рактивности и использовать изощренную графику в пользовательском интерфейсе. Не- которые графические концепции были результатом работы Сеймура Пейперта (Seymour Papert) с языком LOGO, в котором графика помогала детям использовать компьютеры (Papert, 1980). Изначально Кэй представлял себе систему, названную им Dynabook, которая должна была стать универсальным информационным процессором. Частично эта система осно- вывалась на языке Flex, который Кэй помогал разрабатывать. Сам язык Flex базировался главным образом на языке SIMULA 67. Система Dynabook была основана на парадигме 2.15. Объектно-ориентированное программирование: язык Smalltalk 109
обычного стола, на котором лежит много бумаг, часть из которых перекрывается. В фо- кусе внимания всегда находится верхний лист бумаги, тогда как остальные временно не видны. Эту картину дисплей системы Dynabook должен был моделировать с помощью окна на экране. Пользователь взаимодействовал бы с подобным дисплеем посредством клавиатуры и прикосновений пальцами к экрану. После того как эскизный проект систе- мы Dynabook принес ему докторскую степень, целью Кэя стало желание увидеть подоб- ную машину в действии. Это желание привело Кэя в Исследовательский центр компании Xerox в Пало-Альто (Xerox Palo Alto Research Center, Xerox PARC), где он изложил свою идею о системе Dynabook. В результате его приняли на работу в этот центр, и впоследствии возникла на- учно-исследовательская группа Learning Research Group at Xerox. Первой задачей группы стала разработка языка, поддерживающего программную парадигму Кэя, а также ее реа- лизация на лучшем из доступных тогда персональных компьютеров. В результате этой работы появилась система “Interim" Dynabook, состоящая из аппаратного обеспечения Xerox Alto и программного обеспечения Smalltalk-72. Вместе они образовали исследова- тельский инструмент для дальнейших разработок. С этой системой было проведено большое количество исследовательских проектов, в том числе и несколько эксперимен- тов по обучению детей программированию. Параллельно с этими экспериментами шли дальнейшие разработки, приведшие к возникновению ряда языков, последним из кото- рых был рассматриваемый в данной книге язык Smalltalk-80. С развитием языка росла и мощь аппаратного обеспечения, на котором он реализовывался. К 1980 году и язык, и аппаратное обеспечение компании Xerox практически соответствовали представлениям Алана Кэя. 2.15.2. Обзор языка Программными модулями языка Smalltalk являются объекты. Объекты — это струк- туры, инкапсулирующие локальные данные и набор операций, называемых методами, которые доступны другим объектам. Метод определяет реакцию объекта на определен- ное сообщение, соответствующее данному методу. Мир языка Smalltalk заселен только объектами, в число которых входит все: от целочисленных констант до больших слож- ных систем программного обеспечения. Все вычисления на языке Smalltalk выполняются одним и тем же универсальным спо- собом: объекту отправляется сообщение, вызывающее один из его методов. Ответом на сообщение является объект, возвращающий требуемую информацию или просто инфор- мирующий отправителя о завершении запрошенной обработки. Главное различие между сообщением и подпрограммой заключается в том, что сообщение посылается объекту данных, который затем обрабатывается с помощью программного кода, связанного с этим объектом, в то время как вызов подпрограммы, как правило, посылает данные на обработку в саму подпрограмму. С точки зрения моделирования, которое никогда не отделялось от языка Smalltalk, этот язык является моделью совокупности компьютеров (объектов), которые сообщают- ся друг с другом (с помощью сообщений). Каждый объект представляет собой абстрак- цию компьютера, т.е. он хранит данные и предоставляет возможности обработки для ма- нипулирования этими данными. Кроме того, объекты могут посылать и получать сооб- щения. По существу, это и есть основные возможности компьютеров: хранить, обрабатывать данные и обмениваться ими друг с другом. 110 Глава 2. Обзор основных языков программирования
В языке Smalltalk абстракциями объектов являются классы, которые очень похожи на классы языка SIMULA 67. Могут создаваться экземпляры класса, которые затем ставятся объектами программы. Каждый объект имеет собственные локальные данные и пред- ставляет собой отдельный экземпляр своего класса. Различаются объекты одного класса состоянием своих локальных переменных. Как и в языке SIMULA 67. в языке Smalltalk может быть создана иерархия классов. Подклассы данного класса являются его уточнением, наследуя функциональные возмож- ности и локальные переменные родительского класса, или суперкласса. Подклассы могут выделять новую локальную память, добавлять новые функциональные возможности, а также изменять и скрывать унаследованные. Как кратко описывалось в главе 1, язык Smalltalk— это не только язык, но еще и полная среда разработки программного обеспечения. Интерфейс данной среды в значи- тельной степени графический, что затрудняет использование многочисленных перекры- вающихся окон, всплывающих меню и устройств ввода типа “мышь”. 2.15.3. Оценка Язык Smalltalk выполнил великую задачу, активизировав два отдельных аспекта про- граммирования. Во-первых, сисГемы с оконным управлением, являющиеся сейчас ос- новным методом связи с пользователем, произошли от системы Smalltalk. Во-вторых, в наши дни основные методологии разработки программного обеспечения и языки про- граммирования являются объектно-ориентированными. Хотя некоторые идеи объектно- ориентированных языков программирования происходят от языка SIMULA 67, развиты они были только в языке Smalltalk. Очевидно, что язык Smalltalk оказал на компьютер- ный мир большое и долговременное влияние. Ниже приводится пример описания класса в языке Smalltalk. ’’Пример программы на языке Smalltalk” "Ниже следует описание класса, реализация которого может на- рисовать равносторонний многоугольник с любым количеством сторон" class name Polygon superclass Object instance variable names ourPen numSides sideLength "Методы класса" "Создать экземпляр" new А super new getPen "Взять перо для рисования многоугольника" getPen ourPen <- Pen new defaultNib: 2 "Методы экземпляра" "Нарисовать многоугольник" draw numSides timeRepeat: [ourPen go: sideLength; turn: 360 // numSides] 2.15. Объектно-ориентированное программирование: язык Smalltalk 111
"Установить длину сторон" length: len sideLength <- len "Установить число сторон" sides: num numSides <- num Происхождение языка Smalltalk представлено на рис. 2.12. О ALGOL 58 (1958) о ALGOL 60 (1960) и SIMULA 1(1964) о SIMULA 67 (1967) о Smalltalk-80 (1980) Рис. 2.12. Генеалогия языка Smalltalk 2.16. Объединение императивных и объектно- ориентированных свойств: язык C++ Происхождение языка С рассматривалось в разделе 2.12; происхождение языка Smalltalk — в разделе 2.15. Язык C++ представляет собой надстройку над языком С, под- держивающую большинство возможностей, открытых языком Smalltalk. Чтобы улуч- шить императивные свойства языка С и приобрести поддержку объектно-ориентирован- ного программирования, язык C++ прошел через ряд последовательных модификаций. 2.16.1. Процесс разработки Первый шаг на пути от языка С к языку C++ был сделан в 1980 году Бьярни Страустру- пом (Bjame Stroustrup) из компании Bell Laboratories. Изменения заключались во введении проверки и преобразования типов параметров функций и, что представляется более важ- ным. классов, родственных аналогичным структурам языков SIMULA 67 и Smalltalk. По- мимо этого в язык были включены производные классы, управление открытым/закрытым доступом к унаследованным компонентам, конструктор и деструктор, а также дружествен- ные классы. В течение 1981 года были добавлены inline-подставляемые функции, парамет- ры по умолчанию и перегрузка операторов присваивания. Полученный язык был назван языком С with Classes, а его описание выполнено Страуструпом (Stroustrup, 1983). Полезно рассмотреть несколько целей языка С with Classes. Основной его целью бы- ло — предоставить язык, на котором программы могли бы формироваться тем же обра- 112 Глава 2. Обзор основных языков программирования
зом, что и в языке SIMULA 67; т.е. с классами и наследованием. Кроме того, важно было не потерять производительность по сравнению с языком С. Таким образом, язык С with Classes мог использоваться для тех же приложений, что и язык С, при этом были сохра- нены практически все ненадежные свойства языка С. Например, введение проверки диа- пазона изменения индексов массива даже не рассматривалось, поскольку это привело бы к значительному снижению производительности по сравнению с языком С. К 1984 году этот язык был расширен за счет включения виртуальных функций, обес- печивающих динамическое связывание вызовов функций с их описаниями; перегрузки операторов и имен функций; а также ссылочных типов данных. Эта версия языка, на- званная C++, была описана Страуструпом (Stroustrup, 1984). В 1985 году появилась первая доступная реализация языка С4-^ — система Cfront. которая транслировала программы на языке C++ в программы на языке С. Эта версия системы Cfront вместе с реализованной на ее основе версией языка С*+ получила общее название Release 1.0. Описание полученной системы было выполнено Страуструпом (Stroustrup, 1986). Между 1985 и 1989 годами язык C++ продолжал эволюционировать, ориентируясь главным образом на реакцию пользователей его первой распространенной реализации. Следующая версия была названа Release 2.0. Ее реализация с помощью системы Cfront была выполнена в июне 1989 года. В язык была добавлена поддержка множественного наследования (классы, имеющие несколько родительских классов) и абстрактных классов, и внесены другие изменения. Абстрактные классы рассматриваются в главе 11. Версия Release 3.0 была выпущена в конце 1980-х годов. Она содержала специфика- торы шаблонов, предоставлявшие собой параметризованные типы, и обработку исклю- чительных ситуаций. Версии Release 2.0 и Release 3.0 были описаны Эллисом и Страуст- рупом (Ellis and Stroustrup, 1990). 2.16.2. Обзор языка Для поддержки объектно-ориентированного программирования необходим механизм классов/объектов, происходящий от языка SIMULA 67 и являющийся основным свойст- вом языка Smalltalk. Язык C++ предоставлял пользователям набор встроенных классов, а также возможность самостоятельно определять классы. Классы языка С-*-+ — типы дан- ных, экземпляры которых, подобно классам языка Smalltalk, могут создаваться в любом количестве. Результатами создания экземпляров в языке C++ являются просто объекты, или объявления данных. В описании класса указываются объекты данных (данные- члены) и функции (функции-члены). Классы могут называть один или несколько роди- тельских классов, обеспечивая наследование и множественное наследование, соответст- венно. Классы наследуют данные-члены и функции-члены родительского класса, опре- деленные в качестве наследуемых. Операторы в языке C++ могут перегружаться, что означает возможность приписы- вать существующим операторам новый смысл и применять их к определяемым пользо- вателем типам данных. Функции языка C++ также могут перегружаться, что позволяет определять одноименные функций, изменяя либо количество, либо тип используемых в них параметров. Динамическое связывание в языке C++ обеспечивается функциями виртуального класса. Эти функции определяют операции, зависящие от типов, используя для этого пе- регруженные функции, а также набор классов, связанных между собой методами насле- дования. Указатель на объект класса А может указывать и на объекты классов, насле- 2.16. Объединение императивных и объектно-ориентированных свойств... 113
дующих класс А. Когда подобный указатель указывает на перегруженную виртуальную функцию, функция, соответствующая текущему типу, выбирается динамически. На функции и классы может действовать спецификатор шаблона, что означает воз- можность их параметризации. Например, функция может быть записана в виде шаблон- ной функции, что позволяет иметь ее версии для разнообразных типов параметров. По- добную гибкость имеют также и классы. Язык C++ содержит обработку исключительных ситуаций, которая значительно отли- чается от аналогичного свойства языка Ada. Одним из отличий является невозможность обработки исключительных ситуаций, обнаруживаемых на аппаратном уровне. Обработ- ка исключительных ситуаций в языке C++ рассмотрена в главе 13. 2.16.3. Оценка Язык C++ быстро стал и остается очень популярным языком. Одним из факторов его популярности является доступность хороших и недорогих компиляторов. Кроме того, язык С+* практически полностью совместим с предшественником — языком С (что по- зволяет компилировать большинство программ, написанных на языке С, как программы, написанные на языке C++ и связывать команды языка C++ с командами языка С). Нако- нец, программисты сейчас настойчиво интересуются объектно-ориентированным про- граммированием, а язык C++ — его инструмент. Недостатком же языка C++ является его объемность и сложность, т.е. он страдает от тех же изъянов, что и язык PL/I. Язык C++ унаследовал большинство ненадежных мо- ментов языка С, что делает его менее надежным, чем такие языки, как Delphi, Ada и Java. Наконец, частично из-за его происхождения от языка С, язык C++ более, чем язык ALGOL 68, похож на язык PL/I тем, что он представляет собой скорее набор идей, соб- ранных вместе, чем результат комплексного плана по разработке языка. К объектно-ориентированным свойствам языка C++ мы еще вернемся в главе 11. Происхождение языка C++ показано на рис. 2.10. 2.16.4. Родственный язык: Eiffel Язык Eiffel представляет собой еще один смешанный язык, содержащий как импера- 1ивные, так и объектно-ориентированные свойства (Meyer, 1992). Язык Eiffel был разра- ботан одним человеком, Бертраном Мейером (Bertrand Meyer), французом, проживаю- щим в Калифорнии. Этот язык содержит свойства поддержки абстрактных типов данных, наследования и динамического связывания и полностью поддерживает объектно- ориентированное программирование. Самым характерным свойством языка Eiffel явля- ется встроенное в него использование утверждений для создания принудительного “контракта” между подпрограммами и вызывающими их операторами. Эта идея появи- лась еще в языке Plankalkiil, но игнорировалась большинством языков, разработанных после него. Сравним языки Eiffel и C++. Язык Eiffel меньше и проще языка C++ при практически одинаковой выразительности и легкости написания программ. Причины вы- сокой популярности языка C++ на фоне ограниченного использования языка Eiffel опре- делить нетрудно. Язык C++ позволяет компаниям — разработчикам программного обес- печения легко переходить к объектно-ориентированному программированию, поскольку в большинстве случаев их сотрудники уже знакомы с языком С. Язык же Eiffel не имеет такого легкого способа восприятия. Кроме того, в первые несколько лет широкого ис- пользования языка C++ система Cfront была доступной и недорогой, в то время как в 114 Глава 2. Обзор основных языков программирования
первые годы существования языка Eiffel его компиляторы были менее доступными и бо- лее дорогими. Язык С+4- имел за плечами престижную компанию Bell Laboratories, тогда как за языком Eiffel стоял Бертранд Мейер и его относительно небольшая компания по разработке программного обеспечения Interactive Software Engineering. 2.17. Программирование в World Wide Web: язык Java Разработчики языка Java взяли в качестве основы язык С-н-. удалили многочисленные конструкции, что-то изменили, что-то добавили. В результате язык сохранил мощь и гибкость языка C++, но стал меньше, проще и надежнее. 2.17.1. Процесс разработки Язык Java разрабатывался хпя целого набора прикладных областей, в частности про- граммирования таких встроенных бытовых электронных устройств, как тостеры, микро- волновые печи и интерактивные телевизионные системы. Может показаться, что при разработке программного обеспечения для микроволновой печи надежность не важна. Если на печь будет установлено неправильно функционирующее программное обеспече- ние, это не создаст смертельной опасности ни для кого и, вероятно, не приведет к круп- ным судебным разбирательствам. Тем не менее, если программное обеспечение кон- кретной модели будет признано дефектным после продажи миллиона единиц товара, то их изъятие может обернуться потерей значительной суммы. Следовательно, надежность является важной характеристикой при разработке программного обеспечения для быто- вых электронных приборов. В 1990 году компания Sun Microsystems пришла к выводу, что ни один из двух рас- смотренных языков программирования (С и C++) не подходит для разработки про- граммного обеспечения бытовых электронных приборов. Несмотря на то что язык С был относительно небольшим, он не обеспечивал поддержки объектно-ориентированного программирования, которая тогда считалась необходимой. Язык C++ поддерживал объ- ектно-ориентированное программирование, но его размер и сложность были восприняты как слишком большое препятствие. Кроме того, тогда полагали, что ни язык С, ни язык C++ не обеспечивают требуемый уровень надежности. Язык Java должен был обеспечить большую простоту и надежность, чем язык C++. Несмотря на то что изначальным стремлением разработчиков языка Java было созда- ние языка для бытовой электроники, ни один из товаров, вместе с которыми он исполь- зовался в начальные годы своего развития, не появился на рынке. Когда после 1993 года широкое распространение получила World Wide Web, чему в значительной степени спо- собствовали графические броузеры, язык Java оказался полезным инструментом для про- граммирования в Web. За первые несколько лет открытого использования основным применением языка Java стало именно программирование в Web. Команду разработчиков языка Java возглавил Джеймс Гослинг (James Gosling), кото- рый ранее создал редактор emacs для операционной системы Unix, а также систему управления окнами NeWS. 2.17. Программирование в World Wide Web: язык Java 115
2.17.2. Обзор языка Как мы упоминали ранее, язык Java был основан на базе языка C++, но создавался, в частности, как язык, меньший по размерам, более простой и надежный. В языке Java имеются и классы, и типы. Элементарные типы — это не объекты, основанные на клас- сах. В их число входят все скалярные типы, в том числе типы для целых чисел, чисел с плавающей точкой, булевских и символьных данных. Доступ к объектам обеспечивается с помощью ссылок, а доступ к величинам элементарных типов — как к скалярным вели- чинам таких императивных языков, как языки С и Ada. Массивы в языке Ada являются экземплярами встроенного класса, а в языке C++ — нет, хотя многие пользователи языка C++ создают для массивов интерфейсные классы, чтобы ввести такие свойства, как про- верка диапазона изменения индексов массивов, которое задано в языке Java неявно. В языке Java отсутствуют указатели, но ссылочные типы предоставляют некоторые их возможности. Эти ссылки используются для указания на экземпляры классов — фак- тически это единственный способ сделать ссылку на экземпляр класса. Хотя, на первый взгляд, указатели и ссылки очень похожи, существует важное семантическое различие. Указатели указывают на ячейку памяти, а ссылки — на объект. Это делает бессмыслен- ной любую арифметику ссылок, отменяя эту подверженную ошибкам привычку. На про- граммисте часто лежит ответственность за разграничение значений указателя и величи- ны, на которую он указывает, особенно это справедливо для языков, в которых указатели иногда приходится явно разыменовывать. Ссылки же всегда разыменовывают по мере необходимости. Таким образом, они ведут себя аналогично скалярным переменным. В языке Java имеется встроенный булевский тип, используемый преимущественно для условных выражений в его управляющих операторах (например, if и while). В от- личие от языков С и C++ в условных выражениях не могут использоваться арифметиче- ские выражения. В языке Java отсутствуют записи, объединения и перечислимые типы. Одним из существенных различий между языком Java и большинством современных языков, поддерживающих объектно-ориентированное программирование, в том числе языками Ada и C++, является невозможность написания на языке Java независимых под- программ. Все подпрограммы языка Java являются методами и определяются в классах. В языке Java не существует конструкции, которая вызывала бы функцию или подпро- грамму. Более того, методы могут вызываться только через классы или объекты. Другим важным различием между языками C++ и Java является поддержка языком C++ множественного наследования непосредственно в описании класса. Некоторые считают, что множественное наследование слишком все усложняет и запутывает. Язык Java поддерживает только единичное наследование, хотя некоторые выгоды использова- ния множественного наследования могут быть получены благодаря конструкции его ин- терфейса. Язык Java имеет относительно простую форму параллельности, управляемую с по- мощью модификатора synchronize, который может появляться в методах и блоках. В любом случае это вызывает присоединение блокировки. Блокировка страхует от взаимо- исключающего доступа или выполнения. В языке Java относительно просто создавать параллельные процессы, которые в этом языке называются потоками. Эти потоки могут запускаться, приостанавливаться, возобновляться и останавливаться, причем все эти ме- тоды наследуются от родительского класса для всех потоков — класса Thread. В языке Java используется неявное удаление объектов из динамической памяти (все экземпляры классов языка Java, или объекты, размещаются в динамической памяти), 116 Глава 2. Обзор основных языков программирования
часто называемое “сборкой мусора”. Это освобождает программиста от необходимости самому заботиться об освобождении памяти, когда в ней отпадает необходимость. Программы, написанные на языках, требующих явного освобождения памяти, часто страдают от того, что некоторые называют “утечкой памяти*', означающей выделение памяти без последующего ее освобождения. Это может привести к исчерпанию всей дос- тупной памяти. В отличие от языков С и C++ в языке Java имеется приведение типов (неявное преоб- разование типов) только в сторону расширения типа (от “меньшего" типа к “большему*). Таким образом, приведение типа int в тип float будет выполнено, а типа float в тип int — нет. 2.17.3. Оценка Разработчики языка Java удачно удалили чрезмерные и/или ненадежные свойства языка C++. Например, удаление половины приведений типов, имевшихся в языке С-+, несомненно повысило его надежность. Проверка диапазона изменения индексов при доступе к массивам также сделала язык надежнее. Введение параллельности, а также библиотек классов для создания аплетов, графических интерфейсов пользователя и рабо- ты в сети компьютеров расширило область приложений, которые могли быть написаны на данном языке. С другой стороны, язык Java все еще остается слишком сложным. Отсутствие в нем множественного наследования привело к возникновению несколько необычной структу- ры. Например, для запуска аплета как задания, которое может выполняться параллельно с другими, требуется собрать вместе атрибуты аплетов и потоков, а этот союз не обяза- тельно окажется приятным. Мобильность языка Java, по крайней мере в его промежуточной форме, часто припи- сывается структуре языка, но это неверно. Любой язык может быть переведен в проме- жуточную форму и “запуститься" на платформе, имеющей виртуальную машин) для этой промежуточной формы. Цена такого типа мобильности — стоимость интерпрета- ции, которая, обычно, на порядок выше, чем стоимость выполнения в машинных кодах. Разумеется, многие приложения могут выполняться быстрее и, следовательно, требовать наличия компиляторов, которые бы транслировали исходную программу в машинные коды. Однако такие компиляторы для языка Java появляются медленно. Поэтом), но крайней мере сейчас, более объемные приложения все еще пишутся на таких языках, как C++, Delphi и Ada. Сейчас язык Java широко используется для программирования Web-страниц. До его по- явления любое значительное вычисление, требуемое для создания Web-страниц, выполня- лось с помощью интерфейса CG1 и некоторых приложений, запущенных на Web-сервере. Аплеты языка Java представляют собой небольшие программы, запускаемые на Web- клиенте при обнаружении последним вызова аплета в HTML-программе отображаемой страницы. При вызове аплета его промежуточная форма загружается клиентом с сервера и интерпретируется. Результат работы аплета отображается в виде Web-страницы. Рост использования языка Java происходит значительно быстрее, чем любого другого языка программирования. Одной из причин этого является его вклад в программирова- ние тщательно разработанных Web-страниц. Другой причиной является бесплатность и легкость получения через Web-системы компилятора/интерпретатора для языка Java. Понятно, что одной из причин быстрого роста популярности языка Java является его 2.17. Программирование в World Wide Web: язык Java 117
простота, за что структура языка Java и полюбилась программистам. Наконец, некоторое время существовала группа программистов на языке C++, возражавшая против того, что они считали проблемами языка C++. Язык Java предложил им альтернативу: мощность языка C++ и уменьшение количества проблем. Ниже следует пример программы на языке Java. // Пример программы на языка Java // Ввод: Целое число listlen, меньше 100, за которым // следует набор целых чисел в количестве // listlen // Вывод: Количество введенных величин, которые больше их // среднего арифметического import j ava.ic.*; class IntSort { public static void main(String args[]) throws lOException { DatalnputStream in = new DatalnputStream(System.in); int Listlen, counter, sum = 0, average, result = 0; int[] intlist = int[99]; listlen = Integer.parselnt(in.readLine()); if ((listlen > 0) && (listlen < 100)) { /* Считывание входных величин в массив и вычисление суммы */ for (counter = 0 ; counter < listlen; counter++) { Intlist[counter] = Integer.valueOf(in.readLine ;) ) .intValue(); sum += intlist[counter]; /* Вычисление среднего арифметического */ average- = sum / listlen; /* Вычисление количества входных величин, которые больше их среднего */ for (counter = 0; counter < listlen; counters) if (intlist [counter] > average) results-»; /* Вывод результатов */ System.out.ptintIn( "ХпКоличество чисел, которые больше их среднего:” + result); } //*♦ конец блока выбора if ((listlen >0) ... else System.out.ptintln( ’’Ошибка — введена неверная длина списка\п”); } //★★ конец метода main } //** конец класса IntSort 118 Глава 2. Обзор основных языков программирования
Резюме Мы изучили развитие некоторых важнейших языков программирования и среды, в которых они создавались. Эта глава, мы надеемся, станет фундаментом для глубокого обсуждения важных свойств современных языков программирования. Дополнительная литературе Важнейшим источником исторической информации о развитии языков программирования является книга “History of Programming Languages" под редакцией Ричарда Векселблата (Wexelblat, 1981). В ней содержится описание сред и предпосылок к развитию 13 важ- ных, по мнению самих разработчиков, языков программирования. Подобная же работа возникла в результате второй “исторической" конференции, вышедшей на этот раз в ви- де специального выпуска ACM SIGPLAN Notices (АСМ. 1993а). В этой работе рассмат- риваются история и эволюция еще 13 языков программирования. Доклад “Early Development of Programming Languages" (Knuth and Pardo. 1977). являю- щийся частью энциклопедии “Encyclopedia of Computer Science and Technology", представляет собой великолепную 85-страничную работ}', в которой подробно описа- но развитие многих языков, в том числе и языка FORTRAN. В работе содержатся примеры программ, иллюстрирующие свойства многих описываемых языков. Еще одной книгой, представляющей большой исторический интерес, является “Programming Languages: History and Fundamentals", написанная Джин Саммет (Sammet. 1969). В этой книге на 785 страницах подробно описываются 80 языков программирования 1950-60-х годов. Саммет также выпустила несколько скорректи- рованных версий этой книги, например (Sammet, 1976). Вопросы 1. В каком году был разработан язык PlankalkUl? В каком году была опубликована эта разработка? 2. Какие две общие структуры данных были включены в язык PlankalkUl? 3. Как в начале 1950-х годов реализовывались псевдокоды? 4. Система Speedcoding была создана для преодоления двух существенных недостат- ков аппаратного обеспечения компьютеров начала 1950-х. Каких? 5. Почему в начале 1950-х годов медлительность интерпретации программ считалась приемлемой? 6. Какие два важных свойства аппаратного обеспечения появились в компьютерах IBM 704? 7. В каком году началась разработка языка FORTRAN? 8. Какой была основная область применения компьютеров во время создания языка FORTRAN? 9. Что было источником всех управляющих логических операторов языка FORTRAN 1? Вопросы 119
10. Какое самое значительное свойство было добавлено к языку FORTRAN 1 с целью получения языка FORTRAN II? 11. Какой управляющий логический оператор был введен в язык FORTRAN IV для по- лучения языка FORTRAN 77? 12. В какой версии языка FORTRAN впервые появились динамические переменные? 13. В какой версии языка FORTRAN впервые появилась обработка символьных строк? 14. Почему в конце 1950-х годов лингвистов интересовала область искусственного ин- теллекта? 15. Когда и кем был разработан язык LISP? 16. По каким параметрам языки Scheme и COMMON LISP являются полной противо- положностью друг другу? 17. Какой диалект языка LISP использовался в некоторых университетах для вводных курсов программирования? 18. Какие две профессиональные организации сообща разработали язык ALGOL 60? 19. В какой версии языка ALGOL появилась блочная структура? 20. Какой упущенный элемент языка ALGOL 60 разрушил его шансы на широкое ис- пользование? 21. Какой язык был разработан для описания синтаксической структуры языка ALGOL 60? 22. На основе какого языка создан язык COBOL? 23. В каком году началась разработка языка COBOL? 24. Какая структура данных, появившаяся в языке COBOL, была позаимствована из языка PlankalkOl? 25. Какая организация внесла наибольший вклад в ранний успех языка COBOL (с точ- ки зрения объема его использования)? 26. На какую пользовательскую группу была рассчитана первая версия языка BASIC? 27. Почему язык BASIC является важным языком начала 1980-х годов? 28. Какие два языка призван был заменить язык PL/I? 29. Для какой новой серии компьютеров разрабатывался язык PL/1? 30. Какие свойства языка SIMULA 67 сейчас являются важными частями некоторых объектно-ориентированных языков программирования? 31. Почему честь нововведения структуризации данных принадлежит языку ALGOL 68, но часто приписывается языку Pascal? 32. Какой структурный критерий в значительной степени использовался в языке ALGOL 68? 33. В каком языке впервые появился оператор case? 34. Какие операторы языка С копируют подобные операторы языка ALGOL 68? 35. Какие две характеристики языка С делают его менее надежным, чем язык Pascal? 120 Глава 2. Обзор основных языков программирования
36. Что представляют собой непроцедурные языки? 37. Какие два типа утверждений хранятся в базе данных языка Prolog? 38. Для какой основной области разрабатывался язык Ada? 39. Как в языке Ada называются параллельные программные блоки? 40. Какая конструкция языка Ada обеспечивает поддержку абстрактных типов данных? 41. Из чего состоит мир языка Smalltalk? 42. Какие три концепции составляют основу объектно-ориентированного программи- рования? 43. Почему в язык С+- включены ненадежные свойства языка С? 44. Что общего имеют языки Ada и COBOL? 45. Каково первое применение языка Java? 46. Назовите две причины большей надежности языка Java по сравнению с языком C++. У п р а ж н е н и я 1. Какие, по вашему мнению, свойства языка Plankalkiil оказали бы наибольшее влияние на язык FORTRAN 0, если бы разработчики языка FORTRAN были знако- мы с языком Plankalkiil? 2. Определите возможности созданной Бэкусом системы 701 Speedcoding и сравните их с соответствующими возможностями современного микрокалькулятора. 3. Напишите краткую историю систем А-0, А-1 и А-2, разработанных Грейс Хоппер и ее коллегами. 4. В качестве исследовательского проекта сравните средства языка FORTRAN 0 с со- ответствующими средствами системы Ленинга и Цирлера. 5. Какая из трех целей, поставленных перед комитетом разработчиков языка ALGOL, по вашему мнению, была самой трудно достижимой в то время? 6. Выскажите аргументированное предположение о наиболее частой синтаксической ошибке в программах, написанных на языке LISP? 7. Язык LISP начинал как чистый функциональный язык, но постепенно приобретал все больше и больше императивных функций. Почему? 8. Подробно опишите три важнейшие, по вашему мнению, причины отсутствия ши- рокого распространения языка ALGOL 60. 9. Почему, по вашему мнению, в языке COBOL было разрешено использование длинных идентификаторов, тогда как в языках FORTRAN и ALGOL — нет? 10. Какую вы слышали главную причину редкого использования языка BASIC специа- листами по компьютерным наукам? 11. Укажите, какими основными побуждениями руководствовалась корпорация IBM при разработке языка PL/1? Упражнения 121
12. Были ли основные стимулы корпорации IBM при разработке языка PL/1 верными в свете истории развития компьютеров и языков программирования после 1964 года? 13. Опишите своими словами концепцию ортогональности в разработке языков про- граммирования. 14. Назовите основную, по вашему мнению, причину более широкого использования языка PL/I, чем языка ALGOL 68. 15. Какие аргументы можно назвать “за” и “против” языков, не содержащих типы ти- пы данных? 16. Существуют ли языки логического программирования помимо языка Prolog? 17. Как вы относитесь к утверждению, что слишком сложные языки очень опасны для использования, и все языки должны быть маленькими и простыми? 18. Считаете ли вы разработку языка комитетом хорошей идеей? Аргументируйте ва- ше мнение. 19. Языки постоянно эволюционируют. Какие, по вашему мнению, ограничения свой- ственны изменениям в языках программирования? Сравните ваши ответы с эволю- цией языка FORTRAN. 20. Укажите в таблице все основные усовершенствования в области языков програм- мирования, в том числе даты их появления, языки, в которых они впервые прояви- лись, и фамилии разработчиков. 122 Глава 2. Обзор основных языков программирования
3.1. Введение 3.2. Общая задача описания синтаксиса 3.3. Формальные методы описания синтаксиса 3.4. Рекурсивный нисходящий синтаксический анализ 3.5. Атрибутивные грамматики 3.6. Описание смысла программ: динамическая семантика Грейс Хоппер (Grace М. Hopper) Грейс Хоппер, офицер ВМФ США и бывшая сотрудница компании UNIVAC, в первой половине 1950-х годов разработала ряд “компилирующих” систем, ис- пользованных для программиро- вания коммерческих приложений. К 1958 году эти системы вопло- тились в первом языке програм- мирования высокого уровня для коммерческих приложений — языке FLOW-MATIC, на основе которого, большей частью, был создан язык COBOL. Грейс Хоп- пер также участвовала в разра- ботке языка COBOL в качестве консультанта исполнительного комитета конференции CODASYL. Описание синтаксиса и семантики 123
В данной главе рассматриваются следующие темы. Во-первых, даются определе- ния терминов синтаксис и семантика. Затем подробно обсуждается наиболее распространенный метод описания синтаксиса: контекстно-свободные грамматики (формы Бэкуса-Наура (Backus-Naur Form)). Далее следует описание синтаксических гра- фов и краткое введение в рекурсивный нисходящий синтаксический анализ, который яв- ляется обычной техникой синтаксического разбора, основанного непосредственно на контекстно-свободных грамматиках. Далее описываются атрибутивные грамматики, ко- юрые могут использоваться для описания как синтаксиса, так и статической семантики языков программирования. В завершение вводятся три формальных метода описания се- мам шки— операционная, аксиоматическая и денотационная семантики. Из-за сложно- с । и. свойственной методам описания семантики, их обсуждение будет кратким, хотя по каждом} из трех названных методов можно написать целую книгу (что и было сделано некоторыми авторами). 3.1. Введение Задача разработки краткого, но доступного описания языка программирования сложна, но ее решение необходимо для успешного применения языка. Первыми языками, имеющи- ми лаконичное формальное описание, были языки ALGOL 60 и ALGOL 68; правда, в обоих сл\чаях описание было сложным для понимания (отчасти из-за использования новой фор- мы записи). В результате языки проиграли с точки зрения степени их восприятия. С другой стороны, некоторые языки страдают от наличия большого количества диалектов, появив- шихся вследствие доступного, но неформального и неточного описания. Одной из проблем, возникающих при описании языка, является несхожесть людей, для коюрых это описание предназначено. Большинство новых языков программирова- ния тщательно изучается потенциальными пользователями еще в ходе разработки. Успех лой обратной связи напрямую зависит от ясности описания языка. Разработчики систем реализации языков программирования, очевидно, должны уметь определять способ образования выражений, операторов и программных единиц языка, а также ожидаемый эффект от их выполнения. Сложность работы, связанной с реализаци- ей я зыка, также зависит от ясности и точности описания языка. Наконец, пользователи языка должны иметь возможность узнать по справочному ру- ководств}, как именно программировать системы программного обеспечения. Учебники и обучающие курсы дают только общее представление о языке, но единственным авто- ритетным печатным источником информации о языке обычно является именно справоч- ное р> ководство. Илчение языков программирования, подобно изучению естественных языков, может oLaib разделено на исследование синтаксиса и семантики. Синтаксис языка программи- рования — это форма, а семантика — смысл его выражений, операторов и программ- ных единиц. Например, оператор if языка С имеет следующий синтаксис: xf (<выражение>) <оператор> I го семантика состоит в том. что если текущее значение выражения истинно, то бу- дет выполнен указанный оператор. Несмотря на то что для удобства обсуждения семантика и синтаксис часто разделя- ются. они тесно связаны друг с другом. В хорошо разработанном языке программирова- 124 Глава 3. Описание синтаксиса и семантики
ния семантика непосредственно следует из синтаксиса; т.е. форма оператора может в значительной степени определять его смысл. Описывать синтаксис намного легче, чем семантику, отчасти из-за существования краткой и общепринятой формы записи, тогда как для описания семантики подобной формы еще не создано. 3.2. Общая задача описания синтаксиса Как естественные языки (например, английский), так и искусственные (например, Java) представляют собой совокупности строк, состоящих из символов некоторого алфа- вита. Строки, состоящие из символов языка, называются его предложениями (sentenses), или утверждениями (statements). Синтаксические правила языка определяют, какие именно строки, состоящие из символов алфавита, существуют в языке. Английский язык, например, обладает большим и сложным набором правил для определения синтаксиса его предложений. По сравнению с ним языки программирования, даже самые объемные и сложные, очень просты с синтаксической точки зрения. В формальные описания синтаксиса языков программирования для простоты часто не включаются описания синтаксических единиц самого нижнего уровня. Эти элементар- ные единицы называются лексемами (lexemes). Описание лексем может даваться лекси- ческой спецификацией, обособленной от синтаксического описания языка. В число лек- сем языка программирования входят его идентификаторы, литеральные константы, опе- раторы и специальные слова. Можно считать, что программа представляет собой строки, состоящие не из символов, а из лексем. Грамматическая лексема (token) в некотором языке является разновидностью его лексем. Например, идентификатор представляет собой грамматическую лексему, которая может состоять из лексем, или экземпляров, таких как sum и total. В некоторых слу- чаях грамматическая лексема состоит из одной-единственной возможной лексемы. На- пример, грамматическая лексема для символа арифметического оператора +, которая может называться оператор__сложения, имеет только одну возможную лексему. Рассмот- рим следующий пример оператора языка С: index = 2 * count + 17; Лексемы и грамматические лексемы этого оператора следующие: Лехаемы Граммаяические лексемы index идентификатор = знак_равенства 2 целочисленнаЯ—КОнстанта * оператор_умножения count идентификатор + оператор_сложения 17 целочисленная_константа ; точка_с_запятой Приводимые в этой главе описания языков очень просты, и большинство из них со- держит описание лексем. 3.2. Общая задача описания синтаксиса 125
3.2.1. Устройства распознавания языков В общем случае языки могут формально определяться двумя различными способами: путем распознавания (recognition) и путем порождения (generation). (Следует отметить, что ни один из этих способов не дает определения, удобного для людей, пытающихся изучать или хотя бы использовать язык программирования.) Предположим, что у нас есть язык L, использующий алфавит символов Z. Для формального определения языка L посредством метода распознавания нам потребовалось бы создать механизм R. называе- мый устройством распознавания и способный читать строки, состоящие из символов ал- фавита I. Механизм R следует создать так, чтобы он показывал, принадлежит ли данная введенная строка языку L. В действительности, механизм R должен был бы либо прини- мать. либо отклонять заданную строку. Такие устройства подобны фильтрам, отделяю- щим правильные предложения от неправильных. Если механизм R при введении строки символов алфавита I принимает ее только в случае ее принадлежности к языку L, то ме- ханизм R является описанием языка L. Поскольку самые полезные языки, предназначен- ные для практического использования, бесконечны, распознавание может показаться длительным и неэффективным процессом. Впрочем, устройства распознавания не ис- пользуются для перечисления всех предложений языка. Синтаксический анализатор, являющийся частью компилятора, представляет собой устройство распознавания языка, транслируемого компилятором. В таком качестве уст- ройству распознавания не нужно проверять все возможные строки символов, принадле- жащие некоторому множеству, для того чтобы определить, принадлежит ли каждая из них данному языку. Вместо этого ему всего лишь требуется определить принадлежность данной программы к языку. По сути, синтаксический анализатор просто определяет, яв- ляется ли данная программа синтаксически правильной. 3.2.2. Генераторы языков Генератор языка может использоваться в качестве устройства для создания предло- жений языка. Представьте себе кнопку, нажатие которой порождает предложение на данном языке. Поскольку конкретное предложение, возникающее при нажатии кнопки этого устройства, предсказанию не поддается, то генератор кажется мало полезным для описания языка. Тем не менее, люди предпочитают устройствам распознавания языка некоторые формы генераторов, поскольку их легче читать и понимать. Часть компилято- ра, предназначенная для проверки синтаксиса, наоборот, не так полезна для программи- ста в качестве средства описания языка, поскольку ее можно использовать только мето- дом проб и ошибок. Например, для определения с помощью компилятора правильного синтаксиса отдельного оператора программист может только предложить компилятору предполагаемый вариант и посмотреть, примет ли его компилятор. С другой стороны, часто можно определить, верен ли синтаксис отдельного оператора, сравнив его со структурой генератора. Существует тесная связь между формальными устройствами порождения и распозна- вания одного языка. Открытие этого факта оказалось одним из самых плодотворных в компьютерных науках. Большая часть из того, что нам известно сейчас о формальных языках и теории разработки компиляторов, является следствием этого открытия. В сле- дующем разделе мы еше вернемся к взаимосвязи между устройствами порождения и устройствами распознавания языков. 126 Глава 3. Описание синтаксиса и семантики
3.3. Формальные методы описания синтаксиса В данном разделе обсуждаются механизмы порождения формальных языков, широко используемые для описания синтаксиса языков программирования. Эти механизмы часто называются грамматиками. 3.3.1. Форма Бэкуса-Наура и контекстно-свободные грамматики Во второй половине 1950-х годов два человека — Джон Бэкус и Ноам Хомски (Noam Chomsky) — независимо друг от друга изобрели одну и ту же форму записи, которая впоследствии стала широко используемым методом формального описания синтаксиса языков программирования. 3.3.1.1. Контекстно-свободные грамматики В середине 1950-х годов выдающийся лингвист Хомски описал четыре класса порож- дающих устройств, или грамматик, определяющих четыре класса языков (Chomsky. 1956, 1959). Два класса из четырех, названные контекстно-свободной и регулярной грамматиками, оказались полезными для описания синтаксиса языков программирова- ния. С помощью регулярных грамматик можно описывать грамматические лексемы язы- ков программирования. Языки программирования в целом, за редким исключением, опи- сываются с помощью контекстно-свободных грамматик. Поскольку Хомски был лин- гвистом, то в первую очередь его интересовала теоретическая природа естественных языков. В то время он не интересовался искусственными языками, используемыми для общения с компьютерами. Так продолжалось до тех пор, пока его работа не была ис- пользована в области языков программирования. 3.3.1.2. Истоки формы Бэкуса-Наура Вскоре после появления работы Хомски о классах языков группа ACM-GAMM начала разработку языка ALGOL 58. В 1959 году Джоном Бэкусом, выдающимся членом группы ACM-GAMM, на международной конференции была представлена работа по описанию этого языка (Backus, 1959), ставшая вехой в истории языков программирования. В этой ра- боте была введена новая формальная запись для описания синтаксиса языка программиро- вания. Позже эта новая запись была несколько модифицирована Питером Науром для опи- сания языка ALGOL 60 (Naur, 1960). Переработанный метод описания синтаксиса стал из- вестен как форма Бэкуса-Наура (Backus-Naur Form), или просто БНФ. Форма БНФ представляет собой очень естественный способ описания синтаксиса. Похожее средство фактически использовалось Панини (Panini) для описания санскрита за несколько веков до нашей эры (Ingerman, 1967). Несмотря на то что использование формы БНФ в отчете о языке ALGOL 60 не было с го- товностью воспринято пользователями компьютеров, вскоре эта форма стала и все еше оста- ется самым популярным методом краткого описания синтаксиса языка программирования. Примечательно, что форма БНФ практически идентична порождающим устройствам контекстно-свободных языков, названных контекстно-свободными грамматика- ми (context-free grammars). Далее в главе мы будем называть контекстно-свободные грамматики просто грамматиками. Более того, мы будем использовать термины “форма БНФ” и “грамматика” как взаимозаменяемые. 3.3. Формальные методы описания синтаксиса 127
3.3.1.3. Основы Метаязык (metalanguage)— это язык, используемый для описания другого языка. Форма БНФ является метаязыком для языков программирования. Для описания синтаксических структур форма БНФ использует абстракции. Простой оператор присваивания языка С может, например, представляться абстракцией <присвоить>. (Угловые скобки часто используются для разделения названий абстрак- ций.) Фактическое определение оператора <присвоить> можно представить в виде <лрисвоить> —> <переменная> = <выражение> Символ слева от стрелочки, называемый, соответственно, левой стороной выражения (ЛСВ), является определяемой абстракцией. Текст, находящийся справа от стрелочки, является определением левой стороны выражения. Он называется правой стороной вы- ражения (ПСВ) и состоит из смеси лексем, граматических лексем и ссылок на другие аб- стракции. (Фактически, грамматические лексемы также являются абстракцией.) Все оп- ределение называется правилом (rule), или продукцией (production). Очевидно, что, прежде чем абстракция <присвоить> в приведенном выше примере станет пригодной к использованию, должны быть определены абстракции <переменная> и <выражение>. Отдельное правило устанавливает, что абстракция <присвоить> определяется как эк- земпляр абстракции <переменная>, за которой следует лексема =, за которой, в свою очередь, следует экземпляр абстракции <выражение>. Одним из примеров предложения, синтаксическая структура которого описывается этим правилом, является следующее: total = subl + sub2 Абстракции, входящие в форму БНФ, или грамматику, часто называются нетерми- нальными символами (nonterminal symbols), или просто нетерминалами (nonterminals), а лексемы и грамматические лексемы в правилах называются терминальными символами (terminal symbols), или терминалами (terminals). Форма БНФ, или грамматика (grammar), просто является набором правил. Нетерминальные символы могут иметь несколько различных определений, представ- ляющих две (или большее количество) возможные синтаксические формы в языке. Множественные определения записываются одним правилом, в котором различные оп- ределения разделены символом |, означающим логическое ИЛИ. Например, оператор if языка Pascal может быть описан правилами: <услсвнь:й_оператор> —> if <логическое_выражение> then <оператор> <условный_оператор> —> if <логическое_выражение> then <оператор> else <олератор> или <условный_оператор> —> if <логическое_выражение> then <оператор> I if <логическое_выражение> then <ог.ератор> else <оператор> Несмотря на простоту форма БНФ — это довольно мощное средство описания по- давляющего большинства языков программирования. В частности, она может описывать списки одинаковых конструкций, порядок, в котором должны появляться различные 128 Глава 3. Описание синтаксиса и семантики
конструкции, вложенные структуры любой глубины, приоритет оператора и ассоциатив- ность операторов. 3.3.1.4. Описание списков Списки переменной длины часто записываются с использованием эллипсиса (...). на- пример 1, 2, .... Форма БНФ не содержит эллипсиса, поэтому возникает потребность в альтернативном методе для описания списков синтаксических элементов языков про- граммирования (например, списка идентификаторов, входящих в оператор объявления данных). Самой распространенной альтернативой является рекурсия. Правило называет- ся* рекурсивным (recursive), если его левая часть входит в его правую часть. Использо- вание рекурсии для описания списков иллюстрируется следующим правилом: <список_идентификаторов> —> идентификатор I идентификатор, <список_идентификаторов> Данное правило определяет абстракцию <список_идентификаторов> либо как отдельную грамматическую лексему (идентификатор), либо как идентификатор, за которым последова- тельно следуют запятая и другой экземпляр абстракции <список_идентификаторов>. В боль- шинстве грамматик, приводимых в качестве примера далее в главе, для описания списков ис- пользуется рекурсия. 3.3.1.5. Грамматики и правила вывода Форма БНФ является порождающим устройством для определения языков. Предло- жения языка создаются с помощью последовательности правил, которая начинается со специального нетерминального символа грамматики, называемого начальным симво- лом (start symbol). Создание предложения называется выводом (derivation). В граммати- ке для полного языка начальный символ представляет собой полную программу и обыч- но называется <программа>. Для иллюстрации выводов в листинге 3.1 приведена про- стая грамматика. <программа> —> begin <слисок_операторов> end <список_операторов> —> <оператор> I <сператор> ; <слисок_олераторов> <оператор> —> <переменная> := <выражение> <переменная> —> А 1 В I С <выражение> —> <переменная> + <переменная> I <переменная> - <переменная> I <переменная> Язык, описанный в листинге 3.1. имеет только одну операторную форму— присваи- вание. Программа состоит из специального слова begin, за которым следует список операторов, разделенных точками с запятыми, и специального слово end. Выражение — это либо отдельная переменная, либо две переменные, разделенные операторами или Единственными существующими в языке именами переменных являются символы А. В и С. 3.3. Формальные методы описания синтаксиса 129
Ниже следует пример вывода программы на этом языке. <лрограмма> = > begin <список_операторов> end = > begin <оператор> ; <список_операторов> end = > begin <переменная> := <выражение> ; <список_операторов> end = > begin А := <выражение> ; <список_операторов> end = > begin А := <переменная> + <переменная> ; <список_операторов> end = > begin А := В + <переменная> ; <список_операторов> end = > begin А := В + С ; <список_операторов> end = > begin А := В + С ; <оператор> end = > begin А := В + С ; <переменная> := <выражение> end = > begin А := В + С ; В := <выражение> end = > begin А := В + С ; В := <переменная> end = > begin А:=В+С;В:=С end Этот вывод, как и все выводы, начинается с начального символа, в данном случае с символа <программа>. Символ “=>” читается как “порождает”. Каждая последующая строка приведенной последовательности выводится из предыдущей строки с помощью замены одного из нетерминальных символов одним из нетерминальных определений. Каждая строка в выводе, в том числе и строка <программам называется сентенциаль- ной формой (sentential form). В данном выводе всегда заменяется крайний слева нетер- минальный символ предыдущей формы высказывания. Выводы, использующие такой порядок замены, называются левосторонними выводами (leftmost derivations). Вывод продолжается до тех пор, пока сентенциальная форма не перестанет содержать нетерми- нальные символы. Сентенциальная форма, состоящая только из терминальных символов, или лексем, является порожденным предложением. Вывод может быть как левосторонним, так и правосторонним, а также может исполь- зовать другой, отличный от этих двух, порядок замены. Отметим, что на язык, порож- даемый грамматикой, порядок вывода никак не влияет. Выбирая альтернативные правые стороны правил замены нетерминальных символов в выводе, можно порождать различные предложения языка. Весь язык может быть соз- дан путем перебора всех существующих комбинаций альтернатив. Этот язык, как и большинство других, бесконечен, так что за конечное время все предложения языка по- родить невозможно. В листинге 3.2 приведен пример еще одной грамматики для части типичного языка программирования. <присвоить> —> <идентификатор> := <выражение> <идентификатор> —> А | В | С <выражение> —> <идентификатор> + <выражение> I <идентификатор> * <выражение> I ( <выражение> ) | <идентификатор> 130 Глава 3. Описание синтаксиса и семантики
Грамматика, приведенная в листинге 3.2, описывает операторы присваивания, у кото- рых правыми сторонами являются арифметические выражения с операторами умноже- ния и сложения, а также круглыми скобками. Например, оператор А := В * ( А + С ) порождается следующим левосторонним выводом: <присвоить> => <идентификатор> := <выражение> => А := <выражение> => А := <идентификатор> * <выражение> => А := В * <выражение> => А := В * ( <выражение> ) => А := В * ( <идентификатор> + <выражение> ) => А := В * ( А + <выражение> ) => А := В * ( А + <идентификатор> ) => А := В * ( А + С ) 3.3.1.6. Деревья синтаксического анализа Одно из самых привлекательных свойств грамматик состоит в естественности их описания иерархической синтаксической структуры предложений определяемого ими языка. Указанные иерархические структуры называются деревьями синтаксического анализа (parse tree). Например, дерево синтаксического анализа, приведенное на рис. 3.1, показывает структуру полученного выше оператора присваивания. < присвоите <идентификатор> := <выражение> А <цдентификатор> Рис. 3.1. Дерево синтаксического анализа простого оператора А := В * (А + С) Каждый внутренний узел дерева синтаксического анализа помечен нетерминальным символом; а каждый лист — терминальным символом. Каждое поддерево дерева синтак- сического анализа описывает один экземпляр абстракции оператора. 3.3. Формольные методы описания синтаксиса 131
3.3.1.7. Неоднозначность Грамматика, порождающая предложение, для которого существует несколько деревь- ев синтаксического анализа, называется неоднозначной (ambigous). Рассмотрим грамма- тику, показанную в листинге 3.3, незначительно отличающуюся от грамматики, приве- денной в листинге 3.2. <присвоить> —> <идентификатор> := <выражение> <идентификатор> —» А ! В | С <выражение> —» <выражение> + <выражение> I <выражение> * <выражение> I ( <выражение> ) 1 <идентификатор> Грамматика, показанная в листинге 3.3, является неоднозначной, поскольку предло- жение А:=В+С*А имеет два разных дерева синтаксического анализа, показанных на рис. 3.2. < присвоите <присвоить> <идентификатор> := <выражение> <идентификатор> := <выражение> А <выражение> + < выражение > <идентификатор> <выражение> • <выражение> I I I В <идентификатор> <идентификатор> С А Рис. 3.2. Два различных дерева синтаксического анализа для одного выражения А := В + С* А Неоднозначность появляется вследствие определения грамматикой несколько мень- шей синтаксической структуры, чем это было сделано грамматикой, приведенной в лис- тинге 3.2. Данная грамматика в отличие от предыдущей, позволяет дереву синтаксиче- ского анализа выражения расти не только вправо, но и влево. Синтаксическая неодно- значность структур языка представляет собой проблему, поскольку семантику таких структур компиляторы часто определяют, исходя из их синтаксической формы. В част- ности, изучив дерево синтаксического анализа, компилятор решает, какие команды гене- рировать для оператора. Если структура языка допускает существование нескольких де- 132 Глава 3. Описание синтаксиса и семантики
ревьев синтаксического анализа, то значение структуры не может определяться одно- значно. Исследование этой проблемы с помощью двух конкретных примеров проводится в следующих трех разделах. 3.3.7.8. Приоритет оператора Как указывалось ранее, грамматика так описывает определенную синтаксическую структуру, что смысл этой структуры может отчасти определяться деревом синтаксиче- ского анализа. В частности, тот факт, что оператор в арифметическом выражении поро- ждается ниже на дереве синтаксического анализа (и. следовательно, должен вычисляться первым), можно использовать, чтобы указать на более высокий приоритет этого опера- тора. чем у оператора, порождаемого выше. Например, на первом из представленных на рис. 3.2 деревьев синтаксическою анализа оператор умножения генерируется ниже по дереву, что может указывать на его более высокий приоритет по сравнению с операто- ром сложения того же выражения. Впрочем, на втором из изображенных на рис. 3.2 де- ревьев синтаксического анализа демонстрируется противоположное. Следовательно, эти два дерева синтаксического анализа отображают противоречивую информацию о при- оритетности операторов. Отметим, что хотя грамматика, приведенная в листинге 3.2. и является однозначной, она имеет необычный приоритет операторов. В этой грамматике на дереве син1аксиче- ского анализа предложения, состоящего из нескольких операторов, независимо от их конкретного вида, крайний справа оператор выражения находится в самой нижней точке, тогда как остальные операторы постепенно перемещаются вверх по дереву при продви- жении к крайнему слева оператору выражения. Например, на дереве синтаксического анализа, изображенного на рис. 3.1, оператор сложения является крайним справа опера- тором выражения и самым нижним оператором на дереве синтаксического анализа и по- лучает. таким образом, более высокий приоритет при выполнении, чем расположенный левее оператор умножения. Для того чтобы отделить операторы сложения и умножения друг от друга, следует написать грамматику так, чтобы они располагались на дереве синтаксического анализа в порядке от высшего до низшему. Такое упорядочение может поддерживаться вне зави- симости от порядка появления операторов в выражении. Правильный порядок устанав- ливается с помощью отдельной абстракции для операндов операторов, имеющих раз- личные приоритеты. Это требует дополнительных нетерминальных символов и некото- рых новых правил. Пример подобной грамматики приведен в листинге 3.4. <присвоить> —» <мдентификатор> <выражение> <идентификатор> —> А I В I С <выражение> —» <выражение> + <терм> I <терм> <терм> —» <терм> * <множитель> I <множитель> <множитель> —> ( <выражение> ) I <идентификатор> 3.3. Формальные методы описания синтаксиса 133
Грамматика, показанная в листинге 3.4, порождает тот же язык, что и грамматики, показанные в листингах 3.2 и 3.3, но в ней указан привычный приоритет операторов ум- ножения и сложения. Ниже следует вывод выражения А := В + С * А с использованием грамматики, показанной в листинге 3.4. <присвоить> => <идентификатор> := <выражение> => А : = <выражение> => А : = <выражение> + <терм> => А : = <т 'ерм> + <терм> => А : = <множитель> + <терм> => А : = <идентификатор> + <терм> => А : = В + <терм> = > А : = В + <терм> * <множитель> = > А : = В т <терм> * <множитель> => А : = в + <идентификатор> * <множитель> => А : = в + С * <множитель> => А : = в 1- С * <идентификатор> => А : = в + С * А На рис. 3.3 показано дерево синтаксического анализа, которое с использованием грамматики листинга 3.4 однозначно определяется для приведенного выше предложения. <множитель> <множитель> <идентификатор> I I <идентификатор> <идентификатор> А в с Рис, 3,3, Использование однозначной грамматики позволяет однозначно определить дерево синтакси- ческого анализа выражения А . = В + С * А Связь между деревьями синтаксического анализа и выводами очень тесная: одно мо- жет быть легко получено из другого. Любой вывод на основе однозначной грамматики имеет одно дерево синтаксического анализа, хотя это дерево может соответствовать раз- ным выводам. Например, нижеследующий вывод предложения А := В + С * А отличается от данного выше вывода того же выражения. Это — правосторонний вывод, тогда как 134 Глава 3. Описание синтаксиса и семантики
предыдущий был левосторонним выводом. Тем не менее, оба эти выводы представляют- ся одним и тем же деревом синтаксического анализа. <присвоить> => <идентификатор> => <идентификатор> => <идентификатор> := <выражение> := <выражение> := <выражение> + <терм> + <терм> * <множитель> => <идентификатор> := <выражение> + <терм> * <идентификатор> => <идентификатор> := <выражение> + <терм> * => <идентификатор> : = <выражение> + <множитель> => <идентификатор> := <выражение> + <идентификатор> * А => <идентификатор> := <выражение> + С * А => <идентификатор> := <терм> + С м г А => <идентификатор> := <множитель> + С * А => <идентификатср> := <идентификатор> + С * А => <идентификатор> := В + С * А =>А:=В+С*А 3.3.1.9. Ассоциативность операторов Еще одним интересным вопросом, касающимся грамматик выражений, является во- прос, верно ли описана ассоциативность операторов; т.е. в правильном ли порядке появ- ляются соседние операторы, имеющие одинаковый приоритет, на деревьях синтаксиче- ского анализа выражений, содержащих несколько таких операторов? Ниже представлен пример такого оператора присваивания: А := В + С + А Дерево синтаксического анализа этого предложения, определяемое грамматикой, представленной на листинге 3.4, показано на рис. 3.4. Приведенное на рис. 3.4 дерево синтаксического анализа показывает, что левый опе- ратор сложения находится ниже правого. Такой порядок является правильным, если сложение считать левоассоциативным, как обычно. В большинстве случаев ассоциатив- ность сложения в компьютере несущественна (в математике сложение ассоциативно, что означает равнозначность правого и левого ассоциативных порядков, т.е. (А + В) + С = А + (В + С)). Целочисленная машинная арифметика также ассоциативна. В не- которых случаях, правда, сложение с плавающей точкой не является ассоциативным. Предположим, что число с плавающей точкой хранится с точностью до семи цифр, и нужно сложить одиннадцать чисел, одно из которых равно 107, а остальные равны 1. Ес- ли меньшие числа (единицы) будут по одному добавляться к большему, то последнее они никак не изменят, поскольку меньшие числа приходятся на восьмой разряд большого числа. Впрочем, если сначала выполнить сложение всех малых чисел, а результат доба- вить к большому, то результат с точностью в семь знаков составит 1.000001*10 . Вычи- тание и деление не ассоциативны как в математике, так и в компьютерной технике. Сле- довательно, правильная ассоциативность может оказаться существенной для выражений, содержащих какое-либо из указанных действий. 3.3. Формальные методы описания синтаксиса 135
< присвоите <выражение> + <терм> <множи1ель> <терм> <множитель> <идентификатор> <множитель> <идентификатор> А <иденгификатор> С В Рис. 3.4. Дерево синтаксического анализа предложения А := В + С + А, иллюстрирую- щее ассоциативность сложения Если в правиле формы БНФ левая часть содержится в начале правой, правило назы- вается леворекурсивным (leftrecursive). Такая левая рекурсия устанавливает левую ассо- циативность. Например, левая рекурсивность правил грамматики листинга 3.4 приводит к ее левой ассоциативности по отношению к умножению и сложению. В большинстве языков, имеющих такую возможность, оператор возведения в степень является правоассоциативным. Для указания на правоассоциативность используется пра- вая рекурсия. Грамматическое правило называется праворекурсивным (rightrecursive), если его левая часть появляется в правом конце его правой части. Например, правила <множитель> —> <показатель> ** <множитель> ! <показатель> <показатель> —> ( <вь;ражение> ) 1 <идентификатср> могут использоваться для описания оператора возведения в степень как правоассоциа- тивного оператора. 3.3.1.10. Однозначная грамматика для оператора if- then-else Повторим правила формы БНФ, представленные в разделе 3.3.1.3, для отдельной формы оператора if-then-elee: <условный_оператор> —> if <логическое_выражение> then <оператор> 136 Глава 3. Описание синтаксиса и семантики
I xf <логическое_выратечие> then <опесатор> else <олератср> Если, помимо этого, у нас есть правило <оператор> —> <условньз4_сператор>, то наша грамматика неоднозначна. Простейшая сентенциальная форма, иллюстрирующая эту неоднозначность, имеет вид: if <логическое_зыраженке> then if <логическое_выражение> then <оператср> else <оператор> Приведенные на рис. 3.5 два дерева синтаксического анализа показывают неодно- значность этой формы высказывания. Практические проблемы, связанные с проблемой оператора else, подробно рассматриваются в главе 7. if <логическое_выражсние> then <оператор> <условный_оператор> <логическое_выражение> then <оператор> <условный_оператор> if <логическое_выражение> then <оператор> elae <оператор> Рис. 3.5. Два различных дерева синтаксического аначиза для одной и той же формы высказывания Разработаем теперь однозначную грамматику, описывающую данный оператор if. В большинстве языков правило для конструкций if состоит в следующем: оператор else (если он есть) соответствует ближайшему предыдущему оператору then, для ко- торого такое соответствие еще не установлено. Следовательно, между оператором then 3.3. Формольные методы описания синтаксиса 137
и соответствующим ему оператором else не может существовать оператора if без опе- ратора else. Таким образом, в данной ситуации мы должны различать операторы, для которых соответствие найдено, и те, для которых оно не найдено. В последнем случае операторами, для которых соответствующая пара не найдена, будут операторы else, которых будет меньше, чем существующих операторов if, при этом для остальных опе- раторов соответствие должно быть найдено. Проблема описанной выше грамматики за- ключается в том, что в ней все операторы считаются синтаксически эквивалентными, т.е. совпадающими между собой. Для отражения существования различных видов операторов должны использоваться различные абстракции, или нетерминальные символы. Однозначная грамматика, осно- ванная на описанных идеях, приведена ниже: <оператор> —> <согласованное_выражение> I <несогласован.чое_выражение> <согласованное_выражение> —> if <логическое_выражение> then <согласованное__выражение> else <согласованное_выражение> I любое выражение, не содержащее оператор if <несогласованное_выражение> —» if <логическое__выражение> then <оператор> I if <логическое_выражение> then <согласованное_выражение> else <несогласованное__выражение> Если использовать эту грамматику, то для предложения, приведенного на рис. 3.5, существует только одно возможное дерево синтаксического анализа. 3.3.2. Расширенная форма БНФ Из-за некоторых незначительных неудобств, присущих форме БНФ, в нее были вне- сены некоторые дополнения. Версии формы БНФ. для которых таких дополнений было сделано больше всего, стали называться расширенными формами БНФ, или просто РБНФ (EBNF — extended BNF), хотя сами эти формы не совсем идентичны. Данные расширения не увеличивают описательную силу формы БНФ; они всего лишь увеличи- вают удобство ее чтения и использования. Различные версии формы РБНФ, как правило, включают три дополнения. Первое из них обозначает необязательную часть правой стороны правила, которая помешается в квадратные скобки. Например, оператор выбора языка С может быть описан следующим образом. <выбор> —» if ( <выражение> ) <оператор> [else <оператор>]; Без использования квадратных скобок синтаксическое описание данного оператора потребовало бы двух правил. Вторым дополнением является использование в правой части правила фигурных ско- бок, указывающих на то, что заключенная в скобки часть может повторяться неограни- ченное число раз или вообще пропускаться. Это дополнение позволяет создавать списки с помощью одного правила, не используя рекурсию и дополнительное правило. Напри- мер, список идентификаторов, разделенных запятыми, может быть описан так: <сг1исок_идентификаторов> —> <идентификатор> {, <идентификатор>} 138 Глава 3. Описание синтаксиса и семантики
Это правило представляет собой замену рекурсии формой подразумеваемой итера- ции; заключенная в скобки часть может повторяться любое число раз. Третье обычное дополнение связано с многовариантным выбором. Когда из группы должен выбираться один элемент, варианты выбора помешаются в круглые скобки и разделяются оператором ИЛИ ( | ). Например, следующее правило описывает оператор for языка Pascal. <оператор_цикла> —> for <переменная> := <выражение> (to I downto) <выражение> do <сператор> Повторим, что для описания этой структуры потребовалось бы два правила формы БНФ. Квадратные, фигурные и круглые скобки в дополнениях формы РБНФ являются метасимволами. Это означает, что они являются способами обозначения, а не терми- нальными символами в синтаксических объектах, которые они помогают описывать. Ес- ли эти метасимволы являются также терминальными символами описываемого языка, то последние должны подчеркиваться отдельно. форма БНФ: <выражение> —> <выражение> t <терм> I <выражение> - <терм> I <терм> <терм> —» <терм> * <множитель> I <терм> / <множитель> I <множитель> форма РБНФ: <выражение> —> <терм> {(+|-) <терм>} <терм> —» <множитель> {(*!/) <множитель>} Некоторые версии формы РБНФ допускают приписывание к правой фигурной скобке числовых верхних индексов, чтобы указать на верхний предел числа возможных повто- ров фрагмента, заключенного в скобки. В некоторых версиях также используется верх- ний индекс + для указания на одно или несколько повторений. Например, правила <состаЕной__оператор> —> begin <оператор> {<оператор>) end и <составной_оператор> —> begin {<оператор>}+ end эквивалентны. 3.3.3. Синтаксические графы Граф (graph) представляет собой набор узлов, некоторые из которых соединяются линиями, называемыми ребрами. Ориентированный граф (directed graph)— граф с ориентированными ребрами; т.е. на одном из их концов имеется стрелка, указывающая направление. Ограниченной формой ориентированного графа является дерево синтакси- ческого анализа. На ориентированном графе представляется информация о правилах форм БНФ и РБНФ. Такие графы называются синтаксическими графами (syntax praphs), синтакси- 3.3. Формальные методы описания синтаксиса 139
'некими диаграммами или синтаксическими схемами. Для каждой синтаксической еди- ницы используется отдельный синтаксический граф, так же. как нетерминальный символ в грамматике. Дзя представления терминальных и нетерминальных символов правой части грамма- шчсского правила на синтаксическом графе используются различные типы узлов. Пря- моугольные узлы содержат название синтаксической единицы (нетерминального симво- ла). Терминальные символы находятся в кругах или эллипсах. На рис. 3.6 показан синтаксис оператора if языка Ada в форме РБНФ и в форме оин- гаксического графа. Отметим, что и квадратные, и фигурные скобки в описании с помо- щью формы РБНФ являются метасимволами, а не терминальными символами, исполь- зуемыми в языке Ada. УСЛОВНЫЙ- оператор иначе \условный_оператор>->- if <условие> then <операторы> {<иначе>} [else 5операторы> end if <иначе> --► eleif <условие> then <операторы> Рис. 3.6. Синтаксический граф и описание оператора xf языка Ada с помощью формы р:;нф Использование графического представления для описания синтаксиса предоставляет те же выгоды, что и использование графическою представления для описания чего-либо: оно увеличивает читабельность, позволяя нам наглядно представлять описываемый объ- ект в двух измерениях. 3.3.4. Грамматики и устройства распознавания языков Ранее в этой главе мы говорили о существовании тесной связи между устройствами по- рождения и распознавания языка. Действительно, при наличии контекстно-свободной |рамматики су щес!вует алгоритм создания устройства распознавания языка, порождаемого этой грамматикой. Для этого построения было разработано большое количество систем программного обеспечения. Подобные системы позволяют быстро создавать синтаксиче- ский анализатор, являющийся частью компилятора нового языка. Одним из наиболее ши- роко используемых генераторов синтаксических анализаторов является устройство уасс (yet another compiler-compiler — еще один компилятор компиляторов) (Jonhson, 1975). 140 Глава 3. Описание синтаксиса и семантики
Синтаксические анализаторы языков программирования, часто называемые програм- мами синтаксического анализа, создают деревья синтаксического анализа для данной программы. В некоторых случаях дерево синтаксического анализа создается неявно, но во всех случаях информация, содержащаяся в дереве синтаксического анализа, возникает во время самого анализа. Программы синтаксического анализа делятся по принципу направления, в котором они создают деревья синтаксического анализа. Существуют два обширных класса про- грамм синтаксического анализа: нисходящие, в которых дерево порождается от корня вниз к листьям, и восходящие, в которых дерево создается от листьев вверх к корню. Пример нисходящего алгоритма синтаксического анализа кратко описан в разделе 3.4. 3.4. Рекурсивный нисходящий синтаксический анализ Как отмечалось в разделе 3.3.1.7, контекстно-свободная грамматика служит основой синтаксического анализатора, или программы синтаксического анализа, являющегося частью компилятора. Рассмотрим основанный на контекстно-свободной грамматике нисходящий анализатор грамматики, который мы будем называть программой рекурсив- ного нисходящего синтаксического анализа. Синтаксический анализ — это процесс отслеживания или создания дерева синтакси- ческого анализа для данной введенной строки. Основной идеей рекурсивного нисходя- щего синтаксического анализа является существование подпрограммы для каждого не- терминального символа грамматики. Обязанности подпрограммы, относящейся к кон- кретному нетерминальному символу, следующие: при введении строки она создает эскизный вариант дерева синтаксического анализа, корнем которого является данный не- терминальный символ, а листья соответствуют введенной строке. В действительности, подпрограмма рекурсивного нисходящего синтаксического анализа— это устройство синтаксического анализа языка (набора строк), который может порождаться его нетер- минальными символами. Во многих случаях язык, подвергаемый синтаксическому ана- лизу, содержит вложенные синтаксические единицы (например, выражения и операто- ры), так что подпрограмма, выполняющая синтаксический анализ, часто является рекур- сивной. Предположим, что каждый нетерминальный символ содержит одно правило, возможно, с несколькими правыми частями, разделенными операторами ИЛИ. Рассмотрим следующее описание элементарных арифметических выражений в форме РБНФ: <выражение> —> <терм> {(+I-) <терм>} <терм> —> <множитель> {(*1/) <множитель>} <множитель> —> <идентификатор> | ( <выражение> ) Если подпрограммам рекурсивного синтаксического анализа требуется узнать, какое следующее входное обозначение должно быть подвергнуто анализу, то для его получения вызывается подпрограмма лексического анализатора. Напомним, в главе 1 говорилось, что лексический анализатор служит входом синтаксического анализатора. Он объединяет входные символы в лексемы и возвращает грамматические лексемы, связанные с этими лексемами. В приведенной ниже функции рекурсивного синтаксического анализа ехрг эта функция называется соответствующим именем lexical. Она получает следующую грам- матическую лексему и помещает ее в глобальную переменную next token. 3.4. Рекурсивный нисходящий синтаксический анализ 141
Ниже приведена подпрограмма рекурсивного синтаксического анализа, написанная на языке С для первого из приведенных в этом разделе правил. void ехрг() { term(); /* синтаксический анализ первого терма */ while (next_token == plus_code | | next—token == minus_code) lexical(); /* ввод следующей грамматической лексемы */ term(); /* синтаксический анализ следующего терма */ } } В представленной выше функции синтаксического анализа предполагается, что каж- дая рекурсивная функция синтаксического анализа сохраняет следующую входную грамматическую лексему в переменной next—token. Таким образом, всякий раз в на- чале выполнения функции синтаксического анализа гарантируется, что переменная next—token содержит крайнюю левую грамматическую лексему, которая еще не ана- лизировалась. Разбираемая функцией ехрг часть языка состоит из одного или нескольких элемен- тов (term), разделенных операторами сложения или вычитания. Такой язык порождает- ся абстракцией <выражение>. Следовательно, вначале вызывается функция синтаксиче- ского анализа термов. Затем функция ехрг продолжает вызывать эту функцию до тех пор, пока будут обнаруживаться элементарные значения оператор__сложения или опера- тор_вычитания (синтаксический анализ которых пропускается с помощью вызова функ- ции lexical). Эта функция рекурсивного синтаксического анализа проще большинства ей подобных, поскольку ее правила содержат только одну правую сторону. Более того, эта функция не содержит никаких средств обнаружения или устранения синтаксических ошибок. Общий процесс создания программы нисходящего рекурсивного синтаксического анализа для данного нетерминального символа начинается с написания кода для опреде- ления, какая именно правая часть правила для данного нетерминального символа соот- ветствует строке входных обозначений. Это решение принимается на основе первого терминального символа введенной строки, порождаемого нетерминальным символом. Код для каждой правой части правила относительно прост. Каждый терминальный сим- вол сравнивается со следующим обозначением. Если они не совпадают, то возникает синтаксическая ошибка, в противном случае для перехода на следующее входное обо- значение вызывается лексический анализатор. Вызов подпрограммы синтаксического анализа выполняется для каждого нетерминального символа. Функция для нетерминального символа <множитель> должна делать выбор между двумя правыми частями его правила. Она также обнаруживает ошибки. В функции для символа <множитель> при обнаружении ошибки вызывается функция error. В реаль- ной программе синтаксического анализа в этом случае должно выдаваться диагностиче- ское сообщение. Более того, большинство компиляторов должны возобновлять свою ра- боту при появлении ошибки, чтобы процесс синтаксического анализа мог продолжаться. Подобные компиляторы не могут просто останавливаться при обнаружении в программе первой синтаксической ошибки. 142 Глава 3. Описание синтаксиса и семантики
void factor () { if (next_token == id_code) { lexical () ; return; } else if (next_token == left—paren_code) { lexical (); expr (); if (next—token == right—paren_code) { lexical(); return; } else error(); /* обнаружена закрывающая круглая скобка */ ) else error(); /* не обнаружено ни идентификатора, ни открывающей круглой скобки */ } Целью данного краткого обсуждения синтаксического анализа было убедить вас, что программу рекурсивного нисходящего синтаксического анализа легко написать, если для языка существует подходящая грамматика. В данном контексте слово “подходящая” оз- начает, что для того, чтобы грамматику можно было использовать для рекурсивного син- таксического анализа, она должна иметь особую форму. Обсуждение всех характеристик, позволяющих или запрещающих использование конкретной грамматики для рекурсивно- го нисходящего синтаксического анализа, выходит за рамки этой книги. Подробнее об этом вопросе вы можете узнать из работы (Fisher^and LeBlanc, 1988). Одной простой характеристикой, создающей рекурсивному нисходящему граммати- ческому разбору катастрофические проблемы, является левосторонняя рекурсия. Рас- смотрим следующее правило: <А> -> <А> + <В> Подпрограмма рекурсивного нисходящего синтаксического анализа для символа <А> немедленно вызовет себя же. Это приведет к вызову подпрограммы синтаксического анализа символа <А> и еще одному вызову ее же, и еще раз, и еще. Понятно, что это ни к чему не приведет. По этой причине левосторонняя рекурсия не допускается в граммати- ках, для которых должна создаваться программа нисходящего рекурсивного синтаксиче- ского анализа. Существенным фактором важности грамматик является их тесная связь с компиля- торами. 3.5. Атрибутивные грамматики Атрибутивная грамматика (attribute grammar) более подробно описывает структу- ру языка программирования, чем контекстно-свободная грамматика, и является ее рас- ширением. Это расширение позволяет описать такие правила языка, как совместимость типов. Прежде чем мы формально определим форму атрибутивной грамматики, разъяс- ним концепцию статической семантики. 3.5. Атрибутивные грамматики 143
3.5.1. Статическая семантика Существуют некоторые характеристики структуры языков программирования» опи- сать которые с помощью формы БНФ сложно, а иногда — просто невозможно. Приме- ром правил языка, определение которых трудно дать с помощью формы БНФ, являются правила совместимости типов. Например, в языке Java число с плавающей точкой не может быть присвоено переменной целого типа, хотя обратное присвоение возможно. Несмотря на то что подобное ограничение может быть точно описано в форме БНФ, оно потребует дополнительных нетерминальных символов и правил. Если все подобные пра- вила типов языка Java точно определить в рамках формы БНФ, то грамматика станет слишком громоздкой, поскольку ее размер определяет и размер программы синтаксиче- ского анализа. Обычное правило обязательного объявления переменных не может быть определено в форме БНФ. Другой пример: если за оператором end подпрограммы, написанной на языке Лоа. следует некоторое имя. то это имя должно совпадать с именем подпрограммы. Эти две проблемы служат примером правил статической семантики. Статическая семантика (static semantics) языка в основном связана с формами программ, а не с их содержанием (т.е. синтаксисом, а не семантикой). Во многих случаях правила статиче- ской семантики языка устанавливают ограничения, наложенные на типы. Статическая семантика называется так потому, что анализ, требуемый для проверки этих условий, должен быть выполнен во время компиляции. Из-за существования проблемы описания статической семантики с помощью формы БНФ для решения этой задачи было разработано множество более мощных механизмов. Разработка одного из таких механизмов, описывающего синтаксис и статическую семан- тику программ и названного атрибутивными грамматиками, была выполнена Кнутом (Knuth, 1968а). Обсуждение динамической семантики будет проведено в разделе 3.6. 3.5.2. Основные понятия Атрибутивными грамматиками называются грамматики, к которым были добавлены атрибуты, атрибутивные вычислительные функции и предикативные функции. Атрибу- 1ы (attributes), связанные с символами грамматики, подобны переменным в том смысле, что им,могут присваиваться значения. Атрибутивные вычислительные функции (attribute computation functions), иногда называемые семантическими функциями, связаны с грамматическими правилами вычисления значений атрибутов. Предикативные функ- ции (predicate functions), устанавливающие некоторые синтаксические правила и правила статической семантики языка, связаны с грамматическими правилами. 3.5.3. Определение атрибутивных грамматик Атрибутивными называются грамматики, имеющие следующие дополнительные свойства. С каждой грамматикой связывается символ X, обозначающий набор атрибутов А(Х). Набор А(Х) состоит из двух непересекающихся множеств S(X) и 1(Х), назы- ваемых синтезированными и унаследованными атрибутами, соответственно. Син- тезированные атрибуты (synthesized attributes) используются для передачи се- мантической информации вверх по дереву синтаксического анализа, тогда как 144 Глава 3. Описание синтаксиса и семантики
унаследованные атрибуты (inherited attributes) используются для передачи се- мантической информации вниз по дереву. Правило, связанное с каждой грамматикой, является набором семантических функций и, возможно, пустого множества предикативных функций, определенных на атрибутах символов в грамматическом правиле. Для правила Хо -> Х| ...Хп син- тезированные атрибуты правила Хо вычисляются семантической функцией вида S(X0) = f(А(ХО,..., А(ХП)). Таким образом, величина синтезированного атрибута уз- ла дерева синтаксического анализа зависит исключительно от значений атрибутов дочерних узлов этого узла. Унаследованные атрибуты символов Хр 1< j <п (в пра- виле, приведенном выше) вычисляются семантической функцией вида I(Xj) = f(А(Х।),..., А(ХП)). Таким образом, значение унаследованного атрибута узла дерева синтаксического анализа зависит от значения атрибута родительского узла и значений атрибутов узлов того же уровня. Отметим, что во избежание порочно- го круга унаследованные атрибуты часто ограничиваются функцией вида I(Xj) = f(A(Xo),..., A(Xj.|)). Такая форма предотвращает зависимость унаследован- ных атрибутов от самих же себя или от атрибутов, находящихся правее на дереве синтаксического анализа. Предикативная функция имеет вид булевского выражения, заданного на множест- ве атрибутов {А(Х0),..., А(ХП)}. Единственными выводами, разрешенными в рам- ках атрибутивной грамматики, являются те, в которых все предикаты, связанные с каждым нетерминальным символом, истинны. Ложное значение предикативной функции указывает на нарушение синтаксических правил или правил статической семантики языка. Дерево синтаксического анализа атрибутивной грамматики базируется на грамматике формы БНФ, лежащей в основе данной атрибутивной грамматики, с. возможно, пустым множеством значений атрибутов, связанных с каждым узлом. Если вычислены значения всех атрибутов дерева синтаксического анализа, то оно называется полностью опреде- ленным (fully attributed). Хотя на практике подобным образом поступают далеко не все- гда, удобно полагать, что значения атрибутов вычисляются после создания дерева син- таксического анализа. 3.5.4. Внутренние атрибуты Внутренние атрибуты (intrinsic atributes)— это синтезированные атрибуты узлов, значения которых определяются вне дерева синтаксического анализа. Например, тип эк- земпляра переменной в программе может поставляться из таблицы идентификаторов, используемой для хранения имен переменных и их типов. Содержимое таблицы иденти- фикаторов определяется выполненными ранее операторами объявления. Предположим, что изначально было создано дерево с неопределенными значениями атрибутов, и нам нужно их определить. При этом единственными атрибутами, имеющими значения, явля- ются внутренние атрибуты узлов. Поместив значения внутренних атрибутов на дерево синтаксического анализа, мы сможем использовать семантические функции для вычис- ления значений остальных атрибутов. 3.5. Атрибутивные грамматики 145
3.5.5. Примеры атрибутивных грамматик Рассмотрим следующий фрагмент атрибутивной грамматики, описывающий правило, заключающееся в том, что имя, следующее за оператором end в конце процедуры языка Ada, должно совпадать с именем самой процедуры. Атрибут строки <имя_продедуры>, обозначенный как <имя_процедуры>.строка, является реальной строкой символов, кото- рая может обнаруживаться лексическим анализатором. Отметим, что при наличии в син- таксическом правиле атрибутивной грамматики нескольких нетерминальных символов, последние заключаются в скобки лишь с целью их различения. Ни индексы, ни скобки частью описываемого языка не являются. Синтаксическое правило: <определение_процедуры> —> procedure <имя_процедуры>[1] <тело_процедуры> end <имя__процедуры> [2 ] ; Семантическое правило: <имя_процедуры>[1].строка = <имя_процедуры>[2].строка Ниже показано, как с помощью атрибутивной грамматики проверить правила совмес- тимости типов простого оператора присваивания. Синтаксис и семантика этого операто- ра следующие: единственными именами переменных являются А, В и С. Правая сторона присваивания может быть переменной или выражением в форме сложения двух пере- менных. Переменные могут быть двух типов: целые и действительные. Если в правой части присваивания находятся две переменные, они не обязаны иметь один и тот же тип. Значение выражения, в котором фигурируют операнды различных типов, всегда имеют действительный тип. Тип значения выражения с операндами одного типа совпадает с ти- пом операндов. Тип левой части присваивания должен совпадать с типом правой части. Таким образом, типы операндов правой части могут смешиваться, но присваивание кор- ректно только в том случае, если его левая часть и значение, полученное при вычислении правой части, имеют один и тот же тип. Атрибутивная грамматика определяет описан- ные семантические правила. Синтаксическая часть атрибутивной грамматики, описывающей данный пример, име- ет следующий вид: <присваивание> —> <переменная> := <выражение> <выражение> —> <переменная> + <переменная> I <переменная> <переменная> —> А|В|С В следующих абзацах описываются атрибуты для нетерминальных символов данной атрибутивной грамматики. Фактический тип Синтезированный атрибут, связанный с нетерминальными симво- лами <переменная> и <выражение>. Он используется для запоминания факти- ческого типа (в нашем примере целого или действительного) переменной или выражения. Для переменной фактический тип является внутренним. Для выражения он определяется на основе типов дочернего узла или узлов нетерминального символа <выражение>. Ожидаемыйтип Внутренний атрибут, связанный с нетерминальным символом <выражение>. Используется для запоминания типа (целого или действительного), 146 Глава 3. Описание синтаксиса и семантики
ожидаемого от выражения, и определяемого типом переменной, стоящей в левой части оператора присваивания. Полностью атрибутивная грамматика показана в листинге 3.6. 1. Синтаксическое правило: <присвоить> —> <переменная> := <выражение> Семантическое правило: <выражение>.ожидаемый_тип <— <переменная>.фактический_тип 2 . Синтаксическое правило: <выражение> —> <переменная>[2] + <переменная>[3] Семантическое правило: <выражение>.фактический_тип <— if (<переменная>[2].фактический_тип = int) and(<переменная>[3].фактический_тип = int) then int else real end if Предикат: <выражение>.фактический_тип = <выражение>.ожидаемый_тип 3. Синтаксическое правило: <выражение> —> <переменная> Семантическое правило: <выражение>.фактический_тип <— <переменная>.фактический_тип Предикат: <выражение>.фактический_тип = <выражение>.ожидаемый_тип 4. Синтаксическое правило: <переменная> —> А|В|С 5. Семантическое правило: <переменная>.фактический_тип <— поиск(<переменная>.строка) Функция поиска ищет имя данной переменной в таблице идентификаторов и возвра- щает тип переменной. Пример дерева синтаксического анализа предложения А : = А + В, порождаемого грамматикой, описанной в листинге 3.6, показан на рис. 3.7. Как и в самой грамматике, после повторяющихся меток узлов дерева добавлены числа в скобках, так что ни них можно ссылаться однозначно. 3.5. Атрибутивные грамматики 147
<присвоить> Рис. 3.7. Дерево синтаксического анализа пред- ложения А А + В 3.5.6. Вычисление значений атрибутов Рассмотрим теперь процесс заполнения дерева синтаксического анализа атрибутами. Если все атрибуты унаследованные, то этот процесс может полностью происходить в нисходящем порядке, от корня к листьям. Если же все атрибуты синтезированные, то процесс полностью протекает в восходящем порядке, от листьев к корню. Поскольку рассматриваемая грамматика содержит и синтезированные, и унаследованные атрибуты, вычисление значений не может происходить в каком-то одном направлении. Ниже опи- сано вычисление атрибутов в том порядке, в котором они могут вычисляться. В общем случае определение такого порядка вычисления значений атрибутов в общем случае яв- ляется сложной задачей, требующей создания графа зависимостей для прослеживания всех связей между атрибутами. 1. <п.еременная>. фактический_тип поиск (А) (Правило 4) 2. <выражение>. ожидаемьп4__тип <переменная>.фактический__тип 'Правило 1) 3. <переменная> [2] .фактический__тип поиск(А) (Правило 4) <леременная>[3].фактический_тип <— поиск(В) (Правило 4) 4. <вь:ражение>. фактический_тип <— целый или действительный (Правило 2) 5. <выражение>.ожидаемый_тип <— <выражение>.фактический_тип или ИСТИНА или ЛОЖЬ (Правило 2) Де рево, приведенное на рис. 3.8. показывает процесс заполнения значениями атрибутов дерева, приведенного на рис. 3.7. Сплошные линии использованы для изображения дерева синтаксического анализа, пунктирные — процесса вычисления значений атрибутов. Де рево, изображенное на рис. 3.9, показывает окончательные значения атрибутов всех узлов дерева. В данном примере тип переменной А определен действительным, а переменной В — целым. 148 Глава 3. Описание синтаксиса и семантики
Рис. 3.8. Процесс заполнения дерева атрибутами Рис. 3.9. Полностью описанное дерево синтаксического анализа 3.5.7. Оценка Атрибутивные грамматики использовались: для полного описания синтаксиса и ста- тической семантики языков программирования (Watt. 1979); в качестве формального оп- ределения языка для создания систем генерации компиляторов (Farrow, 1982); как основа для некоторых синтаксически управляемых систем редактирования (Teitelbaum and Reps, 1981; Fisher et al, 1984). Помимо этого, атрибутивные грамматики применяются в систе- мах обработки текстов на естественных языках. Одним из основных недостатков атрибутивной грамматики для описания всего син- таксиса и статической семантики реального современного языка программирования яв- ляется ее сложность и громоздкость. Большое число атрибутов и грамматических пра- вил, требуемое для полного описания языка программирования, значительно затрудняет написание и чтение подобной грамматики. Более того, вычисление значений афибутов большого дерева синтаксического анализа дорого обходится. С другой стороны, менее 3.5. Атрибутивные грамматики 149
формальные атрибутивные грамматики представляют собой мощный и широко исполь- зхемый инструмент для разработчиков компиляторов, которых больше интересует соз- дание компилятора, чем формализм грамматики. 3.6. Описание смысла программ: динамическая семантика Перейдем к сложной задаче описания динамической семантики (dynamic semantics), или смысла, выражений, операторов и программных единиц. Благодаря мощи и естест- венности формы записи описание синтаксиса является относительно простым делом. С другой стороны, для динамической семантики еще не изобрели общепринятой формы записи. В данном разделе кратко описаны несколько разработанных методов. Термин семантика будет означать динамическую семантику; статическую семантику мы будем называть полностью. Существует несколько причин, по которым следует заниматься описанием семантики. Во-первых, программисты должны точно знать, что именно делают операторы языка. Однако зачастую они узнают это, читая неточные и неполные англоязычные объяснения в руководствах по языку. Разработчики компиляторов также обычно определяют семан- тику языков, для которых они создают компиляторы, руководствуясь описаниями на английском языке. Все эти неформальные описания заменяют сложные формальные описания семантики. Цель исследований очевидна — найти семантический формализм, который мог бы использоваться программистами и разработчиками компиляторов. Было проделано несколько экспериментальных работ по автоматическому порождению ком- пиляторов языков программирования непосредственно из их семантических описаний. В заключение отметим, что доказательство корректности программ опирается на некото- рое формальное описание семантики языка. 3.6.1. Операционная семантика Операционная семантика (operational semantics), сводится к описанию смысла про- граммы посредством выполнения ее операторов на машине, реальной или виртуальной. Смысл оператора определяется изменениями, произошедшими в состоянии машины по- сле выполнения данного оператора. Для того чтобы разобраться в этой концепции, рас- смотрим команду на машинном языке. Пусть состояние компьютера— это значения всех его регистров и ячеек памяти, в том числе коды условий и регистры состояний. Ес- ли просто записать состояние компьютера, выполнить команду, смысл которой нужно определить, а затем изучить новое состояние машины, то семантика этой команды станет понятной: она представляется изменением в состоянии компьютера, вызванным выпол- нением этой команды. 3.6.1.1. Основной процесс Описание операционной семантики операторов языков программирования высокого уровня требует создания реального или виртуального компьютера. В главе 1 говорилось, что аппаратное обеспечение компьютера является чистым интерпретатором его машин- ного языка. Чистый интерпретатор любого языка программирования может быть создан с помощью программных средств, которые становятся виртуальным компьютером для данного языка. Семантику языка высокого уровня можно описать, используя чистый ин- 150 Глава 3. Описание синтаксиса и семантики
терпретатор данного языка. При таком подходе, правда, существуют две проблемы. Во- первых, сложность и индивидуальные особенности аппаратного обеспечения компьюте- ра и операционной системы, используемых для запуска чистого интерпретатора, затруд- няют понимание происходящих действий. Во-вторых, выполненное таким образом се- мантическое определение будет доступно только для людей с абсолютно идентичной конфигурацией компьютера. Этих проблем можно избежать, заменив реальный компьютер виртуальным компью- тером низкого уровня, реализованным с помощью программного моделирования. Реги- стры, память, информация о состоянии и процесс выполнения операторов— все это можно моделировать. Набор команд можно создать так, чтобы семантику каждой от- дельной команды было легко понять и описать. Таким образом, машина была бы идеали- зирована и значительно упрощена, что облегчило бы понимание изменений ее состояния. Использование операционного метода для полного описания семантики языка про- граммирования L требует создания двух компонентов. Во-первых, для преобразования операторов языка L в операторы выбранного языка низкого уровня нужен транслятор. Во-вторых, для этого языка низкого уровня необходима виртуальная машина, состояние которой изменяется с помощью команд, полученных при трансляции данного оператора языка высокого уровня. Именно изменения состояния этой виртуальной машины опре- деляют смысл данного оператора. Основной процесс операционной семантики не является чем-то необычным. Факти- чески эта концепция часто используется в учебниках по программированию и справоч- никах по языкам программирования. Семантику конструкции for языка С. например, можно описать в терминах следующих простых команд. Оператор языка С Операционная семантика for (exprl; ехрг2; ехргЗ){ exrpl ... loop: if expr2 = 0 goto out } ехргЗ; goto loop out: ... Человек, читающий подобное описание, является “виртуальным компьютером” и предполагается способным правильно “выполнить” команды описания и распознать эф- фект такого “выполнения”. В качестве примера низкоуровневого языка, который можно применять для операци- онной семантики, рассмотрим следующий список операторов, соответствующих про- стым управляющим операторам типичного языка программирования: ident := var isent := ident + 1 ident := ident -1 goto label if var relop var goto label Здесь relop— одни из операторов отношений из набора { = , <>f >, <, >=, <=}, ident — идентификатор, a var — идентификатор или константа. Все эти опера- торы просты и легки для понимания и реализации. 3.6. Описание смысла программ: динамическая семантика 151
Незначительное обобщение приведенных выше трех операторов присваивания позво- ляет описывать более обшие арифметические выражения и операторы присваивания. Новые операторы выглядят следующим образом: Loen’s := var bin_op var ident := un_op var Здесь bin_op— бинарный арифметический оператор, a un_op— унарный опера- тор. Многочисленные арифметические типы данных и автоматическое преобразование 1 инов, конечно, несколько усложняют это обобщение. Введение небольшого количества новых относительно простых команд позволит описывать семантику массивов, записей, указателей и подпрограмм. Семантика различных управляющих операторов с использованием операционной се- мантики описана в главе 7. 3.6.1.2. Оценка Первым и самым значительным использованием формальной операционной семанти- ки было описание семантики языка PL/I (Wegner. 1972). Эта абстрактная машина и пра- вила трансляции языка PL/I были названы общим именем Vienna Definition Language (VDL) в честь города, в котором они были созданы корпорацией IBM. Операционная семантика является эффективной до тех пор. пока описание языка ос- тается простым и неформальным. К сожалению, описание VDL языка PLZI настолько сложно, что практическим целям оно фактически не служит Операционная семантика зависит от алгоритмов, а не от математики. Операторы од- ного языка программирования описываются в терминах операторов другого языка про- граммирования. имеющего более низкий уровень. Этот подход может привести к пороч- ном} круг}', когда концепции неявно выражаются через самих себя. Методы, описывае- мые в следующих двух разделах, значительно более формальны в том смысле, что они опираются на логику и математику, а не на машины. 3.6.2. Аксиоматическая семантика Аксиоматическая семантика (axiomatic semantics) была создана в процессе разра- ботки метода доказательства правильности программ. Такие доказательства показывают, что программа выполняет вычисления, описанные ее спецификацией. В доказательстве каждый оператор программы сопровождается предшествуюши.м и последующим логиче- скими выражениями, устанавливающими ограничения на переменные в программе. Эти выражения используются для определения смысла оператора вместо полного описания состояния абстрактной машины (как в операционной семантике). В качестве формы за- писи используется исчисление предикатов, являющееся фактическим языком аксиомати- ческой семантики. Простых булевских выражений не всегда бывает достаточно для вы- ражения ограничений. 3.6.2.1. Утверждения Аксиоматическая семантика основана на математической логике. Логические выра- жения называются предикатами, или утверждениями (assertions). Утверждение, непо- средственно предшествующее оператору программы, описывает ограничения, наложен- ные на переменные в данном месте программы. Утверждение, следующее непосредст- венно за оператором программы, описывает новые ограничения на те же (а возможно, и 152 Глава 3. Описание синтаксиса и семантики
другие) переменные после выполнения оператора. Эти утверждения называются соот- ветственно предусловием (precondition) и постусловием (postcondition) оператора. Раз- работка аксиоматического описания или доказательства данной программы требует на- личия у каждого оператора программы предусловий и постусловий. В следующих разделах при изучении утверждений мы будем предполагать, что пре- дусловия операторов вычисляются на основе постусловий, хотя этот процесс можно рас- сматривать и с противоположной точки зрения. Предположим, что все переменные яв- ляются целочисленными Рассмотрим в качестве примера оператор присваивания и по- стусловие: sum := £. 4 х -г 1 sum > 1} Предусловия и постусловия помещаются в фигурные скобки, для того чтобы отличать их от операторов программы. Одним из возможных предусловий данного оператора мо- жет быть {х > 10). 3.6.2.2. Слабейшие предусловия Слабейшими предусловиями (weakest preconditions) называются наименьшие пре- дусловия, обеспечивающие выполнение соответствующего постусловия. Например, для приведенного выше оператора и его постусловия предусловия «х > 10}. . х > 5С> и {х > 1000 > являются правильными. Слабейшим из всех предусловий в данном случае будет {х > 0 }. Если для каждого оператора языка по заданным постусловиям можно вычислить сла- бейшее предусловие, то для программ на данном языке может быть построено коррект- ное доказательство. Доказательство начинается с использования результатов, которые надо получить при выполнении программы, в качестве постусловия последнего операто- ра программы, и выполняется с помощью отслеживания программы от конца к началу с последовательным вычислением слабейших предусловий для каждого оператора. При достижении начала программы первое ее предусловие отражает условия, при которых программа вычислит требуемые результаты. Для некоторых операторов программы вычисление слабейшего предусловия на осно- ве оператора и его постусловия является достаточно простым и может быть задано с по- мощью аксиомы. Однако в большинстве случаев слабейшее предусловие вычисляется только с помощью правила логического вывода. Аксиомой (axiom) называется логиче- ский оператор, предполагаемый истинным; правилом логического вывода (inference rule) является метод выведения истинности одного утверждения на основе значений ос- тальных утверждений. Чтобы использовать аксиоматическую семантику с данным языко.м программирова- ния для доказательства правильности программ или для описания формальной семанти- ки. для каждого вида операторов языка должны быть определены аксиомы или правила логического вывода. В следующих подразделах мы приводим аксиому для операторов присваивания и правила логического вывода для последовательности операторов, опера- торов ветвления и логически управляемых циклов с предусловиями. 3.6.2.3. Операторы присваивания Пусть х = Е — обычный оператор присваивания, a Q— его постусловие. Тогда пре- дусловие Р того же оператора определяется аксиомой Р ~ Qx~*E 3.6. Описание смысла программ: динамическая семантика 153
Эта аксиома означает, что предусловие Р вычисляется как условие Q, в котором все переменные х заменены переменными Е. Пусть, например, есть оператор присваивания и постусловие а = Ь / 2 -1 {а < 10) Тогда слабейшее предусловие вычисляется подстановкой b / 2 - 1 в утверждение «.а < 10): b / 2 -1 < 10 Ь < 22 Таким образом, слабейшим предусловием для данных оператора и его постусловия является {Ь < 22). Отметим, что истинность аксиомы присваивания гарантируется только при отсутствии побочных эффектов. Побочный эффект оператора присваивания может проявляется, если оператор изменяет значения переменных, не входящих в его ле- вую часть. Ниже представлена обычная запись для задания аксиоматической семантики данного оператора: {P}S{Q}, где Р — предусловие, Q — постусловие, aS — операторная форма. Для оператора при- сваивания форма записи такова: {Qx_>e} х = Е {Q} Рассмотрим другой пример вычисления предусловия для оператора присваивания: X = 2 * у - 3 {х > 25} В данном случае предусловие вычисляется следующим образом: 2 * у - 3 > 25 у > 14 Следовательно, {у > 14 } — слабейшее предусловие при заданных операторе при- сваивания и его постусловии. Отметим, что появление левой части оператора присваивания в его правой части не влияет на процесс вычисления слабейшего предусловия. Например, для оператора х = х + у - 3 {х> 10} слабейшим предусловием является х + у - 3 > 10 у > 13 - х В начале нашего обсуждения мы утверждали, что аксиоматическая семантика была разработана для доказательства правильности программ. В свете этого утверждения ес- тественным будет поинтересоваться, как вообще аксиома для оператора присваивания может использоваться для доказательства чего-либо. А вот как: данный оператор при- сваивания с предусловием и постусловием может рассматриваться как теорема; теорема считается доказанной, если аксиома присваивания, применяемая к постусловию и опера- тору присваивания, дает заданное предусловие. Рассмотрим, логическое утверждение {х > 3} х « х - 3 {х > 0} 154 Глава 3. Описание синтаксиса и семантики
Применение аксиомы присваивания к постусловию и оператору х = х - 3 {х > 0} дает {х > 3}, что и является заданным предусловием. Следовательно, предыдущее ло- гическое утверждение доказано. Далее, рассмотрим логическое выражение {х > 5} х = х - 3 {х > 0} В этом случае заданное предусловие {х > 5} не соответствует утверждению, порож- даемому применением аксиомы. Несмотря на это, очевидно, что {х > 5 } => {х > 3}. Для использования этого факта в доказательстве нам необходимо правило логического вы- вода, называемое правилом логического следствия (rule of consequence). Общая форма правила логического вывода следующая: S1,S2..Sn S В ней утверждается, что из истинности выражений SI, S2, ... и Sn может быть выве- дена истинность утверждения S. Правило логического следствия имеет форму {PISlQbP^P.Q^Q' {P'}S{Q} Символ “=>” означает “следует”, a S может быть любым оператором программы. Правило формируется следующим образом: если истинно логическое утверждение {P}S{Q}, то из утверждения Р' следует утверждение Р, а из утверждения Q' следует ут- верждение Q, тогда можно доказать, что {P'}S{Q'}. Проще говоря, правило логического следствия утверждает, что постусловие всегда может быть ослаблено, а предусловие — усилено. В доказательстве программы этот факт довольно полезен, поскольку он позво- ляет, например, завершить доказательство последнего из приведенных выше логических выражений. Если мы будем считать, что в нашем случае Р— это {х >3}, Q и Q'— {х> 0}, а Р' — {х > 5}, то мы получим (х > 3}х = х - 3{х >0},(х >5) => (х > 3),(х > 0) => (х > 0) {х >5}х = х -3{х > 0} Доказательство завершено. 3.6.2Л. Последовательности Слабейшее предусловие последовательности операторов невозможно описать с по- мощью аксиомы, поскольку предусловие зависит от конкретного вида операторов в по- следовательности. В этом случае единственной возможностью описания предусловий является использование правила логического вывода. Пусть S1 и S2— соседние опера- торы программы. При следующих предусловиях и постусловиях этих операторов {Pl} SI {Р2} {Р2} S2 {РЗ} правило логического вывода подобной двухоператорной последовательности будет таким: 3.6. Описание смысла программ: динамическая семантика 155
{P1}S1{P2}.(P2}S2{P3} {P1}S1;S2{P3} Итак, для приведенного выше примера выражение {PI}S1;S2{P3} описывает аксиома- шческую семантику последовательности операторов S1;S2. Если операторы S1 и S2 яв- ляются операторами присваивания х! =Е1 и х2 = Е2, г*> имеем {РЗч2_>Е2}х2 = Е2{РЗ} {(РЗх2-»Е2)ч1-»Г1 }xl=El {РЗх2^:}. Следовательно, слабейшим предусловием последовательности xl = Е1; х2 =Е2 с по- c. условием РЗ является {(Р3х2—»E2)xl —»Е 1}. Рассмотрим последовательность операторов и постусловие: у 3 * х + 1; х - у + 3; (х < 10} Предусловием последнего оператора присваивания является У < / Оно же становится постусловием для первого оператора. Теперь мы можем вычис- ли '! ь предусловие первого оператора присваивания: 3 - х + 1 < 7 х < 2 3.6.2.5. Выбор Далее мы рассмотрим правило логического вывода для операторов ветвления. Мы б>дем изучать только операторы ветвления, содержащие оператор else. Правило логи- ческою вывода для таких выражений записывается следующим образом: {В and Р} SI {Q},{(not В) and Р} S2 {Q) {Р} if В then SI else S2 {Q} Это правило указывает, что данные операторы ветвления должны быть доказаны для обоих случаев. Первое из написанных над чертой логических выражений относится к оператору then, второе — к оператору else. Рассмотрим пример вычисления с использованием правила логического вывода для операторов ветвления. Возьмем следующий оператор ветвления: if (х >0) v у -1 else у - у + 1 Предположим, что постусловием для этого оператора выбора является {у > 0}. В случае выбора оператора then мы можем использовать аксиому присваивания: у у - 1 {у > 0} 156 Глава 3. Описание синтаксиса и семантики
Это дает условие {у - 1 > 0} или {у > 1}. Применим ту же аксиому к варианту else: у = у + 1 {у > 0} Это порождает предусловие (у * 1 > 0} или {у > -1}. Поскольку {у > - 1} => {у > -1}, то правило логического следствия разрешает нам использовать в качестве предусловия оператора выбора условие {у > 1}. 3.6.2.6. Логически управляемый цикл с предварительной проверкой условия Другой необходимой конструкцией императивных языков программирования являет- ся цикл с предварительной проверкой условия, или цикл while. Вычисление слабейше- го предусловия для такого цикла изначально предполагается более сложным, чем для по- следовательности операторов, поскольку в общем случае число повторений цикла опре- делить заранее невозможно. Если число повторений цикла известно, то такой цикл можно трактовать как последовательность операторов. Проблема вычисления слабейшего предусловия для циклов подобна проблеме дока- зательства теоремы обо всех натуральных числах. В последнем случае обычно использу- ется индукция, тот же метод мы используем и для циклов. Основным шагом индукции является определение индуктивного предположения. Соответствующим шагом в аксио- матической семантике для цикла while является поиск утверждения, называемого ин- вариантом цикла (loop invariant), что является ключевым этапом обнаружения слабей- шего предусловия. Правило логического вывода для вычисления предусловия цикла while выглядит следующим образом: __________(I and В) S{I}______ {1} while В do S end {I and (not B)} где I — инвариант цикла. Аксиоматическое описание цикла while записывается следующим образом: {Р} while В do S end {Q} Инвариант цикла должен удовлетворять большому количеству условий. Во-первых, из слабейшего предусловия цикла while должна следовать истинность инварианта цик- ла. Во-вторых, из истинности инварианта цикла должна следовать истинность постусло- вия после завершения цикла. Эти ограничения приводят нас от правила логического вы- вода к аксиоматическому описанию. Во время выполнения цикла результаты вычисления булевского выражения, управляющего циклом, и операторы тела цикла не должны вли- ять на истинность инварианта цикла. Поэтому он и называется инвариантом. Еще одним усложняющим фактором для циклов while является вопрос о заверше- нии цикла. Если Q— постусловие, выполняющееся непосредственно после выхода из цикла, то предусловие цикла Р обеспечивает истинность условия Q на момент выхода из цикла, а также собственно завершение цикла. Полное аксиоматическое описание цикла while требует истинности всех следующих условий (через 1 обозначен инвариант цикла): 3.6. Описание смысла программ: динамическая семантика 157
р=>1 {1} В {1} {land В} S {1} (I and (not В)) => Q завершение цикла Чтобы найти инвариант цикла, используем метод, подобный методу определения ин- д\ктивного предположения математической индукции. Для нескольких случаев вычис- ляются зависимости в надежде найти общую закономерность. При таком подходе удобно рассматривать процесс создания слабейшего предусловия как функцию wp. В общем случае: wp (оператор, постусловие) = предусловие Для того чтобы обнаружить инвариант цикла I, используем постусловие цикла Q для вычисления предусловий нескольких повторений тела цикла, начав с нуля повторений. Если тело цикла содержит один оператор присваивания, то для операторов присваивания используется аксиома. Рассмотрим пример цикла while у <> xdoy = y+lend {у = х} Будьте осторожны и помните, что знак равенства в данном случае имеет два различ- ных смысла. В утверждениях он означает математическое равенство; вне их он является оператором присваивания. Для нулевого количества повторений цикла значение слабейшего предусловия оче- видно: {у = х} Для одного повторения: wp(у = у + 1, {у=х})={у+1=х}, или {у = х - 1) Для двух wp(у = у + 1, {у=х-1})=(у+1=х-1}, или {у - х - 2} Для трех повторений wp(y = у + 1, {у=х-2))={у+1=х-2}, или {у = х - 3) Уже понятно, что в случае одного или нескольких повторений условия {у < х) бу- дет достаточно. Объединив это условие с условием {у = х), полученным для нулевого числа повторений цикла, окончательно получим условие {у <= х), которое и будем использовать в качестве инварианта цикла. Предусловие для оператора while можно определить из инварианта цикла. В нашем примере можно использовать условие Р = I. Мы должны убедиться, что наш выбор соответствует пяти критериям, упомянутым выше. Проверяем первый: поскольку Р = I, то Р => 1. Второе требование состоит в том, чтобы на инвариант цикла I не оказывало влияния вычисление значения булевского вы- ражения, которым в нашем случае является выражение у о х. В этом выражении ни- какие значения не изменяются, так что на инвариант цикла I оно также не влияет. Далее, должно быть справедливым следующее выражение: {I and В} S {1} 158 Глава 3. Описание синтаксиса и семантики
В нашем примере имеем: {у <= х and у <> х} у = у + 1 {у <= х} Применим к последнему выражению аксиому присваивания у = у + 1 {у <= х} Тогда мы получим условие {у + 1 <= х}, что эквивалентно условию {у < х}. следующему из условий {у <= х} и {у о х}. Таким образом, приведенное выше утверждение доказано. Далее, должно быть справедливым условие (I and (not В)) => Q В нашем случае имеем {(у <« х) and not (у о х)) => {у = х} {(у <= х) and not (у = х)) => {у = х} {у = х} => {у = х} Эти утверждения, очевидно, истинны. Далее должно быть рассмотрено завершение цикла. В нашем примере, это вопрос о том, завершается ли цикл {у <= х} while y<>xdoy=y+l end {у = х} Поскольку х и у предполагаются целыми числами, ясно, что число повторений цикла не бесконечно. Предусловие гарантирует, что изначально значение переменной у не пре- вышает значения переменной х. Каждое повторение тела цикла увеличивает значение переменной у до тех пор, пока оно не станет равным значению переменной х. Не важно, насколько изначально значение переменной у было меньше значения переменной х, в конечном счете оба значения станут равными, т.е. выполнение цикла завершится. По- скольку наш выбор условия 1 удовлетворяет всем пяти поставленным критериям, то оно подходит на роль инварианта цикла и предусловия цикла. Описанный выше процесс вычисления инварианта цикла не всегда порождает утвер- ждение, являющееся слабейшим предусловием (хотя для приведенного выше примера это было именно так). В качестве другого примера нахождения инварианта цикла рассмотрим следующий оператор цикла: while s>ldos=s/2 end {s = 1} Как и в описанном выше случае, мы используем аксиому присваивания для определе- ния инварианта и предусловия цикла. Для нулевого числа повторов цикла слабейшим предусловием будет {s = 1}. Для одного повтора: wp(s = s / 2, {s = 1}) = {s / 2 = 1}, или {s = 2} Для двух повторов цикла: wp(s = s / 2, {s = 2}) = {s / 2 = 2}, или {s = 4} Для трех: 3.6. Описание смысла программ: динамическая семантика 159
wp(s = s / 2, {s = 4}) = {s / 2 = 4}, или {s = 8} Таким образом, инвариант равен: {s — неотрицательная степень числа 2} Напомним, что вычисленный инвариант цикла 1 может служить предусловием цикла Р и удовлетворяет пяти поставленным требованиям. Однако в отличие от предыдущего примера поиска предусловия цикла найденное условие отнюдь не является слабейшим Рассмотрим использование предусловия {s > 1}. Можно легко доказать логическое выражение {s > 1} while s>ldos=s/2 end {s = 1) Это предусловие значительно шире предусловия, найденного выше. Цикл и преду- словие удовлетворяются любым положительным значением переменной^, а не только степенью двойки, как было показано ранее. В силу правила логического следствия ис- пользование предусловия, более сильного, чем слабейшее предусловие, не опровергает доказательства. Найти инварианты цикла не всегда легко. Для этого следует понять их природу. Во- первых, инвариант цикла является ослабленным вариантом постусловия и одновременно предусловием цикла. Таким образом, условие I должно быть достаточно слабым для удовлетворения его в начале выполнения цикла, но в то же время в сочетании с условием выхода из цикла оно должно быть достаточно сильным, чтобы гарантировать истинность постусловия. Вследствие трудности доказательства завершения цикла это условие часто просто иг- норируется. Если завершение цикла может быть показано, то аксиоматическое описание цикла называется доказательством полной корректности (total correctness). Если ос- тальные условия удовлетворены, но завершение цикла не гарантировано, то такое описа- ние называется доказательством частичной корректности (partial correctness). Поиск подходящего инварианта в более сложных циклах даже для доказательства его частичной корректности требует известной доли изобретательности. Поскольку поиск предусловия для цикла while зависит от обнаружения инварианта цикла, то доказатель- ство с использованием аксиоматической семантики правильности программ, содержа- щих подобные циклы, может оказаться сложным. Ниже следует пример доказательства правильности программы, написанной на псев- докоде и вычисляющей факториал. {п >= 0} счетчик = п; факториал = 1; while счетчик о 0 do факториал = факториал * счетчик; счетчик = счетчик - 1; end {факториал = п’} Описанный выше метод нахождения инварианта цикла не подходит для цикла в дан- ном примере. Здесь требуется определенная находчивость, проявлению которой поможет краткое изучение программы. Данный цикл вычисляет факториал, начиная с последнего множителя; т.е. первым выполняется умножение (п - 1) * п (предполагается, что п больше единицы). Таким образом, частью инварианта может быть 160 Глава 3. Описание синтаксиса и семантики
факториал = (счетчик + 1) * (счетчик + 2)* ... * (п - 1; * п Однако мы также должны гарантировать, чтобы переменная счетчик всегда была неотрицательной. Для этого следует добавить требуемое условие к полученной выше части: I = (факториал = (счетчик + 1) * ... * n) AND (счетчик >= 0) Затем надо проверить, чтобы условие I удовлетворяло требованиям, предъявляемым инвариантам. Снова используем условие I в качестве условия Р, таким образом, из усло- вия Р будет явно следовать условие 1. Вычисление булевского выражения оператора while (в данном случае счетчик о 0), очевидно, никак не повлияет на условие I. Следующий вопрос, требующий доказательства: {I и В} S {1} Условие (I and В) означает: ((факториал = (счетчик + 1) * ... * n) AND (счетчик >= 0)) AND (счетчик о 0) В упрощенном виде можно записать: (факториал = (счетчик +1) * ... * n) AND (счетчик > 0) В нашем случае мы должны вычислить предусловие тела цикла, используя значение инварианта для постусловия. Для условия {Р} счетчик = счетчик - 1 {I) мы находим, что условие Р имеет вид { (факториал « счетчик * (счетчик +1) * ... * n) AND (счетчик >= 1)) Используем это в качестве постусловия для первого присваивания в теле цикла {Р} факториал = факториал * счетчик {(факториал = счетчик * (счетчик + 1) * ... * n) AND (счетчик >= 1)} В этом случае условием Р будет следующее {(факториал « (счетчик + 1) * ... * n) AND (счетчик >= 1)) Очевидно, что из условий I и В следует условие Р, т.е. по правилу логического след- ствия утверждение {I AND В} S {1} истинно. Последняя проверка условия Г. I AND (NOT В) => Q Для нашего примера это означает ((факториал = (счетчик + 1) * ... * n) AND (счетчик >= 0)) AND (счетчик =0) => факториал = п! 3.6. Описание смысла программ: динамическая семантика 161
Это, очевидно, истинно при значении переменной счетчик = 0, таким образом, первая часть является в точности определением факториала. Итак, наш выбор условия I удовле- творяет всем требованиям, предъявляемым инвариантам цикла. Теперь мы можем ис- пользовать условие Р (аналогичное условию I) для цикла с условием while в качестве постусловия второго присваивания программы: {Р) факториал = 1 {(факториал = (счетчик + 1) * ... * n) AND (счетчик >= 0)} Из него вытекает следующее значение условия Р: {(1 = (счетчик + 1) * ...* п) AND (счетчик >= 0)} Используем это в качестве постусловия первого присваивания программы {Р} счетчик = п { (1 = (счетчик + 1) * ... * n) AND (счетчик >= 0)) Тогда мы получим следующее значение условия Р: {((п + 1) * ... * п = 1) AND (п >= 0)) Левый операнд оператора AND истинный (поскольку 1 = 1), а правый операнд в точности равен предусловию всего фрагмента программы {п >« 0}. Следовательно, правильность программы доказана. 3.6.2.7. Оценка При определении семантики полного языка программирования с использованием ак- сиоматического метода для каждого вида операторов языка должны быть сформулиро- ваны аксиома или правило логического вывода. Доказано, что определение аксиом и правил логического вывода для некоторых операторов языков программирования — очень сложная задача. Напрашивающимся решением такой проблемы является разработ- ка языка, подразумевающего использование аксиоматического метода, т.е. содержащего только те операторы, для которых могут быть написаны аксиомы или правила логиче- ского вывода. К сожалению, подобный язык оказался бы слишком маленьким и простым, что отражает нынешнее состояние аксиоматической семантики как науки. Аксиоматическая семантика является мощным инструментом для исследований в об- ласти доказательств правильности программ, она также создает великолепную основу для анализа программ, как во время их создания, так и позднее. Однако ее полезность при описании содержания языков программирования весьма ограничена как для пользо- вателей языка, так и для разработчиков компиляторов. 3.6.3. Денотационная семантика Денотационная семантика (denotational semantics) — самый строгий широко извест- ный метод описания значения программ. Она прочно опирается на теорию рекурсивных функций. Всестороннее рассмотрение денотационной семантики — длительное и сложное дело. Мы намереваемся ознакомить читателя лишь с ее основными принципами. Основной концепцией денотационной семантики является определение для каждой сущности языка некоего математического объекта и некоей функции, отображающей эк- земпляры этой сущности в экземпляры этого математического объекта. Поскольку объ- екты определены строго, то они представляют собой точный смысл соответствующих им 162 Глава 3. Описание синтаксиса и семантики
сущностей. Сама идея основана на факте существования строгих методов оперирования математическими объектами, а не конструкциями языков программирования. Сложность использования этого метода заключается в создании объектов и функций отображения. Название метода “денотационная семантика” происходит от английского слова denote (обозначать), поскольку математический объект обозначает смысл соответствующей синтаксической сущности. 3.6.3.1. Два простых примера Для введения в денотационный метод мы используем очень простую языковую кон- струкцию — двоичные числа. Синтаксис этих чисел можно описать следующими грам- матическими правилами: <двоичное_число> —> О I 1 • I <двоичное_число> О I <двоичное_число> 1 На рис. 3.10 показано дерево синтаксического анализа для выбранного в качестве примера двоичного числа 110. 1 Рис. 3.10. Дерево синтаксического анализа двоичного числа ПО Для описания двоичных чисел с использованием денотационной семантики и грамма- тических правил, указанных выше, их фактическое значение связывается с каждым пра- вилом, имеющим в своей правой части один терминальный символ. Объектами в данном случае являются десятичные числа. В нашем примере значащие объекты должны связываться с первыми двумя грамма- тическими правилами. Остальные два правила являются, в известном смысле, правилами вычислений, поскольку они объединяют терминальный символ, с которым может ассо- циироваться объект, с нетерминальным, который может представлять собой некоторую конструкцию. Если предположить, что процесс вычисления проходит вверх по дереву синтаксического анализа, то нетерминальный символ, находящийся в правой части, уже имел бы приписанное ему значение. В таком случае синтаксическое правило потребова- ло бы наличия функции, вычислившей значение левой части правила, которое, в свою очередь, представляет собой значение всей правой части. 3.6. Описание смысла программ: динамическая семантика 163
Пусть область определения семантических значений объектов представляет собой множество неотрицательных десятичных целых чисел N. Это именно те объекты, кото- рые мы хотим связать с двоичными числами. Семантическая функция Mbin отображает синтаксические объекты в объекты множества N согласно указанным выше грамматиче- ским правилам. Сама функция МЫп определяется следующим образом: МЫп(‘0’) = 0 МЫп(‘1’)=1 Мьт(<Двоичное_число> ‘0’) = 2 ♦ МЫп(<двоичное_число>) Мьш(<Двоичное_число> ‘ Г) = +2 * МЬ1П(<двоичное_число>) + I Отметим, что мы заключили синтаксические цифры в апострофы, чтобы отличать их от математических цифр. Отношение между этими категориями подобно отношениям между цифрами в кодировке ASCII и математическими цифрами. Когда программа счи- тывает число как строку, то прежде, чем это число сможет использоваться в программе, оно должно быть преобразовано в математическое число. Значения, или обозначенные объекты (которыми в данном случае являются десятич- ные числа), могут быть приписаны к узлам указанного выше дерева синтаксического анализа, образуя дерево, показанное на рис. 3.11. Это— синтаксически управляемая се- мантика. Синтаксические сущности отображаются в математические объекты, имеющие конкретное значение. 6 <двоичное_число> <двоичное_число> О <двоичное число> 1 Г 1 Рис. 3.11. Дерево синтаксического анали- за числа 110 с обозначенными объектами Приведем подобный пример для описания значения десятичных синтаксических ли- теральных констант, который понадобится нам в дальнейшем. <десятичное_число> —> 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <десятичное_число> (0|1|2|3|4|5|6|7|8|9) Денотационные отображения для этих синтаксических правил имеют следующий вид: Mdcc(‘O') = 0, Mdec(‘r), Mdcc(‘2\ ..., MU‘9’) = 9 Маес(<десятичное_число> ‘0’) = 10 ♦ Масс(<двоичное_число>) М<1ес(<десятичное_число> ‘Г) = 10 * Масс(<десятичное_число>) + 1 Маес(<десятичное_число> ‘9’) = 10 * Mdcc(<десятичное_число>) + 9 164 Глава 3. Описание синтаксиса и семантики
В следующих разделах мы представим денотационные семантики для нескольких простых конструкций. Важнейшим сделанным здесь упрощением является предположе- ние о том, что и синтаксис, и семантика конструкций корректны. Помимо этого, предпо- лагается, что существует только два скалярных типа — целый и булевский. 3.6.3.2. Состояние программы Денотационную семантику программы можно определить в терминах изменений со- стояния идеального компьютера. Подобным образом определялись операционные се- мантики. приблизительно так же определяются и денотационные. Правда, для простоты они определяются только в терминах значений всех переменных, объявленных в про- грамме. Операционная и денотационная семантики различаются тем. что изменения со- стояний в операционной семантике определяются запрограммированными алгоритмами, а в денотационной семантике они определяются строгими математическими функциями. Пусть состояние s программы определяется следующим набором упорядоченных пар: {<ii, vj>, <i2, v2>, ...,<in, vn>} Каждый параметр i является именем переменной, а соответствующие параметры v являются текущими значениями данных переменных. Любой из параметров v может иметь специальное значение undef, указывающее, что связанная с ним величина в дан- ный момент не определена. Пусть VARMAP — функция двух параметров, имени пере- менной и состояния программы. Значение функции VARMAP(ij, s) равно Vj (значение, со- ответствующее параметру ij в состоянии s). Большинство семантических функций ото- бражения для программ и программных конструкций отображают состояния в состояния. Эти изменения состояний используются для определения смысла программ и программных конструкций. Отметим, что такие языковые конструкции, как выражения, отображаются не в состояния, а в величины. 3.6.3.3. Выражения Выражения являются основой большинства языков программирования и не имеют побочных эффектов. Более того, мы имеем дело только с очень простыми выражениями. Единственными операторами являются операторы + и *; выражения могут содержать не более одного оператора; единственными операндами являются скалярные переменные и целочисленные литеральные константы; круглые скобки не используются; значение вы- ражения является целым числом. Ниже следует описание этих выражений в форме БНФ: <выражение> —> <десятичное_число> | <переменная> I<двоичное_выражение> <двоичное_выражение> —> <выражение_слева> <оператор> <выражение_справа> <оператор> —> + | * Единственной рассматриваемой нами ошибкой в выражениях является неопределен- ное значение переменной. Разумеется, могут появляться и другие ошибки, но большин- ство из них зависят от машины. Пусть Z— набор целых чисел, a error— ошибочное значение. Тогда множество Z u {error} является множеством значений, для которых вы- ражение может быть вычислено. Ниже приведена искомая функция отображения для данного выражения Ей состоя- ния s. Чтобы отличать определения математической функции от операторов присваива- 3.6. Описание смысла программ: динамическая семантика 165
ния языков программирования, для определения математических функций мы использо- вали символ Д= Ме(< выражение >,s) Д= case < выражение > of < десятичное число > => Mdcc(< десятичное_число >, s) < переменная > => if VARMAP (< переменная >, s) = undef then error else VARMAP(< переменная >, s) < двоичноевыражение > => iflMe(< двоичное_выражение >.< выражениеслева >, s) = undef OR Me(< двоичное выражение >.< выражениесправа >, s) = undef) then error else if (< двоичное_выражение >.< оператор > = *+’ then М€ (< двоичное выражение >.< выражение слева >, s) + Ме(< двоим ноевы раже ние >.< выражениесправа >,s) else Ме(< двоичное выражение >.< выражение_слева >,s) * Ме(< двоичное выражение >.< выражение справа >, s) 3.6.3.4. Операторы присваивания Оператор присваивания — это вычисление выражения плюс присваивание его значе- ния переменной, находящейся в левой части. Сказанное можно описать следующей функцией: Ма(х = Е, s) Д= if Ме(Е, s) = error then error else s' = {< i/, v/ >, < i2', v2' < in', vn' >} where forj = 1, 2,n, v/ - VARMAP(ij«, s) if ij <>x; Me(E, s) if ij = x Отметим, что два сравнения, выполняющиеся в двух последних строках (ij<> х и ii = х), относятся к именам, а не значениям. 3.6.3.5. Логически управляемые циклы с предварительной проверкой условия Денотационная семантика простого логически управляемого цикла обманчиво про- ста. Для простоты дальнейшего обсуждения предположим, что существуют две другие функции MS| и Мь, отображающие списки операторов в состояния, а булевские выраже- ния — в булевские значения (или значение error), соответственно. Эта функция имеет следующий вид: M|(while В do L, s) Д = if Mb(B, s) = undef then error else ifMb(B, s) = false then s else if Msj(L, s) = error then error else M|(while b do L, M$t(L, s)) 166 Глава 3. Описание синтаксиса и семантики
Значение цикла является просто значением переменных, объявленных в программе, после заданного количества выполнений операторов цикла (предполагается, что ошибок при этом не было). В сущности, цикл был превращен из итерации в рекурсию, причем контроль над последней математически определялся другими рекурсивными функциями отображений состояний. Математически строго описать рекурсию легче, чем итерацию. Это определение, подобно реальным циклам программ, может ничего не вычислять вследствие своей бесконечности. 3.6.3.6. Оценка Объекты и функции, подобные использованным в приведенных выше выражениях, могут определяться и для других синтаксических сущностей языков программирования. После определения полной системы для заданного языка ее можно использовать для оп- ределения смысла полных программ этого языка. Это создает основу для очень строгого способа мышления в программировании. Денотационная семантика может использоваться для разработки языка. Например, операторы, описать которые с помощью денотационной семантики трудно, могут ока- заться сложными и для понимания пользователями языка, и тогда разработчику* следует подумать об альтернативной конструкции. Значительное количество работ было посвящено возможности использования денота- ционных описаний языка для автоматического порождения компиляторов (Jones, 1980; Milos et al., 1984; Bodwin et al., 1982). Эти работы показали, что этот метод вполне осу- ществим, но он ни разу не использовался для порождения пригодного к использованию компилятора. С одной стороны, денотационные описания очень сложны, с другой — они дают ве- ликолепный метод краткого описания языка. Несмотря на то что использование денотационной семантики обычно приписывается Скотту и Стречи (Scott and Strachey, 1971), происхождение общего денотационного под- хода к описанию языка можно проследить до девятнадцатого века (Frege, 1892). Р е з ю м е Форма Бэкуса-Наура и контекстно-свободные грамматики эквивалентны метаязыкам, которые практически идеально подходят для описания синтаксиса языков программиро- вания. Точными методами описания являются не только они, но и деревья синтаксиче- ского анализа, которые вместе с порождающими действиями, образуют графическое представление основных синтаксических структур. Более того, они естественным обра- зом связаны с устройствами распознавания порождаемых ими языков, что приводит к относительно легкому построению синтаксических анализаторов, входящих в состав компиляторов этих языков. Графическим представлением грамматик являются синтаксические графы. Атрибутивная грамматика— описательный формализм, который может описывать как синтаксис, так и статическую семантику языка. Существуют три основных метода описания семантики языка: операционный, аксио- матический и денотационный. Операционная семантика является методом описания смысла языковых конструкций в терминах их воздействия на идеальную машину. Ак- сиоматическая семантика, основанная на формальной логике, была придумана как сред- Резюме 167
ство доказательства правильности программ. В денотационной семантике для выражения смысла языковых конструкций используются математические объекты. Преобразование языковых сущностей в математические объекты выполняется с помощью рекурсивных функций. Дополнигельнсй литература Описание синтаксиса с использованием контекстно-свободных грамматик и формы БНФ всесторонне рассмотрено в книге Кливленда и Узгалиса (Cleaveland and Uzgalis. 1976). Синтаксические графы были разработаны в компании Burroughs с целью ис- пользования их в качестве компактного описания синтаксиса языка ALGOL 60 в рам- ках проекта по разработке компилятора (Taylor et. al.. 1961). Позже они были моди- фицированы А.Шаи (A.Schai), руководителем вычислительного центра в Швейцар- ском федеральном технологическом институте (ЕТН) в Цюрихе. В печати эта модифицированная версия впервые появилась в виде книги Рутишаузера о языке ALGOL 60 (Rutishauser, 1967). Исследование аксиоматической семантики было начато Флойдом (Floyd, 1967) и позже развито Хоаром (Ноаге, 1969). С помощью этого метода Хоаром и Виртом (Hoare and Wirth, 1973) была описана семантика большей части языка Pascal. В число неописан- ных моментов входили побочные эффекты функций и операторы goto, поскольку эти объекты считались сложными для описания. Техника использования предусловий и постусловий во время разработки программ опи- сывалась (и пропагандировалась) Дийкстрой (Dijkstra, 1976), также они подробно рассмотрены в работе Гриса (Gries, 1981). Хорошими вводными курсами в денотационную семантику могут служить работы Гор- дона (Gordon, 1979) и Стоя (Stoy, 1977). Введение во все три метода семантического описания, представленные в данной главе, можно найти в книге Маркотти (Marcotty et al., 1976). Еще одним полезным справочником по большей части описанного мате- риала может служить книга Пагана (Pagan, 1981). Форма использованных в главе функций денотационной семантики подобна форме, использованной в книге Мейера (Meyer, 1990). В О П р О С bf 1. Дайте определение понятий “синтаксис" и “семантика". 2. Для кого создается описание языка? 3. Опишите работу обычного генератора языка. 4. Опишите работ)' обычного устройства распознавания языка. 5. Чем отличается “предложение" от “сентенциальной формы"? 6. Дайте определение леворекурсивного грамматического правила. 7. Какие три дополнения обычно делаются в формах РБНФ? 8. Опишите статическую и динамическую семантики. 9. Для чего в атрибутивной грамматике служат предикаты? 168 Глава 3. Описание синтаксиса и семантики
10. Чем различаются синтезированный и унаследованный атрибут? 11. Как определяется порядок вычисления атрибутов для деревьев с заданной атрибу- тивной грамматикой? 12. Назовите основное применение атрибутивной грамматики. 13. В чем заключается проблема операционной семантики при использовании про- граммно реализованного чистого интерпретатора? 14. Объясните смысл в аксиоматической семантике предусловий и постусловий данно- го оператора. 15. Опишите подход к использованию аксиоматической семантики для доказательства правильности данной программы. 16. Опишите основную концепцию денотационной семантики. 17. Чем отличается операционная семантика от денотационной? Упражнения 1. Двумя математическими моделями описания языка являются порождение и распо- знавание. Опишите, каким образом каждая из них может определять синтаксис языка программирования. 2. Создайте описания в форме РБНФ и синтаксического графа следующих объектов. 2.1. Оператор заголовка procedure языка Pascal. 2.2. Оператор вызова procedure языка Pascal. 2.3. Оператор switch языка С. 2.4. Определение union языка С. 2.5. Литеральные константы float языка С. 3. Используя грамматику, приведенную в листинге 3.2, продемонстрируйте дерево синтаксического анализа и левосторонний вывод для следующих выражений. 3.1. А := А * (В + (С * А) ) 3.2. В := С * (А * С + В) 3.3. А := А * (В + (С) ) 4. Используя грамматику, приведенную в листинге 3.4. продемонстрируйте дерево синтаксического анализа и левосторонний вывод для следующих выражений. 4.1. А : = (А + В) * С 4.2. А := В + С + А 4.3. А := А * (В + С) 4.4. А := В * (С * (А + В) ) 5. Докажите неоднозначность следующей грамматики: <S> -4 <А> Упражнения 169
<А> -» <А> + <А> | <идентификатор> <идентификатор> -4 а|Ь|с 6. Модифицируйте грамматику, приведенную в листинге 3.4, для введения унарного оператора вычитания, имеющего более высокий приоритет, чем операторы + и ♦. 7. Опишите на русском языке язык, определяемый следующей грамматикой: <S> -4 <А> <ВхС> <А> —> а <А>|а <В> —> b <В> I ь <C>-4c<C>jc 8. Рассмотрите следующую грамматику: <S> <А> а <В> b <А> -> <А> b I Ь <В> -> а <В> | а Какие из следующих предложений принадлежат к языку, генерируемому этой грамматикой? 8.1. baab. 8.2. bbbab. 8.3. bbaaaaa. 8.4. bbaab. 9. Рассмотрите следующую грамматику: <S> -» a <S> с <В> | <А> | b <А> -» с <А> | с <В> —>d|<А> Какие из следующих предложений принадлежат к языку, генерируемому этой грамматикой? 9.1. abed. 9.2. acccbd. 9.3. acccbcc. 9.4. acd. 9.5. accc. 10. Напишите грамматику для языка, состоящего из строк, имеющих и (п > 0) экземп- ляров буквы а, за которыми следует такое же число экземпляров буквы Ь. К этому языку, например, принадлежат строки ab, aaaaabbbb и aaaaaaaabbbbbbbb, но не при- надлежат строки a, abb. ba и aaabb. 11. Создайте деревья синтаксического анализа для предложений aabcc и aaabbbc, полу- ченных при использовании грамматики, описанной в упражнении 10. 170 Глава 3. Описание синтаксиса и семантики
12. Используя команды виртуальной машины, представленные в разделе 3.6.1.1. дайте в рамках операционной семантики определение следующих объектов. 12.1. Оператор repeat языка Pascal. 12.2. Оператор for-downto языка Pascal. 12.3. Оператор DO языка FORTRAN в форме DO N К = start, end, step. 12.4. Структура if-then-else языка Pascal. 12.5. Оператор for языка С. 12.6. Оператор switch языка С. 13. Найдите слабейшее предусловие для каждого из следующих присваиваний. 13.1. а:=2*(Ь-1)-1{а>0) 13.2. Ь := (с + 10) / 3 {Ь > 6} 13.3. а:=а + 2*Ь-1{а>1) 13.4. х:=2*у + х-1(х>11) 14. Найдите слабейшее предусловие для каждой последовательности присваиваний и относящихся к ним постусловий. 14.1. а := 2 * b + 1; b := а - {Ь < 0} 3 14.2. а : = 3 * (2 * b + а) ; Ь : = 2 * а - 1 {Ь > 5) 15. Напишите функцию отображения денотационной семантики для следующих опе- раторов. 15.1. Оператор for языка Pascal. 15.2. Оператор repeat языка Pascal. 15.3. Булевское выражения языка Pascal. 15.4. Оператор for языка С. 15.5. Оператор switch языка С. 16. В чем различие между внутренними и остальными синтезированными атрибутами? 17. Напишите атрибутивную грамматику, основанную на форме БНФ, приведенной в листинге 3.6 раздела 3.5.5, но со следующими правилами языка: типы данных в выражениях не могут смешиваться, но операторы присваивания не обязательно должны иметь один и тот же тип по обе стороны оператора присваивания. 18. Напишите атрибутивную грамматику, основанную на форме БНФ, аналогичной форме, приведенной в листинге 3.2, но с правилами типов, описанными в разде- ле 3.5.5. Упражнения 171
19. Докажите правильность следующей программы: {X = Vx and у = Vy} temp = х; х = у; у = temp; {X = Vy and у = Vx} 20. Докажите правильность следующей программы: {п > 0} count = п; sum = 0; while count о 0 do sum = sum + count; count = count - 1; end {sum = 1 + 2 + ... + n} 172 Глава 3. Описание синтаксиса и семантики
Имена, связывание, проверка типов и области видимости ВЙэ т о й главе'. Бьярн Страуструп (Bjame Stroustrup) Бьярн Страуструп, с 1979 года работавший в компании AT&T Labs, является создателем языка C++. Этот язык сделал объектно- ориентированное программиро- вание понятным и доступным разработчикам программного обеспечения, моделирующего объекты реального мира. В зна- чительной степени благодаря популярности языка С** в нача- ле 1990-х годов объектно-ориен- тированное программирование стало широко распространенной парадигмой. Страуструп являет- ся автором книги “The C++ Programming Language and The 4.1. Введение 4.2. Имена 4.3. Переменные 4.4. Концепция связывания 4.5. Проверка типов 4.6. Строгая типизация 4.7. Совместимость типов 4.8. Область видимости 4.9. Область видимости переменных и время их жизни 4.10. Среды ссылок 4.11. Именованные константы 4.12. Инициализация переменных Design and Evolution of C++". Имена, связывание, проверка типов и области видимости 173
В этой главе затрагиваются фундаментальные семантические проблемы, связан- ные с использованием переменных. Сначала освещается самая главная тема: природа имен и специальных слов в языках программирования. Затем рассматриваются атрибуты переменных, в том числе их тип, адрес и значение. Обсуждаются также альтер- нативные имена. Далее вводятся важные понятия связывания и времени связывания. Раз- личные варианты времени связывания атрибутов с переменными определяют четыре различные категории переменных. За их описанием следует основательный разбор про- верки типов, строгой типизации и правил совместимости типов. Затем описываются два способа обзора данных: динамический и статический, одновременно рассматривается концепция среды ссылок оператора. В заключение описываются именованные константы и способы инициализации переменных. 4.1. Введение Императивные языки программирования— это (в различной степени) абстракция компьютерной архитектуры фон Неймана, лежащей в их основе. Двумя основными ком- понентами этой архитектуры являются память, в которой хранятся данные и команды, и процессор, позволяющий изменять содержимое памяти. В языке программирования аб- стракциями ячеек памяти машины являются переменные. В некоторых случаях характе- ристики абстракций и ячеек очень близки; примером этому служит переменная целого типа, которая обычно точно представляется в виде отдельного слова аппаратной памятц. В других случаях абстракции довольно далеки от ячеек памяти. Так, для поддержания трехмерного массива требуется программная функция отображения. Охарактеризовать переменные можно с помощью набора свойств, или атрибутов, важнейшим из которых является тип — основное понятие в языках программирования. Изучение структуры типов данных в языке требует рассмотрения разнообразных вопро- сов. К важнейшим из них относятся область видимости и время жизни переменных. С этими вопросами связаны также проверка типов и инициализация. Чтобы разобраться в императивных языках программирования, необходимо изучить эти концепции. Еще одной важной частью структуры типов данных языка является совместимость типов. Далее в книге мы будем часто ссылаться на семейства языков как на один язык. На- пример, ссылаясь на язык FORTRAN, мы подразумеваем все его версии. Сказанное от- носится и к семействам языков Pascal и Ada. Упоминания о языке С включают исходную версию этого языка и язык ANSI С. Конкретную версию языка мы будем рассматривать, только если она отличается от остальных представителей семейства. 4.2. Имена Прежде чем начать обсуждение переменных, рассмотрим такой фундаментальный ат- рибут переменных, как имена, которые используются не только для именования пере- менных. Имена связаны с понятием меток, подпрограмм, формальных параметров и дру- гими программными конструкциями. Синонимом термина имя часто является термин идентификатор. 174 Глава 4. Имена, связывание, проверка типов и области видимости
4.2.1. Вопросы структуры Ниже приводятся основные вопросы, связанные с именами. Какова максимально допустимая длина имени? Может ли в именах использоваться соединительный символ? Зависят ли имена от регистра, в котором набраны буквы? Являются специальные слова зарезервированными или ключевыми? Эти вопросы обсуждаются в следующих двух разделах, в которые также включены примеры некоторых проектных решений. 4.2.2. Виды имен Имя (name) — это строка символов, используемая для идентификации некоторой сущ- ности в программе. В первых языках программирования использовались имена, состоящие только из одного символа. Это было естественно, поскольку ранние языки программирова- ния были в основном математическими, а математики давно использовали имена, состоя- щие из одного символа, для формального обозначения неизвестных параметров. Эта традиция была нарушена с появлением языка FORTRAN I, в котором в именах раз- решалось использовать до шести символов. Это ограничение длины имени шестью симво- лами сохранилось и в языке FORTRAN 77, но в языках FORTRAN 90 и С разрешенное ко- личество символов в именах увеличилось до 31; в языке Ada вообще нет ограничения на длину имени, и все имена являются значащими. В некоторых языках, например C++, также нет ограничения на длину имени, хотя иногда такое ограничение вводится создателями конкретных систем реализаций этих языков, чтобы таблица имен, в которой во время ком- пиляции хранятся идентификаторы, не была слишком большой и сложной. Общепринятым видом имени является строка с разумным ограничением длины (или без него), содержащая такие соединительные символы, как символ подчеркивания (_). Символ подчеркивания используется с той же целью, что и пробел в русских текстах, но при этом он не разрывает строку имени, содержащую его. Большинство современных языков программирования позволяют использовать в именах соединительные символы. В некоторых языках, особенно в языках С, C++ и Java, различаются прописные и строчные буквы; т.е. имена в этих языках зависят от регистра (case sensitive). Напри- мер, в языке C++ три следующих имени различны: rose, ROSE и Rose. В определенном смысле это значительно ухудшает читабельность, поскольку имена, внешне выглядящие очень похоже, на самом деле означают различные объекты. В этом отношении зависи- мость от регистра нарушает принцип проектирования, гласящий, что языковые конст- рукции, имеющие одинаковый вид, должны иметь одинаковый смысл. Разумеется, не все согласятся с утверждением, что зависимость от регистра — это не- удачное свойство имен. В языке С, например, зависимости от регистра можно избежать, используя исключительно имена, состоящие из строчных букв. Однако в языке Java по- добным образом проблемы избежать не удается, поскольку многие предопределенные имена содержат и прописные, и строчные буквы. Например, используемый в языке Java метод преобразования строки в целочисленное значение называется parselnt, а если написать его в виде Parselnt или parseint, то команда не будет распознана. Это проблема связана с легкостью создания программы, а не с ее читабельностью, поскольку необходимость запоминать необычные правила написания слов затрудняет создание 4.2. Имена 175
корректных программ. Она представляет собой разновидность нетерпимости части раз- работчиков языка, навязываемой компилятором. В версиях языка FORTRAN вплоть до версии FORTRAN 90 в именах могли исполь- зоваться только прописные буквы, что было излишним ограничением. Многие реализа- ции языка FORTRAN 77 (подобно языку FORTRAN 90) позволяют использовать строч- ные буквы — просто они переводятся в прописные для внутреннего использования во время компиляции. 4.2.3. Специальные слова В языках программирования специальные слова улучшают читабельность программ, называя выполняемые действия. Они также используются для обособления синтаксических сущностей программ. В большинстве языков программирования эти слова классифициру- ются как зарезервированные, но в некоторых они являются всего лишь ключевыми. Ключевое слово (keyword) имеет особое значение только в определенном контексте. Примером языка, в котором специальные слова являются ключевыми, служит язык FORTRAN. Если слово REAL языка FORTRAN встречается в начале предложения, а за ним следует имя, то это слово рассматривается как ключевое, указывающее на то, что данное выражение является оператором объявления переменной. В, то же время, если за словом REAL следует оператор присваивания, то это слово рассматривается как имя пе- ременной. Ниже проиллюстрированы эти варианты использования слова REAL: REAL APPLE REAL =3.4 Компиляторы языка FORTRAN и читатели его программ должны различать имена и специальные слова по контексту. Зарезервированное слово (reserved word) не может использоваться в качестве име- ни. При разработке языка лучше использовать зарезервированные слова, поскольку воз- можность переопределения ключевых слов может ухудшить читабельность. Например, в языке FORTRAN могут встретиться следующие операторы: INTEGER REAL REAL INTEGER Эти операторы объявляют, что переменная программы REAL имеет тип INTEGER, а пе- ременная INTEGER— тип REAL. Помимо странного вида этих операторов объявления переменных, появление слов REAL и INTEGER в качестве имен переменных может вве- сти читателя такой программы в заблуждение. В данной книге в примерах фрагментов программ зарезервированные слова выделя- ются жирным шрифтом. Многие языки содержат предопределенные имена, которые в некотором смысле явля- ются чем-то средним между зарезервированными словами и именами, определяемыми пользователем. Они имеют предопределенный смысл, но могут переопределяться пользо- вателем. Например, такие встроенные в язык Ada имена типов данных, как INTEGER и FLOAT, предопределены. Эти имена не зарезервированы: они могут переопределяться лю- бой программой на языке Ada. В языке Pascal предопределенные имена иногда называются стандартными идентификаторами. В этом языке обычные имена подпрограмм ввода- вывода, в том числе и названия подпрограмм readin и writein, предопределены. 176 Глава 4. Имена, связывание, проверка типов и области видимости
Определения предопределенных имен в языках Pascal и Ada должны быть видимы компиляторами этих языков, поскольку проверка типов в таких языках производится во время компиляции. В обоих языках приведенные выше примеры предопределенных имен видимы компилятором неявно. Другие предопределенные имена языка Ada, на- пример, стандартные подпрограммы ввода и вывода GET и PUT, сделаны видимыми явно с помощью оператора with, указанного пользователем. В языках С и C++ многие имена предопределены в библиотеках, используемых пользо- вательскими программами. Например, имена функций ввода и вывода языка С printf и scanf определяются в библиотеке stdio. Доступ компилятора к именам, предопределен- ным в библиотеках, возможен через соответствующие заголовочные файлы. 4.3. Переменные Переменная в программе представляет собой абстракцию ячейки памяти компьютера или совокупности таких ячеек. Программисты часто думают о переменных как об име- нах ячеек памяти, но переменная — это не только имя. Переход с машинных языков на языки ассемблера происходил в основном путем замены абсолютных числовых адресов ячеек памяти именами, при этом значительно повысилась читабельность программ, а значит, они стали более легкими для создания и эксплуатации. Этот шаг также позволил избежать возникновения проблем, связанных с абсолютной адресацией, поскольку транслятор преобразовывает имена в фактические адреса, которые сам выбирает. Переменные можно охарактеризовать шестеркой атрибутов (имя, адрес, значение, тип, время жизни, область видимости). Несмотря на то что такое описание может пока- заться слишком сложным для внешне простой концепции, оно позволяет ясно объяснить различные свойства переменных. Обсуждение атрибутов переменных приводит к исследованию важных концепций, свя- занных с этим вопросом: альтернативных имен, связывания, времени связывания, объявле- ний, проверки типов, строгой проверки типов, правил обзора данных и сред ссылок. Имя, адрес, тип и значение переменных рассматриваются в следующих подразделах. Вре- мя жизни и область видимости атрибутов описаны в разделах 4.4.3 и 4.8, соответственно. 4'ЗЛ. Имя Имена переменных — самые распространенные имена в программах. Они детально рассматривались в разделе 4.2 в общем контексте имен сущностей, из которых состоят программы. Большинство переменных имеют имена. Переменные, у которых нет имен, рассматриваются в разделе 4.4.3.3. Часто вместо слова “имя” используется слово “идентификатор”. 4.3.2. Адрес Адрес (address) переменной — это ячейка памяти, с которой связана данная перемен- ная. Эта связь не так проста, как может показаться на первый взгляд. Во многих языках одно и то же имя можно связать с разными адресами в разных местах программы и в разное время. Например, программа может иметь две подпрограммы subl и sub2, в каждой из которых определяется переменная с одним и тем же именем, скажем, пере- менная sum. Поскольку эти переменные не зависят друг от друга, то обращение к пере- 4.3. Переменные 177
менной sum в подпрограмме subl не связано с обращением к переменной sum подпро- граммы sub2. Подобным образом большинство языков позволяют связать одно имя с разными адресами в разные моменты времени выполнения программы. Например, ре- курсивно вызываемая подпрограмма содержит многочисленные версии каждой локально объявленной переменной, по одной на каждую активацию подпрограммы. Связывание переменных с адресами будет обсуждаться в разделе 4.4.3. Модель реализации подпро- грамм и их активаций для языков, подобных языку ALGOL, рассматривается в главе 9. Адрес переменной иногда называется ее левым значением (l-value), поскольку именно он нужен, если в левой части оператора присваивания указана переменная. 4.3.2.1. Альтернативные имена Иногда несколько идентификаторов одновременно ссылаются на один и тот же адрес. Если для доступа к отдельной ячейке памяти можно использовать несколько имен пере- менных, то эти имена называются альтернативными (aliases). Совмещение имен ухуд- шает читабельность программы, поскольку оно позволяет переменным изменять свои значения при присвоении их другим переменным. Например, если имена переменных А и В являются альтернативными, то любое изменение переменной А приводит к изменению переменной В, и наоборот. Человек, читающий программу, всегда должен помнить, что А и В — разные названия одной и той же ячейки памяти. Поскольку в программе может быть любое количество альтернативных имен, то на практике это создает серьезные трудности. Кроме того, совмещение имен затрудняет верификацию программы. Альтернативные имена могут создаваться в программе несколькими различными способами. В языке FORTRAN для этого используется оператор EQUIVALENCE. Они также могут создаваться с помощью вариантных записей в таких языках, как Pascal и Ada, или с помощью объединений в языках С и C++. Альтернативные имена, создавае- мые этими типами данных, экономят память, позволяя переменным разного типа исполь- зовать в разное время одни и те же ячейки памяти. Они также могут применяться для на- рушения правил работы с типами в некоторых языках, допускающих эту возможность. Подробно вариантные записи и объединения рассмотрены в главе 5. Две переменные указателя являются альтернативными, если они указывают на одну и ту же ячейку памяти. Это справедливо и для ссылок. Такая разновидность совмещения имен не экономит место, а просто является побочным эффектом природы указателей и ссылок. Если указатель в языке C++ адресует именованную переменную, то при разыме- новании указатель и имя переменной являются альтернативными именами. Эти и другие свойства указателей и ссылок рассмотрены в главе 5. Во многих языках программирования имена можно совмещать, используя параметры подпрограмм. Эта разновидность альтернативных имен рассмотрена в главе 8. Некоторых причин, оправдывавших использование альтернативных имен, уже не су- ществует. Когда языковая конструкция создает альтернативные имена для повторного использования памяти, их можно заменить схемой управления динамической памятью, позволяющей повторное использование памяти, но не обязательно создающей при этом альтернативные имена. Более того, современная память компьютера значительно боль- ше, чем была в те времена, когда разрабатывались языки, подобные языку FORTRAN, так что сейчас память — уже не такой дефицитный товар. Момент времени, когда переменная связывается с адресом, очень важен для понима- ния языков программирования. Подробнее этот вопрос рассмотрен в разделе 4.4.3. 178 Глава 4. Имена, связывание, проверка типов и области видимости
4.3.3. Тип Тип (type) переменной определяет диапазон значений, которые может иметь пере- менная, и набор операций, предусмотренных для переменных этого типа. Например, для типа INTEGER в некоторых реализациях языка FORTRAN устанавлен диапазон значений от -32 768 до 32 767 и определены арифметические операции сложения, вычитания, ум- ножения, деления и возведения в степень, а также некоторые библиотечные функции для выполнения других операций, например, вычисления абсолютной величины числа. 4.3.4. Значение Значение переменной — это содержимое ячейки или ячеек памяти, связанных с дан- ной переменной. Память компьютера удобно представлять себе в терминах абстракт- ных ячеек, а не физических. Ячейки, или отдельно адресуемые единицы, большинства современных компьютеров имеют размер, равный байту, как правило, содержащему во- семь битов. Этот размер слишком мал для большинства программных переменных. Мы будем считать, что абстрактная ячейка памяти имеет размер, достаточный для хранения связанной с ней переменной. Несмотря на то что числа с плавающей точкой в отдельной реализации конкретного языка могут занимать четыре физических байта, мы считаем, что число с плавающей точкой занимает одну абстрактную ячейку памяти. Мы полагаем, что величина любого элементарного неструктурированного типа занимает отдельную аб- страктную ячейку. С этого момента под ячейкой памяти мы будем подразумевать абст- рактную ячейку памяти. Значение переменной иногда называется ее правым значением (r-value), поскольку именно оно необходимо при использовании переменной, указанной в правой части опе- ратора присваивания. Для того чтобы получить доступ к правому значению переменной, вначале следует определить ее левое значение. Значительно усложнить этот процесс мо- гут, например, правила обзора данных, как это показано в разделе 4.8. 4.4. Концепция связывания В общем смысле, связывание (binding) представляет собой процесс установления связи, аналогичной существующей между атрибутом и объектом или между операцией и символом. Момент времени, когда эта связь устанавливается, называется временем свя- зывания (binding time). Связывание и время связывания — важные понятия семантики языков программирования. Связывание может происходить во время разработки или реализации языка; при компиляции, загрузке или выполнении программы. Звездочка (*), например, обычно связывается с операцией умножения во время разработки языка. Тип данных, например тип INTEGER в языке FORTRAN, связывается с диапазоном возмож- ных значений во время реализации языка. В языках С и Pascal переменная связывается с конкретным типом данных во время компиляции программы. Вызов библиотечной под- программы связывается с командами подпрограммы при компиляции. Переменная мо- жет связываться с ячейкой памяти при загрузке программы в память. Аналогичное свя- зывание в некоторых случаях не происходит вплоть до времени выполнения программы, например, для переменных, объявленных в подпрограммах языка Pascal, или функций языка С (если объявления не содержат спецификатор static). 4.4. Концепция связывания 17»
Рассмотрим следующий оператор присваивания языка С с указанным определением переменной count: int count; count = count + 5; Ниже приведены некоторые виды связывания и времен связывания для частей опера- тора присваивания. Множество возможных типов переменной count: связывание во время разработ- ки языка. Тип переменной count: связывание во время компиляции. Множество возможных значений переменной count: связывание при разработке компилятора. Значение переменной count: связывание во время выполнения указанного оператора. Множество возможных значений функционального символа +: связывание во время разработки языка. Смысл функционального символа + в данном операторе: связывание во время компиляции. Внутреннее представление константы 5: связывание во время разработки компи- лятора. Дтя того чтобы разобраться в семантике языка программирования, необходимо понять, что такое время связывания арибутов с сущностями программы. Например, необходимо знать, как при вызове подпрограммы фактические параметры связываются с формальными параметрами в ее определении. Для того чтобы определить текущее значение переменной, нужно узнать, когда данная переменная была связана с ячейкой памяти. 4.4.1. Связывание атрибутов с переменными Связывание называется статическим (static), если оно выполняется до выполнения программы и не меняется во время ее выполнения. Если связывание происходит во вре- мя выполнения программы или может меняться в ходе ее выполнения, то оно называется динамическим (dynamic). Физическое связывание переменной с ячейкой в среде вирту- альной памяти — сложный процесс, поскольку страница или сегмент адресного про- странства, в котором находится ячейка, во время выполнения программы может много- кратно загружаться и выгружаться из памяти. В некотором смысле такие переменные многократно связываются и открепляются. Такие связи, однако, поддерживаются аппа- ратным обеспечением компьютера, а программе и пользователю эти изменения не вид- ны. Поскольку это не представляет интереса для обсуждения, мы не фокусируем внима- ние на аппаратном связывании. Главным моментом для нас являются различия между статическим и динамическим связываниями. 4.4.2. Связывание типов Прежде чем к переменной программы можно будет обращаться, она должна быть свя- зана с типом данных. При этом необходимо рассмотреть два важных аспекта этого связы- вания: каким образом указывается этот тип и когда происходит связывание. Типы могут определяться статически с помощью некоторой формы явного или неявного объявления. 180 Глава 4. Имена, связывание, проверка типов и области видимости
4.4.2.1. Объявление переменных Явное объявление (explicit declaration)— это оператор программы, перечисляющий имена переменных и устанавливающий, что они имеют определенный тип. Неявное объявление (impicit declaration) — это средство связывания переменных с типами по- средством принятых по умолчанию соглашений, а не операторов объявления. В этом случае первое появление имени переменной в программе является ее неявным объявле- нием. И явное, и неявное объявления создают статические связи с типами. Большинство языков программирования, созданных с середины 1960-х годов, требу- ют явного*объявления всех переменных (двумя исключениями являются языки Perl и ML). Некоторые широко используемые языки, первоначальные работы по созданию ко- торых были проведены в конце 1960-х годов, допускают неявные объявления перемен- ных, особенно это касается языков FORTRAN. PL/I и BASIC. Например, в программах на языке FORTRAN идентификатор, для которого не было выполнено явного объявле- ния, объявляется неявно в соответствии со следующим соглашением: если идентифика- тор начинается с одной из букв I, J. К, L. М или N, то неявно объявлено, что он имеет тип INTEGER; в противном случае неявно объявлено, что он имеет тип REAL. Неявные объявления могут нанести серьезный ущерб надежности программы, по- скольку они препятствуют выявлению на этапе компиляции различных опечаток или программистских ошибок. Переменные, которые программистом были случайно остав- лены необъявленными, получат типы по умолчанию и будут иметь неожиданные атрибу- ты, что может вызвать неявные ошибки, которые трудно обнаружить. Некоторых проблем, связанных с неявными объявлениями, можно избежать, требуя, чтобы имена отдельных типов начинались с конкретных специальных символов. Напри- мер, в языке Perl все имена, начинающиеся с символа $. являются скалярными величи- нами, которые могут быть строкой или числом. Если имя начинается с символа 0. то именуемая им сущность является массивом; если имя начинается с символа %, то оно обозначает хешированную структуру (hash structure). Подобный подход создает различ- ные пространства имен для различных типов переменных. При таком сценарии имена @apple и Я apple не связаны между собой, поскольку принадлежат различным про- странствам имен. Более того, тип переменной определяется ее именем. В языках С и C++ необходимо различать объявления и определения. Объявления ус- танавливают типы и другие атрибуты, но не приводят к распределению памяти. Опреде- ления устанавливают атрибуты и вызывают распределение памяти. Для конкретного имени в программе на языке С может содержаться любое количество согласованных объявлений, но только одно определение. В языке С можно объявлять переменные, внешние по отношению к функции. Объявление указывает компилятору тип переменной и то, что она где-то была определена. Эта идея переносится на функции языков С и C++, в которых прототипы объявляют имена и интерфейсы, но не команды функций. С другой стороны, определения функций являются полными. 4.4.2.2. Динамическое связывание типов При динамическом связывании в операторе объявления тип не указывается. Вместо этого переменная связывается с типом при присвоении ей значения оператором присваи- вания. При выполнении оператора присваивания переменная, которой присваивается значение, связывается с типом переменной, выражения или значения, находящегося в правой части оператора присваивания. 4.4. Концепция связывания 181
Языки, в которых типы связываются динамически, значительно отличаются от языков со статическим связыванием типов. Основным преимуществом динамического связыва- ния переменных с типом является то, что оно обеспечивает значительную гибкость про- граммирования. Например, программу обработки таблицы данных в языке, использую- щем динамическое связывание типов, можно написать в виде настраиваемой программы. Это означает, что программа сможет работать с данными любого типа. Любой тип вход- ных данных будет приемлемым, поскольку переменные, предназначенные для их хране- ния, после ввода этих данных будут связываться с соответствующим типом во время присваивания. В отличие от динамического статическое связывание типов не позволяет написать на языках С или Pascal программу обработки таблицы данных без уточнения типа данных. В языках APL и SNOBOL4 связывание переменных с типом происходит динамиче- ски. Например, в программе на языке APL может содержаться следующий оператор: LIST <- 10.2 5.1 0.0 Независимо от предыдущего типа переменной LIST в результате этого присваивания она станет обозначать одномерный массив длины 3, содержащий числа с плавающей точкой. Если оператор LIST <- 47 будет выполнен после написанного выше присваивания, то переменная LIST станет це- лочисленной скалярной переменной. У динамического связывания типов есть два недостатка. Во-первых, поскольку по обе стороны оператора присваивания могут находиться величины двух любых типов, то воз- можность обнаружения компилятором ошибок снижается по сравнению с языками со статическим связыванием типов. Неверные типы в правой стороне оператора присваива- ния не будут расценены как ошибки; вместо этого просто произойдет изменение типа ле- вой стороны оператора присваивания на этот неверный тип. Предположим, что в кон- кретной программе i и х — целочисленные переменные, а у — массив, содержащий числа с плавающей точкой. Предположим также, что в программе необходим оператор присваивания i := х Однако при наборе оператор был записан в виде i := у В языке с динамическим связыванием типов ни система компиляции, ни система под- держки выполнения программ не обнаружат ошибку. Тип переменной i просто будет изменен на тип массива, содержащего числа с плавающей точкой. Поскольку вместо правильной перемененной х была использована переменная у, результаты программы будут ошибочными. В языке со статическим связыванием типов компилятор обнаружит ошибку, и программа не будет выполнена. Отметим, что этот недостаток до некоторой степени присутствует и в таких языках, использующих статическое связывание типов, как FORTRAN, С и C++, которые во мно- гих случаях автоматически преобразовывают тип правой части оператора присваивания в тип его левой части. 182 Глава 4. Имена, связывание, проверка типов и области видимости
Другим недостатком динамического связывания типов является его цена, которая весьма значительна, особенно во время выполнения. Именно в это время должна произ- водиться проверка типов. Более того, каждая переменная должна содержать дескриптор, связанный с нею, для запоминания текущего типа. Память, используемая для хранения переменной, должна быть переменного размера, поскольку значения различных типов требуют различных объемов памяти. Языки, имеющие динамическое связывание типов переменных, часто реализовыва- ются с помощью интерпретаторов, а не компиляторов. Это происходит отчасти из-за сложности динамического изменения типов переменных в машинных кодах. Более того, время, необходимое для динамического связывания типов, перекрывается общим време- нем интерпретации, так что в этой среде динамическое связывание кажется более деше- вым. С другой стороны, языки со статическим связыванием типов редко реализуются с помощью интерпретаторов, поскольку программы, написанные на этих языках, легко могут транслироваться в эффективные версии в машинных кодах. 4.4.2.3. Логический вывод типа Относительно недавно был разработан язык ML, поддерживающий как функциональ- ное, так и императивное программирование (Miller et al., 1990). Этот язык использует интересный механизм логического вывода типа, в котором типы большинства выраже- ний могут определяться без участия программиста. Например, объявление функции fun circumf(r) = 3.14159 * г * г; определяет функцию, аргумент и результат которой имеют действительный тип. Их тип логически выводится из типа константы, входящей в выражение. Подобным образом в функции fun timeslO(x) = 10 * х; логически выводится целый тип аргумента и значения функции. Система языка ML отвергнет функцию fun square(х) = х * х; Это происходит потому, что тип оператора * определить невозможно. В подобных слу- чаях программист может дать системе подсказку, подобную указанной ниже, в которой функция устанавливается как имеющая тип int. fun square(х) : int = х * х; Факта, что значение функции указано целочисленным, достаточно для логического вывода, что аргумент также является целым. Вполне дозволены также следующие опре- деления: fun square(х : int) = х * х; fun square(х) = (х : int) * х; fun square(х) = х * (х : int); Логический вывод типа также используется в чисто функциональных языках про- граммирования Miranda и Haskell. 4.4. Концепция связывания 183
4.4.3. Связывание переменных с ячейками памяти и время их жизни Основные свойства языка программирования в значительной степени определяются разработкой способов связывания ячеек памяти с переменными, которые в них хранятся Из этого следует важность четкого понимания этих связей. Ячейки памяти, с которыми связываются переменные, каким-то образом должны из- влекаться из пула доступной памяти. Этот процесс называется размещением в памяти (allocation). Удаление из памяти (deallocation)— это процесс помещения ячейки памя- ти. открепленной от переменной, обратно в пул доступной памяти. (Следует обратить внимание на то. что с ячейкой и переменной, хранящейся в этой ячейке, производятся прямо противоположные действия. Для связывания с некоей переменной свободная ячейка извлекается из пула свободной памяти, а переменная помещается в эту ячейку, 1 е. размещается в памяти. При разрыве связи между переменной и ячейкой переменная удаляется из памяти, а ячейка возвращается обратно в пул свободной памяти. По этой причине обычно термины размещение и удаление относятся к переменной, а не к ячей- ке. — Прим, ред.) Время жизни переменной — это время, в течение которого переменная связана с оп- ределенной ячейкой памяти. Таким образом, время жизни переменной начинается при ее связывании с определенной ячейкой памяти и заканчивается при ее откреплении от этой ячейки. Для изучения связывания памяти с переменными удобно разделить скалярные (неструктурированные) переменные на четыре категории согласно их временам жизни. Мы назовем эти категории статическими (static), автоматическими (stack-dynamic), яв- ными динамическими (explicit heap-dynamic) и неявными динамическими переменными (implicit heap-dynamic). В следующих разделах рассматриваются эти четыре категории, в том числе их цели, достоинства и недостатки. 4.4.3.1. Статические переменные Статическими называются переменные, которые связываются с ячейкой памяти до начала выполнения программы и остаются связанными с той же самой ячейкой памяти вплоть до прекращения выполнения программы. Переменные, которые статически свя- зываются с памятью, имеют несколько полезных применений в программировании. Оче- видно. что глобальные переменные часто используются на всем протяжении программы, чго делает необходимым их привязку к одному месту памяти в течение всего времени выполнения программы. Иногда бывает удобно, чтобы переменные, объявляемые в под- программах. зависели от предыстории (history sensitive), т.е. сохраняли свое значение между отдельными выполнениями подпрограммы. Это как раз и является характеристи- кой переменной, статически связанной с памятью. Другим плюсом статических переменных является их эффективность. Вся адресация статических переменных может быть прямой, тогда как другие типы переменных часто требуют более медленной косвенной адресации. Более того, на размещение статических переменных в памяти и удаление их из памяти в процессе выполнения программы не за- трачивается дополнительное время. Одним из недостатков статического связывания с памятью является уменьшение гиб- кости; в частности, в языках, имеющих только статические переменные, не поддержива- ются рекурсивные подпрограммы. Еще одним недостатком является невозможность со- вместного использования памяти несколькими переменными. Предположим, что в про- 184 Глава 4. Имена, связывание, проверка типов и области видимости
грамме есть две подпрограммы, причем обеим нужны большие, не связанные между со* бой массивы. Если они являются статическими, то память, в которой они хранятся, нель- зя использовать совместно. В языках FORTRAN I. II и IV все переменные были статическими. Языки С. С*+ и Java позволяют программисту включать в определение локальных переменных специфи- катор static, делая их статическими. В языке Pascal статические переменные не пре- дусмотрены. 4.4.3.2. Автоматические переменные Автоматическими называются переменные, связывание памяти с которыми выпол- няется при обработке их операторов объявления, но типы которых связываются статиче- ски. Обработка (elaboration) такого объявления означает распределение памяти и вы- полнение процессов связывания, указанных в объявлении. Эти действия происходят при достижении фрагмента кода, с которым связано объявление, в процессе выполнения про- граммы. Следовательно, обработка происходит во время выполнения программы. На- пример. процедура языка Pascal состоит из раздела объявлений и раздела операторов. Раздел объявлений обрабатывается непосредственно перед началом выполнения раздела операторов, происходящего при вызове процедуры. Память для переменных, находя- щихся в разделе объявления, выделяется во время обработки объявлений и освобождает- ся после возврата процедурой управления вызывающему оператору. Как показывает их название, память автоматическим переменным выделяется из стека выполняемой про- граммы (run-time stack). Структура языка ALGOL 60 и последующих языков позволяет использовать рекур- сивные подпрограммы. Для того чтобы быть полезными, по крайней мере в большинстве случаев, рекурсивным подпрограммам требуется некоторая динамическая локальная па- мять для того, чтобы каждая активная копия рекурсивной подпрограммы имела свою собственную версию локальных переменных. Для удовлетворения этих требований ис- пользуются автоматические переменные. Даже при отсутствии рекурсии наличие дос- тупной для подпрограмм локальной памяти с динамическим стеком имеет свои положи- тельные стороны, поскольку для хранения локальных переменных все подпрограммы со- вместно используют одну область памяти. Недостатками автоматических переменных являются затраты времени на размещение в памяти и удаление из нее, а также то. что ло- кальные переменные не могут зависеть от предыстории. Языки FORTRAN 77 и FORTRAN 90 позволяют разработчикам систем их реализации ис- пользовать для локальных вычислений автоматические переменные, но содержат оператор SAVE list Этот оператор позволяет программисту указать, что некоторые или все перечисленные в списке list переменные в подпрограмме, содержащей данный оператор SAVE, будут статическими. В языках С и C++ локальные переменные по умолчанию являются автоматическими. В языках Pascal и Ada все определенные в подпрограммах нединамические переменные являются автоматическими. Все атрибуты, кроме памяти, статически связываются с автоматическими переменными. Исключения (для некоторых структурированных типов) рассматриваются в главе 5. Разме- щение в памяти и удаление из нее автоматических переменных описаны в главе 9. 4.4. Концепция связывания 185
4.4.3.3. Явные динамические переменные Явные динамические переменные— это безымянные (абстрактные) ячейки памяти, размещаемые и удаляемые с помощью явных команд периода выполнения, определяемых программистом. Обращаться к этим переменным можно только с помощью указателей и ссылок. Куча (heap) представляет собой набор ячеек памяти с крайне неорганизованной структурой, вызванной непредсказуемостью их использования. Явные динамические пере- менные создаются либо оператором (например в языках Ada и C++), либо вызовом преду- смотренной для этого системной подпрограммы (например в языке С). Существующий в языке C++ оператор распределения памяти new в качестве операн- да использует имя типа. При выполнении этого оператора создается явная динамическая переменная, имеющая тип операнда, и возвращается указатель на нее. Поскольку явная динамическая переменная связывается с типом во время компиляции, то это связывание является статическим. Тем не менее, подобные переменные связываются с некоторой ячейкой памяти во время их создания, т.е. при выполнении программы. Помимо подпрограмм или операторов для создания явных динамических перемен- ных, в некоторых языках есть средства их уничтожения. В качестве примера явных динамических переменных рассмотрим следующий фраг- мент программы на языке C++: int *intnode; intnode = new int /* связывает ячейку int */ delete intnode; /* освобождает ячейкуt на которую указывает указатель intnode */ В этом примере явная динамическая переменная, имеющая тип int, создается опера- тором new. Обращаться к этой переменной можно с помощью указателя intnode. За- тем переменная удаляется из памяти оператором delete. В объектно-ориентированном языке Java все данные, за исключением основных ска- лярных величин, являются объектами. Объекты языка Java представляют собой явные динамические объекты, и доступ к ним открывают ссылки. В языке Java нет способа яв- ного уничтожения динамических переменных; вместо этого используется неявная “сборка мусора”. Явные динамические переменные часто используются в таких динамических структу- рах, как связные списки и деревья, которым необходимо расти и/или сокращаться во время выполнения программы. Подобные структуры удобнее создавать с помощью ука- зателей или ссылок, а также явных динамических переменных. Недостатком явных динамических переменных является сложность корректного ис- пользования указателей и ссылок, а также стоимость ссылок на переменные, размещения в памяти и удаления из нее. Эти соображения, указатели и ссылки, а также методы реа- лизации явных динамических переменных подробно рассмотрены в главе 5. 4.4.3.4. Неявные динамические переменные Неявные динамические переменные связываются с ячейкой динамической памяти только при присваивании им значений. Все их атрибуты фактически связываются каж- дый раз при присвоении переменным некоторого значения. Эти переменные, в некото- ром смысле, — просто имена, приспособленные для любого использования, которое от 186 Глава 4. Имена, связывание, проверка типов и области видимости
них потребуется. Плюсом таких переменных является их высокая степень гибкости, по* зволяюшая писать крайне общие программы. Минусом являются затраты на поддержку во время выполнения программы всех динамических атрибутов, в число которых, поми* мо прочих, могут входить типы и диапазоны индексов массивов. Другим недостатком является потеря возможности обнаружения компилятором некоторых ошибок (см. раз- дел Д.4.2.2). В том же разделе приводятся примеры неявных динамических переменных в языке APL. Примеры этих переменных в языке ALGOL 68 даны в главе 2, где они назы- ваются массивами flex. 4.5. Проверка типов Для обсуждения проверки типов обобщим понятия операндов и операторов, чтобы включить в эти понятия подпрограммы и операторы присваивания. Мы будем считать подпрограммы операторами, операндами которых являются их параметры. Символ при- сваивания мы будем рассматривать как бинарный оператор, а операндами будут его це- левая переменная и выражение. Проверка типов (type checking) обеспечивает совместимость типов операндов опера- тора. Совместимым (compatible) типом называется тип, который либо изначально допус- кается для данного оператора, либо правила языка позволяют с помощью команд компиля- тора неявно преобразовать его в тип, допускаемый для данного оператора. Это автоматиче- ское преобразование называется приведением (coercion). Применение оператора к операнду неприемлемого типа называется ошибкой определения типа (type error). Если в языке все связывания переменных с типами являются статическими, то про- верка типов практически всегда может выполняться статически. Динамическое связыва- ние типов требует проверки типов во время выполнения программы, называющейся ди- намической проверкой типов. Некоторые языки, например APL и SNOBOL4, имеющие динамическое связывание типов, допускают только динамическую проверку типов. Намного лучше обнаружить ошибки во время компиляции, чем во время выполнения, поскольку исправление на ран- них стадиях, как правило, обходится дешевле. Платой за статическую проверку типов является снижение гибкости программирования. При этом разрешается использовать значительно меньше сокращений и уловок. Правда, подобные технические приемы це- нятся сейчас не очень высоко. Проверка типов осложняется, если язык позволяет хранить в ячейке памяти в разное время выполнения программы величины разных типов. Это можно сделать, например, с помощью вариантных записей языков Ada и Pascal, оператора EQUIVALENCE языка FORTRAN и объединений языков С и C++. В этих случаях проверка типов, если она производится, должна быть динамической, кроме того, она требует, чтобы система под- держки выполнения программ хранила типы текущих значений, записанных в таких ячейках памяти. Таким образом, даже если в языках, подобных С и Pascal, все перемен- ные статически связаны с типами, не все ошибки определения типа будут выявлены при статической проверке типов. 4.5. Проверка типов 187
4.6. Строгая типизация Одной из новых идей в области структуры языков, проявившейся во время так назы- ваемой структурной революции в программировании 1970-х годов, является строгая типизация (strong typing). Строгая типизация общепризнанна как крайне полезная кон- цепция. К сожалению, довольно часто она определяется неточно, а иногда в компьютер- ной литературе используется вообще без определения. Ниже следует простое, но неполное определение строго типизированного языка. В программах, написанных на таком языке, каждое имя имеет отдельный связанный с ним тип, причем этот тип известен во время компиляции. Сутью этого определения является то. что все типы связываются статически. Слабостью этого определения является игно- рирование в нем следующей возможности: хотя тип переменной и может быть известен, ячейка памяти, с которой связана эта переменная, в разное время может содержать вели- чины разных типов. Для того чтобы учесть эту возможность, мы определим строго ти- пизированный язык как такой, в котором всегда обнаруживаются ошибки типов. Для этого требуется, чтобы типы всех операндов могли определяться либо во время компи- ляции, либо во время выполнения. Важность строгой типизации заключается в возмож- ности выявления всех неправильных употреблений переменных, приводящих к ошибкам определения типов. Строго типизированные языки позволяют также обнаружить (в про- цессе выполнения) использование величин некорректных типов в переменных, которые могут содержать величины нескольких типов. Язык FORTRAN не принадлежит к строго типизированным языкам, поскольку отноше- ния между фактическими и формальными параметрами не подвергаются проверке соответ- ствия типов. Кроме того, использование оператора EQUIVALENCE между переменными различных типов позволяет переменной одного типа ссылаться на переменную другого ти- па. причем, если на одну из переменных, входящих в оператор EQUIVALENCE, ссылаются или присваивают ей значение, то система не может проверить тип этой величины. Провер- ка типов переменных, входящих в оператор EQUIVALENCE, фактически сводит на нет большинство достоинств этих переменных. Язык Pascal относится к почти строго типизированным языкам, причем этим “почти” он обязан своей структуре вариантных записей, поскольку последние позволяют опус- кать метку, содержащую текущий тип переменной и являющуюся средством проверки корректного типа величины. Вариантные записи и потенциальные проблемы, возникаю- щие при их использовании, рассматриваются в главе 5. Язык Ada также является почти строго типизированным языком. Ссылки на перемен- ные. входящие в вариантные записи, динамически проверяются на наличие величин кор- ректных типов. Это значительное усовершенствование по сравнению с языками Pascal и Modula-2, в которых проверка вообще невозможна и намного менее необходима. Однако язык Ada позволяет программистам нарушать правила проверки типов, вводя отдельное требование о временной отсрочке проверки для конкретного преобразования типов. Та- кая отстрочка возможна только при использовании библиотечной функции UNCHECKED_CONVERSION. Эта функция, версия которой может существовать для лю- бого типа данных, принимает переменную ее типа в качестве параметра и возвращает . року битов, представляющую собой текущее значение этой переменной. Действитель- го преобразования при этом не происходит; это просто средство извлечения значения переменной одного типа и использования его в качестве значения переменной другого 188 Глава 4. Имена, связывание, проверка типов и области видимости
типа. Это может оказаться полезным в определяемых пользователем операциях по раз- мещению переменных в памяти и освобождению памяти, при которых с адресами обра- щаются как с целыми числами, в то время как они должны использоваться как указатели. Поскольку в функции UNCHECKED-CONVERSION нет проверки типов, то за осмыслен- ное использование переменных, полученных из этой функции, ответственность несет программист. Те же функции, что и функция UNCHECKED-CONVERSION языка Ada. выполняет имеющаяся в языке Modula-З встроенная процедура LOOPHOLE. Языки С и C++ не относятся к строго типизированным языкам, поскольку в них до- пускается существование функций, тип параметров которых не проверяется. Более того, типы объединений этих языков также не проверяются. Строго типизированным языком является язык ML, правда с некоторыми отличиями от императивных языков. В языке ML есть переменные, все типы которых известны ста- тически или из объявлений, или из правил логического вывода типов, описанных в раз- деле 4.4.2.3. Язык Java, хотя он в значительной степени создан на основе языка C++, является строго типизированным в том же смысле, что и язык Ada. Типы могут приводиться явно, что может вызвать ошибку определения типа. Тем не менее, не существует неявных пу- тей, позволяющих ошибкам определения типа остаться незамеченными. Правила приведения типов языка значительно влияют на результат проверки типов. Например, выражения в языке Pascal являются строго типизированными. Несмотря на это, допускается использование арифметического оператора с одним операндом, являю- щимся числом с плавающей точкой (в языке Pascal называемым real) и одним целым операндом. Значение целого операнда приводится к виду числа с плавающей точкой, и в результате получается операция над числами с плавающей точкой. Обычно это — имен- но то, что задумывал программист. Тем не менее, это также приводит к потере одной из причин строгой типизации— обнаружения ошибок. Таким образом, приведение типов снижает результат строгой типизации. Языки, в которых широко используется приведе- ние типов, например FORTRAN, С и C++, значительно менее надежны, чем языки, в ко- торых приведение типов применяется нечасто, например Ada. В языке Java содержится в два раза меньше разновидностей приведений типов, чем в языке C++. Подробнее вопрос приведения типов освещается в главе 6. 4.7. Совместимость типов Понятие совместимости типов было определено при освещении вопроса проверки типов. В данном разделе мы рассмотрим различные правила совместимости типов. Структура существующих в языке правил совместимости типов важна, поскольку она влияет на структуру типов данных и операции, производимые над величинами этих ти- пов. Вероятно, важнейшим следствием того, что две переменные имеют совместимые типы, является то, что любой из них может быть присвоено значение другой. Существуют два различных вида совместимости типов: совместимость имен типов и совместимость структур типов. Совместимость имен типов (name type compatibility) означает, что две переменные имеют совместимые типы только в том случае, если они были объявлены в одном объявлении или в объявлении, использующем одно и то же имя типа. Совместимость структур типов (structure type compatibility) означает, что две пе- 4.7. Совместимость типов 189
ременные имеют совместимые типы в том случае, если у их типов одинаковые структу- ры. Существуют некоторые разновидности этих двух методов, и в большинстве языков используются комбинации различных способов. Совместимость имен типов легко реализуется, но крайне ограничивает программиста. При строгой интерпретации переменная, принадлежащая к ограниченному типу целых чисел, не будет совместимой с переменной, имеющей целый тип. Предположим, что в языке Pascal используется строгая совместимость типов имен, и рассмотрим следующий фрагмент программы: type indextype = 1..100; (ограниченный тип) var count : integer; index : indextype; Переменные count и index не будут совместимы; значение переменной count не сможет присваиваться переменной index, и наоборот. Другая проблема, связанная с совместимостью имен типов, возникает при передаче структурированного типа между подпрограммами через параметры. Такой тип должен определяться только один раз, глобально. Подпрограмма не может устанавливать тип подобных формальных параметров локально, как было в исходной версии языка Pascal. Совместимость структур типов значительно более гибкая, чем совместимость имен типов, но гораздо сложнее реализуется. При определении совместимости имен типов должны сравниваться только имена двух типов, а при использовании совместимости структур типов — целые структуры двух типов. Выполнить это сравнение не всегда лег- ко. (Рассмотрите, например, такую структуру данных, ссылающуюся на собственный тип, как связный список.) При этом может возникнуть еще один вопрос. Являются ли, например, два комбинированных или структурных типа совместимыми, если они имеют одинаковую структуру, но разные имена полей? Совместимы ли два одномерных масси- ва в программе на языке Pascal или Ada, если они содержат элементы одного типа, но различаются областью значений индекса: 0. . 10 и 1. . 11? Совместимы ли два перечис- лимых типа, если они содержат одинаковое число компонентов, но по-разному образо- вывают литеральные константы? Еще одной трудностью, связанной с совместимостью структур типов, является то, что она не признает различий между типами, имеющими одинаковую структуру. Рассмотрим следующее объявление, которое могло бы появиться в программе на языке Pascal: type Celsius = real; fahrenheit = real; Переменные указанных типов считаются совместимыми при проверке совместимости структур типов. Это позволяет им смешиваться в выражениях, что, очевидно, в данном случае нежелательно. Вообще, типы с различными именами, вероятнее всего, являются абстракциями различных категорий сущностей задачи, и не должны рассматриваться как эквивалентные. В исходном определении языка Pascal (Wirth, 1971) явно не устанавливается, когда должна использоваться совместимость структур типов, а когда— совместимость их имен. Это крайне вредно для мобильности программ, поскольку программа, корректная в одной системе реализации языка, не должна быть некорректной в другой. Стандарт язы- ка Pascal, созданный Международной организацией по стандартизации (ISO, 1982), явно 190 Глава 4. Имена, связывание, проверка типов и области видимости
устанавливает правила совместимости типов для данного языка, частично — по имени, частично — по структуре. В большинстве случаев используется структура, а имя приме- няется для формальных параметров и в некоторых других ситуациях. Рассмотрим, на- пример, следующие объявления: type typel = array [1..10] of integer; type2 = array [1..10] of integer; type3 = type2; В этом примере типы typel и type2 несовместимы, что свидетельствует об исполь- зовании совместимости структур типов. Более того, тип type2 совместим с типом type3, из чего можно заключить, что эквивалентность имен также не используется строго. Такая форма совместимости иногда называется эквивалентностью объявлений (declaration equivalence), поскольку при определении типа с помощью имени другого ти- па, оба они являются совместимыми, даже если они несовместимы по именам типов. В языке Ada используется совместимость имен типов, но при этом имеются две кон- струкции — подтипы и производные типы, — позволяющие устранить проблемы, возни- кающие при этом виде совместимости. Производным (derived) называется новый тип, основанный на некотором ранее определенном типе, с которым он несовместим, несмот- ря на то, что они имеют идентичную структуру. Производные типы наследуют все свой- ства родительских типов. Рассмотрим следующий пример: type Celsius is new FLOAT; type fahrenheit is new FLOAT; Переменные данных типов несовместимы, хотя и имеют идентичную структуру. Более того, переменные этих типов несовместимы ни с каким другим типом чисел с плавающей точкой. Исключением из правила являются только литеральные константы. Литеральная константа, например 3.0, имеет тип универсальных действительных чисел и совместима с любым типом чисел с плавающей точкой. Производные типы также могут содержать огра- ничения диапазона родительского типа, наследуя при этом все его операции. Подтип (subtype) в языке Ada — версия существующего типа с возможно ограничен- ным диапазоном. Подтип совместим с породившим его типом. Рассмотрим следующее объявление: subtype SMALL__TYPE is INTEGER range 0..99; Переменные, имещие тип SMALL_TYPE, совместимы с переменными, имеющими тип INTEGER. Правила совместимости типов в языке Ada более важны, чем соответствующие правила языков, в которых широко используется приведение типов. Например, два операнда, вхо- дящие в оператор сложения в языке С, могут иметь практически любую комбинацию чи- словых типов этого языка. Один из операндов при этом просто приводится к типу другого. Однако в языке Ada нет приведения типов операндов арифметического оператора. В языке С используется структурная эквивалентность для всех типов, за исключением структур (записи языка С) и объединений, для которых используется эквивалентность объявлений. Правда, если две структуры или объединения определяются в двух различ- ных файлах, используется эквивалентность типов структур. 4.7. Совместимость типов 191
В языке C++ используется эквивалентность имен. Отметим, что оператор typedef языков С и C++ не вводит новый тип. Он просто определяет новое имя для уже сущест- вующего типа. Во многих языках переменные могут объявляться без использования имен типа, соз- давая безымянные типы. Рассмотрим следующий пример из языка Ada: А : array (1..10) of INTEGER; В этом случае переменная А имеет безымянный, но неоднозначно определенный тип. В объявлении В : array (1..10) of INTEGER; переменные А и В будут принадлежать к безымянным, но различным и несовместимым типам, хотя они имеют идентичную структуру. Множественное объявление Q, D : array (1..10) of INTEGER; создаст два безымянных типа: один для переменной С, другой — для переменной D, не- совместимых между собой. Фактически эти объявления можно рассматривать как сле- дующие два объявления: С : array (1..10) of INTEGER; D : array (1..10) of INTEGER; Результатом этого будет несовместимость переменных С и D. Однако в объявлении type LIST_10 is array (1..10) of INTEGER; C, D ; LIST-10; переменные С и D будут совместимыми. Очевидно, что в языках, не позволяющих пользователям определять и называть типы, например FORTRAN и COBOL, эквивалентность имен использоваться не может. Возникновение таких объектно-ориентированных языков, как Java и C++, подняло вопрос о другом типе совместимости типов. Это — вопрос о совместимости объектов и его связи с иерархией наследования. Подробнее об этом рассказывается в главе 11. Совместимость типов в выражениях рассмотрена в главе 6; совместимость типов па- раметров подпрограмм описана в главе 8. 4.8. Область видимости Одной из важнейших характеристик переменных является область видимости. Об- ласть видимости (scope) переменных программы — это ряд операторов, в которых пе- ременная видима. Переменная является видимой (visible) в операторе, если к перемен- ной, входящей в оператор, можно обратиться. Правила обзора данных в языке определяют, как именно конкретное появление имени связано с переменной. В частности, правила обзора данных определяют, каким образом ссылки на переменные, объявленные вне выполняющейся в данный момент подпро- граммы или блока, связаны с их объявлениями и, вследствие этого, с их атрибутами (блоки рассматриваются в разделе 4.8.2). Следовательно, для написания или чтения про- грамм на данном языке необходимо полное знание этих правил. 192 Глава 4. Имена, связывание, проверка типов и области видимости
Как определялось в разделе 4.4.3.2. переменная является локальной в программной единице или блоке, если она там объявлена. (В данной главе мы считаем программными единицами главный программный модуль или подпрограммы. Единицы, подобные клас- сам языков C++ или Java, рассматриваются в главе 10.) Нелокальными переменными (nonlocal variables) программной единицы или блока называются переменные, которые видимы в этой программной единице или блоке, но не объявляются в них. 4.8.1. Статическая область видимости В языке ALGOL 60 был введен метод связывания имен с нелокальными переменными, названный статическим обзором данных (static scoping). Позже этот метод был позаимст- вован большинством императивных, а также многими неимперативными языками. Исполь- зование статического обзора данных получило свое название из-за возможности статиче- ского (т.е. до периода выполнения) определения области видимости любой переменной. Большинство отдельных статических областей видимости в императивных языках связаны с определениями программных единиц. Предположим, что все области видимо- сти связаны с программными единицами. В данной главе мы также будем полагать, что для обращения к нелокальным переменным в обсуждаемых языках используются только области видимости. Последнее не совсем справедливо даже для языков со статическим обзором данных, но такое предположение упрощает обсуждение. Дополнительные мето- ды обращения к нелокальным переменным рассматриваются в главе 8. Во многих языках подпрограммы создают собственные области видимости. Во всех распространенных языках со статическим обзором данных, за исключением языков С, C++, Java и FORTRAN, подпрограммы могут вкладываться в другие подпрограммы, что создает в программе иерархию областей видимости. Когда в языке, использующем статический обзор данных, компилятор обнаруживает переменную, ее атрибуты определяются путем поиска объявившего ее оператора. В язы- ках, использующих статический обзор данных, при наличии вложенных подпрограмм этот процесс протекает следующим образом. Предположим, что сделано обращение к переменной х подпрограммы subl. Соответствующее объявление вначале разыскивает- ся в объявлениях подпрограммы subl. Если для данной переменной объявления не най- дено, то поиск продолжается в объявлениях подпрограммы, объявившей подпрограмму subl. называемой статическим родителем (static parent) подпрограммы subl. Если объявление переменной х не найдено и в этой подпрограмме, то поиск продолжается в следующей внешней единице (в модуле, объявившем родителя подпрограммы subl) и так далее, пока не будет найдено объявление переменной х, или поиск в самом внешнем блоке не увенчается успехом. В последнем случае будет зафиксирована ошибка необъяв- ленной переменной. Статический родитель подпрограммы subl, его статический роди- тель и так далее вплоть до основной программы называются статическими предками (static ancestors) подпрограммы subl. Отметим, что методы реализации статического обзора данных, рассматриваемые в главе 9, значительно эффективнее, чем только что описанный процесс. Рассмотрим следующую процедуру языка Pascal: procedure big; var x : integer; procedure subl; begin { subl } 4.8. Область видимости 193
. . . X . . . end; { subl ) procedure sub2; var x : integer; begin { sub2 } end; { sub2 } begin { big } end; { big ) При использовании статического обзора данных ссылка на переменную х подпро- граммы subl относится к переменной х, объявленной в процедуре big. Это действи- тельно так, поскольку поиск объявления переменной х начался в той процедуре, в кото- рой встречается ссылка, но объявления этой переменной в ней найдено не было. Далее поиск продолжился в статическом родителе подпрограммы subl (подпрограмме big), в котором и было найдено объявление переменной х. Наличие предопределенных имен, рассмотренных в разделе 4.2.3, несколько затруд- няет описанный процесс. Иногда предопределенное имя подобно ключевому слову и может переопределяться пользователем. В таких случаях предопределенное имя исполь- зуется, только если пользовательская программа не содержит переопределения. В других случаях предопределенное имя может быть зарезервировано, что означает начало поиска значения данного имени в списке предопределенных имен, который выполняется даже до проверки объявлений локальной области видимости. В языках со статическим обзором данных объявления некоторых переменных могут быть скрыты от некоторых подпрограмм. Рассмотрим следующую скелетную программу на языке Pascal: program main; var x : integer; procedure subl; var x : integer; begin { subl } . . . x. . . end; { subl } begin {main} end. {main} Ссылка на переменную x в процедуре subl относится к объявившей переменную х процедуре subl. В этом случае переменная х программы main скрыта от команд про- цедуры subl. Вообще, объявление переменной эффективно скрывает любое объявление одноименной переменной, содержащейся во внешней области видимости. В языке Ada к переменным, скрытым от областей видимостей предков, можно полу- чить доступ с помощью селективных ссылок, содержащих имя области видимости пред- ка. В предыдущей программе, например, к переменной х процедуры subl можно обра- титься с помощью ссылки main. х. Несмотря на то что в языках С и C++ не разрешено использование подпрограмм, вложенных в определения других подпрограмм, глобальные переменные в этих языках есть. Эти переменные объявляются вне определения любой подпрограммы. Как и в язы- 194 Глава 4. Имена, связывание, проверка типов и области видимости
ке Pascal, локальные переменные могут скрывать эти глобальные переменные. В языке C++ к таким скрытым глобальным переменным можно обращаться с помощью операто- ра доступа (: :). Например, если переменная х является глобальной переменной, скры- той в подпрограмме локальной переменной х. то обратиться к глобальной переменной можно в форме : : х. 4.8.2. Блоки Многие языки позволяют создавать новые статические области видимости во время выполнения программы. Эта мощная концепция, впервые появившаяся в языке ALGOL 60, позволяет фрагменту7 программы иметь собственные локальные переменные с минимизированной областью видимости. Подобные переменные, как правило, являют- ся автоматическими, так что память выделяется им в начале выполнения фрагмента про- граммы, а освобождается по окончании его выполнения. Подобный фрагмент программ- ного кода получил название блока (block). Как показано ниже, в языке Ada блоки задаются оператором declare: declare TEMP : integer; begin TEMP := FIRST; FIRST := SECOND; SECOND := TEMP; end; От понятия “блок" произошло выражение язык с блочной структурой (block- structured language). Хотя языки Pascal и Modula-2 и называются языками с блочной структурой, они не имеют непроцедурных блоков. В языках С, С4 + и Java любой составной оператор (последовательности операторов, заключенной в фигурные скобки) может содержать объявления и таким образом опреде- лять новую область видимости. Такие составные операторы являются блоками. Напри- мер, если list — массив целых чисел, то можно написать следующий код: if (list [i] < list [ j]) { int temp; temp = list [i]; list[i] = list [ j]; 1i s t [ j] = t emp; } Области видимости, создаваемые блоками, трактуются точно так же, как области ви- димости, создаваемые подпрограммами. Обращения к переменным блока, не объявляе- мым в этом блоке, связываются с их объявлениями путем поиска по возрастающей во внешних областях. В языках C++ и Java определять переменные можно в любом месте функции. Если определение появляется не в начале функции, то область видимости данной переменной начинается с оператора определения и заканчивается концом функции. Оператор for языков C++ и Java позволяет определять переменные в выражениях, инициализирующих счетчики цикла. В ранних версиях языка C++ область видимости та- кой переменной начиналась с ее определения и заканчивалась в конце наименьшего бло- 4.8. Область видимости 195
ка, содержащего данную переменную. Впрочем, в предварительной стандартной версии область видимости таких переменных была ограничена телом цикла for, так же пос i \ - пили и в языке Java. Определения класса и метода в объектно-ориентированных языках программирова- ния также порождают вложенные статические области видимости. Этот вопрос рассмаг ривается в главе 11. 4.8.3. Оценка статического обзора данных Использование статических областей видимости представляет собой метод нелокаль- ного доступа, хорошо работающий во многих ситуациях. Впрочем, у этого метода есть и недостатки. Рассмотрим программу, скелетная структура которой показана на рис. 4.1. При этом будем считать, что все области видимости создаются определениями основной программы и процедур. Программа содержит общую область видимости для блока main, помимо этого су- ществуют две процедуры А и В, определяющие области видимости внутри блока main. Внутри процедуры А расположены области видимости процедур С и D. Внутри процеду- ры В находится область видимости процедуры Е. Мы предполагаем, что необходимое обращение к данным и процедурам определяется структурой рассматриваемой програм- мы. Требуемое обращение к процедуре имеет следующий вид: программа main может вызывать блоки А и В, блок А — блоки С и D, а блок В — блоки А и Е. Структуру программы удобно представить в виде дерева, каждый узел которого представляет процедуру и, следовательно, область видимости. Представление програм- мы, показанной на рис. 4.1, изображено на рис. 4.2. Структура этой программы может показаться весьма естественной организацией программы, отчетливо отражающей структурные требования. Впрочем, изображенный на рис. 4.3 граф возможных вызовов процедур этой программы показывает, что возможно значительно большее количество вызовов, чем необходимо. Рис. 4.1. Структура программы Рис. 4.2. Древовидная структура программы, показанной на рис. 4.1 196 Глава 4. Имена, связывание, проверка типов и области видимости
На рис. 4.4 показаны требуемые вызовы рассматриваемой программы. Различия меж- ду рис. 4.3 и 4.4 показывают число возможных, а не необходимых вызовов. Программист может по ошибке вызвать подпрограмму, вызов которой не должен до- пускаться, причем это действие не будет расценено компилятором как ошибка. В резуль- тате ошибка будет обнаружена только при выполнении программы, что может повысить стоимость ее исправления. Следовательно, доступ к процедурам должен быть ограничен только необходимыми процедурами. Рис, 4,3, Г&аф вызовов, возможных в Рис. 4.4. Граф вызовов, необходимых для программе, показанной на рис. 4.1 программы, изображенной на рис. 4.1 С этой проблемой связано слишком интенсивное обращение к данным. Например, все переменные, объявленные в основной программе, видимы для всех процедур, причем избежать этого невозможно. Следующий сценарий описывает другие проблемы, возникающие при использовании статического обзора данных. Предположим, что после разработки и тестирования про- граммы потребовалось изменить ее спецификацию. В частности, необходимо открыть процедуре Е доступ к некоторым переменным из области видимости процедуры D. Для того чтобы решить возникшую проблему, можно переместить процедуру Е внутрь облас- ти видимости процедуры D. Однако в этом случае процедура Е уже не сможет обращать- ся к области видимости процедуры В, что, по-видимому, требуется (иначе зачем она там находится?). Другим решением является перемещение переменных, определяемых в процедуре D и необходимых в процедуре Е, в блок main. Это позволит обращаться к этим переменным из любой процедуры, которых может оказаться больше, чем надо, что может спровоцировать некорректный доступ. Например, неверно написанный идентифи- катор процедуры может быть воспринят не как ошибка, а как ссылка на идентификатор в некоторой внешней области видимости. Предположим, что переменная, перемешенная в блок main, называется х и необходима в процедурах D и Е. Допустим, что переменная с таким же именем объявляется в процедуре А. Это приведет к сокрытию требуемой пере- менной х от ее изначального владельца — процедуры D. Последней проблемой, возникающей при перемещении объявления переменной х в блок main, является пагубное влияние этого действия на читабельность, выражающееся в том, что объявления переменных находятся слишком далеко от места их использования. 4.8. Область видимости 197
Проблемы, связанные с видимостью переменных при использовании статических об- ластей видимости, характерны и для обращений к подпрограммам. Предположим, что в программе, изображенной на рис. 4.2, вследствие некоторых изменений спецификации возникла потребность вызова процедурой Е процедуры D. Чтобы это стало возможным, следует вложить процедуру D непосредственно в блок main, предполагая, что эта проце- дура также требуется процедурам А или С. При этом перемещении процедура D теряет доступ к переменным, определенным в процедуре А. Этот способ решения проблемы, если использовать его часто, приведет к возникновению программ с длинными списками служебных процедур низкого уровня. Таким образом, нарушение правил статического обзора данных может привести к то- му, что структуры программ будут лишь отдаленно походить на оригинал, причем даже в тех областях программы, в которых изменения не проводились. Разработчиков поощря- ют использовать излишнее количество глобальных переменных. Все процедуры в конце концов окажутся вложенными в основную программу на одном и том же уровне. При этом будут использоваться не более глубокие уровни вложения, а глобальные перемен- ные. Кроме того, окончательная структура может быть неуклюжей, запутанной и не со- ответствовать основной структурной концепции. У статического обзора данных есть и другие недостатки, подробно рассмотренные в книге Clarke, Wileden and Wolf (1980). Для решения проблем, связанных со статическим обзором данных, во многих новейших языках программирования используется конструкция инкапсуляции, подробно рассмот- ренная в главе 8. 4.8.4. Динамические области видимости Область видимости переменных в таких языках, как APL, SNOBOL4 и ранние версии языка LISP, является динамической. Динамический обзор данных (dynamic scoping) опирается на последовательность вызова подпрограмм, а не на их пространственную взаимосвязь. Следовательно, область видимости можно определить только во время вы- полнения программы. Рассмотрим повторно процедуру big из раздела 4.8.1: procedure big; var х : integer; procedure subl; begin { subl ) ...x... end; { subl ) procedure sub2; var x : integer; begin { sub2 ) end; { sub2 } begin { big } end; { big } Предположим, что правила динамического обзора данных применимы к нелокальным ссылкам. Значение идентификатора х, к которому обращаются в подпрограмме subl, — динамическое, оно не может определяться во время компиляции. В зависимости от по- 198 Глава 4. Имена, связывание, проверка типов и области видимости
следовательности вызова это значение может относиться к переменной из предыдущего объявления переменной х. Для того чтобы определить корректное значение переменной х во время выполнения программы, можно начать его поиск среди локальных объявлений. Этим же способом начинается поиск и при статическом обзоре данных, правда, на этом их сходство закан- чивается. При неудачном завершении поиска среди локальных объявлений рассматрива- ются объявления динамического родителя, или вызывающей процедуры. Если объявле- ние переменной х не будет найдено и там, то поиск продолжается в динамическом роди- теле этой процедуры, и так далее, пока не будет найдено объявление переменной х. Если оно не будет найдено ни в одном из динамических предков, то это будет ошибкой перио- да выполнения программы (run-time error). Рассмотрим две различные последовательности вызовов процедуры subl в приве- денном выше примере. В первом случае процедура big обращается к процедуре sub2, которая вызывает процедуру subl. При этом поиск перейдет от локальной процедуры subl к вызывающей ее процедуре sub2, в которой находится объявление переменной х. Таким образом, в данном случае обращение к переменной х процедуры subl являет- ся обращением к переменной х, объявленной в процедуре sub2. Во втором случае про- цедура subl вызывается непосредственно из процедуры big. При этом динамическим родителем процедуры subl является процедура big, и обращение будет направлено к переменной х, объявленной в процедуре big. 4.8.5. Оценка динамического обзора данных Воздействие динамического обзора данных на программирование значительно. Кор- ректные атрибуты нелокальных переменных, видимые операторам программы, невоз- можно определить статически. Более того, такие переменные не всегда одинаковы. Опе- ратор подпрограммы, содержащей ссылки на нелокальные переменные, во время раз- личных вызовов этой подпрограммы может обращаться к различным нелокальным переменным. Некоторые проблемы программирования связаны непосредственно с дина- мическим обзором данных. Во-первых, в период между вызовом подпрограммы и ее завершением все локальные переменные этой подпрограммы видимы для всех выполняющихся подпрограмм, вне за- висимости от их буквальной близости. От такой общедоступности локальных перемен- ных нет защиты. Подпрограммы всегда выполняются в непосредственной среде вызы- вающей программы; следовательно, использование динамического обзора данных поро- ждает менее надежные программы, чем использование статического обзора. Второй проблемой, связанной с динамическим обзором данных, является невозмож- ность статической проверки типов при обращении к нелокальным переменным. Это про- исходит из-за невозможности статически определить объявление переменной, на кото- рую ссылаются как на нелокальную. Использование динамического обзора данных также затрудняет чтение программ, по- скольку для определения нелокальных переменных, на которые имеются ссылки, должна быть известна последовательность вызова подпрограмм. Если программу читает человек, то определить последнее практически невозможно. В заключение отметим, что обращение к нелокальным переменным в языках с дина- мическим обзором данных происходит значительно медленнее, чем то же обращение, но в языках со статическим обзором. Причины этого подробно описаны в главе 9. 4.8. Область видимости 199
С другой стороны, у динамического обзора данных есть и достоинства. В некоторых случаях параметры, передаваемые от одной подпрограммы к другой, являются простыми переменными, определяемыми в вызывающем модуле. В языках с динамическим обзо- ром данных не требуется передачи ни одной из этих переменных, поскольку все они не- явно видимы в вызываемой подпрограмме. Нетрудно понять причины более широкого распространения статического, а не динами- ческого обзора данных. Программы на языках со статическим обзором данных легче чи- тать, они надежнее и выполняются быстрее, чем аналогичные программы на языках, ис- пользующих динамический обзор. Именно по этим причинам в большинстве современных диалектов языка LISP динамический обзор данных был заменен статическим. Далее в гла- ве 9 рассматриваются методы реализации статического и динамического обзора данных. 4.9. Область видимости переменных и время их жизни Иногда область видимости и время жизни переменных оказываются связанными между собой. Рассмотрим, например, переменную, объявленную в процедуре языка Pascal, не со- держащей вызовов подпрограмм. Область видимости такой переменной — от ее объявления до зарезервированного слова end в процедуре. Время жизни такой переменной начинается при входе в процедуру и заканчивается при достижении команды end (в языке Pascal нет оператора возврата). Хотя и очевидно, что область видимости и время жизни переменной не совпадают, поскольку статическая область видимости представляет собой буквальную, или пространственную, концепцию, а время жизни — концепция временная, в данном случае эти две концепции связаны между собой, или, по крайней мере, кажутся такими. В других случаях связь между областью видимости и временем жизни переменной не настолько очевидна. В языках С и C++, например, переменная, объявленная в* функции с использованием спецификатора static, статически связана с областью видимости этой функции и с памятью. Таким образом, ее область видимости статична и локальна по от- ношению к функции, но ее время жизни распространяется на все выполнение програм- мы, частью которой она является. Область видимости и время жизни не связаны также при вызовах подпрограмм. Рас- смотрим следующую функцию языка C++: void printheader() { } /* конец функции printheader*/ void compute{) { int sum; printheader() ; ) /* конец функции compute */ Область видимости переменной sum полностью находится внутри функции compute. Она не распространяется на тело функции printheader, хотя эта функция выполняется в середине функции compute. Тем не менее, время жизни переменной sum распространяется на время выполнения функции printheader. Какая бы ячейка памя- ти не была выделена переменной sum до вызова функции printheader, эта связь со- храняется во время и после выполнения этой функции. 200 Глава 4. Имена, связывание, проверка типов и области видимости
4.10. Среды ссылок Средой ссылок (referencing environment) оператора называется совокупность всех имен, видимых в данном операторе. Среда ссылок оператора в языках со статическим обзором данных состоит из переменных, объявленных в его локальной области видимо- сти, и совокупности всех видимых переменных из областей видимости его предков. В таком языке среда ссылок оператора нужна во время компиляции оператора, поэтому можно создавать команды и структуры данных, позволяющие доступ к переменным из других областей видимости во время выполнения программы. Методы реализации сред ссылок на нелокальные переменные в языках, использующих статический и динамиче- ский обзор данных, рассматриваются в главе 9. В языке Pascal, в котором области видимости создаются исключительно определе- ниями процедур, среда ссылок оператора содержит локальные переменные, все перемен- ные, объявленные в процедурах, содержащих данный оператор, а также переменные, объявленные в основной программе (за исключением переменных в нелокальных облас- тях видимости, скрытых объявлениями ближних процедур). Каждое определение проце- дуры создает новую область видимости, а следовательно, и новую среду. Рассмотрим следующую скелетную программу на языке Pascal: program example; var a, b : integer; procedure subl; var x, у : integer; begin { subl } . . . <----------------1 end; { subl } procedure sub2; var x : integer; procedure sub3; var x : integer; begin { sub3 } . . . <----------------2 end; { sub3 } begin { sub2 } . , . <---------------3 end; { sub2 } begin { example } . . . <---------------4 end. { example } В указанных точках данная программа имеет следующие среды ссылок: Точка Среда ссылок 1 переменные х и у процедуры subl, переменные а и b программы example 2 переменная х процедуры sub3, (переменная а процедуры sub2 скрыта), переменные а и b программы example 4.10. Среды ссылок 201
3 переменная х процедуры sub2, переменные а и b программы example 4 переменные а и b программы example Рассмотрим теперь объявления переменных в скелетной программе. Отметим, во- первых, что, хотя область видимости процедуры subl и выше уровнем (она менее глубоко вложена), чем область видимости процедуры sub3, область видимости процедуры subl не является статическим предком процедуры sub3, поэтому процедура sub3 не имеет доступа к переменным, объявленным в процедуре subl. Для этого существует важная причина. Переменные, объявленные в процедуре subl, являются автоматическими, поэто- му они не связаны с памятью в то время, когда процедура subl не выполняется. Посколь- ку процедура sub3 может выполняться в то время, когда процедура subl не выполняет- ся, она не должна иметь доступа к переменным процедуры subl, которые не обязательно должны связываться с ячейками памяти во время выполнения процедуры sub3. Подпрограмма называется активной, если ее выполнение началось, но еще не за- вершилось. Среда ссылок оператора в языке с динамическим обзором данных состоит из локально объявленных переменных и переменных всех других активных на данный мо- мент подпрограмм. Повторимся, некоторые переменные активных подпрограмм могут быть скрыты от среды ссылок. Новые активации подпрограммы могут содержать объяв- ления переменных, скрывающих переменные с теми же именами в предыдущих актива- циях подпрограммы. Рассмотрим следующую программу. Предположим, что единственными возможными вызовами функций являются следующие: функция main вызывает функцию sub2, кото- рая вызывает функцию subl. void subl() { int a , b; . . . <-------------1 ) /* конец функции subl */ void sub2() { int b, c; • . . <-------------2 subl; } /* конец функции sub2 */ void main() { int c, d; . . . <-------------3 sub2(); ) /* конец функции main */ В указанных точках данная программа имеет следующие среды ссылок: Точка Среда ссылок 1 переменные а и b процедуры subl, переменная с процедуры sub2, переменная d функции main (переменная с функции main и переменная b процедуры sub2 скрыты) 2 переменные b и с процедуры sub2, переменная d блока main (переменная с функции main скрыта) 3 переменные с и d функции main 202 Глава 4. Имена, связывание, проверка типов и области видимости
4.11. Именованные константы Именованной константой (named constant) называется переменная, связываемая со своим значением только во время связывания ее с ячейкой памяти: значение именован* ной константы невозможно изменить оператором присваивания или ввода. Данный объ* ект помогает улучшить читабельность программы: значительно удобнее, например, ис- пользовать имя pi, а не константу 3.14159. Другим полезным применением именованных констант являются программы, обра- батывающее заданное количество данных, например, 100. В подобных программах во многих местах, как правило, используется константа 100 для объявления диапазонов значений индексов массивов, пределов изменения счетчиков цикла и т.п. Рассмотрим следующую скелетную программу на языке Pascal: program example; type intarray = array [1..100] of integer; realarray = array [1..100] of real; begin { example ) for index := 1 to 100 do begin end; for count := 1 to 100 do begin end; average := sum div 100; end. {example} Для того чтобы эту программу модифицировать для работы с другим количеством значений, следует обнаружить все места, в которых написана цифра 100. и все числа за- менить новыми. В большой программе это может оказаться трудоемким и подвержен- ным ошибкам процессом. Более легким и надежным методом является использование именованной константы: program example; count listlen = 100; type intarray = array [1..listlen] of integer; realarray = array [1..listlen] of real; begin { example } for index := 1 to listlen do begin 4.11. Именованные константы 203
end; for count := 1 to listlen do begin end; average := sum div listlen; end. {example} Если в такой программе потребуется изменить количество данных, то изменять при- дется только одну строку, вне зависимости от количества упоминаний константы 100 в программе. Вот еще один пример преимущества абстракции: имя listlen является аб- стракцией для количества элементов некоторых массивов и количества повторений не- которых циклов. Приведенный пример иллюстрирует, как именованные константы могут облегчить модификацию программы. Объявление именованных констант в языке Pascal требует просто наличия некоторого значения справа от оператора =. Вместе с тем, языки Modula-2 и FORTRAN 90 позволя- ют использовать константные выражения, которые могут содержать предварительно объявленные именованные константы, постоянные величины и операторы. Язык Pascal ограничивается только константами, а язык Modula-2 — константными выражениями, поскольку в этих языках используется статическое связывание величин с именованными константами. Именованные константы в языках со статическим связыванием величин иногда еще называются манифестными константами (manifest constants). Языки Ada, C++ и Java допускают динамическое связывание величин с именованны- ми константами. Это позволяет в объявлениях присваивать константам выражения, со- держащие переменные. Например, оператор МАХ : constant integer := 2 * WIDTH + 1 языка Ada объявляет переменную МАХ именованной константой целого типа, значение которой соответствует значению выражения 2 * WIDTH + 1, причем значение пере- менной WIDTH должно быть видимым при выделении памяти константе МАХ и связыва- нии этой константы со своим значением. Помимо этого, язык Ada допускает существо- вание именованных констант перечислимого и структурированного типов, рассматри- ваемых в главе 5. 4.12. Инициализация переменных Обсуждение связывания значений с именованными константами естественным обра- зом приводит к теме инициализации переменных, поскольку связывание величины с именованной константой является таким же процессом, только постоянным. В большинстве случаев удобно, чтобы переменные имели значения до начала выпол- нения программы или подпрограммы, в которой они объявляются. Связывание перемен- ной со значением во время связывания ее с ячейкой памяти называется инициализацией (initialization). Если переменная статически связана с ячейкой памяти, то связывание и инициализация происходят до выполнения программы. При динамическом же связыва- нии с памятью инициализация также является динамической. 204 Глава 4. Имена, связывание, проверка типов и области видимости
В языке FORTRAN начальные значения переменных могут устанавливаться в опера- торе DATA: REAL PI INTEGER SUM DATA SUM /0/, PI /3.14159/ В этом операторе переменная SUM инициализируется значением С. а переменная PI — значением 3.14159. В данном случае фактическая инициализация происходит во время компиляции. Как только начнется выполнение программы, переменные SUM и PI не бу- дут ничем отличаться от других переменных. Во многих языках исходные значения переменных могут задаваться в операторах объявления, как в объявлении языка Ada: SUM : INTEGER := С; Ни язык Pascal, ни язык Modula-2 не содержат иного способа инициализации переменных, кроме инициализации во время выполнения с помощью операторов при- сваивания. Вообще, инициализация статических переменных происходит только один раз, но для таких динамических переменных, как локальные переменные в процедуре языка Ada, она происходит каждый раз при распределении памяти. Р е з ю м е Форма имен в языке может воздействовать как на читабельность этого языка, так и на удобство его использования. Другим значительным конструкторским решением является взаимосвязь имен и специальных слов, которыми могут быть зарезервированные или ключевые слова. Охарактеризовать переменные можно шестеркой атрибутов: именем, адресом, значе- нием, типом, временем жизни и областью видимости. Альтернативными именами называются имена, связанные с одним адресом памяти. С точки зрения надежности они расцениваются как вредные, но полностью исключить их из языка трудно. Связывание — это установление связи между программ