Generic-пакеты и подпрограммы: основы

Generic в Ada — это шаблон единицы компиляции с параметрами: вы описываете алгоритм или структуру один раз, а инстанцируете под конкретные типы безопасно и без потери контроля.

Generic (обобщённая единица) — параметризованный шаблон пакета или подпрограммы: он не является готовым кодом, а служит «заготовкой», из которой инстанцированием (new) создаются конкретные пакеты и подпрограммы под заданные типы, значения и операции.

Желание написать «сортировку вообще» или «стек чего угодно» — древнее и естественное. Без обобщённости приходится либо копировать код под каждый тип (сортировка целых, сортировка строк, сортировка дат — три почти одинаковых файла), либо терять типобезопасность, работая через «универсальные указатели». Ada выбрала третий путь, заложенный ещё в 1983 году: generics — параметрический полиморфизм с явным, проверяемым контрактом параметров. Это духовный предок шаблонов C++ и обобщений Java/C#, но устроенный строже и предсказуемее их всех.

Зачем нужны обобщённые единицы

Главная цель — переиспользование без жертв. Хороший компонент должен работать с разными типами, но при этом сохранять полную проверку типов: попытка положить строку в стек целых обязана отлавливаться компилятором, а не превращаться в runtime-сюрприз. Generics дают именно это. Вы пишете обобщённый пакет Generic_Stack один раз, а затем инстанцируете его: package Int_Stack is new Generic_Stack (Integer); и package Str_Stack is new Generic_Stack (Unbounded_String);. Получаются два полноценных, типобезопасных пакета из одного исходника.

Принципиальное отличие от подхода «контейнер указателей на void» (как в старом C) — отсутствие приведения типов и связанных с ним ошибок. Обобщённый экземпляр знает свой тип точно: Int_Stack.Pop возвращает именно Integer, а не «нечто, что надо привести». Безопасность не приносится в жертву переиспользованию.

--  generic_stack.ads — ОБОБЩЁННЫЙ пакет (шаблон, не готовый код)
generic
   type Element is private;          -- формальный параметр: тип элемента
   Capacity : Positive;              -- формальный параметр: значение (размер)
package Generic_Stack is
   procedure Push (Item : in Element);
   function  Pop return Element;
   function  Is_Empty return Boolean;
   Overflow, Underflow : exception;
end Generic_Stack;

-- generic_stack.adb
package body Generic_Stack is
   Data : array (1 .. Capacity) of Element;
   Top  : Natural := 0;

   procedure Push (Item : in Element) is
   begin
      if Top >= Capacity then raise Overflow; end if;
      Top := Top + 1;
      Data (Top) := Item;
   end Push;

   function Pop return Element is
   begin
      if Top = 0 then raise Underflow; end if;
      Top := Top - 1;
      return Data (Top + 1);
   end Pop;

   function Is_Empty return Boolean is (Top = 0);
end Generic_Stack;

Инстанцирование: из шаблона — конкретный код

Сам по себе Generic_Stack вызвать нельзя — это шаблон. Чтобы им воспользоваться, его инстанцируют: подставляют конкретные значения формальных параметров и получают именованный экземпляр. Инстанцирование — это объявление вида package Имя is new Шаблон (фактические_параметры);.

with Generic_Stack;
with Ada.Text_IO; use Ada.Text_IO;
procedure Demo is
   --  Два разных экземпляра из одного шаблона:
   package Int_Stack is new Generic_Stack (Element => Integer, Capacity => 100);
   package Chr_Stack is new Generic_Stack (Element => Character, Capacity => 10);
