Стандартная библиотека и первая полноценная программа

Обзорная экскурсия по стандартной библиотеке Ada и сборка первой полноценной программы: что лежит в иерархии Ada, как устроены пакеты Numerics, Calendar, Containers, и как всё это складывается в работающее приложение.

Стандартная библиотека Ada — иерархия пакетов с корнем Ada (плюс Interfaces и System), описанная прямо в стандарте языка и доступная в любой реализации.

Карта стандартной библиотеки

Когда вы освоили ввод-вывод, переменные и пакеты, полезно увидеть карту того, что даёт стандарт «из коробки». В отличие от языков с гигантскими внешними экосистемами, стандартная библиотека Ada сравнительно компактна, но тщательно специфицирована: её поведение — часть стандарта, а значит, одинаково на любой платформе и в любом сертифицированном компиляторе. Это важно для критичных систем: вы не зависите от капризов случайной библиотеки с непредсказуемым качеством. Иерархия делится на три корневых пакета:

  • Ada — основная библиотека: ввод-вывод, строки, математика, контейнеры, время, исключения.
  • Interfaces — типы фиксированной разрядности и средства взаимодействия с другими языками (C, Fortran).
  • System — низкоуровневые, зависящие от платформы сущности (адреса, приоритеты).

Сосредоточимся на самых полезных дочерних пакетах Ada.

Математика: Ada.Numerics

Пакет Ada.Numerics и его потомки дают математику. Сама вершина содержит фундаментальные константы:

with Ada.Numerics;            use Ada.Numerics;
with Ada.Text_IO;             use Ada.Text_IO;
with Ada.Float_Text_IO;       use Ada.Float_Text_IO;

procedure Show_Pi is
begin
   Put ("Число Пи равно ");
   Put (Pi, Fore => 1, Aft => 5, Exp => 0);   -- Pi из Ada.Numerics
   New_Line;
end Show_Pi;

Вывод:

Число Пи равно 3.14159

Элементарные функции (синус, косинус, логарифм, корень) лежат в дочернем пакете Ada.Numerics.Elementary_Functions для типа Float. Подключив его, можно писать Sqrt (2.0), Sin (X), Log (X). Здесь снова проявляется типизированность: функции определены для конкретного вещественного типа, а для других точностей есть обобщённая версия, настраиваемая под нужный тип.

Время: Ada.Calendar

Пакет Ada.Calendar работает с датами и временем. Он даёт тип Time и функцию Clock, возвращающую текущий момент, а также средства разложить время на год, месяц, день, секунды:

with Ada.Calendar;  use Ada.Calendar;
-- ...
Now  : Time := Clock;
Year : Year_Number := Ada.Calendar.Year (Now);

Тип Year_Number — это, как многое в Ada, поддиапазон целых с осмысленными границами (примерно от 1901 до далёкого будущего). Время — отличный пример того, как Ada оборачивает «сырые» секунды в типобезопасные сущности с проверяемыми диапазонами.

Контейнеры: Ada.Containers

Начиная с Ada 2005, стандарт включает богатую иерархию Ada.Containers — типобезопасные коллекции: векторы (Vectors), двусвязные списки (Doubly_Linked_Lists), отображения по ключу (Hashed_Maps, Ordered_Maps), множества. Это аналоги коллекций из других языков, но с двумя отличиями в духе Ada: они строго типизированы (вектор целых не примет строку) и существуют в обычной и «ограниченной» версиях — последние не используют динамическое выделение памяти, что критично для встраиваемых систем реального времени, где куча запрещена. Подробная работа с контейнерами — продвинутая тема, но важно знать: для типовых структур данных не нужны внешние библиотеки, всё есть в стандарте.

Строки: семейство Ada.Strings

Под Ada.Strings собраны средства работы с текстом: Ada.Strings.Fixed (операции над строками фиксированной длины), Ada.Strings.Bounded (строки с ограниченным максимумом) и Ada.Strings.Unbounded (строки произвольной длины, растущие по мере надобности). Это отражает инженерную реальность: иногда нужна предсказуемая фиксированная память, иногда — гибкость. Ada даёт выбор вместо «одной строки на все случаи». Детально разберём в разделе про составные типы.

