Вступление
Тот, кто не будет применять новые лекарства, должен ожидать нового зла: ибо время - величайший новатор, и если время, конечно, изменит положение вещей к худшему, а мудрость и совет не изменят их к лучшему, каков будет конец? ---Френсис Бэкон
История
6 ноября 1986 года Морис Уилкс написал Никлаусу Вирту с предложением пересмотреть и стандартизировать язык Modula-2+ в качестве преемника Modula-2. Вирт благословил этот проект, и так родился комитет Модула-3.
На первом заседании комитет единогласно согласился придерживаться духа Modula-2, выбирая простые, безопасные, проверенные функции, а не экспериментируя с нашими собственными непроверенными идеями. Когда мы перешли к деталям, мы обнаружили, что добиться единодушия труднее.
Modula-3 поддерживает интерфейсы, объекты, универсальные шаблоны, легкие потоки управления ( "нити"), изоляцию небезопасного кода, сборку мусора, исключения и подтипы. Некоторые из наиболее проблемных функций Modula-2 были удалены, например, вариантные записи и встроенный числовой тип данных без знака. Modula-3 существенно проще других языков с сопоставимой мощностью.
Modula-3 во многом основан на Modula-2+, который был разработан в Системном исследовательском центре Digital Equipment Corporation и использовался для создания системы Topaz [ McJones89 , Rovner86 ]. Дизайн Modula-3 был совместным проектом Digital и Olivetti. Определение языка было опубликовано в августе 1988 года, и сразу после этого обе компании начали внедрять его. В январе 1989 года комитет пересмотрел формулировку, чтобы отразить опыт этих групп по внедрению. К публикации этой книги было внесено несколько окончательных исправлений.
Перспектива
Системное программирование сегодня, в большинстве случаев, выполняется на семействе языков BCPL, которое включает B, Bliss и C. Красота этих языков заключается в скромной стоимости, с которой они смогли сделать большой шаг вперед от языка ассемблера. Чтобы полностью оценить их, вы должны принять во внимание технические ограничения машин 1960-х годов. Какой язык, разработанный в 1980-х годах, имеет компилятор, который умещается в четыре тысячи 18-битных слов, как компилятор B Кена Томпсона для PDP-7? Самым успешным из этих языков был C, который к началу 1970-х годов почти полностью вытеснил язык ассемблера в системе Unix.
BCPL подобные языки легко эффективно реализовать по той же причине , что они являются привлекательными для скептических программистов на ассемблере: они представляют собой модель программирования, которая близка к целевой машине. Указатели идентифицируются с помощью массивов, а адресная арифметика применяется повсеместно. К сожалению, эта модель низкоуровневого программирования опасна по своей сути. Многие ошибки столь же ужасны, как и в машинном языке. Система типов скудна и выявляет достаточно причуд целевой машины, поэтому даже опытные и дисциплинированные программисты иногда пишут непереносимый код просто случайно. Самый современный язык в этом семействе, C++, обогатил C, добавив объекты; но он также отказался от главного достоинства C - простоты - без устранения худшего недостатка C - модели низкоуровневого программирования.
На другом полюсе находятся такие языки, как Lisp, ML, Smalltalk и CLU, модели программирования которых берут начало в математике. Лисп - это гибрид лямбда-исчисления и теории парной функции; ML происходит от теории полиморфных типов; Smalltalk из теории объектов и наследования; CLU из теории абстрактных типов данных. У этих языков есть красивые модели программирования, но их трудно реализовать эффективно, потому что единообразная обработка значений в модели программирования предполагает систему времени выполнения, в которой значения единообразно представлены указателями. Если разработчик не предпримет шагов, чтобы избежать этого, простейшее выражение n: = n + 1может потребовать выделения, поиска метода или того и другого. Хорошие реализации позволяют избежать большей части затрат, а языки этого семейства успешно используются для системного программирования. Но их общая предрасположенность к распределению в куче, а не к распределению стека сохраняется, и они не стали популярными среди системных программистов. Системы выполнения, необходимые для повышения эффективности этих языков, часто изолируют их в закрытых средах, которые не могут поддерживать программы, написанные на других языках. Если вы поклонник этих языков, вам может показаться, что Modula-3 излишне прагматичен; но все равно продолжайте читать и дайте нам шанс показать, что прагматические ограничения не исключают привлекательных решений.
Между крайностями BCPL и Lisp находится семейство языков Algol, современные представители которого включают Pascal, Ada, Modula-2 и Modula-3. У этих языков есть модели программирования, которые отражают технические ограничения машин с произвольным доступом, но скрывают детали любой конкретной машины. Они отказываются от красоты и математической симметрии семейства Lisp, чтобы сделать возможными эффективные реализации без особых уловок; у них также есть строгие системы типов, которые избегают большинства опасных и машинно-зависимых функций семейства BCPL.
В 1960-х годах в семействе Algol была тенденция к созданию функций для потока управления и структурирования данных. В 1970-х годах была тенденция к функциям, скрывающим информацию, таким как интерфейсы, непрозрачные типы и обобщения. Совсем недавно в семействе Algol появилась тенденция заимствовать тщательный отбор методов из семейств Lisp и BCPL. Эту тенденцию демонстрируют Modula-3, Oberon и Cedar, чтобы назвать три языка, на которых за последние несколько лет появились переносимые реализации.
Modula-3, Oberon и Cedar обеспечивают сборку мусора, ранее считавшуюся роскошью, доступной только в закрытых системах исполнения семейства Lisp. Но мир начинает понимать, что сборка мусора - единственный способ достичь адекватного уровня безопасности и что современные сборщики мусора могут работать в открытых средах выполнения.
В то же время эти три языка допускают небольшой набор небезопасных, машинно-зависимых операций, которые обычно ассоциируются с семейством BCPL. В Modula-3 небезопасные операции разрешены только в модулях, явно помеченных как небезопасные. Комбинация сборки мусора с явной изоляцией небезопасных функций дает язык, подходящий для программирования целых систем, от приложений самого высокого уровня до драйверов устройств самого низкого уровня.
Функции
Остальная часть введения - это обзор наиболее важного функционала Modula-3.
Интерфейсы
Одна из самых успешных особенностей Modula-2 - это предоставление явных интерфейсов между модулями. Интерфейсы сохранены, практически без изменений в Modula-3. Интерфейс модуля - это набор объявлений, раскрывающих открытые части модуля; вещи в модуле, которые не объявлены в интерфейсе, являются частными. Модуль импортирует интерфейсы, от которых он зависит, и экспортирует интерфейс (или, в Modula-3, интерфейсы), который он реализует.
Интерфейсы делают раздельную компиляцию типобезопасной; но несправедливо смотреть на них так ограниченно. Интерфейсы позволяют думать о больших системах, не держа в голове сразу всю систему.
Программисты, которые никогда не использовали интерфейсы в стиле Modula, склонны недооценивать их, замечая, например, что все, что можно сделать с интерфейсами, также можно сделать с помощью включаемых файлов в стиле C. Это упускает из виду главное: с помощью включаемых файлов можно сделать многое, чего нельзя сделать с помощью интерфейсов. Например, значение включаемого файла можно изменить, определив макросы в среде, в которую он включен. Включаемые файлы соблазняют программистов на ярлыки, выходящие за рамки абстракции. Чтобы большие программы были хорошо структурированы, вам понадобится либо сверхчеловеческая сила воли, либо надлежащая языковая поддержка интерфейсов.
Объекты
Чем лучше мы понимаем наши программы, тем больше строительные блоки мы используем для их структурирования. После инструкции - инструкция, после инструкции - процедура, после процедуры - интерфейс. Следующим шагом, похоже, будет абстрактный тип .
На теоретическом уровне абстрактный тип - это тип, определяемый спецификациями его операций, а не представлением его данных. В современных языках программирования значение абстрактного типа представлено «объектом», операции которого реализуются набором значений процедур, называемых «методами» объекта. Новый тип объекта может быть определен как подтип существующего типа, и в этом случае новый тип имеет все методы старого типа, а также, возможно, новые (наследование). Новый тип может предоставлять новые реализации для старых методов (переопределение).
Объекты были изобретены в середине шестидесятых дальновидными дизайнерами Simula [ Birtwistle ]. Объекты в Modula-3 очень похожи на объекты в Simula: они всегда являются ссылками, у них есть как поля данных, так и методы, и у них есть одиночное наследование, но не множественное наследование.
Небольшие примеры часто используются для разъяснения основной идеи: грузовик как подтип транспортного средства; прямоугольник как подтип многоугольника. Modula-3 нацелена на более крупные системы, которые иллюстрируют, как типы объектов обеспечивают структуру для больших программ. В Modula-3 основные усилия при проектировании сосредоточены на описании свойств одного абстрактного типа - потока символов, окна на экране. Затем кодируются десятки интерфейсов и модулей, которые обеспечивают полезные подтипы центральной абстракции. Абстрактный тип обеспечивает основу для целого семейства интерфейсов и модулей. Если центральная абстракция хорошо спроектирована, то полезные подтипы могут быть легко получены, а первоначальная стоимость дизайна окупится с лихвой.
Сочетание типов объектов с Modula-2 непрозрачными типами производит что-то новое: частично непрозрачный тип , где некоторые из полого объекта является видимыми в области видимости и другие скрыты. Поскольку у комитета не было опыта работы с частично непрозрачными типами, первая версия Модулы-3 строго ограничивала их; но после года опыта стало ясно, что они хороши, и язык был изменен с целью снятия ограничений.
Можно использовать объектно-ориентированные методы даже в языках, которые не были разработаны для их поддержки, путем явного выделения записей данных и наборов методов. Этот подход работает достаточно гладко, когда нет подтипов; однако наибольшее преимущество объектно-ориентированные методы обеспечивают выделение подтипов. Этот подход плохо работает, когда требуется разбиение на подтипы: либо вы выделяете записи данных для разных частей объекта индивидуально (что дорого и громоздко с точки зрения нотации), либо вы должны полагаться на непроверенные передачи типов, что небезопасно. Какой бы подход ни был выбран, все отношения подтипов находятся в голове программиста: только с объектно-ориентированным языком можно получить объектно-ориентированную статическую проверку типов.
Дженерики
Универсальный модуль - это шаблон, в котором некоторые из импортированных интерфейсов рассматриваются как формальные параметры, которые должны быть привязаны к фактическим интерфейсам при создании экземпляра универсального. Например, общий модуль хеш-таблицы может быть создан для создания таблиц целых чисел, таблиц текстовых строк или таблиц любого желаемого типа. Различные универсальные экземпляры компилируются независимо: исходная программа используется повторно, но скомпилированный код, как правило, будет отличаться для разных экземпляров.
Для упрощения универсальных шаблонов Modula-3 они ограничены уровнем модуля: общие процедуры и типы не существуют изолированно, а общие параметры должны быть целыми интерфейсами.
В том же духе простоты нет отдельной проверки типов, связанной с дженериками. Ожидается, что реализации будут расширять общий и проверять тип результата. Альтернативой могло бы быть изобретение системы полиморфных типов, достаточно гибкой, чтобы выражать ограничения на интерфейсы параметров, которые необходимы для компиляции общего тела. Это было достигнуто для ML и CLU, но еще не было достигнуто удовлетворительно в семействе языков Algol, где системы типов менее однородны. (Правила, связанные с дженериками Ada, на наш взгляд, слишком сложны.)
Потоки
Разделение вычислений на параллельные процессы (или потоки управления) - это фундаментальный метод разделения задач. Например, предположим, что вы программируете эмулятор терминала с мигающим курсором: наиболее удовлетворительный способ отделить код мигающего курсора от остальной части программы - сделать его отдельным потоком. Или предположим, что вы дополняете программу новым модулем, который обменивается данными по буферизованному каналу. Без потоков остальная часть программы будет заблокирована всякий раз, когда новый модуль блокируется в своем буфере, и, наоборот, новый модуль не сможет обслуживать буфер всякий раз, когда блокируется какая-либо другая часть программы. Если это неприемлемо (как это почти всегда), невозможно добавить новый модуль, не найдя и не изменив каждый оператор программы, который может блокироваться.Эти модификации разрушают структуру программы, вводя нежелательные зависимости между модулями, которые в противном случае были бы независимыми.
Эти положения для потоков в Модуле-2 являются слабыми, составляя по существу к сопрограммам. Мониторы Хоара [Hoare] - надежная основа для параллельного программирования. Мониторы использовались в Месе, где они работали хорошо; за исключением того, что требование, чтобы контролируемая структура данных представляла собой целый модуль, было утомительным. Например, часто бывает полезно, чтобы отслеживаемая структура данных была объектом, а не модулем. Меса ослабил это требование, немного изменил детали семантики примитива «Сигнал» Хоара и ввел примитив Broadcast для удобства [Lampson]. Примитивы Mesa были упрощены в конструкции Modula-2+, и результат оказался достаточно успешным, чтобы быть включенным без каких-либо существенных изменений в Modula-3.
Пакет нитей - это инструмент с очень острой кромкой. Распространенная ошибка программирования - доступ к общей переменной без получения необходимой блокировки. Это приводит к возникновению состояния гонки, которое может бездействовать во время тестирования и запускаться после отправки программы. Теоретическая работа по алгебре процессов породила надежды на то, что модель параллелизма рандеву может быть более безопасной, чем модель совместной памяти, но опыт с Ada, который принял рандеву, в лучшем случае дает двусмысленную поддержку этой надежде - Ада по-прежнему допускает общие переменные , и, видимо, они широко используются.
Безопасность
Функция языка небезопасна, если ее неправильное использование может повредить систему времени выполнения, так что дальнейшее выполнение программы не будет соответствовать семантике языка. Примером небезопасной функции является присвоение массива без проверки границ: если индекс выходит за границы, то произвольное местоположение может быть сбито, и адресное пространство может стать фатально поврежденным. Ошибка в безопасной программе может привести к прерыванию вычислений с сообщением об ошибке времени выполнения или к неправильному ответу, но не может привести к сбою вычислений из-за обломков битов.
Безопасные программы могут использовать одно и то же адресное пространство, каждая из которых защищена от повреждения ошибками других. Чтобы получить аналогичную защиту для небезопасных программ, необходимо разместить их в отдельных адресных пространствах. По мере того, как становятся доступными большие адресные пространства, и программисты используют их для создания тесно связанных приложений, безопасность становится все более важной.
К сожалению, как правило, невозможно программировать самые низкие уровни системы с полной безопасностью. Ни компилятор, ни исполняющая система не могут проверить правильность адреса шины для контроллера ввода-вывода, а также не могут ограничить последующий ущерб, если он недействителен. Это ставит разработчика языков перед дилеммой. Если он будет стремиться к безопасности, тогда код низкого уровня придется программировать на другом языке. Но если он примет небезопасные функции, то его гарантия безопасности повсюду аннулируется.
Языки семейства BCPL полны небезопасных функций; языки семейства Lisp обычно не имеют (или не задокументированы). В этой области Modula-3 следует примеру Cedar, приняв небольшое количество небезопасных функций, которые разрешены только в модулях, явно помеченных как небезопасные. В безопасном модуле компилятор предотвращает любые ошибки, которые могут повредить исполняющую систему; в небезопасном модуле ответственность за их предотвращение лежит на программисте.
Сборка мусора
Классическая небезопасная ошибка во время выполнения, чтобы освободить структуру данных, которая все еще достижима активными ссылками (или «висячими указателями»). Ошибка устанавливает бомбу замедленного действия, которая взрывается позже, когда хранилище повторно используется. Если, с другой стороны, программисту не удается освободить записи, которые стали недоступными, результатом будет «утечка памяти», и вычислительное пространство будет неограниченно расти. Проблемы из-за висящих указателей и утечек памяти, как правило, сохраняются долгое время после обнаружения и устранения других ошибок. Единственный надежный способ избежать этих проблем - автоматическое освобождение недоступного хранилища или сборка мусора.
Таким образом, Modula-3 предоставляет «отслеживаемые ссылки», которые похожи на указатели Modula-2, за исключением того, что хранилище, на которое они указывают, хранится в «отслеживаемой куче», где оно будет автоматически освобождено, когда все ссылки на него исчезнут.
Еще одно большое преимущество сборки мусора - упрощение интерфейсов. Без сборки мусора интерфейс должен указывать, несет ли клиент или реализация ответственность за освобождение каждой выделенной ссылки, а также условия, при которых это безопасно. Это может усложнить интерфейс. Например, Modula-3 поддерживает текстовые строки с помощью простого необходимого интерфейса Text , а не с помощью встроенного типа. Без сборки мусора этот подход не был бы таким привлекательным.
Новые усовершенствования в сборке мусора постоянно появляются на протяжении более двадцати лет, но их все еще сложно реализовать эффективно. Для многих программ время программирования, сэкономленное за счет упрощения интерфейсов и устранения утечек памяти и висящих указателей, делает сборку мусора выгодной сделкой, но нижние уровни системы могут быть не в состоянии себе это позволить. Например, в системе Topaz SRC часть операционной системы, которая управляет файлами и тяжелыми процессами, полагается на сборку мусора, а внутренний «кусок», реализующий виртуальную память и переключение контекста потоков, этого не делает. Практически все прикладные программы Topaz полагаются на сборку мусора.
Для программ, которые не могут позволить себе сборку мусора, Modula-3 предоставляет набор ссылочных типов, которые не отслеживаются сборщиком мусора. Во многих других отношениях отслеживаемые и неотслеживаемые ссылки ведут себя одинаково.
Исключения
Исключением является управление конструкцией , которая выходит много областей сразу. Возникновение исключения многократно выходит из активных областей до тех пор, пока для исключения не будет найден обработчик, и передает управление обработчику. Если обработчика нет, вычисление завершается каким-то системно-зависимым способом - например, при входе в отладчик.
Есть много аргументов за и против исключений, большинство из которых вращаются вокруг неубедительных вопросов стиля и вкуса. Один аргумент в их пользу, за которым стоит весомый опыт, заключается в том, что исключения - хороший способ справиться с любой ошибкой времени выполнения, которая обычно, но не обязательно, является фатальной. Если исключения недоступны, каждая процедура, которая может столкнуться с ошибкой времени выполнения, должна возвращать дополнительный код вызывающей стороне, чтобы определить, произошла ли ошибка. Это может быть неуклюже и имеет практический недостаток, заключающийся в том, что даже осторожные программисты могут случайно пропустить проверку кода возврата ошибки. Частота игнорирования возвращаемых кодов ошибок стала чем-то вроде постоянной шутки в мире Unix / C. Вызов исключения более надежен, поскольку он останавливает программу, если для нее нет явного обработчика.
Система типов
Как и все языки семейства Algol, Modula-3 строго типизирован. Основная идея строгой типизации - разделить пространство значений на типы, ограничить переменные для хранения значений одного типа и ограничить операции, применяемые к операндам фиксированных типов. На самом деле строгая типизация редко бывает такой простой. Например, каждое из следующих осложнений присутствует по крайней мере в одном языке семейства Algol: переменная типа [0..9] может быть безопасно присвоена INTEGER , но не наоборот (подтип). Такие операции, как абсолютное значение, могут применяться как к REAL, так и к INTEGER, а не к одному типу (перегрузка). Типы литералов (например, NIL) может быть неоднозначным. Тип выражения может определяться тем, как оно используется (целевое типирование). Несоответствие типов может вызвать автоматическое преобразование вместо ошибок (например, когда дробное действительное число округляется при присвоении целому числу).
Мы приняли несколько принципов, чтобы сделать систему типов Модулы-3 как можно более единообразной. Во-первых, нет неоднозначных типов или целевой типизации: тип каждого выражения определяется его подвыражениями, а не его использованием. Во-вторых, нет автоматических преобразований. В некоторых случаях представление значения изменяется при его назначении (например, при назначении упакованному полю типа записи), но само абстрактное значение передается без изменений. В-третьих, правила совместимости типов определены в терминах отношения одного подтипа. Отношение подтипа требуется для обработки объектов с помощью наследования, но оно также полезно для определения правил совместимости типов для обычных типов.
Простота
На заре проекта Ada генерал из Программного офиса Ada высказал мнение, что «очевидно, что Министерство обороны не заинтересовано в искусственно упрощенном языке, таком как Паскаль». Modula-3 представляет противоположную точку зрения. Мы использовали все уловки, которые могли найти или изобрести, чтобы упростить язык.
Чарльз Хоар предположил, что, как показывает опыт, язык слишком сложен, если его нельзя точно и разборчиво описать на пятидесяти страницах. Комитет Modula-3 поднял это до принципа дизайна: мы выделили для себя «бюджет сложности» в пятьдесят страниц и выбрали наиболее полезные функции, которые могли бы уместиться в рамках этого бюджета. В итоге бюджет был превышен на шесть строк плюс синтаксические уравнения. Эта политика несколько произвольна, но есть так много хороших идей в дизайне языков программирования, что кажется необходимым какой-то произвольный бюджет, чтобы язык не стал слишком сложным.
Оглядываясь назад, можно сказать, что особенности, которые сделали сокращение, были направлены на достижение двух основных целей. Интерфейсы, объекты, универсальные шаблоны и потоки предоставляют фундаментальные шаблоны абстракции, которые помогают структурировать большие программы. Изоляция небезопасного кода, сборка мусора и исключений помогают сделать программы более безопасными и надежными. Из методов, которые мы использовали для поддержания внутренней непротиворечивости языка, наиболее важным было определение чистой системы типов на основе отношения подтипов. Ни в одной из этих функций по отдельности нет особенной новизны, но в их сочетании есть простота и мощность.