begin
   Int_Stack.Push (42);
   Chr_Stack.Push ('A');
   Put_Line (Integer'Image (Int_Stack.Pop));   -- 42
   --  Int_Stack.Push ('A');  -- ОШИБКА КОМПИЛЯЦИИ: 'A' не Integer
end Demo;

Каждый экземпляр — самостоятельная сущность со своим состоянием: Int_Stack и второй экземпляр того же шаблона не делят данные. Имена параметров при инстанцировании можно указывать позиционно или именованно (Element => Integer) — именованная форма читабельнее, особенно когда параметров много.

Обобщённые подпрограммы

Обобщать можно не только пакеты, но и отдельные процедуры/функции. Классика — обмен значений (Swap), работающий для любого присваиваемого типа.

generic
   type T is private;                -- любой тип с присваиванием
procedure Swap (X, Y : in out T);

procedure Swap (X, Y : in out T) is
   Temp : constant T := X;
begin
   X := Y;
   Y := Temp;
end Swap;

--  Инстанцирование под конкретные типы:
procedure Swap_Int is new Swap (Integer);
procedure Swap_Flt is new Swap (Float);
--  Использование:
--  Swap_Int (A, B);   Swap_Flt (P, Q);

Как работает под капотом

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

Самое важное отличие Ada от шаблонов C++ — момент и характер проверки. В C++ ошибки в шаблоне исторически всплывали только при инстанцировании, порождая печально знаменитые «простыни» сообщений (концепты C++20 это смягчили). В Ada контракт параметров объявлен заранее и проверяется на двух сторонах: тело обобщённой единицы компилируется и проверяется относительно формальных параметров (что они обещают), а инстанцирование проверяется относительно фактических (что они предоставляют). Если фактический тип не удовлетворяет формальному требованию, ошибка возникает в точке инстанцирования и указывает прямо на несоответствие контракта. Эта «двусторонняя проверка по контракту» делает обобщённый код Ada предсказуемым и дружелюбным к сопровождению.

Generics против макросов и шаблонов: глубокое различие

Чтобы по-настоящему понять обобщённые единицы, важно отделить их от двух похожих, но принципиально иных механизмов — макросов и шаблонов C++. Макрос препроцессора (как #define в C) — это слепая текстовая подстановка: препроцессор не понимает типов, не проверяет согласованность, и ошибки в раскрытом тексте всплывают как загадочные сообщения о коде, которого вы не писали. Макрос «обобщает» лишь по форме, ничего не гарантируя по смыслу. Generics Ada — полная противоположность: это семантическая конструкция, встроенная в систему типов, с объявленным контрактом параметров, проверяемая компилятором и до, и в момент инстанцирования.

Шаблоны C++ ближе к Ada — это тоже параметрический полиморфизм, а не текст, — но между ними есть глубокое философское различие в подходе к проверке. Классический шаблон C++ исторически проверялся «по факту использования»: компилятор подставлял аргументы и пытался скомпилировать результат, а если что-то не складывалось (тип не поддерживал нужную операцию), выдавал каскад сообщений из глубин библиотеки. Контракт параметров был неявным — он сводился к тому, какие операции случайно использует тело. Ada требует явного контракта: вы заранее объявляете, что параметр должен быть, скажем, дискретным типом с операцией "<", и это обещание проверяется с обеих сторон. Концепты C++20 — позднее признание правоты этого подхода: они добавили в C++ именно явные контракты параметров шаблонов, к которым Ada пришла на десятилетия раньше.

Почему явный контракт важен для больших систем

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

Обобщённый стек как образец компонента

Вернёмся к нашему Generic_Stack и осмыслим, почему он — хороший учебный образец компонента. Во-первых, у него ясный, минимальный контракт параметров: тип элемента (private — нужно лишь присваивание и сравнение) и ёмкость (значение). Он не требует от элемента ничего лишнего, поэтому подходит максимально широкому кругу типов — от Integer до пользовательских записей. Это иллюстрирует принцип «требуй ровно необходимое»: чем скромнее контракт, тем шире переиспользуемость. Во-вторых, он полностью инкапсулирован: внутреннее представление (массив и индекс вершины) спрятано в теле, клиент видит только операции. В-третьих, он сигнализирует об ошибках через исключения (Overflow, Underflow), а не молча, — в духе надёжности Ada.

Из такого компонента естественно вырастает целое семейство. Захотев стек неограниченной ёмкости, мы заменим внутренний массив на динамическую структуру, не трогая интерфейс. Захотев потокобезопасный стек, обернём операции защищённым объектом. Захотев стек, пригодный для SPARK, добавим контракты и уберём динамику. Каждая вариация переиспользует контракт и идею, меняя лишь реализацию или добавляя аспект. Так обобщённая единица становится не разовым решением, а семенем, из которого вырастает линейка родственных компонентов под разные требования. Этот образ мышления — проектировать не отдельный класс, а параметризованное семейство — и есть суть того, ради чего generics были задуманы как инструмент инженерии в большом масштабе.

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

Частые ошибки

  • Пытаться использовать обобщённую единицу без инстанцирования. Generic_Stack.Push напрямую не вызвать — это шаблон. Сначала создайте экземпляр через new.
  • Ожидать общего состояния у разных экземпляров. Каждый инстанс независим; два экземпляра одного шаблона не делят переменные.
  • Применять операцию, не гарантированную формальным параметром. Если параметр объявлен type T is private;, внутри доступны лишь присваивание и =; для сложения или сравнения нужно объявить соответствующие формальные операции (тема следующего урока).
  • Считать generics макросами. Это не текстовая подстановка препроцессора, а типобезопасный параметрический полиморфизм с проверяемым контрактом — путать их вредно для понимания.

Итоги

  • Generic — параметризованный шаблон пакета или подпрограммы; сам по себе не исполняется.
  • Инстанцирование (package X is new G (...);) превращает шаблон в конкретную типобезопасную единицу.
  • Каждый экземпляр независим и хранит собственное состояние; параметры подставляются позиционно или именованно.
  • Обобщать можно и подпрограммы (Swap), не только пакеты.
  • Ключевое преимущество Ada — двусторонняя проверка по контракту параметров: тело проверяется по формальным, инстанцирование — по фактическим, ошибки точны и ранние.
Проверьте себя
1. Можно ли вызвать подпрограмму обобщённого пакета (например, Generic_Stack.Push) напрямую, без инстанцирования?
AДа, generic-пакеты работают как обычные
BНет: обобщённый пакет — это шаблон, его нужно сначала инстанцировать через new
CДа, но только для типа Integer
DТолько если включить специальную прагму
2. Чем проверка обобщённого кода в Ada выгодно отличается от классических шаблонов C++?
AВ Ada обобщений нет вовсе
BКонтракт формальных параметров объявлен заранее: тело проверяется по формальным, инстанцирование — по фактическим, ошибки ранние и точные
CAda вообще не проверяет типы в generics
DВ Ada все экземпляры делят общее состояние