Собираем полноценную программу

Теперь объединим всё изученное в разделе — объявления, константы, ввод-вывод, математику — в одну осмысленную программу. Она спросит радиус и посчитает длину окружности и площадь круга:

with Ada.Text_IO;                          use Ada.Text_IO;
with Ada.Float_Text_IO;                    use Ada.Float_Text_IO;
with Ada.Numerics;                         use Ada.Numerics;

procedure Circle is
   Radius        : Float;
   Circumference : Float;
   Area          : Float;
begin
   Put ("Введите радиус: ");
   Get (Radius);                           -- чтение вещественного

   Circumference := 2.0 * Pi * Radius;
   Area          := Pi * Radius * Radius;

   New_Line;
   Put ("Длина окружности: ");
   Put (Circumference, Fore => 1, Aft => 3, Exp => 0);
   New_Line;
   Put ("Площадь круга:    ");
   Put (Area, Fore => 1, Aft => 3, Exp => 0);
   New_Line;
end Circle;

Если ввести радиус 2.0, программа напечатает:

Длина окружности: 12.566
Площадь круга:    12.566

Разберём ключевые моменты. Все величины — Float, потому что радиус и результаты вещественные. Важная деталь: мы пишем 2.0, а не 2. В Ada вещественный литерал обязан иметь точку: 2.0 — это Float, а 2 — это целое, и смешивать их в одном выражении нельзя без явного преобразования (об этом — целый урок в следующем разделе). Pi пришёл из Ada.Numerics и тоже вещественный, поэтому выражение однородно. Чтение Get (Radius) использует Get из Ada.Float_Text_IO. Форматирование вывода задано именованными аргументами Fore/Aft/Exp.

Как работает под капотом подключение библиотеки

Каждая with-оговорка наверху — это явная декларация зависимости. Компилятор и система сборки по ним строят граф: программа Circle зависит от трёх пакетов, каждый — от своих внутренностей. Принципиально, что эти зависимости видны прямо в исходнике. Открыв любой файл Ada, вы по списку with сразу понимаете, на что он опирается — это часть культуры читаемости. В больших проектах это позволяет инструментам анализировать связи, находить лишние зависимости, понимать, что пересобрать при изменении. Сравните с языками, где зависимости размазаны по коду или прячутся в неявных импортах: там понять структуру системы куда труднее. Стандартная библиотека Ada при этом — надёжная опора: её интерфейсы зафиксированы стандартом и не «уплывут» от версии к версии.

Исключения: что происходит, когда что-то идёт не так

Наша программа про окружность таит ловушку, о которой стоит поговорить, потому что она вводит важнейший механизм языка. Что если пользователь вместо числа введёт буквы? Функция Get не сможет разобрать ввод и поднимет исключение — в данном случае Data_Error. Исключение — это структурированный сигнал об исключительной ситуации, который прерывает обычный ход выполнения и ищет обработчик. Если обработчика нет, программа аварийно завершится с сообщением. Ada позволяет перехватить исключение блоком exception:

with Ada.Text_IO;        use Ada.Text_IO;
with Ada.Float_Text_IO;  use Ada.Float_Text_IO;

procedure Safe_Read is
   X : Float;
begin
   Put ("Введите число: ");
   Get (X);
   Put_Line ("Спасибо!");
exception
   when Data_Error =>
      Put_Line ("Это не похоже на число.");
end Safe_Read;

Раздел exception в конце тела (перед end) перехватывает указанные исключения. Ветвь when Data_Error => сработает, если в основном блоке возникнет ошибка разбора, и вместо краха программа вежливо сообщит о проблеме. Синтаксис намеренно перекликается с case: те же when и стрелка =>. Можно перехватывать несколько видов исключений разными ветвями и использовать when others => для любого непредусмотренного.

Почему это так важно для философии Ada? Потому что исключения — это часть той же стратегии «дорого ошибиться»: ошибки не игнорируются молча. В языках, где деление на ноль или неверный ввод дают неопределённое поведение или тихий мусор, ошибка просачивается дальше и проявляется катастрофой далеко от своей причины. В Ada нарушение поднимает явное, именованное исключение точно в месте, где оно произошло, — Constraint_Error при выходе за диапазон, Data_Error при неверном вводе, Storage_Error при нехватке памяти. Вы либо обрабатываете его осознанно, либо программа честно останавливается, но в любом случае ошибка видима и локализована, а не размазана. Эта встроенная, типизированная обработка исключительных ситуаций — одно из исходных требований Steelman, и она пронизывает весь язык: почти каждая проверка, о которой мы говорили (границы, типы, полнота), при нарушении выражается через соответствующее исключение. Мы ещё вернёмся к исключениям подробно, но уже сейчас важно понимать: за каждой проверкой Ada стоит механизм, который превращает «что-то пошло не так» в управляемое, наблюдаемое событие.

Полезно осознать и масштаб того, что мы только что затронули: исключения — это не «аварийный костыль», а полноценная, проектируемая часть архитектуры надёжной программы. В критичных системах продумывают стратегию обработки: какие исключения перехватывать локально и восстанавливаться, какие — пропускать наверх к обработчику уровня подсистемы, а какие должны переводить систему в безопасное состояние (graceful degradation). Ada даёт для этого богатый набор: предопределённые исключения (Constraint_Error, Data_Error, Storage_Error, Program_Error), возможность объявлять свои исключения для доменных ошибок, и средства получить информацию о возникшей ситуации через пакет Ada.Exceptions. Так обработка ошибок становится не разрозненными проверками, а связной, документированной частью дизайна — что для систем, которым доверяют жизни, столь же важно, как и основная логика. Мы вернёмся к этому подробно, но уже видно: «что делать, когда что-то пошло не так» в Ada — первоклассный вопрос, а не запоздалая мысль.

Частые ошибки и заблуждения

  • Писать целые литералы там, где нужны вещественные. 2 и 2.0 — разные типы; в вещественных выражениях используйте 2.0, 0.5 и т.д.
  • Искать математику в Ada.Numerics напрямую. Константы (Pi) — в корне, а функции (Sqrt, Sin) — в дочернем Ada.Numerics.Elementary_Functions, его надо подключить отдельно.
  • Думать, что для коллекций нужны внешние пакеты. Векторы, списки, отображения есть в Ada.Containers прямо в стандарте, причём типобезопасные.
  • Игнорировать «ограниченные» версии. Для встраиваемых систем важны контейнеры и строки без динамической памяти — стандарт их специально предусматривает.
  • Не перечислять зависимости явно. Каждый используемый пакет нужно подключить через with; неявных импортов в Ada нет, и это сознательно.

Итоги

  • Стандартная библиотека Ada компактна, но строго специфицирована стандартом, и потому одинакова на всех платформах — ценное свойство для критичных систем.
  • Иерархия делится на корни Ada (основное), Interfaces (разрядность, межъязыковость) и System (низкий уровень).
  • Ключевые пакеты: Ada.Numerics (математика), Ada.Calendar (время), Ada.Containers (типобезопасные коллекции), семейство Ada.Strings (текст).
  • В полноценной программе важно соблюдать типы литералов: вещественные пишутся с точкой (2.0), целые — без.
  • Все зависимости объявляются явно через with наверху файла, что делает структуру программы читаемой и анализируемой.
Проверьте себя
1. Почему в Ada важно писать 2.0, а не 2, в вещественных выражениях?
AДля красоты
BПотому что 2 это Integer, а 2.0 это Float — разные типы, их нельзя смешивать
CНет разницы
D2.0 быстрее
2. Где в стандартной библиотеке лежат типобезопасные коллекции (векторы, списки, отображения)?
AНужны внешние библиотеки
BВ пакете Ada.Containers прямо в стандарте
CВ Ada.Text_IO
DИх нет в Ada