Оригинал:
Язык, разработанный по заказу Министерства обороны США и названный в честь первой в мире программистки Ады Лавлейс, окружают много мифов и непонимания. Ты наверняка о нем слышал, но, скорее всего, это были мифы об устаревшем, сложном и медленном языке.
Однако ада активно используется для управления самолетами, поездами, космическими аппаратами и прочими интересными штуками. Давай посмотрим на язык без призмы мифов и разберемся, какую пользу мы можем из него извлечь, даже если пока не собираемся в космос.
Мифы об аде
Миф об устаревшем языке опровергается одним запросом к поисковику: последняя редакция вышла в 2012 году. Если судить о сложности языка по внешним признакам, то все тоже не так страшно: спецификация ады содержит чуть менее тысячи страниц, тогда как спецификация C++ - около 1400 страниц.
Миф о низкой производительности пошел со времен первой редакции 1983 года, когда массовому пользователю были доступны разве что ZX Spectrum и IBM PC с i8086, на которых любой современный язык был бы медленным. Ада компилируется в машинный код, и любители успешно пишут на ней для Arduino с ATmega328 и прочих микроконтроллеров.
Распространенный миф о том, что по вине ады упала ракета Ariane 5 в 1996 году, нужно рассмотреть отдельно. Ракета действительно упала из-за ошибки, но проблема была в другом: компьютер, который управлял траекторией полета, был взят из Ariane 4 без изменений, несмотря на то что Ariane 5 поддерживала более широкий диапазон траекторий.
Хуже того, проверка на выход значений за возможный диапазон была намеренно отключена, поэтому, когда навигационный компьютер выдал недопустимую с точки зрения Ariane 4 команду, закончилось все предсказуемо. От этой проблемы, увы, не смог бы защитить ни один язык или какое-либо программное решение вообще. Сама Ariane 4 совершила 113 успешных полетов из 116 за свою историю, а Ariane 5 уже 96 успешных из 101.
Языки и надежность программ
Ракеты — это предельный случай требований к надежности программ, но и в куда более приземленном коде ошибки могут обойтись пользователям очень дорого. В уязвимостях вроде
Ада разрабатывалась именно для написания надежных и безопасных программ. Когда говорят о безопасности, прежде всего думают, какие ограничения язык или другой инструмент накладывает на пользователя.
На мой взгляд, в первую очередь нужно говорить о том, какие выразительные средства инструмент дает разработчику, чтобы точно отразить объекты реального мира в коде и определить законы их взаимодействия. Наблюдение за выполнением этих законов лучше поручить компилятору — он не устает к концу рабочего дня.
В первую очередь, конечно, инструмент не должен делать работу человека сложнее, чем она и так есть. Когда Министерство обороны США разрабатывало требования к новому языку для конкурса, в котором победила ада, они в первую очередь упомянули об этом. Документ с требованиями известен как
«Одни и те же символы и ключевые слова не должны иметь разные значения в разном контексте». Почти вся первая часть рассказывает о необходимости однозначности синтаксиса, удобочитаемости кода, определенности семантики и поведения (вспомним i++ + ++i).
Но и требования к выразительным средствам для своего времени там передовые. Любопытно, что обработка исключений и средства обобщенного программирования были еще в первой редакции, задолго до С++.
Давай напишем первую несложную программу, а потом рассмотрим, какие средства ада предоставляет, чтобы точнее выразить в коде свои намерения.
Реализации
Далеко идти за реализацией не придется: компилятор ады включен в GCC под названием GNAT (GNU New [York University] Ada Translator) и доступен на всех системах, где есть GCC.
Если у тебя Linux или FreeBSD, можешь ставить из стандартных репозиториев. В Debian/Ubuntu пиши apt-get install gnat, в Fedora — dnf install gnat.
Компания
AdaCore является, по сути, основным разработчиком GNAT и распространяет две версии компилятора: сертифицированный GNAT Pro за деньги и GNAT Libre бесплатно, но с рантайм-библиотекой под лицензией GPLv3.
Есть еще проприетарные реализации ады вроде
Первая программа
Традиционный Hello world дает очень мало представления о языке, поэтому для первой программы мы возьмем что-нибудь более реалистичное, например алгоритм Пардо — Кнута. Дональд Кнут и Луис Трабб Пардо предложили его как раз для этой цели.
Мы немного усложним задачу и будем заодно проверять правильность ввода значения и запрашивать их заново, если ввод был некорректным. Вернее, на уровне системы типов ограничим диапазон допустимых значений и обработаем возникшие исключения.
Вот наша программа. Ее нужно будет сохранить в файл с названием pardo_knuth.adb. Несовпадение имени файла с именем основной процедуры, которая служит точкой входа, вызовет предупреждение компилятора.
Скомпилировать программу можно командой gnatmake pardo_knuth.adb. Созданный исполняемый файл будет называться pardo_knuth.
Замечу, что синтаксис ады нечувствителен к регистру символов. В стандартной библиотеке GNAT по каким-то причинам укоренился непривычный Смешанный_Регистр, и я следую стилю стандартной библиотеки. Но если кому-то он кажется неэстетичным, можно использовать любой другой по вкусу — на работу программ это не повлияет.
Заголовок программы
Перед началом программы находится комментарий. Все комментарии в аде однострочные и начинаются с символов --.
Программа начинается с подключения модулей. Теперь гибкими возможностями их подключения никого не удивишь, но ада была одним из первых языков, где такое стало возможно.
Ключевое слово use делает модули видимыми в пространстве имен программы. Например, после with Ada.Text_IO мы могли бы вызывать процедуру Ada.Text_IO.Put_Line по ее полному имени. Но мы использовали конструкцию use Ada.Text_IO, которая импортирует все публичные символы этого модуля в наше пространство имен, поэтому мы можем вызвать Put_Line, Put и New_Line без имени модуля.
Модули Ada.Strings.Unbounded и Ada.Text_IO.Unbounded_IO предназначены для работы со строками произвольной длины. По умолчанию строки в аде фиксированной длины, что не всегда удобно для обработки пользовательского ввода. Строки произвольной длины легко преобразовать в фиксированные, что нередко приходится делать, потому что многие функции стандартной библиотеки ожидают именно фиксированные.
Дальше мы определяем основную процедуру, которая служит точкой входа в нашу программу. Желательно, чтобы ее имя совпадало с именем файла, хотя и не обязательно. В аде есть различие между функциями и процедурами: функции могут возвращать значения, а процедуры — только изменять переменные, которые им передают в аргументах.
Внутри нашей процедуры мы переименовали модули с длинными именами для удобства, аналогично import foo as bar в Python. Теперь мы сможем вызывать, к примеру, Ada.Strings.Unbounded.To_String как US.To_String.
Алгоритм Пардо — Кнута требует сообщать пользователю о переполнении. Чтобы упростить появление переполнений, мы создали намеренно ограниченный тип-диапазон Small_Float, с возможными значениями от -100.0 до +100.0. Для таких типов у ады есть встроенные проверки: присвоение недопустимой константы в коде приведет к ошибке компиляции, а появление недопустимых значений в ходе работы программы вызовет исключение.
Строка «package Float_IO is new Ada.Text_IO.Float_IO (Small_Float)» заслуживает отдельного внимания. Это уже не простое переименование, а специализация обобщенного (generic) модуля для нашего типа Small_Float.
В аде нет аналога printf, что бывает очень неудобным, но для ее целей выглядит оправданным — типо безопасный форматированный вывод возможен только в языках с совершенно другой системой типов. В С printf("%d", "foo") вызовет segmentation fault, в Go аналогичная конструкция приведет к ошибке времени выполнения — ни то ни другое в критичной по надежности программе не принесет пользователю особой радости.
Поэтому хотя бы для относительного удобства авторы стандартной библиотеки ады написали несколько модулей для ввода и вывода значений распространенных типов.
Для алгоритма Пардо — Кнута нам требуется определенная пользователем функция. Функции и процедуры в аде могут быть вложенными, чем мы и воспользуемся и определим простейшую функцию Square в заголовке основной процедуры нашей программы. Тип возвращаемых значений указан с помощью return Small_Float. Это обязательная часть синтаксиса — еще раз отметим, что создать функцию, которая не возвращает значений, невозможно, для этой цели нужно использовать процедуры.
Далее идут объявления переменных. В аде все переменные должны быть объявлены перед использованием. Глобальных переменных в смысле C в ней нет, их область видимости всегда чем-то ограничена: пакетом (то есть модулем), процедурой, функцией или блоком declare, который мы рассмотрим позже. Видимые внутри всей процедуры переменные должны быть объявлены в ее заголовке, что мы и сделаем.
С помощью Input : Array (0 .. 10) of Small_Float мы объявляем массив Input, где будут храниться введенные пользователем значения. Мы используем диапазон индексов от 0 до 10, но вообще индексы могут быть любыми, в том числе отрицательными, например от -5 до 5. Еще вместо явных индексов можно было бы создать целочисленный тип-диапазон и сослаться на него.
Переменная Index, очевидно, будет использоваться как индекс элемента массива в цикле — мы могли бы объявить ее позже, но для демонстрации оставим ее здесь. Ее начальное значение — ноль, присваивается одновременно с объявлением переменной. Все присваивания в языке производятся с помощью оператора :=, оператор = используется только для проверки равенства.
Переменная Debug будет служить только для демонстрации условного оператора. Она имеет тип Boolean с возможными значениями True и False. Условный оператор в аде требует выражения логического типа и никакого другого, привычное в C-подобных языках if(0) вызовет ошибку — никакие неоднозначности, связанные с интерпретацией произвольных значений в логическом контексте, в этом языке возникнуть не могут.
Заголовок основной процедуры нашей программы наконец закончился — переходим к ее телу.
Тело программы
Мы начинаем программу с довольно глупой демонстрации условного оператора — выводим другое приветствие, если переменная Debug выставлена в True. Этого не произойдет, если не поменять ее начальное значение руками, но суть не в этом, а в синтаксисе.
Условный оператор имеет вид if <условие> then <высказывание> else <высказывание> end if. Часть про end if обязательна. Вообще, в аде почти нигде нельзя написать просто end, не указав, что именно здесь закончилось. Читать такой исходный код куда проще, хоть писать и дольше, но правильная настройка редактора решает эту проблему. В объявлении функции Square мы уже видели, что она кончается словами end Square, хоть и не заостряли на этом внимание.
Для циклов можно даже указать имена: цикл ввода переменных объявлен с помощью Input_Loop: while ... loop и закончен end loop Input_Loop. Перепутать границы вложенных циклов при таком подходе очень сложно. Синтаксис циклов радует своим единообразием — циклы с предусловием и циклы с параметром отличаются только словами перед ключевым словом loop.
Бесконечный цикл
Бесконечный цикл создать очень просто: не указывать ни тип, ни условия.
Циклов с постусловием в явном виде нет, но можно указать условие выхода внутри тела цикла:
Условие цикла — while Index <= Input'Last — требует некоторых пояснений. Мы могли бы явно указать максимальное значение индекса, но вместо этого мы используем атрибут нашего массива Last, возвращающий максимальное значение его индекса. Минимальное значение можно получить с помощью атрибута First, так что эту часть нудной работы компилятор делает за нас. Чуть позже мы остановимся на атрибутах подробнее.
Внутри цикла Input_Loop мы читаем переменные и помещаем их в наш массив Input. Специально для промежуточного хранения пользовательского ввода мы создадим локальную переменную Raw_Value. Поскольку объявлять переменные где придется в аде нельзя, нам потребуется блок declare, который создает отдельную область видимости.
Между declare и begin можно добавить любые объявления, которые мы могли бы поместить в заголовок процедуры. В нашем случае это единственная переменная Raw_Value : Ada.Strings.Unbounded.Unbounded_String (пакет, который мы для удобства переименовали в US). Это тип динамических строк неограниченной длины, которые мы читаем с помощью функции UIO.Get_Line.
В вычислениях с плавающей точкой от строковых данных никакой пользы, поэтому нам нужно конвертировать эти строки в числа. Мы могли бы использовать функции из пакета Float_IO или даже определить потоки ввода-вывода, но мы пойдем другим путем — используем функцию-атрибут нашего типа Small_Float.
Атрибуты — одна из самых необычных концепций ады. В общих чертах это ассоциированные с типами функции. Имя типа можно рассматривать как своеобразное пространство имен — атрибут отделяется от него апострофом. В данном случае мы используем атрибут Small_Float'Value, который представляет собой функцию от типа String и возвращает значения типа Small_Float.
Нашу динамическую строку мы для этого конвертируем в статическую, поэтому все выражение имеет вид Small_Float'Value (US.To_String (Raw_Value)).
Противоположность атрибуту Value — атрибут Image, который конвертирует значение в строку. Существует множество других атрибутов, например для получения размера значений типа в битах, но мы не будем их рассматривать — про них можно прочитать в документации.
Что произойдет, если ввод будет некорректным, например если пользователь введет foo вместо числа или число, выходящее за границы диапазона? Функция Small_Float'Value завершится с исключением Constraint_Error. Это исключение нужно обработать, что мы и делаем в конце блока declare, после ключевого слова exception. Эквивалента try ... catch в аде нет, обработку исключений можно производить только в конце процедур и функций или, если мы хотим обработать их внутри функции, в конце блоков declare.
После того как ввод от пользователя получен, остается только применить нашу функцию Square к каждому значению и вывести результаты. Для этого мы используем цикл с параметром. С помощью атрибута нашего массива Input'Range мы можем это сделать с не меньшим удобством, чем предоставляют итераторы в объектно ориентированных языках, — этот атрибут возвращает диапазон индексов.
Поскольку наш тип Small_Float ограничен, исключение Constraint_Error может возникнуть и при возведении значений в квадрат, если они выйдут за границы диапазона, поэтому мы обрабатываем их аналогичным образом в конце блока declare, в этот раз уже без объявления каких-либо переменных в его заголовке перед begin.
Выражение Float_IO.Put (Square (Input(I)), Exp => 0, Fore => 4, Aft => 2), очевидно, вызывает функцию для вывода значений с плавающей точкой. Аргументы Exp, Fore и Aft — это число знаков экспоненты, число знаков до точки и число знаков после точки соответственно.
По умолчанию используется научный формат вывода в стиле 1.2E2, но установка параметра Exp в ноль отключает это. Интересный момент: в аде любые параметры функций и процедур можно использовать как именованные. К примеру, нашу функцию Square мы могли бы вызывать с помощью Square (X => Input(I)).
Если порядок фактических параметров в вызове совпадает с порядком формальных параметров функции, то имена можно не указывать, но в Float_IO.Put мы использовали именованные параметры, чтобы поменять порядок, — вызов без именованных параметров выглядел бы так: Float_IO.Put (Square (X => Input(I)), 4, 2, 0).
Модули и обобщенное программирование
Обобщенное программирование
Мы начнем с простейшего примера обобщенного программирования, потому что оно не ограничивается модулями — обобщенными могут быть и отдельные процедуры и функции. Мы напишем функцию Do_Nothing, которая просто возвращает переданное ей значение в неизменном виде и может быть специализирована для любого типа.
Как видно, обобщенные конструкции создаются с помощью ключевого слова generic, у которого есть раздел объявлений типов. Тип T в данном случае — это тип-параметр, для которого наша процедура может быть специализирована. Затем мы описываем нашу функцию с использованием типа T. В function Do_Nothing_To_Integer is new Do_Nothing (Integer) она специализируется для типа Integer. Мы уже видели это ранее, когда подключали обобщенные пакеты из стандартной библиотеки.
Ключевое слово new, в отличие от многих других языков, здесь не имеет никакого отношения к объектам — объектов в традиционном понимании в аде нет, хотя инкапсуляция, наследование и полиморфизм есть, просто они в значительной степени отделены друг от друга и реализуются другими способами.
Модули
Модули, или пакеты, — встроенная и очень важная часть языка. Модули разделены на интерфейс и реализацию на уровне языка. Интерфейсы модулей хранятся в файлах с расширением .ads, а реализации — в файлах с расширением .adb.
Для демонстрации мы напишем модуль для работы с условной базой данных пользователей. У каждой учетной записи будут идентификатор, имя пользователя (строка до 255 символов) и флаг блокировки. Мы сделаем модуль обобщенным, чтобы в качестве идентификатора можно было использовать значения разных типов.
Кроме того, мы сделаем работу с типом учетных записей только через функции самого модуля.
Рассмотрим интерфейс нашего модуля, файл accounts.ads.
С помощью слова generic мы делаем его обобщенным, с типом-параметром Identifier_T. После слов package Accounts is идут описания публичных полей модуля, которые будут доступны при его импорте, а после ключевого слова private — описания закрытых полей.
Можно заметить, что тип User_Record описан дважды: первый раз в публичной части модуля как type User_Record is private и второй раз в его закрытой части как настоящий тип-запись. И вот почему: первое описание говорит, что тип User_Record — абстрактный и за пределами модуля будет известно только его имя, но не детали реализации. Это сделает невозможным прямой доступ к полям нашей записи для любых функций, которые не принадлежат модулю.
Более того, даже если мы знаем, как на самом деле устроен тип, создать значения типа Accounts.User_Record можно будет только с помощью функции Accounts.Create. Любые другие значения будут несовместимы с остальными функциями из него. Так реализуется инкапсуляция. Если бы мы включили описание типа в секцию private, но не включили type User_Record is private в публичную часть, этот тип был бы вовсе не виден за пределами модуля и воспользоваться им было бы невозможно.
Реализацию мы сделаем тривиальной, вот файл accounts.adb:
В завершение приведем простейшую программу с использованием нашего модуля, файл user_test.adb. Для сборки проекта достаточно положить все три файла в один каталог и выполнить команду gnatmake user_test.adb.
Заключение
Многие возможности языка остались за кадром, такие как наследование с помощью расширений типов или многозадачность. Заинтересованные читатели могут продолжить изучение языка по
Конечно, многие языки хорошо выглядят только на бумаге, но любые попытки их использования разбиваются о суровую реальность. В следующий раз мы рассмотрим историю создания небольшого проекта с открытым исходным кодом и ознакомимся с использованием ады на практике.
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Язык, разработанный по заказу Министерства обороны США и названный в честь первой в мире программистки Ады Лавлейс, окружают много мифов и непонимания. Ты наверняка о нем слышал, но, скорее всего, это были мифы об устаревшем, сложном и медленном языке.
Однако ада активно используется для управления самолетами, поездами, космическими аппаратами и прочими интересными штуками. Давай посмотрим на язык без призмы мифов и разберемся, какую пользу мы можем из него извлечь, даже если пока не собираемся в космос.
Несмотря на свое американское происхождение, в разгар холодной войны ада использовалась и в СССР. На нее даже существуетВы должны зарегистрироваться, чтобы увидеть внешние ссылки, который стоит почитать ради одной только терминологии: например, исключения там «возбуждаются».
Мифы об аде
Миф об устаревшем языке опровергается одним запросом к поисковику: последняя редакция вышла в 2012 году. Если судить о сложности языка по внешним признакам, то все тоже не так страшно: спецификация ады содержит чуть менее тысячи страниц, тогда как спецификация C++ - около 1400 страниц.
Миф о низкой производительности пошел со времен первой редакции 1983 года, когда массовому пользователю были доступны разве что ZX Spectrum и IBM PC с i8086, на которых любой современный язык был бы медленным. Ада компилируется в машинный код, и любители успешно пишут на ней для Arduino с ATmega328 и прочих микроконтроллеров.
Распространенный миф о том, что по вине ады упала ракета Ariane 5 в 1996 году, нужно рассмотреть отдельно. Ракета действительно упала из-за ошибки, но проблема была в другом: компьютер, который управлял траекторией полета, был взят из Ariane 4 без изменений, несмотря на то что Ariane 5 поддерживала более широкий диапазон траекторий.
Хуже того, проверка на выход значений за возможный диапазон была намеренно отключена, поэтому, когда навигационный компьютер выдал недопустимую с точки зрения Ariane 4 команду, закончилось все предсказуемо. От этой проблемы, увы, не смог бы защитить ни один язык или какое-либо программное решение вообще. Сама Ariane 4 совершила 113 успешных полетов из 116 за свою историю, а Ariane 5 уже 96 успешных из 101.
Языки и надежность программ
Ракеты — это предельный случай требований к надежности программ, но и в куда более приземленном коде ошибки могут обойтись пользователям очень дорого. В уязвимостях вроде
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
можно винить разработчиков, но разве смысл компьютеров не в том, чтобы автоматизировать нудную работу и позволить людям сосредоточиться на творческих задачах?Ада разрабатывалась именно для написания надежных и безопасных программ. Когда говорят о безопасности, прежде всего думают, какие ограничения язык или другой инструмент накладывает на пользователя.
На мой взгляд, в первую очередь нужно говорить о том, какие выразительные средства инструмент дает разработчику, чтобы точно отразить объекты реального мира в коде и определить законы их взаимодействия. Наблюдение за выполнением этих законов лучше поручить компилятору — он не устает к концу рабочего дня.
В первую очередь, конечно, инструмент не должен делать работу человека сложнее, чем она и так есть. Когда Министерство обороны США разрабатывало требования к новому языку для конкурса, в котором победила ада, они в первую очередь упомянули об этом. Документ с требованиями известен как
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и содержит, например, такую фразу:«Одни и те же символы и ключевые слова не должны иметь разные значения в разном контексте». Почти вся первая часть рассказывает о необходимости однозначности синтаксиса, удобочитаемости кода, определенности семантики и поведения (вспомним i++ + ++i).
Но и требования к выразительным средствам для своего времени там передовые. Любопытно, что обработка исключений и средства обобщенного программирования были еще в первой редакции, задолго до С++.
Давай напишем первую несложную программу, а потом рассмотрим, какие средства ада предоставляет, чтобы точнее выразить в коде свои намерения.
Реализации
Далеко идти за реализацией не придется: компилятор ады включен в GCC под названием GNAT (GNU New [York University] Ada Translator) и доступен на всех системах, где есть GCC.
Если у тебя Linux или FreeBSD, можешь ставить из стандартных репозиториев. В Debian/Ubuntu пиши apt-get install gnat, в Fedora — dnf install gnat.
Компания
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
предоставляет коммерческую поддержку для GNAT и занимается другими связанными проектами. Например, там работают над графической средой разработки GNAT Programming Studio (GPS).AdaCore является, по сути, основным разработчиком GNAT и распространяет две версии компилятора: сертифицированный GNAT Pro за деньги и GNAT Libre бесплатно, но с рантайм-библиотекой под лицензией GPLv3.
Использование GPLv3 не позволяет разрабатывать программы с любыми лицензиями, кроме GPL. Однако в дистрибутивы свободных ОС включена версия FSF GNAT, лицензия которой делает исключение для библиотек. Так что ее можно использовать для разработки программ с любой лицензией.
Есть еще проприетарные реализации ады вроде
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
, но для пользователей вне аэрокосмической отрасли и ВПК они малодоступны и особого интереса не представляют.Первая программа
Традиционный Hello world дает очень мало представления о языке, поэтому для первой программы мы возьмем что-нибудь более реалистичное, например алгоритм Пардо — Кнута. Дональд Кнут и Луис Трабб Пардо предложили его как раз для этой цели.
- Прочитать одиннадцать чисел со стандартного ввода.
- Применить к ним всем некоторую функцию и вывести результаты в обратном порядке.
- Если применение функции вызвало переполнение, вывести сообщение об ошибке.
Мы немного усложним задачу и будем заодно проверять правильность ввода значения и запрашивать их заново, если ввод был некорректным. Вернее, на уровне системы типов ограничим диапазон допустимых значений и обработаем возникшие исключения.
Вот наша программа. Ее нужно будет сохранить в файл с названием pardo_knuth.adb. Несовпадение имени файла с именем основной процедуры, которая служит точкой входа, вызовет предупреждение компилятора.
Код:
-- Trabb Pardo-Knuth program
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Strings.Unbounded;
with Ada.Text_IO.Unbounded_IO;
procedure Pardo_Knuth is
package UIO renames Ada.Text_IO.Unbounded_IO;
package US renames Ada.Strings.Unbounded;
type Small_Float is new Float range -100.0 .. 100.0;
package Float_IO is new Ada.Text_IO.Float_IO (Small_Float);
function Square (X : Small_Float) return Small_Float is
begin
return X * X;
end Square;
Input : Array (0 .. 10) of Small_Float;
Index : Integer := 0;
Debug : Boolean := False;
begin
if Debug then
Put_Line ("Pardo-Knuth program is started");
else
Put_Line ("Welcome to Pardo-Knuth program written in Ada!");
end if;
Input_Loop: while Index <= Input'Last loop
declare
Raw_Value : US.Unbounded_String;
begin
Put ("Enter a value: ");
UIO.Get_Line (Raw_Value);
Input(Index) := Small_Float'Value (US.To_String (Raw_Value));
Index := Index + 1;
exception
when Constraint_Error =>
begin
Put_Line ("Incorrect value! Enter a number from -100 to 100");
end;
end;
end loop Input_Loop;
Put_Line ("Results:");
for I in reverse Input'Range loop
declare
-- No declarations
begin
Float_IO.Put (Square (Input(I)), Exp => 0, Fore => 4, Aft => 2);
exception
when Constraint_Error => Put_Line ("Overflow occured!");
end;
New_Line;
end loop;
end Pardo_Knuth;
Скомпилировать программу можно командой gnatmake pardo_knuth.adb. Созданный исполняемый файл будет называться pardo_knuth.
Замечу, что синтаксис ады нечувствителен к регистру символов. В стандартной библиотеке GNAT по каким-то причинам укоренился непривычный Смешанный_Регистр, и я следую стилю стандартной библиотеки. Но если кому-то он кажется неэстетичным, можно использовать любой другой по вкусу — на работу программ это не повлияет.
Заголовок программы
Перед началом программы находится комментарий. Все комментарии в аде однострочные и начинаются с символов --.
Программа начинается с подключения модулей. Теперь гибкими возможностями их подключения никого не удивишь, но ада была одним из первых языков, где такое стало возможно.
Ключевое слово use делает модули видимыми в пространстве имен программы. Например, после with Ada.Text_IO мы могли бы вызывать процедуру Ada.Text_IO.Put_Line по ее полному имени. Но мы использовали конструкцию use Ada.Text_IO, которая импортирует все публичные символы этого модуля в наше пространство имен, поэтому мы можем вызвать Put_Line, Put и New_Line без имени модуля.
Код:
Распространенные функции из Ada.Text_IO
Put_Line — выводит данные с символом новой строки на конце.
Put — выводит данные без переноса строки.
New_Line — выводит символ переноса строки.
Модули Ada.Strings.Unbounded и Ada.Text_IO.Unbounded_IO предназначены для работы со строками произвольной длины. По умолчанию строки в аде фиксированной длины, что не всегда удобно для обработки пользовательского ввода. Строки произвольной длины легко преобразовать в фиксированные, что нередко приходится делать, потому что многие функции стандартной библиотеки ожидают именно фиксированные.
Дальше мы определяем основную процедуру, которая служит точкой входа в нашу программу. Желательно, чтобы ее имя совпадало с именем файла, хотя и не обязательно. В аде есть различие между функциями и процедурами: функции могут возвращать значения, а процедуры — только изменять переменные, которые им передают в аргументах.
Внутри нашей процедуры мы переименовали модули с длинными именами для удобства, аналогично import foo as bar в Python. Теперь мы сможем вызывать, к примеру, Ada.Strings.Unbounded.To_String как US.To_String.
Алгоритм Пардо — Кнута требует сообщать пользователю о переполнении. Чтобы упростить появление переполнений, мы создали намеренно ограниченный тип-диапазон Small_Float, с возможными значениями от -100.0 до +100.0. Для таких типов у ады есть встроенные проверки: присвоение недопустимой константы в коде приведет к ошибке компиляции, а появление недопустимых значений в ходе работы программы вызовет исключение.
Ада использует именную, а не структурную эквивалентность типов, то есть два типа с разными именами несовместимы, даже если объявлены одинаковым образом, и их нельзя использовать в одном выражении без явного приведения. Эту особенность часто используют, чтобы выразить логическую несовместимость величин, например предотвратить случайное сложение дюймов с сантиметрами.
Строка «package Float_IO is new Ada.Text_IO.Float_IO (Small_Float)» заслуживает отдельного внимания. Это уже не простое переименование, а специализация обобщенного (generic) модуля для нашего типа Small_Float.
В аде нет аналога printf, что бывает очень неудобным, но для ее целей выглядит оправданным — типо безопасный форматированный вывод возможен только в языках с совершенно другой системой типов. В С printf("%d", "foo") вызовет segmentation fault, в Go аналогичная конструкция приведет к ошибке времени выполнения — ни то ни другое в критичной по надежности программе не принесет пользователю особой радости.
Поэтому хотя бы для относительного удобства авторы стандартной библиотеки ады написали несколько модулей для ввода и вывода значений распространенных типов.
Для алгоритма Пардо — Кнута нам требуется определенная пользователем функция. Функции и процедуры в аде могут быть вложенными, чем мы и воспользуемся и определим простейшую функцию Square в заголовке основной процедуры нашей программы. Тип возвращаемых значений указан с помощью return Small_Float. Это обязательная часть синтаксиса — еще раз отметим, что создать функцию, которая не возвращает значений, невозможно, для этой цели нужно использовать процедуры.
Далее идут объявления переменных. В аде все переменные должны быть объявлены перед использованием. Глобальных переменных в смысле C в ней нет, их область видимости всегда чем-то ограничена: пакетом (то есть модулем), процедурой, функцией или блоком declare, который мы рассмотрим позже. Видимые внутри всей процедуры переменные должны быть объявлены в ее заголовке, что мы и сделаем.
С помощью Input : Array (0 .. 10) of Small_Float мы объявляем массив Input, где будут храниться введенные пользователем значения. Мы используем диапазон индексов от 0 до 10, но вообще индексы могут быть любыми, в том числе отрицательными, например от -5 до 5. Еще вместо явных индексов можно было бы создать целочисленный тип-диапазон и сослаться на него.
Переменная Index, очевидно, будет использоваться как индекс элемента массива в цикле — мы могли бы объявить ее позже, но для демонстрации оставим ее здесь. Ее начальное значение — ноль, присваивается одновременно с объявлением переменной. Все присваивания в языке производятся с помощью оператора :=, оператор = используется только для проверки равенства.
Для оператора «не равно» используется символ /=. Вполне вероятно, что Haskell заимствовал его из ады.
Переменная Debug будет служить только для демонстрации условного оператора. Она имеет тип Boolean с возможными значениями True и False. Условный оператор в аде требует выражения логического типа и никакого другого, привычное в C-подобных языках if(0) вызовет ошибку — никакие неоднозначности, связанные с интерпретацией произвольных значений в логическом контексте, в этом языке возникнуть не могут.
Заголовок основной процедуры нашей программы наконец закончился — переходим к ее телу.
Тело программы
Мы начинаем программу с довольно глупой демонстрации условного оператора — выводим другое приветствие, если переменная Debug выставлена в True. Этого не произойдет, если не поменять ее начальное значение руками, но суть не в этом, а в синтаксисе.
Условный оператор имеет вид if <условие> then <высказывание> else <высказывание> end if. Часть про end if обязательна. Вообще, в аде почти нигде нельзя написать просто end, не указав, что именно здесь закончилось. Читать такой исходный код куда проще, хоть писать и дольше, но правильная настройка редактора решает эту проблему. В объявлении функции Square мы уже видели, что она кончается словами end Square, хоть и не заостряли на этом внимание.
Компилятор удаляет заведомо недостижимый код, как внутри if Debug, из исполняемых файлов, но только после проверки всех типов и всего прочего, что можно проверить на этапе компиляции. Такие константы — стандартный способ реализации feature toggles и отладочного кода, куда более безопасная альтернатива условной компиляции с помощью препроцессора. Конечно, такой подход не работает для машинно зависимого кода, который просто не скомпилируется вне его целевой платформы. В этом случае можно использовать внешний препроцессор — gnatprep.
Для циклов можно даже указать имена: цикл ввода переменных объявлен с помощью Input_Loop: while ... loop и закончен end loop Input_Loop. Перепутать границы вложенных циклов при таком подходе очень сложно. Синтаксис циклов радует своим единообразием — циклы с предусловием и циклы с параметром отличаются только словами перед ключевым словом loop.
Бесконечный цикл
Бесконечный цикл создать очень просто: не указывать ни тип, ни условия.
Код:
loop
Put_Line ("This loop never ends!");
end loop;
Циклов с постусловием в явном виде нет, но можно указать условие выхода внутри тела цикла:
Код:
Counter := 0;
My_Loop: loop
Counter := Counter + 1;
exit My_Loop when Counter > 10;
end loop My_Loop;
Условие цикла — while Index <= Input'Last — требует некоторых пояснений. Мы могли бы явно указать максимальное значение индекса, но вместо этого мы используем атрибут нашего массива Last, возвращающий максимальное значение его индекса. Минимальное значение можно получить с помощью атрибута First, так что эту часть нудной работы компилятор делает за нас. Чуть позже мы остановимся на атрибутах подробнее.
Внутри цикла Input_Loop мы читаем переменные и помещаем их в наш массив Input. Специально для промежуточного хранения пользовательского ввода мы создадим локальную переменную Raw_Value. Поскольку объявлять переменные где придется в аде нельзя, нам потребуется блок declare, который создает отдельную область видимости.
Между declare и begin можно добавить любые объявления, которые мы могли бы поместить в заголовок процедуры. В нашем случае это единственная переменная Raw_Value : Ada.Strings.Unbounded.Unbounded_String (пакет, который мы для удобства переименовали в US). Это тип динамических строк неограниченной длины, которые мы читаем с помощью функции UIO.Get_Line.
В вычислениях с плавающей точкой от строковых данных никакой пользы, поэтому нам нужно конвертировать эти строки в числа. Мы могли бы использовать функции из пакета Float_IO или даже определить потоки ввода-вывода, но мы пойдем другим путем — используем функцию-атрибут нашего типа Small_Float.
Атрибуты — одна из самых необычных концепций ады. В общих чертах это ассоциированные с типами функции. Имя типа можно рассматривать как своеобразное пространство имен — атрибут отделяется от него апострофом. В данном случае мы используем атрибут Small_Float'Value, который представляет собой функцию от типа String и возвращает значения типа Small_Float.
Нашу динамическую строку мы для этого конвертируем в статическую, поэтому все выражение имеет вид Small_Float'Value (US.To_String (Raw_Value)).
Противоположность атрибуту Value — атрибут Image, который конвертирует значение в строку. Существует множество других атрибутов, например для получения размера значений типа в битах, но мы не будем их рассматривать — про них можно прочитать в документации.
Что произойдет, если ввод будет некорректным, например если пользователь введет foo вместо числа или число, выходящее за границы диапазона? Функция Small_Float'Value завершится с исключением Constraint_Error. Это исключение нужно обработать, что мы и делаем в конце блока declare, после ключевого слова exception. Эквивалента try ... catch в аде нет, обработку исключений можно производить только в конце процедур и функций или, если мы хотим обработать их внутри функции, в конце блоков declare.
Исключения в аде не являются ни классами, ни объектами. Это делает невозможным создание иерархий исключений, но уменьшает накладные расходы на их обработку. Создать свое исключение можно с помощью вот такой конструкции в заголовке процедуры: Not_My_Fault : exception;.
После того как ввод от пользователя получен, остается только применить нашу функцию Square к каждому значению и вывести результаты. Для этого мы используем цикл с параметром. С помощью атрибута нашего массива Input'Range мы можем это сделать с не меньшим удобством, чем предоставляют итераторы в объектно ориентированных языках, — этот атрибут возвращает диапазон индексов.
Поскольку наш тип Small_Float ограничен, исключение Constraint_Error может возникнуть и при возведении значений в квадрат, если они выйдут за границы диапазона, поэтому мы обрабатываем их аналогичным образом в конце блока declare, в этот раз уже без объявления каких-либо переменных в его заголовке перед begin.
Выражение Float_IO.Put (Square (Input(I)), Exp => 0, Fore => 4, Aft => 2), очевидно, вызывает функцию для вывода значений с плавающей точкой. Аргументы Exp, Fore и Aft — это число знаков экспоненты, число знаков до точки и число знаков после точки соответственно.
По умолчанию используется научный формат вывода в стиле 1.2E2, но установка параметра Exp в ноль отключает это. Интересный момент: в аде любые параметры функций и процедур можно использовать как именованные. К примеру, нашу функцию Square мы могли бы вызывать с помощью Square (X => Input(I)).
Если порядок фактических параметров в вызове совпадает с порядком формальных параметров функции, то имена можно не указывать, но в Float_IO.Put мы использовали именованные параметры, чтобы поменять порядок, — вызов без именованных параметров выглядел бы так: Float_IO.Put (Square (X => Input(I)), 4, 2, 0).
Модули и обобщенное программирование
Обобщенное программирование
Мы начнем с простейшего примера обобщенного программирования, потому что оно не ограничивается модулями — обобщенными могут быть и отдельные процедуры и функции. Мы напишем функцию Do_Nothing, которая просто возвращает переданное ей значение в неизменном виде и может быть специализирована для любого типа.
Код:
with Ada.Text_IO; use Ada.Text_IO;
procedure Simple_Generic is
generic
type T is private;
function Do_Nothing (X : T) return T;
function Do_Nothing (X : T) return T is
begin
return X;
end Do_Nothing;
function Do_Nothing_To_Integer is new Do_Nothing (Integer);
begin
Put_Line (Integer'Image (Do_Nothing_To_Integer (65535)));
end Simple_Generic;
Как видно, обобщенные конструкции создаются с помощью ключевого слова generic, у которого есть раздел объявлений типов. Тип T в данном случае — это тип-параметр, для которого наша процедура может быть специализирована. Затем мы описываем нашу функцию с использованием типа T. В function Do_Nothing_To_Integer is new Do_Nothing (Integer) она специализируется для типа Integer. Мы уже видели это ранее, когда подключали обобщенные пакеты из стандартной библиотеки.
Ключевое слово new, в отличие от многих других языков, здесь не имеет никакого отношения к объектам — объектов в традиционном понимании в аде нет, хотя инкапсуляция, наследование и полиморфизм есть, просто они в значительной степени отделены друг от друга и реализуются другими способами.
Модули
Модули, или пакеты, — встроенная и очень важная часть языка. Модули разделены на интерфейс и реализацию на уровне языка. Интерфейсы модулей хранятся в файлах с расширением .ads, а реализации — в файлах с расширением .adb.
Для демонстрации мы напишем модуль для работы с условной базой данных пользователей. У каждой учетной записи будут идентификатор, имя пользователя (строка до 255 символов) и флаг блокировки. Мы сделаем модуль обобщенным, чтобы в качестве идентификатора можно было использовать значения разных типов.
Кроме того, мы сделаем работу с типом учетных записей только через функции самого модуля.
Рассмотрим интерфейс нашего модуля, файл accounts.ads.
Код:
generic
type Identifier_T is private;
package Accounts is
type User_Record is private;
function Create (Identifier : Identifier_T; Name : String) return User_Record;
procedure Disable (User : in out User_Record);
private
type User_Record is record
Identifier : Identifier_T;
Name : String (1..255);
Disabled : Boolean;
end record;
end Accounts;
С помощью слова generic мы делаем его обобщенным, с типом-параметром Identifier_T. После слов package Accounts is идут описания публичных полей модуля, которые будут доступны при его импорте, а после ключевого слова private — описания закрытых полей.
Можно заметить, что тип User_Record описан дважды: первый раз в публичной части модуля как type User_Record is private и второй раз в его закрытой части как настоящий тип-запись. И вот почему: первое описание говорит, что тип User_Record — абстрактный и за пределами модуля будет известно только его имя, но не детали реализации. Это сделает невозможным прямой доступ к полям нашей записи для любых функций, которые не принадлежат модулю.
Более того, даже если мы знаем, как на самом деле устроен тип, создать значения типа Accounts.User_Record можно будет только с помощью функции Accounts.Create. Любые другие значения будут несовместимы с остальными функциями из него. Так реализуется инкапсуляция. Если бы мы включили описание типа в секцию private, но не включили type User_Record is private в публичную часть, этот тип был бы вовсе не виден за пределами модуля и воспользоваться им было бы невозможно.
Реализацию мы сделаем тривиальной, вот файл accounts.adb:
Код:
package body Accounts is
function Create (Identifier : Identifier_T; Name : String) return User_Record is
begin
return (Identifier, Name, False);
end Create;
procedure Disable (User : in out User_Record) is
begin
User.Disabled := True;
end;
end Accounts;
В завершение приведем простейшую программу с использованием нашего модуля, файл user_test.adb. Для сборки проекта достаточно положить все три файла в один каталог и выполнить команду gnatmake user_test.adb.
Код:
with Accounts;
procedure User_Test is
package My_Accounts is new Accounts(Integer);
User : My_Accounts.User_Record;
begin
User := My_Accounts.Create (0, "root");
end User_Test;
Заключение
Многие возможности языка остались за кадром, такие как наследование с помощью расширений типов или многозадачность. Заинтересованные читатели могут продолжить изучение языка по
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
компании AdaCore или
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.Конечно, многие языки хорошо выглядят только на бумаге, но любые попытки их использования разбиваются о суровую реальность. В следующий раз мы рассмотрим историю создания небольшого проекта с открытым исходным кодом и ознакомимся с использованием ады на практике.