• Уменьшение отступа

    Обратная связь

    (info@ru-sfera.pw)

На заметку Вся правда об Аде


virt

Просветленный
Просветленный
Регистрация
24.11.2016
Сообщения
706
Репутация
228
Оригинал:

Язык, разработанный по заказу Министерства обороны США и названный в честь первой в мире программистки Ады Лавлейс, окружают много мифов и непонимания. Ты наверняка о нем слышал, но, скорее всего, это были мифы об устаревшем, сложном и медленном языке.

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

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

Мифы об аде

Миф об устаревшем языке опровергается одним запросом к поисковику: последняя редакция вышла в 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 дает очень мало представления о языке, поэтому для первой программы мы возьмем что-нибудь более реалистичное, например алгоритм Пардо — Кнута. Дональд Кнут и Луис Трабб Пардо предложили его как раз для этой цели.
  1. Прочитать одиннадцать чисел со стандартного ввода.
  2. Применить к ним всем некоторую функцию и вывести результаты в обратном порядке.
  3. Если применение функции вызвало переполнение, вывести сообщение об ошибке.
С помощью такой программы уже можно показать, как определить и заполнить массив, как написать и вызвать функцию, как использовать циклы и условия и как использовать ввод-вывод. Опять же, если 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 или .

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