Формальные параметры: типы, значения, операции

Формальные параметры generic — это контракт: какой тип, какое значение и какие операции обязан предоставить тот, кто инстанцирует. Чем точнее контракт, тем выразительнее обобщение.

Формальные параметры (generic formal parameters) — объявленные в заголовке generic требования к будущим фактическим аргументам: формальные типы (с разной степенью «обещаний»), формальные значения (объекты) и формальные подпрограммы (операции над типом).

В прошлом уроке параметр type Element is private; позволял лишь присваивать и сравнивать элементы — потому что private обещает ровно это. Но настоящая сила обобщённого программирования раскрывается, когда мы можем потребовать от типа больше: «этот тип умеет складываться», «по нему можно итерировать», «для него есть функция сравнения». Формальные параметры Ada — это язык, на котором обобщённая единица формулирует свои требования. И ключевая идея: внутри тела доступно ровно то, что объявлено в контракте, — ни больше, ни меньше.

Формальные типы: лесенка обещаний

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

Формальный типЧто обещает телу
type T is private;присваивание и = (любой неограниченный тип)
type T is limited private;почти ничего (даже без присваивания)
type T is (<>);дискретный тип: атрибуты 'First, 'Last, 'Succ, порядок
type T is range <>;знаковый целочисленный тип со своей арифметикой
type T is digits <>;плавающая точка
type T is array (...) of E;массив заданной структуры

Особого внимания заслуживает синтаксис (<>) — «коробочка» (box). type T is (<>); читается как «формальный дискретный тип»: фактическим аргументом может быть любой целочисленный, перечислимый или символьный тип, а тело получает право пользоваться дискретными атрибутами вроде T'Succ и T'Pred. Запись range <> сужает до целочисленных и даёт полноценную арифметику.

--  Обобщённая функция суммирования: требует ЦЕЛОЧИСЛЕННЫЙ тип
generic
   type Number is range <>;            -- обещает + и арифметику
package Math_Ops is
   function Sum (A, B : Number) return Number;
end Math_Ops;

package body Math_Ops is
   function Sum (A, B : Number) return Number is (A + B);  -- '+' доступен!
end Math_Ops;

Формальные значения и формальные подпрограммы

Помимо типов, параметром может быть значение (как Capacity : Positive в прошлом уроке) — это позволяет настраивать размеры, пороги, флаги на этапе инстанцирования. Но самый мощный вид параметра — формальная подпрограмма: операция, которую обобщённая единица требует «снаружи». Это и есть способ обобщить алгоритм по поведению, а не только по типу.

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

generic
   type Element is private;
   type Index   is (<>);
   type Arr is array (Index range <>) of Element;
   with function "<" (L, R : Element) return Boolean;   -- ФОРМАЛЬНАЯ операция
procedure Generic_Sort (Container : in out Arr);

procedure Generic_Sort (Container : in out Arr) is
   Tmp : Element;
