Структура программы: главная процедура, with и use
Анатомия программы на Ada: где у неё точка входа, почему главная единица — это процедура, и как with/use подключают чужой код. Первая программа, разобранная до последнего символа.
Процедура в Ada — это подпрограмма, выполняющая действия и не возвращающая значения (в отличие от функции); любая программа стартует с одной главной процедуры.
Где у программы начало
В разных языках точка входа устроена по-разному: где-то это функция с зарезервированным именем, где-то верхнеуровневый код файла. В Ada всё строго и явно: программа — это главная процедура, и при сборке вы указываете, какая именно из процедур главная. Здесь нет магического имени вроде main; вы вольны назвать её осмысленно, например Hello или Flight_Control. Компоновщик просто берёт указанную процедуру как стартовую. Это согласуется с духом языка: ничего не происходит неявно, всё называется своими именами.
Вот канонический первый пример. Разберём его буквально по строкам:
with Ada.Text_IO;
procedure Hello is
begin
Ada.Text_IO.Put_Line ("Привет, мир!");
end Hello;
Строка with Ada.Text_IO; — это контекстная оговорка (context clause). Она объявляет: «этой программе нужен пакет Ada.Text_IO». Без неё компилятор не даст обращаться к средствам ввода-вывода. Дальше идёт сама процедура. Слово procedure Hello is открывает объявление; между is и begin могла бы быть зона объявлений (локальные переменные, типы), но здесь она пуста. Между begin и end — тело, последовательность операторов. Завершается всё end Hello; — обратите внимание, что имя процедуры повторяется в end. Это не обязательно технически, но настоятельно рекомендуется и принято: при чтении длинного файла сразу видно, что именно закрывается.
Точка с запятой и регистр
Два бытовых, но важных факта. Во-первых, операторы и объявления в Ada разделяются точкой с запятой ;, и она ставится в конце, а не как разделитель между. Пропуск ; — самая частая опечатка новичка. Во-вторых, Ada нечувствительна к регистру в идентификаторах и ключевых словах: Put_Line, put_line и PUT_LINE — одно и то же имя для компилятора. Но сообщество твёрдо договорилось о стиле: ключевые слова пишут строчными (procedure, begin, is), а имена — с Заглавных_Букв_Через_Подчёркивание (Put_Line, Ada.Text_IO). Этот стиль настолько устоялся, что отступление от него выглядит как ошибка, даже если компилируется. Регистронезависимость — сознательное решение в пользу читаемости: нельзя завести две разные сущности Count и count и запутать читателя.
Точечная нотация и оговорка use
Заметьте, как мы вызывали процедуру: Ada.Text_IO.Put_Line. Это полное имя: пакет Ada, внутри него дочерний пакет Text_IO, внутри него процедура Put_Line. Точка разделяет уровни вложенности, как путь в файловой системе. Полные имена надёжны и однозначны, но многословны. Чтобы их сократить, есть оговорка use:
with Ada.Text_IO;
use Ada.Text_IO;
procedure Hello is
begin
Put_Line ("Привет, мир!"); -- теперь без префикса
end Hello;
После use Ada.Text_IO; содержимое пакета становится видимым напрямую, и можно писать просто Put_Line. Удобно, но есть нюанс: если два разных пакета через use вносят имена, которые совпадают, возникает неоднозначность. Поэтому в больших проектах use применяют осторожно, иногда предпочитая полные имена ради ясности — чтобы читатель сразу видел, откуда взялась каждая операция. Существует и компромисс — use type, открывающий только операторы конкретного типа, но об этом позже. Важно понять различие: with делает пакет доступным (создаёт зависимость), а use делает его имена видимыми без префикса (удобство записи). Это два разных действия, и use без with бессмысленен.
Что такое пакет и зачем он
Мы уже несколько раз сказали «пакет», пора закрепить. Пакет (package) — основная единица модульности в Ada: именованный контейнер для типов, констант, переменных и подпрограмм, объединённых по смыслу. Стандартная библиотека Ada — это иерархия пакетов с корнем Ada. Вот несколько, которые встретятся постоянно:
| Пакет | Назначение |
Ada.Text_IO | текстовый ввод-вывод: печать строк, чтение |
Ada.Integer_Text_IO | ввод-вывод целых чисел |
Ada.Float_Text_IO | ввод-вывод чисел с плавающей точкой |
Ada.Strings.Unbounded | строки переменной длины |
Ada.Numerics | математические константы и функции |
Точка в имени Ada.Text_IO означает, что Text_IO — дочерний пакет пакета Ada. Иерархия дочерних пакетов — мощный способ организовывать большие библиотеки: связанные средства группируются под общим родителем, а подключать можно ровно то, что нужно.
Как работает под капотом сборка
Когда вы пишете with Ada.Text_IO;, компилятор должен где-то найти спецификацию этого пакета. Он ищет уже скомпилированные единицы и их интерфейсы. Утилита gnatmake (или gprbuild) выстраивает граф зависимостей: ваша процедура зависит от Ada.Text_IO, тот — от своих внутренностей, и так далее. Затем в правильном порядке всё компилируется и связывается. Ключевая идея — раздельная компиляция с проверкой совместимости: каждая единица компилируется отдельно, но компилятор сверяет, что вызовы соответствуют опубликованным спецификациям. Если вы измените спецификацию пакета, всё, что от неё зависит, будет пересобрано — и любое рассогласование вылезет ошибкой компиляции, а не загадочным сбоем в рантайме. Это прямое следствие требования модульности из Steelman.
Чуть больше тела: объявления перед begin
Покажем процедуру с непустой зоной объявлений, чтобы увидеть полную форму:
with Ada.Text_IO;
use Ada.Text_IO;
procedure Greeting is
Name : constant String := "Ада";
begin
Put_Line ("Здравствуй, " & Name & "!");
end Greeting;
Между is и begin объявлена константа Name типа String. Оператор & здесь — конкатенация строк (в HTML амперсанд экранируется как &, в коде это один символ &). Получаем строку «Здравствуй, Ада!». Заметьте чёткое деление: сначала всё объявляем, потом действуем. Нельзя объявить переменную посреди исполняемого кода — это дисциплинирует и делает структуру предсказуемой.
Вложенные подпрограммы и область видимости
Стоит расширить картину структуры программы за пределы одной плоской процедуры, потому что Ada устроена удивительно «вложенно». В зоне объявлений (между is и begin) можно объявлять не только переменные и константы, но и другие подпрограммы. Они становятся локальными помощниками, видимыми только внутри объемлющей процедуры:
with Ada.Text_IO; use Ada.Text_IO;
procedure Report is
Title : constant String := "Отчёт";
procedure Line (Text : String) is -- локальная вложенная процедура
begin
Put_Line (" " & Text); -- видит Title из объемлющей области
end Line;
begin
Put_Line (Title);
Line ("первый пункт");
Line ("второй пункт");
end Report;
Вывод:
Отчёт первый пункт второй пункт
Вложенная процедура Line существует только внутри Report и недоступна снаружи. Более того, она видит всё, что объявлено в объемлющей области до неё — например, константу Title. Это называется лексической областью видимости: внутренняя подпрограмма имеет доступ к именам внешней, как будто они её собственные. Такое вложение позволяет разбивать сложную процедуру на понятные локальные шаги, не засоряя глобальное пространство имён вспомогательными функциями. В отличие от многих языков, где функции живут только на верхнем уровне или в классах, Ada разрешает иерархию подпрограмм произвольной глубины, и это мощный структурный инструмент.
Из области видимости вытекает важное правило порядка: имя должно быть объявлено до места использования. Ada читает объявления сверху вниз, и сослаться на ещё не объявленную сущность нельзя (есть специальные приёмы для взаимных ссылок, но базовое правило именно такое). Это снова про предсказуемость: читая код линейно, вы всегда видите объявление прежде, чем встретите использование. Вместе с явной зоной объявлений перед begin это формирует очень регулярную, легко прослеживаемую структуру — каждая сущность имеет ясное место рождения и ясную границу жизни. Для больших систем, где над кодом работают многие, такая регулярность снижает когнитивную нагрузку и число ошибок «а откуда взялось это имя».
Эта вложенность объясняет и то, как Ada масштабируется от крошечной программы до огромной системы. Маленькую задачу решает одна процедура с парой локальных помощников. Когда логики становится много, помощники выносят в отдельный пакет — с публичной спецификацией и скрытым телом, — и главная процедура лишь связывает пакеты воедино. Принцип всюду один: чёткие интерфейсы и спрятанные реализации, будь то вложенная процедура внутри одной единицы или пакет на тысячи строк. Не нужно переучиваться при росте проекта — те же правила области видимости, объявлений и зависимостей работают на любом масштабе. Именно эта однородность структуры, от первой программы до бортового комплекса, делает Ada языком, на котором большие команды годами строят системы, не утопая в хаосе: каждый кусок кода имеет ясные границы, ясные зависимости и ясное место в иерархии.
Частые ошибки и заблуждения
- Забыть
with. Без контекстной оговорки пакет недоступен;useбезwithне работает — это разные действия (доступность против видимости). - Искать
main. В Ada нет зарезервированного имени точки входа; главную процедуру вы называете сами и указываете при сборке. - Считать язык регистрозависимым. Идентификаторы регистронезависимы, но соглашение о стиле (строчные ключевые слова, Заглавные_Имена) соблюдают строго.
- Объявлять переменные среди операторов. Объявления идут только в зоне между
isиbegin(или в declare-блоке), а не вперемешку с кодом. - Путать
;и его отсутствие. Точка с запятой завершает каждое объявление и оператор; её пропуск — типичная ошибка компиляции.
Итоги
- Программа на Ada — это главная процедура; зарезервированного имени точки входа нет, её задают при сборке.
withсоздаёт зависимость от пакета (делает доступным),useделает его имена видимыми без префикса — это разные вещи.- Полные имена через точку (
Ada.Text_IO.Put_Line) надёжны и однозначны;useсокращает запись ценой риска неоднозначности. - Язык регистронезависим, но сообщество твёрдо придерживается стиля: строчные ключевые слова, Имена_С_Заглавных.
- Пакеты — основная единица модульности; стандартная библиотека образует иерархию с корнем
Adaи раздельной компиляцией с проверкой совместимости.