Формальные параметры: типы, значения, операции
Формальные параметры 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 <>задаёт умолчание формальной подпрограммы — «возьми одноимённую видимую операцию», убирая рутину.- Тело проверяется по формальным параметрам, инстанцирование — по фактическим: операция, не объявленная в контракте, телу недоступна.