Структура программы: главная процедура, 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 и раздельной компиляцией с проверкой совместимости.
Проверьте себя
1. В чём разница между with и use?
AЭто синонимы
Bwith создаёт зависимость (делает пакет доступным), use делает его имена видимыми без префикса
Cuse создаёт зависимость, with — нет
DОба бесполезны
2. Как в Ada задаётся точка входа программы?
AФункцией с именем main
BГлавной процедурой, которую вы называете сами и указываете при сборке
CПервой строкой файла
DСпециальным ключевым словом start
3. Чувствительна ли Ada к регистру в идентификаторах?
AДа, строго
BНет: Put_Line и put_line — одно имя, но соблюдают стиль
CТолько в строках
DТолько в числах