begin
   for I in Container'First .. Index'Pred (Container'Last) loop
      for J in Index'Succ (I) .. Container'Last loop
         if Container (J) < Container (I) then   -- используем формальный "<"
            Tmp := Container (I);
            Container (I) := Container (J);
            Container (J) := Tmp;
         end if;
      end loop;
   end loop;
end Generic_Sort;

Ключевая строка — with function "<" (L, R : Element) return Boolean;. Слово with здесь объявляет, что у инстанцирующего будет затребована функция сравнения. При инстанцировании её передают:

type Int_Array is array (Positive range <>) of Integer;

procedure Sort_Ints is new Generic_Sort
   (Element   => Integer,
    Index     => Positive,
    Arr       => Int_Array,
    "<"       => Standard."<");      -- передаём встроенный "<" для Integer

Значения по умолчанию для формальных подпрограмм

Часто нужная операция «и так есть» у фактического типа. Ada позволяет задать формальной подпрограмме умолчание двумя удобными формами: is <> («возьми одноимённую видимую операцию автоматически») и is Имя (конкретное умолчание). Запись with function "<" (L, R : Element) return Boolean is <>; означает: «если при инстанцировании не передали явно, подставь видимый оператор "<" для фактического типа». Это убирает рутину: для Integer сравнение подставится само.

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

Формальная часть generic — это полноценный интерфейсный контракт, который компилятор использует дважды. При компиляции тела он проверяет, что тело обращается только к тому, что обещано формальными параметрами: если внутри Generic_Sort вдруг написать Container (I) + 1, не объявив "+" формальным, компилятор откажет — тело не имеет права на необъявленную операцию. При инстанцировании он проверяет, что фактические аргументы удовлетворяют контракту: фактический тип в категории, фактическая функция имеет нужную сигнатуру. Так обобщённый код оказывается проверен до любого конкретного использования и в каждом использовании — двойная гарантия, благодаря которой обобщения Ada не преподносят сюрпризов при сборке больших систем.

Категории формальных типов как система «концептов»

Лесенка формальных типов Ada заслуживает того, чтобы взглянуть на неё системно — как на встроенную в язык систему «концептов», то есть категорий, описывающих, что тип умеет. Каждая категория — это контракт определённой силы, и выбор правильной категории есть акт проектирования. Слишком слабая категория (private там, где нужна арифметика) лишит тело необходимых операций; слишком сильная (range <> там, где хватило бы private) необоснованно сузит множество подходящих фактических типов и снизит переиспользуемость. Искусство в том, чтобы потребовать ровно столько, сколько нужно алгоритму, — не больше и не меньше.

Рассмотрим спектр подробнее. type T is (<>) — дискретный тип: годятся целые, перечислимые, символьные; телу доступны 'First, 'Last, 'Succ, 'Pred и порядок. Это идеально для алгоритмов, перебирающих значения по порядку, но не нуждающихся в арифметике. type T is range <> сужает до целочисленных, зато даёт полноценную арифметику (+, -, *). type T is digits <> — для плавающей точки с её операциями и атрибутами точности. type T is delta <> — для фиксированной точки, важной во встраиваемых системах без аппаратного FPU. Есть категории для массивов, для тэгированных (объектных) типов, для access-типов. Эта палитра позволяет выразить требования к параметру с хирургической точностью.

Формальные подпрограммы и обобщение по поведению

Если формальные типы обобщают по структуре данных, то формальные подпрограммы обобщают по поведению — и это качественный скачок в выразительности. Передавая операцию (сравнение, преобразование, действие) как параметр, обобщённая единица становится настраиваемой не только под тип, но и под алгоритмическую политику. Один и тот же обобщённый сортировщик с разными переданными функциями "<" упорядочит данные по возрастанию, по убыванию или по любому экзотическому критерию — и всё это без единой правки его тела. Возможность задать формальной подпрограмме умолчание через is <> делает типичный случай бесшумным: для Integer встроенное сравнение подставится само, а для пользовательского типа вы передадите своё. Это сочетание гибкости и удобства — то, ради чего формальные подпрограммы и существуют: они превращают обобщённую единицу из «шаблона под тип» в «настраиваемый компонент под задачу».

Сигнатурные пакеты и абстракция над операциями

Когда обобщённой единице нужно несколько связанных операций над типом (скажем, для «числоподобного» параметра — сложение, умножение, ноль, единица), перечислять их по отдельности как формальные подпрограммы громоздко. Здесь помогает приём сигнатурного пакета (signature package) — обобщённого пакета без тела, который служит лишь группировкой формальных параметров, описывающих «концепт». Такой пакет инстанцируют, связывая абстрактные операции с конкретными для нужного типа, а затем передают его экземпляр целиком в другую обобщённую единицу через формальный пакет. Получается переиспользуемое описание набора требований к типу — по сути, именованный «концепт», который можно применять многократно.

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

Закрепим главную мысль о формальных параметрах. Они — это язык, на котором обобщённая единица формулирует требования к будущим аргументам, и точность этого языка определяет и безопасность, и переиспользуемость компонента. Формальные типы образуют лесенку обещаний от private до арифметических категорий; формальные значения настраивают размеры и пороги; формальные подпрограммы обобщают по поведению, а умолчания is <> убирают рутину. Ключевой принцип — требовать ровно столько, сколько нужно алгоритму: слишком слабый контракт лишит тело необходимых операций, слишком сильный без нужды сузит круг подходящих типов. Тело проверяется по формальным параметрам, инстанцирование — по фактическим, и операция, не объявленная в контракте, телу недоступна. Эта дисциплина явного контракта, к которой C++ пришёл лишь с концептами C++20, и делает обобщённый код Ada предсказуемым основанием для долгоживущих библиотек, а умение точно подбирать категории параметров — признак зрелого владения обобщениями.

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

  • Использовать в теле операцию, не объявленную формально. Если параметр type T is private;, то + или < внутри недоступны — объявите их как формальные подпрограммы или возьмите более «сильную» категорию типа (range <>).
  • Забыть передать формальную подпрограмму при инстанцировании. Если для параметра не задано is <>/умолчание, его обязательно передать явно, иначе ошибка инстанцирования.
  • Путать формальный тип (<>) и фактическую «пустоту». (<>) — это «коробочка», обозначающая дискретный формальный тип, а не отсутствие параметра.
  • Брать слишком слабую категорию типа. Объявив private там, где нужна арифметика, вы лишаете тело нужных операций; выбирайте категорию по реальным потребностям алгоритма.

Итоги

  • Формальные параметры — это контракт обобщённой единицы: что обязан предоставить инстанцирующий.
  • Формальные типы образуют лесенку обещаний: private (присваивание/=) → (<>) (дискретный) → range <> (арифметика) и т. д.
  • Формальное значение настраивает размеры/пороги; формальная подпрограмма (with function ...) обобщает по поведению.
  • is <> задаёт умолчание формальной подпрограммы — «возьми одноимённую видимую операцию», убирая рутину.
  • Тело проверяется по формальным параметрам, инстанцирование — по фактическим: операция, не объявленная в контракте, телу недоступна.
Проверьте себя
1. Что обещает телу обобщённой единицы формальный параметр вида type Number is range <>?
AТолько присваивание и сравнение на равенство
BЧто фактический тип — знаковый целочисленный, и телу доступна его арифметика (например, +)
CЧто это обязательно тип Integer
DЧто тип является записью (record)
2. Зачем обобщённой сортировке нужен формальный параметр with function "<" (L, R : Element) return Boolean?
AЭто лишнее украшение, без него тоже работает
BЧтобы получить операцию сравнения извне и обобщить алгоритм по поведению, не зная, как именно сравниваются конкретные элементы
CЧтобы запретить сортировку строк
DЧтобы ускорить компиляцию