Дженерики в Ada ── контракты через типы и повторное использование без накладных расходов
· Го Комура · Ada, Язык программирования, Дженерики, Система типов, Статическая типизация, Контрактная модель, Абстракции без накладных расходов, GNAT, Alire, Высокая надёжность, Повторное использование кода
1. Введение ── не «принимать что угодно», а «что обещано»
При попытке писать переиспользуемый код на статически типизированном языке быстро упираешься в одну и ту же проблему. Стек, написанный для целых чисел, хочется использовать и для строк. Ту же статистическую обработку — применить и к массиву чисел с плавающей точкой. Логику сортировки по возрастанию — использовать и для сортировки по убыванию. Но если для каждого типа копировать один и тот же код, где-то неизбежно забудешь внести исправление. А если пойти в другую сторону и принимать всё подряд через void* или приведения типов, разрушается типобезопасность.
Ответ Ada — дженерики (generic units).
Дженерики в Ada — это не простая текстовая подстановка. Они принимают в качестве формальных параметров типы, значения, подпрограммы и даже сами пакеты, а статическая проверка типов выполняется в момент инстанцирования. То есть вместо того, чтобы во время выполнения проверять «действительно ли этот тип подходит», механизм ещё на этапе компиляции устанавливает, «удовлетворяет ли данный компонент заданному контракту».
flowchart LR
A[Код, который хочется переиспользовать] --> B{Как переиспользовать?}
B --> C[Копирование и вставка]
B --> D[void* / Object / приведение типов]
B --> E[Дженерики Ada]
C --> C1[Легко забыть внести исправление]
D --> D1[Легко получить ошибку времени выполнения или потерю типобезопасности]
E --> E1[Типобезопасность]
E --> E2[Проверка на этапе компиляции]
E --> E3[Без лишней диспетчеризации в рантайме]
В этой статье обобщённое программирование в Ada рассматривается в следующем порядке.
- Обобщённые подпрограммы
- Обобщённые пакеты
- Параметры-типы, параметры-значения, параметры-подпрограммы
- Категории типов:
private,range <>,digits <>и другие - Примеры реализации: сортировка, стек, статистическая обработка,
Count_If, хранилище ключ-значение - Дженерики высшего порядка на основе формальных параметров-пакетов
- Contract model в Ada и практические принципы проектирования
Эта тема продолжает серию статей нашего блога: «Притягательность языка Ada», «Введение в формальную верификацию с SPARK», «Безопасная конкурентность в Ada» и «Программирование систем реального времени в Ada». Здесь мы раскрываем философию Ada «типы формулируют дизайн» через призму дженериков.
2. Карта статьи
Сначала охватим общую картину с помощью схемы. Если понимать дженерики Ada только как «механизм, принимающий тип в качестве аргумента», это будет слишком узкий взгляд. На практике, в зависимости от того, что именно требуется переиспользовать, комбинируются подпрограммы, пакеты, параметры-подпрограммы, параметры-значения и формальные параметры-пакеты.
mindmap
root((Ada Generics))
Обобщённая подпрограмма
Swap
Count_If
Sort
Обобщённый пакет
Stack
Statistics
KV Store
Формальные параметры
Тип
private
limited private
range box
mod box
digits box
delta box
discrete box
Объект
Max_Size
Threshold
Подпрограмма
Функция Less
Функция Equals
Predicate
Пакет
with package P is new Generic
Идеи проектирования
Contract Model
Статическая проверка
Абстракции без накладных расходов
Разделение спецификации и тела
Читать эту статью просто. В первой половине мы разбираем синтаксис, во второй — вопросы проектирования. Если вы впервые видите Ada, не пытайтесь сразу запомнить все детали синтаксиса — обращайте внимание на то, «что взято в качестве формального параметра» и «какие операции разрешены для этого параметра».
3. Среда выполнения и способ компиляции
Код в этой статье рассчитан на GNAT версии 15.x и выше. GNAT — ведущий компилятор Ada, его можно установить через Alire. Alire — это менеджер пакетов для Ada / SPARK, который также используется для управления инструментальной цепочкой (toolchain) и сборки.
gnat --version
# GNAT 15.2.1
Установите GNAT через Alire (менеджер пакетов Ada) командой alr install gnat_native gprbuild и добавьте его в PATH.
Примеры, рассматриваемые в этой статье, предполагается размещать в репозитории следующим образом.
flowchart TB
R[ada-generic-programming/] --> S[src/]
S --> N[snippets/]
N --> A[01_swap.ada]
N --> B[02_stack.ada]
N --> C[03_sort.ada]
N --> D[04_statistics.ada]
N --> E[05_filter.ada]
N --> F[06_kv_store.ada]
R --> README[README.md]
Пример, где несколько единиц компиляции объединены в один файл, сначала разбивается с помощью gnatchop, а затем собирается через gnatmake.
mkdir work
cd work
gnatchop ../src/snippets/01_swap.ada
gnatmake -gnata swap_demo
./swap_demo
Флаг -gnata включает проверку assertion-ов. Для самих дженериков он не обязателен, но в учебных примерах облегчает проверку контрактов и граничных условий.
sequenceDiagram
participant Dev as Разработчик
participant Chop as gnatchop
participant Build as gnatmake
participant Exe as Исполняемый файл
Dev->>Chop: Передаёт один файл .ada
Chop-->>Dev: Разбивает на .ads / .adb / main
Dev->>Build: gnatmake -gnata main
Build-->>Dev: Выполняет связывание и линковку
Dev->>Exe: ./main
Exe-->>Dev: Результат выполнения
4. Базовая модель дженериков в Ada
Дженерики в Ada проще всего представить как последовательность из трёх шагов.
- Написать обобщённый модуль (generic unit)
- Указать формальные параметры в секции
generic - На стороне использования выполнить инстанцирование через
new
flowchart LR
G[Объявление generic] --> F[Формальные параметры]
F --> B[Тело дженерика]
B --> I[Инстанцирование через new]
I --> U[Используется как обычная подпрограмма или пакет]
F --> F1[Тип]
F --> F2[Значение]
F --> F3[Подпрограмма]
F --> F4[Пакет]
Например, если сделать обобщённой операцию обмена двух значений, единственным формальным параметром может быть тип.
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
На этом этапе Generic_Swap ещё нельзя вызвать. Это «шаблон операции обмена, применимый к любому типу Element». Только после передачи конкретного типа он становится обычной процедурой.
procedure Swap_Integer is new Generic_Swap (Integer);
Схематически это выглядит так.
flowchart TB
Template[Generic_Swap<br/>type Element is private] -->|Передаём Integer| SwapInt[Swap_Integer]
Template -->|Передаём Character| SwapChar[Swap_Character]
Template -->|Передаём My_Record| SwapRecord[Swap_My_Record]
SwapInt --> ICall[Обмен переменных Integer]
SwapChar --> CCall[Обмен переменных Character]
SwapRecord --> RCall[Обмен переменных My_Record]
Важно, что тело шаблона написано только с использованием операций, доступных для Element. При объявлении type Element is private; доступны базовые операции — присваивание и сравнение на равенство, но не сравнение «больше/меньше» и не арифметика. Иными словами, само объявление дженерика говорит о том, «на что этот компонент вправе рассчитывать».
5. Виды формальных параметров ── словарь дженериков Ada
В дженерики Ada можно передавать не только типы — и в этом их принципиальное отличие от типичных дженериков C# или Java.
flowchart TB
P[Формальные параметры generic] --> T[Параметры-типы]
P --> O[Объекты / параметры-значения]
P --> S[Параметры-подпрограммы]
P --> PKG[Параметры-пакеты]
T --> T1[type Element is private]
T --> T2[type Index is box]
T --> T3[type Real is digits box]
O --> O1[Max_Size : Positive]
O --> O2[Default_Value : Element]
S --> S1[with function Less...]
S --> S2[with procedure Put ...]
PKG --> P1[with package P is new ...]
В таблице ниже перечислены типичные формальные параметры.
| Вид | Пример | Значение |
|---|---|---|
| Параметр-тип | type Element is private; |
Базовая форма: принимает любой definite не-limited тип |
| Ограниченный (limited) параметр-тип | type Element is limited private; |
Принимает и типы, которые нельзя копировать |
| Дискретный тип | type Index is (<>); |
Целочисленные или перечисляемые типы, пригодные для индексации массива |
| Целый тип со знаком | type Count is range <>; |
Можно рассчитывать на +, -, сравнение и другие целочисленные операции |
| Модульный целый тип | type Word is mod <>; |
Битовые операции, модульная арифметика |
| Тип с плавающей точкой | type Real is digits <>; |
Float, Long_Float, пользовательские типы с плавающей точкой |
| Тип с фиксированной точкой | type Money is delta <>; |
Арифметика с фиксированной точкой |
| Параметр-значение | Max_Size : Positive; |
Размер, порог и т. п., фиксируемые для каждого экземпляра |
| Параметр-подпрограмма | with function Predicate (...) return Boolean; |
Внедрение поведения: функции сравнения, предикаты и т. п. |
| Параметр-пакет | with package P is new Some_Generic (<>); |
Принимает уже созданный экземпляр обобщённого пакета как компонент |
Благодаря этому словарю в Ada естественным образом получается писать код по принципу «принимаем только те типы, которые поддерживают нужные операции», а не «принимаем что угодно и рискуем внутри».
6. Обобщённые подпрограммы ── минимальная конструкция на примере Generic_Swap
В качестве первого примера рассмотрим Generic_Swap — обмен двух переменных произвольного типа.
6.1 Спецификация
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
Часть после generic — это список формальных параметров. Здесь принимается тип с именем Element. is private означает, что с точки зрения тела дженерика внутреннее представление этого типа неизвестно.
Из этого объявления понятны две вещи.
Generic_Swapработает для любого типаElement- Тело не зависит ни от внутренней структуры
Element, ни от сравнения «больше/меньше»
6.2 Тело
procedure Generic_Swap (A, B : in out Element) is
Temp : constant Element := A;
begin
A := B;
B := Temp;
end Generic_Swap;
В этом теле для Element используется только присваивание. Ни A < B, ни A + B не задействованы. Поэтому оно естественным образом подходит для Integer, Character, типов-записей, перечислений — для любого типа, допускающего присваивание.
flowchart LR
subgraph Before[До вызова]
A1[A = 10]
B1[B = 20]
end
A1 --> T[Temp = A]
B1 --> A2[A = B]
T --> B2[B = Temp]
subgraph After[После вызова]
A2[A = 20]
B2[B = 10]
end
6.3 Инстанцирование
На стороне использования применяется new.
procedure Swap_Int is new Generic_Swap (Integer);
procedure Swap_Char is new Generic_Swap (Character);
Теперь Swap_Int и Swap_Char можно вызывать как обычные процедуры.
with Ada.Text_IO; use Ada.Text_IO;
procedure Swap_Demo is
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
procedure Generic_Swap (A, B : in out Element) is
Temp : constant Element := A;
begin
A := B;
B := Temp;
end Generic_Swap;
procedure Swap_Int is new Generic_Swap (Integer);
X : Integer := 10;
Y : Integer := 20;
begin
Put_Line ("Before: X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
Swap_Int (X, Y);
Put_Line ("After : X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
end Swap_Demo;
Пример вывода:
Before: X= 10, Y= 20
After : X= 20, Y= 10
Передать переменную типа Float в Swap_Int (X, Y); нельзя — Swap_Int является обычной процедурой, инстанцированной именно для Integer. Дженерики проще понять не как «дыру, куда влезает что угодно», а как механизм, создающий для каждого типа свой безопасный конкретный вариант.
7. Обобщённые пакеты ── тип и значение как параметры
Когда нужно переиспользовать не одну подпрограмму, а сразу набор операций вместе с внутренним состоянием, используют обобщённый пакет. Классический пример — стек.
У стека базовая логика одинакова — меняются только тип элемента и максимальный размер.
flowchart TB
G[Generic_Stack] --> P1[Element_Type]
G --> P2[Max_Size]
G --> Ops[Push / Pop / Size / Is_Empty / Is_Full]
G --> I1[Int_Stack<br/>Element=Integer<br/>Max_Size=5]
G --> I2[Float_Stack<br/>Element=Float<br/>Max_Size=3]
G --> I3[String_Stack<br/>Element=Unbounded_String<br/>Max_Size=20]
7.1 Спецификация
generic
type Element_Type is private;
Max_Size : Positive;
package Generic_Stack is
procedure Push (Item : Element_Type);
function Pop return Element_Type;
function Is_Empty return Boolean;
function Is_Full return Boolean;
function Size return Natural;
Stack_Overflow : exception;
Stack_Underflow : exception;
end Generic_Stack;
Здесь используются два вида формальных параметров.
Element_Type— параметр-типMax_Size— параметр-значение
Поскольку Max_Size имеет тип Positive, инстанцировать с размером 0 или меньше нельзя. Таким образом, параметры-значения тоже можно ограничивать типом.
7.2 Тело
package body Generic_Stack is
subtype Index_Type is Positive range 1 .. Max_Size;
type Storage_Type is array (Index_Type) of Element_Type;
Data : Storage_Type;
Top : Natural := 0;
procedure Push (Item : Element_Type) is
begin
if Top = Max_Size then
raise Stack_Overflow;
end if;
Top := Top + 1;
Data (Top) := Item;
end Push;
function Pop return Element_Type is
Result : Element_Type;
begin
if Top = 0 then
raise Stack_Underflow;
end if;
Result := Data (Top);
Top := Top - 1;
return Result;
end Pop;
function Is_Empty return Boolean is
begin
return Top = 0;
end Is_Empty;
function Is_Full return Boolean is
begin
return Top = Max_Size;
end Is_Full;
function Size return Natural is
begin
return Top;
end Size;
end Generic_Stack;
Важно, что в теле этого пакета Data и Top создаются отдельно для каждого экземпляра.
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
Оба этих пакета созданы из одного и того же шаблона, но внутреннее состояние у них не общее.
flowchart LR
Template[Generic_Stack] --> IntStack[Int_Stack]
Template --> FloatStack[Float_Stack]
subgraph I[Состояние Int_Stack]
ITop[Top]
IData[Data : массив Integer]
end
subgraph F[Состояние Float_Stack]
FTop[Top]
FData[Data : массив Float]
end
IntStack --> I
FloatStack --> F
7.3 Переходы состояний стека
Стек удобно рассматривать как конечный автомат.
stateDiagram-v2
[*] --> Empty
Empty --> NonEmpty: Push
NonEmpty --> NonEmpty: Push / Pop
NonEmpty --> Empty: Pop извлекает последний элемент
NonEmpty --> Full: Push достигает Max_Size
Full --> NonEmpty: Pop
Full --> Overflow: Push
Empty --> Underflow: Pop
7.4 Пример использования
with Ada.Text_IO; use Ada.Text_IO;
procedure Stack_Demo is
generic
type Element_Type is private;
Max_Size : Positive;
package Generic_Stack is
procedure Push (Item : Element_Type);
function Pop return Element_Type;
function Is_Empty return Boolean;
function Is_Full return Boolean;
function Size return Natural;
Stack_Overflow : exception;
Stack_Underflow : exception;
end Generic_Stack;
package body Generic_Stack is
subtype Index_Type is Positive range 1 .. Max_Size;
type Storage_Type is array (Index_Type) of Element_Type;
Data : Storage_Type;
Top : Natural := 0;
procedure Push (Item : Element_Type) is
begin
if Top = Max_Size then
raise Stack_Overflow;
end if;
Top := Top + 1;
Data (Top) := Item;
end Push;
function Pop return Element_Type is
Result : Element_Type;
begin
if Top = 0 then
raise Stack_Underflow;
end if;
Result := Data (Top);
Top := Top - 1;
return Result;
end Pop;
function Is_Empty return Boolean is (Top = 0);
function Is_Full return Boolean is (Top = Max_Size);
function Size return Natural is (Top);
end Generic_Stack;
package Int_Stack is new Generic_Stack (Integer, 5);
begin
Int_Stack.Push (10);
Int_Stack.Push (20);
Int_Stack.Push (30);
Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
end Stack_Demo;
На практике обобщённые пакеты хорошо работают для «небольших контейнеров», «буферов фиксированной длины», «кольцевых буферов», «очередей для логирования», «слоёв аппаратной абстракции». В Ada проектирование, при котором размер статически фиксируется через параметры-типы и параметры-значения, а не меняется во время выполнения, особенно хорошо сочетается с высоконадёжными системами.
8. Формальные параметры-подпрограммы ── внедрение поведения
Одних только типов иногда недостаточно для выражения нужной логики. Например, для сортировки нужен не только тип элемента, но и логика сравнения — «что должно идти первым».
В Ada саму функцию сравнения можно сделать формальным параметром дженерика.
flowchart LR
A[Generic_Insertion_Sort] --> T[Item_Type]
A --> I[Index]
A --> ARR[Item_Array]
A --> CMP[Функция сравнения]
CMP --> ASC[Использовать стандартное сравнение]
CMP --> DESC[Передать Greater для убывания]
CMP --> CUSTOM[Передать собственный порядок]
8.1 Спецификация
generic
type Item_Type is private;
type Index is (<>);
type Item_Array is array (Index range <>) of Item_Type;
with function "<" (Left, Right : Item_Type) return Boolean is <>;
procedure Generic_Insertion_Sort (Items : in out Item_Array);
Здесь четыре формальных параметра.
Item_Type— тип элемента массиваIndex— тип индекса массиваItem_Array— конкретный тип массива"<"— функция сравнения
type Index is (<>); принимает дискретный тип. Это не только целочисленные типы, но и перечисления. Возможность использовать в качестве индекса массива не только Positive, но и перечисление вроде Day — характерная черта Ada.
is <> в with function "<" ... is <>; означает: если фактический параметр опущен, используется видимый в этой точке стандартный оператор или подходящая функция. То есть для типа вроде Integer, где < уже определён, явно указывать функцию сравнения не требуется.
8.2 Тело
procedure Generic_Insertion_Sort (Items : in out Item_Array) is
J : Index;
Key : Item_Type;
begin
if Items'Length <= 1 then
return;
end if;
for I in Index'Succ (Items'First) .. Items'Last loop
Key := Items (I);
J := I;
while J > Items'First and then Key < Items (Index'Pred (J)) loop
Items (J) := Items (Index'Pred (J));
J := Index'Pred (J);
end loop;
Items (J) := Key;
end loop;
end Generic_Insertion_Sort;
Сортировка вставками не подходит для больших массивов, зато отлично подходит для объяснения дженериков: заменив только функцию сравнения, ту же структуру цикла можно использовать и для сортировки по возрастанию, и по убыванию.
flowchart TB
Start[Неотсортированный массив] --> Pick[Берём Key слева направо]
Pick --> Compare{"Key стоит перед предыдущим элементом?"}
Compare -->|Да| Shift[Сдвигаем предыдущий элемент вправо]
Shift --> Compare
Compare -->|Нет| Insert[Вставляем Key]
Insert --> Done{Обработали все элементы?}
Done -->|Нет| Pick
Done -->|Да| End[Отсортированный массив]
8.3 Возрастающий и убывающий порядок из одного тела
type Int_Array is array (Positive range <>) of Integer;
procedure Sort_Asc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array);
function Greater (Left, Right : Integer) return Boolean is
(Left > Right);
procedure Sort_Desc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array,
"<" => Greater);
Sort_Asc использует стандартный <. А в Sort_Desc функция сравнения заменена через "<" => Greater.
flowchart LR
Data[99, 3, 47, 12] --> A[Sort_Asc<br/>стандартное сравнение]
Data --> D[Sort_Desc<br/>Greater передан как функция сравнения]
A --> AO[3, 12, 47, 99]
D --> DO[99, 47, 12, 3]
Этот механизм близок к передаче функционального объекта сравнения в качестве аргумента шаблона в C++ или к требованию порядка через границы трейтов в Rust. Однако в Ada это выражается явно, через формальный параметр-подпрограмму: «нужно передать функцию именно такой сигнатуры».
9. Категории типов ── контракты точнее, чем private
type T is private; удобен, но не всесилен. Для типа private нельзя просто так рассчитывать на арифметику или сравнение «больше/меньше». Поэтому в Ada формальному параметру-типу можно задать категорию.
flowchart TB
FormalType[Формальный тип] --> Private[private]
FormalType --> Limited[limited private]
FormalType --> Discrete[discrete box: дискретный тип]
FormalType --> Signed[range box: целый со знаком]
FormalType --> Modular[mod box: модульный целый]
FormalType --> Float[digits box: плавающая точка]
FormalType --> Fixed[delta box: фиксированная точка]
FormalType --> Access[тип access]
Discrete --> Enum[Перечисление]
Discrete --> Int[Целый тип]
Float --> F1[Float]
Float --> F2[Long_Float]
Float --> F3[Пользовательский тип с плавающей точкой]
9.1 Зачем указывать категорию
Например, для вычисления среднего и дисперсии нужны сложение, вычитание, умножение и деление. На типе private рассчитывать на эти операции нельзя. Поэтому ограничимся типами с плавающей точкой.
generic
type Real is digits <>;
type Real_Array is array (Positive range <>) of Real;
package Generic_Statistics is
function Mean (Values : Real_Array) return Real;
function Variance (Values : Real_Array) return Real;
end Generic_Statistics;
Благодаря type Real is digits <>; становится понятно, что Real — тип с плавающей точкой. Значит, в теле дженерика можно использовать +, -, *, / и другие операции.
9.2 Тело
package body Generic_Statistics is
function Mean (Values : Real_Array) return Real is
Sum : Real := 0.0;
begin
if Values'Length = 0 then
return 0.0;
end if;
for V of Values loop
Sum := Sum + V;
end loop;
return Sum / Real (Values'Length);
end Mean;
function Variance (Values : Real_Array) return Real is
M : constant Real := Mean (Values);
Sum : Real := 0.0;
begin
if Values'Length = 0 then
return 0.0;
end if;
for V of Values loop
declare
D : constant Real := V - M;
begin
Sum := Sum + D * D;
end;
end loop;
return Sum / Real (Values'Length);
end Variance;
end Generic_Statistics;
9.3 Использование с Float и Long_Float
type Float_Array is array (Positive range <>) of Float;
type Long_Array is array (Positive range <>) of Long_Float;
package Float_Stats is new Generic_Statistics (Float, Float_Array);
package Long_Stats is new Generic_Statistics (Long_Float, Long_Array);
Одну и ту же статистическую обработку можно переиспользовать для типов с плавающей точкой разной точности.
flowchart LR
Stats[Generic_Statistics<br/>Real is digits box] --> FS[Float_Stats]
Stats --> LS[Long_Stats]
Stats --> MS[My_Real_Stats]
FS --> FCalc[Mean / Variance для Float]
LS --> LCalc[Mean / Variance для Long_Float]
MS --> MCalc[Mean / Variance для пользовательского Real]
9.4 Указание категории — «спецификация на уровне типов»
Указание категории — не просто синтаксис, чтобы успокоить компилятор. Это ещё и спецификация, сообщающая читателю, «чего требует данный компонент».
| Что нужно реализовать | Подходящий формальный тип | Причина |
|---|---|---|
| Обмен, сохранение, извлечение | private |
Достаточно присваивания |
| Управление некопируемыми ресурсами | limited private |
Присваивание не предполагается |
| Индексация массива, обход состояний перечисления | (<>) |
Доступны First, Last, Succ, Pred |
| Суммирование целых, счётчики | range <> |
Можно рассчитывать на целочисленную арифметику |
| Битовые маски, циклические счётчики | mod <> |
Можно рассчитывать на модульную арифметику |
| Среднее, дисперсия, числовые вычисления | digits <> |
Можно рассчитывать на арифметику с плавающей точкой |
| Денежные суммы, величины с фиксированной точностью | delta <> |
Можно рассчитывать на арифметику с фиксированной точкой |
10. Внедрение предиката ── Count_If в стиле Ada
Формальный параметр-подпрограмма годится не только для функций сравнения, но и для предикатов — функций, принимающих значение и возвращающих Boolean.
Роль, близкую к Func<T, bool> в C#, Predicate<T> в Java или лямбдам и функциональным объектам в C++, в Ada можно выразить как формальную подпрограмму дженерика.
10.1 Спецификация
generic
type Element is private;
type Index is (<>);
type Array_Type is array (Index range <>) of Element;
with function Predicate (Item : Element) return Boolean;
function Generic_Count_If (Arr : Array_Type) return Natural;
Здесь у Predicate нет is <>. Стандартной видимой предикатной функции по умолчанию не существует, поэтому вызывающая сторона обязана передать её явно.
10.2 Тело
function Generic_Count_If (Arr : Array_Type) return Natural is
Count : Natural := 0;
begin
for Item of Arr loop
if Predicate (Item) then
Count := Count + 1;
end if;
end loop;
return Count;
end Generic_Count_If;
Логика обработки проста.
flowchart LR
Arr[Массив] --> Loop[Обходим каждый элемент]
Loop --> P{"Predicate(Item)?"}
P -->|True| Inc[Увеличиваем Count]
P -->|False| Skip[Ничего не делаем]
Inc --> Next[Следующий элемент]
Skip --> Next
Next --> Result[Возвращаем Count]
10.3 Подсчёт чётных чисел и подсчёт по порогу
type Int_Array is array (Positive range <>) of Integer;
function Is_Even (N : Integer) return Boolean is
(N mod 2 = 0);
function Is_Large (N : Integer) return Boolean is
(N > 50);
function Count_Even is new Generic_Count_If
(Element => Integer,
Index => Positive,
Array_Type => Int_Array,
Predicate => Is_Even);
function Count_Large is new Generic_Count_If
(Element => Integer,
Index => Positive,
Array_Type => Int_Array,
Predicate => Is_Large);
Из одной и той же логики обхода можно создать две функции, которые отличаются только условием.
flowchart TB
G[Generic_Count_If] --> E[Count_Even<br/>Predicate = Is_Even]
G --> L[Count_Large<br/>Predicate = Is_Large]
Data[12, 7, 88, 3, 56, 91, 44, 19, 62] --> E
Data --> L
E --> ER[Количество чётных]
L --> LR[Количество больше 50]
В этом примере обход массива, управление счётчиком и возврат результата — всё общее. А то, «что именно считать», внедряется отдельной функцией. Это базовая форма проектирования высшего порядка в Ada.
11. Композиция нескольких параметров ── универсальное хранилище ключ-значение
В реальных компонентах редко хватает одного параметра-типа. Нужно комбинировать сразу несколько условий: типы ключа и значения, способ сравнения ключей, максимальное число записей.
В качестве примера рассмотрим простое хранилище ключ-значение фиксированной длины.
flowchart TB
KV[Generic_KV_Store] --> K[Key_Type]
KV --> V[Value_Type]
KV --> EQ[Функция сравнения ключей]
KV --> M[Max_Entries]
KV --> Ops[Put / Get / Contains]
Ops --> Use1[Хранилище конфигурационных значений]
Ops --> Use2[Небольшой кэш]
Ops --> Use3[Встраиваемый словарь фиксированной длины]
11.1 Спецификация
generic
type Key_Type is private;
type Value_Type is private;
with function "=" (Left, Right : Key_Type) return Boolean is <>;
Max_Entries : Positive := 50;
package Generic_KV_Store is
procedure Put (Key : Key_Type; Val : Value_Type);
function Get (Key : Key_Type) return Value_Type;
function Contains (Key : Key_Type) return Boolean;
Key_Not_Found : exception;
Store_Full : exception;
end Generic_KV_Store;
У этого пакета четыре формальных параметра.
| Параметр | Вид | Роль |
|---|---|---|
Key_Type |
Тип | Тип ключа |
Value_Type |
Тип | Тип значения |
"=" |
Подпрограмма | Проверка совпадения ключей |
Max_Entries |
Значение | Максимальное число записей |
Для Max_Entries задано значение по умолчанию := 50. Поэтому, если не указать иное, получится хранилище на 50 записей.
11.2 Тело
package body Generic_KV_Store is
subtype Index_Type is Positive range 1 .. Max_Entries;
type Key_Array is array (Index_Type) of Key_Type;
type Value_Array is array (Index_Type) of Value_Type;
type Used_Array is array (Index_Type) of Boolean;
Keys : Key_Array;
Values : Value_Array;
Used : Used_Array := (others => False);
function Find_Index (Key : Key_Type) return Natural is
begin
for I in Index_Type loop
if Used (I) and then Keys (I) = Key then
return I;
end if;
end loop;
return 0;
end Find_Index;
function Find_Free return Natural is
begin
for I in Index_Type loop
if not Used (I) then
return I;
end if;
end loop;
return 0;
end Find_Free;
procedure Put (Key : Key_Type; Val : Value_Type) is
Pos : Natural := Find_Index (Key);
begin
if Pos = 0 then
Pos := Find_Free;
if Pos = 0 then
raise Store_Full;
end if;
Used (Pos) := True;
Keys (Pos) := Key;
end if;
Values (Pos) := Val;
end Put;
function Get (Key : Key_Type) return Value_Type is
Pos : constant Natural := Find_Index (Key);
begin
if Pos = 0 then
raise Key_Not_Found;
end if;
return Values (Pos);
end Get;
function Contains (Key : Key_Type) return Boolean is
begin
return Find_Index (Key) /= 0;
end Contains;
end Generic_KV_Store;
Эта реализация использует линейный поиск, поэтому не подходит для больших объёмов данных. Но там, где важны фиксированный размер, небольшой масштаб и отсутствие динамического выделения памяти, такая форма удобна.
sequenceDiagram
participant App as Вызывающая сторона
participant Store as Экземпляр Generic_KV_Store
participant Data as Keys/Values/Used
App->>Store: Put(Key, Value)
Store->>Data: Find_Index(Key)
alt Ключ уже существует
Store->>Data: Values(Pos) := Value
else Новый ключ
Store->>Data: Find_Free
Store->>Data: Keys(Pos) := Key
Store->>Data: Values(Pos) := Value
Store->>Data: Used(Pos) := True
end
App->>Store: Get(Key)
Store->>Data: Find_Index(Key)
Data-->>Store: Pos
Store-->>App: Values(Pos)
11.3 Пример инстанцирования
with Ada.Strings.Unbounded;
use Ada.Strings.Unbounded;
procedure KV_Demo is
package Int_String_Store is new Generic_KV_Store
(Key_Type => Integer,
Value_Type => Unbounded_String,
Max_Entries => 10);
begin
Int_String_Store.Put (1, To_Unbounded_String ("Ada"));
Int_String_Store.Put (2, To_Unbounded_String ("SPARK"));
if Int_String_Store.Contains (1) then
-- значение можно получить через Get (1)
null;
end if;
end KV_Demo;
"=" здесь опущен. У Integer есть стандартный оператор равенства, и is <> использует именно его.
Если ключом служит, например, строка без учёта регистра, можно передать собственную функцию равенства.
function Same_Key (Left, Right : Unbounded_String) return Boolean is
(To_Lower (To_String (Left)) = To_Lower (To_String (Right)));
package String_Key_Store is new Generic_KV_Store
(Key_Type => Unbounded_String,
Value_Type => Integer,
"=" => Same_Key,
Max_Entries => 100);
12. Формальные параметры-пакеты ── дальнейшая модуляризация дженериков
В дженериках Ada формальным параметром может быть сам пакет. Это позволяет передать «экземпляр, созданный из одного обобщённого пакета» на вход другому дженерику.
flowchart LR
GS[Generic_Stack] --> IS[Int_Stack]
IS --> Logger[Generic_Stack_Logger]
Logger --> Logged[Int_Stack_Logger_Instance]
12.1 Логгер, принимающий стек
Предположим, мы хотим создать логгер, который принимает экземпляр Generic_Stack и выводит его текущий размер.
generic
with package Stack is new Generic_Stack (<>);
package Generic_Stack_Logger is
procedure Print_Size;
end Generic_Stack_Logger;
Тело выглядит так.
with Ada.Text_IO; use Ada.Text_IO;
package body Generic_Stack_Logger is
procedure Print_Size is
begin
Put_Line ("Stack size =" & Natural'Image (Stack.Size));
end Print_Size;
end Generic_Stack_Logger;
На стороне использования сначала создаётся стек, а затем он передаётся логгеру.
package Int_Stack is new Generic_Stack
(Element_Type => Integer,
Max_Size => 10);
package Int_Stack_Logger is new Generic_Stack_Logger
(Stack => Int_Stack);
Такое проектирование позволяет комбинировать обобщённые компоненты друг с другом.
flowchart TB
subgraph Layer1[Первый этап]
T1[Generic_Stack] --> I1[Int_Stack]
end
subgraph Layer2[Второй этап]
T2[Generic_Stack_Logger] --> I2[Int_Stack_Logger]
end
I1 --> T2
I2 --> API[Print_Size]
По назначению это близко к template template parameters в C++, но в Ada можно явно указать: «принимается экземпляр именно этого обобщённого пакета». В крупных проектах на Ada это удобно, когда контейнеры, алгоритмы, логирование, проверки и вспомогательные тестовые средства разделены и комбинируются между собой.
13. Contract Model ── главная идея дженериков Ada
Для понимания дженериков Ada важнее всего понятие contract model — модели контракта.
Тело дженерика обязано использовать только те операции, которые обещаны формальными параметрами. Например, если объявлено только type Element is private;, использовать < для Element нельзя. Чтобы использовать <, нужно либо явно указать его как формальную подпрограмму, либо уточнить категорию типа.
flowchart TB
Spec[Формальная часть generic = контракт] --> Body[Тело generic: реализация в рамках контракта]
Body --> Check1[Тело проверяется типами независимо]
Spec --> Inst[Инстанцирование]
Actual[Фактические параметры: типы, функции, значения] --> Inst
Inst --> Check2[Проверка соответствия фактических параметров контракту]
Check2 --> Instance[Результат: обычный пакет или подпрограмма]
Такое проектирование защищает не только того, кто использует дженерик, но и того, кто его пишет.
13.1 Отличие от того, как выглядят шаблоны C++
Шаблоны C++ мощны, но исторически обладали свойством «ошибка проявляется только при инстанцировании тела шаблона». Concepts в C++20 заметно это улучшили, но дженерики Ada с самого начала строятся на модели явного контракта.
flowchart LR
subgraph Ada[Ada]
A1[Контракт записан в формальной части] --> A2[Тело проверяется в рамках контракта]
A2 --> A3[При инстанцировании проверяются фактические параметры]
end
subgraph CPP[Шаблоны C++]
C1[Пишем тело шаблона] --> C2[Требования конкретизируются при инстанцировании]
C2 --> C3[Concepts позволяют явно выразить ограничения]
end
В дженериках Java и C# в центре проектирования — ссылочные типы, ограничения, стирание типов (type erasure) и связь с представлением во время выполнения. Дженерики Ada, напротив, тяготеют к идее создания конкретных экземпляров на этапе компиляции.
| Аспект | Ada | C++ | Java | Rust |
|---|---|---|---|---|
| Как выражается контракт | В формальной части: типы, значения, функции, пакеты | Templates / concepts | Параметры типа с bounds | Trait bounds |
| Проверка тела | В рамках контракта формальных параметров | В основном при конкретизации во время инстанцирования | В рамках bounds | В рамках trait bounds |
| Затраты во время выполнения | По умолчанию статическое разрешение | По умолчанию статическая генерация | Подвержено влиянию стирания типов | По умолчанию мономорфизация |
| Параметры-значения | Да | Да | Ограниченно | const generics |
| Подпрограмма как формальный параметр | Да | Выражается через функциональные объекты и т. п. | Лямбда / функциональный интерфейс | Замыкание / функция / trait |
| Пакет как формальный параметр | Да | Template template и т. п. | Нет | Отдельно от структуры модулей |
Детали языковых механизмов у всех разные, но особенность Ada в том, что «контракт записывается синтаксически заранее».
14. Практические решения при проектировании ── что стоит делать обобщённым
Дженерики удобны, но делать обобщённым абсолютно всё не стоит. На практике снизить риск ошибки помогает следующая логика принятия решений.
flowchart TB
Start[Есть код, который хочется переиспользовать] --> Q1{Меняется только тип?}
Q1 -->|Да| GType[Рассмотреть параметр-тип]
Q1 -->|Нет| Q2{Меняется ещё и размер или порог?}
Q2 -->|Да| GObject[Добавить параметр-значение]
Q2 -->|Нет| Q3{Меняется поведение сравнения или условия?}
Q3 -->|Да| GSubp[Добавить формальную подпрограмму]
Q3 -->|Нет| Q4{Нужно объединить внутреннее состояние и API?}
Q4 -->|Да| GPackage[Обобщённый пакет]
Q4 -->|Нет| Normal[Достаточно обычной подпрограммы]
14.1 Когда подходит обобщённая подпрограмма
Обобщённые подпрограммы подходят для алгоритмов без состояния.
SwapSortCount_IfFind- Преобразования в духе
Map Min/Max
Когда тело алгоритма короткое, а вход и выход чётко определены, подпрограмма читается легче, чем пакет.
14.2 Когда подходит обобщённый пакет
Обобщённые пакеты подходят, когда вместе с типом нужно объединить несколько операций и внутреннее состояние.
- Стек фиксированной длины
- Кольцевой буфер
- Небольшой словарь
- Набор статистической обработки
- Абстракция ввода-вывода для конкретного устройства
- Набор операций для числовых типов с единицами измерения
В Ada спецификация пакета — это публичный API, а тело пакета — реализация, и это разделение сохраняется. Поэтому обобщённый пакет можно использовать как «типобезопасный шаблон модуля».
flowchart LR
Spec[Спецификация пакета = публичный API] --> User[Сторона использования]
Body[Тело пакета = внутренняя реализация] -.скрыто.-> User
Formal[Формальная часть generic = контракт на типы, значения, функции] --> Spec
Formal --> Body
14.3 Начинать с минимума формальных параметров
Если формальных параметров слишком много, инстанцирование становится трудно читать. Безопаснее начинать с минимума и добавлять параметры только тогда, когда появляется конкретная причина что-то заменить.
-- Пример, который легко становится трудночитаемым
package X is new Generic_Foo
(A, B, C, D, E, F, G);
-- Именованное сопоставление сохраняет замысел
package X is new Generic_Foo
(Element_Type => Integer,
Index_Type => Positive,
Buffer_Size => 128,
"<" => Less);
В Ada при инстанцировании можно использовать именованное сопоставление. Поскольку важные для проектирования решения проявляются именно в момент инстанцирования, в практическом коде именованная форма чаще всего проще в сопровождении.
15. Частые подводные камни
Дженерики Ada мощны, но есть моменты, о которые легко споткнуться в самом начале.
15.1 Для типа private нельзя сравнивать «больше/меньше»
Такое тело написать не получится.
generic
type Element is private;
function Bad_Min (A, B : Element) return Element;
function Bad_Min (A, B : Element) return Element is
begin
if A < B then -- здесь ошибка
return A;
else
return B;
end if;
end Bad_Min;
Поскольку Element объявлен только как private, < не гарантированно доступен. Если нужно сравнение, его следует добавить в контракт, как показано ниже.
generic
type Element is private;
with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
flowchart LR
Need[В теле нужно сравнение] --> Contract[Добавить функцию сравнения в формальную часть]
Contract --> OK[При инстанцировании проверяется возможность сравнения]
Need --> NoContract[Только private]
NoContract --> Error[Ошибка компиляции в теле дженерика]
15.2 is <> — не «автоматический вывод чего угодно»
is <> удобен, но это не волшебство. В точке инстанцирования подходящий оператор или подпрограмма должны быть видимы. Если собственная функция сравнения находится в другом пакете, безопаснее либо корректно оформить with и use, либо передать её явно по имени.
procedure Sort_By_Age is new Generic_Insertion_Sort
(Item_Type => Person,
Index => Positive,
Item_Array => Person_Array,
"<" => Younger_Than);
15.3 Исключения тоже свои для каждого экземпляра
Если исключение объявлено в спецификации обобщённого пакета, у каждого экземпляра оно становится отдельным.
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
В этом случае Int_Stack.Stack_Overflow и Float_Stack.Stack_Overflow трактуются как разные исключения. Если нужно общее исключение, стоит рассмотреть вариант с определением его вне дженерика.
flowchart TB
Generic[Generic_Stack<br/>объявляет Stack_Overflow] --> I[Int_Stack.Stack_Overflow]
Generic --> F[Float_Stack.Stack_Overflow]
I -.разные исключения.-> F
15.4 Размер кода может расти
Дженерики позволяют легко избегать лишней косвенности во время выполнения, но поскольку под каждый тип создаётся отдельный экземпляр, при большом числе инстанцирований размер кода может вырасти.
Это тот же компромисс, что встречается у шаблонов C++ и мономорфизации Rust. В разработке для высоконадёжных, встраиваемых и близких к реальному времени систем принято снижать неопределённость во время выполнения ценой контроля за размером артефактов сборки.
flowchart LR
Generic[Одно тело generic] --> I1[Вариант Integer]
Generic --> I2[Вариант Float]
Generic --> I3[Вариант Long_Float]
Generic --> I4[Вариант My_Type]
I1 --> Code[Сгенерированный код]
I2 --> Code
I3 --> Code
I4 --> Code
Code --> Pros[Легко избежать проверки типа и упаковки во время выполнения]
Code --> Cons[При росте числа инстанцирований растёт и размер]
15.5 Когда стоит использовать limited private
type Element is private; предполагает присваивание. Для файловых дескрипторов, блокировок, дескрипторов устройств — всего, что не должно копироваться — стоит рассмотреть limited private.
generic
type Resource is limited private;
with procedure Close (R : in out Resource);
procedure Generic_Use_And_Close (R : in out Resource);
При работе с некопируемыми типами безопаснее не контейнеры, хранящие значения, а алгоритмы, применяющие процедуры, или проектирование с явными ссылками.
16. Небольшой сборник паттернов проектирования
Далее коротко перечислим формы, часто используемые на практике.
16.1 Предоставлять Min только для сравнимых значений
generic
type Element is private;
with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
function Generic_Min (A, B : Element) return Element is
begin
if A < B then
return A;
else
return B;
end if;
end Generic_Min;
flowchart LR
T[Element] --> C[Требуется функция сравнения]
C --> M[Generic_Min]
M --> R[Возвращает меньшее значение]
16.2 Порог как параметр-значение
generic
type Count_Type is range <>;
Threshold : Count_Type;
function Generic_Is_Over (Value : Count_Type) return Boolean;
function Generic_Is_Over (Value : Count_Type) return Boolean is
begin
return Value > Threshold;
end Generic_Is_Over;
Параметры-значения подходят для величин, которые хочется зафиксировать как свойство экземпляра, а не как настраиваемое во время выполнения значение.
16.3 Внедрение способа вывода
generic
type Element is private;
with procedure Put (Item : Element);
procedure Generic_Print_Twice (Item : Element);
procedure Generic_Print_Twice (Item : Element) is
begin
Put (Item);
Put (Item);
end Generic_Print_Twice;
В такой форме можно подменять место вывода: стандартный вывод, лог, тестовый буфер.
flowchart TB
Print[Generic_Print_Twice] --> Put[Put принимается как формальная подпрограмма]
Put --> Console[Вывод в консоль]
Put --> Log[Вывод в лог]
Put --> Test[Тестовый буфер]
16.4 Не фиксировать тип индекса массива
В Ada тип индекса массива тоже важная типовая информация. Вместо того чтобы жёстко закладывать Positive, стоит при необходимости сделать тип индекса формальным параметром — это повышает переиспользуемость.
generic
type Element is private;
type Index is (<>);
type Array_Type is array (Index range <>) of Element;
procedure Generic_Clear (Arr : in out Array_Type; Value : Element);
При таком проектировании можно работать не только с массивами, индексируемыми Positive, но и с массивами, индексируемыми перечислением.
flowchart LR
Index[Index — дискретный тип] --> Positive[Диапазон Positive]
Index --> Day[Перечисление Day]
Index --> State[Перечисление State]
Index --> Slot[Собственный целый тип]
17. Чек-лист для API в духе Ada
При написании дженерика в конце полезно пересмотреть его со следующих точек зрения — это делает код более читаемым.
flowchart TB
C[Проверка проекта generic] --> C1[Минимален ли набор формальных параметров?]
C --> C2[Явно ли указаны в формальной части нужные операции?]
C --> C3[Уместны ли категории private / range / digits и т.п.?]
C --> C4[Можно ли читаемо инстанцировать через именованное сопоставление?]
C --> C5[Учтены ли состояние и исключения для каждого экземпляра?]
C --> C6[Приемлем ли рост размера кода?]
C --> C7[Подготовлены ли тестовые экземпляры?]
Если сформулировать словами, получится следующее.
- Любая операция, используемая в теле, должна быть видна как часть контракта формальных параметров.
- Если достаточно
private— используйтеprivate. Если нужна арифметика —range <>илиdigits <>. - Сравнение, хеширование, вывод, преобразование — всё, что меняется от типа к типу, делайте формальной подпрограммой.
- Если размер или порог — это свойство экземпляра, делайте его параметром-значением.
- Для состояния — сначала рассматривайте обобщённый пакет, для его отсутствия — обобщённую подпрограмму.
- Чем больше аргументов при инстанцировании, тем больше оснований использовать именованное сопоставление.
- Проектируйте с расчётом на то, что исключения и внутреннее состояние независимы для каждого экземпляра.
18. Пример организации всех примеров
Если разносить примеры статьи по файлам, читаемой получится следующая структура.
flowchart TB
Root[ada-generic-programming] --> Src[src]
Src --> G[generics]
Src --> D[demos]
G --> SwapSpec[generic_swap.ads]
G --> SwapBody[generic_swap.adb]
G --> StackSpec[generic_stack.ads]
G --> StackBody[generic_stack.adb]
G --> SortSpec[generic_insertion_sort.ads]
G --> SortBody[generic_insertion_sort.adb]
G --> StatsSpec[generic_statistics.ads]
G --> StatsBody[generic_statistics.adb]
G --> CountSpec[generic_count_if.ads]
G --> CountBody[generic_count_if.adb]
G --> KVSpec[generic_kv_store.ads]
G --> KVBody[generic_kv_store.adb]
D --> SwapDemo[swap_demo.adb]
D --> StackDemo[stack_demo.adb]
D --> SortDemo[sort_demo.adb]
D --> StatsDemo[statistics_demo.adb]
D --> CountDemo[count_if_demo.adb]
D --> KVDemo[kv_demo.adb]
Для небольших примеров статьи удобно объединить всё в один файл и разбить через gnatchop, но если думать о промышленной разработке и долгосрочном сопровождении, разделение на спецификацию .ads и тело .adb — более характерная для Ada организация.
19. Заключение ── типы задают границы переиспользования
Обобщённое программирование в Ada — это не просто «возможность писать код, не зависящий от типа». Его суть, скорее, в том, чтобы явно выразить, чего требует переиспользуемый компонент, в виде контракта типов, подпрограмм и значений.
flowchart LR
Contract[Пишем контракт] --> Generic[Пишем тело дженерика]
Generic --> Instance[Передаём типы, значения, функции — инстанцируем]
Instance --> Safe[Используем типобезопасно]
Safe --> Reuse[Переиспользуем без копирования]
Как мы увидели в этой статье, формальным параметром дженерика в Ada может быть следующее.
- Тип
- Значение
- Подпрограмма
- Пакет
Кроме того, для типов можно указывать весьма детальные категории: private, limited private, range <>, mod <>, digits <>, delta <>, (<>). Благодаря этому тело дженерика не зависит от «операций, о доступности которых неизвестно», а безопасно реализуется только через операции, прописанные в контракте.
В унаследованном коде на C или старом C++ для переиспользования нередко применяют макросы, void*, указатели на функции и написанное вручную ветвление по типу. Дженерики Ada способны заменить большинство таких приёмов типобезопасной и легко читаемой формой. Особенно в долгосрочно сопровождаемом, встраиваемом, реального времени и высоконадёжном ПО такое проектирование — «определять границы на этапе компиляции» — приносит ощутимую ценность.
20. Смежные области консультирования
ООО «Komura Soft» занимается разработкой Windows-приложений, исследованием и модернизацией существующих активов, наведением порядка на границах COM / ActiveX / 32-бит / 64-бит, техническими консультациями и ревью архитектуры. Помимо статически типизированного, ориентированного на высокую надёжность проектирования в духе Ada, на практике часто встречается близкая тема — как упорядочить существующие активы на C/C++, C#, VB6, MFC, COM, чтобы продлить их жизненный цикл или выполнить миграцию.
- Разработка Windows-приложений
- Разработка COM-компонентов
- Замена Windows-приложений
- Технические консультации и ревью архитектуры
- Контакты
Справочные материалы
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Притягательность языка Ada — типы как язык проектирования и опора для ПО, работающего десятилетиями
Рассказываем о притягательности языка Ada: строгая типизация, ограничения диапазона, разделение спецификации и реализации через пакеты, к...
Программирование систем реального времени на Ada — приоритеты, периодичность и контроль времени выполнения на практике
Изучаем Annex D Ada (системы реального времени) на 8 практических примерах кода: приоритеты задач, протокол Ceiling_Locking, периодическо...
Безопасная конкурентность в Ada — практическое руководство по задачам и защищённым объектам
Вводная статья о встроенной в язык Ada конкурентности — задачах и защищённых объектах. Рассматриваем рандеву (entry/accept), выборочный a...
Введение в формальную верификацию с SPARK ── От контрактов Ada к математическому доказательству
Практическое введение в формальную верификацию с помощью SPARK — подмножества языка Ada. В статье рассматривается переход от контрактов (...
Обработка ошибок и повторные попытки в Power Automate — как не допустить, чтобы работающий поток незаметно остановился
Сборник паттернов проектирования, которые не дают потокам Power Automate незаметно останавливаться: значения политики повторов по умолчан...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Что такое дженерики в Ada?
- Это механизм повторного использования, который принимает в качестве формальных параметров типы, значения, подпрограммы и даже целые пакеты, а статическая проверка типов выполняется в момент инстанцирования через new. Это не простая подстановка текста: уже на этапе компиляции определяется, «удовлетворяет ли данный компонент заданному контракту». Обобщённую подпрограмму нельзя вызвать сразу после объявления — она становится обычной процедурой или функцией только после инстанцирования с конкретным типом.
- Чем дженерики Ada отличаются от шаблонов C++?
- Ada с самого начала использует contract model — модель явного контракта: тело дженерика пишется только с использованием операций, обещанных формальными параметрами, и проверяется типами независимо от точки инстанцирования. У шаблонов C++ исторически ошибки проявлялись только при инстанцировании — это заметно улучшили concepts в C++20. Кроме того, в Ada, помимо параметров-значений и параметров-подпрограмм, формальным параметром может быть сам пакет, что позволяет комбинировать обобщённые компоненты друг с другом.
- Зачем в формальном параметре-типе указывать категорию типа?
- Чтобы явно закрепить в контракте, какие операции доступны в теле дженерика. При type T is private можно рассчитывать только на базовые операции — присваивание и сравнение на равенство, но не на сравнение «больше/меньше» или арифметику. Если нужна целочисленная арифметика, указывают range <>, для операций с плавающей точкой — digits <>, для битовых операций — mod <>. Указание категории служит ещё и спецификацией на уровне типов: оно сообщает читателю, чего именно требует данный компонент.
- На что стоит обратить внимание при работе с дженериками Ada?
- Поскольку под каждый тип создаётся отдельный экземпляр, при большом числе инстанцирований может заметно вырасти размер кода — тот же компромисс встречается у шаблонов C++ и мономорфизации Rust. Кроме того, исключение, объявленное в спецификации обобщённого пакета, становится отдельным исключением в каждом экземпляре. Практические рекомендации: начинать с минимального набора формальных параметров и расширять его только при появлении конкретной причины, а при инстанцировании с большим числом аргументов использовать именованное сопоставление (named association).
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки