Generic Programming in Ada — Contracts, Types, and Zero-Cost Reuse
· Go Komura · Ada, Programming Language, Generics, Type System, Static Typing, Contract Model, Zero Cost Abstraction, GNAT, Alire, High Integrity, Code Reuse
1. Introduction — Contracts, Not Just “Accept Anything”
Statically typed languages all hit the same wall when you try to write reusable code. You want a stack that holds integers to also hold floats. You want the same sort to handle ascending and descending order. You want the same statistics code to work with single-precision and double-precision floats.
The Quick-and-Dirty Answers Are Familiar:
C reaches for void* and function pointers — type safety evaporates. Java uses Object and casts — runtime errors lurk. C++ uses templates — powerful, but historically, errors surface only at instantiation time.
Ada’s answer is generics — but Ada’s generics are not simple text substitution. They accept types, values, subprograms, and even entire packages as formal parameters. Full type checking happens at instantiation time, and runtime overhead is zero. The idea of a static contract — what C++ later explored with concepts and Rust with trait bounds — has been part of Ada’s language specification since 1983.
flowchart LR
A[Reusable Logic] --> B{How to Reuse?}
B --> C[Copy and Paste]
B --> D[void* / Object / Casts]
B --> E[Ada Generics]
C --> C1[Prone to missed fixes]
D --> D1[Prone to runtime errors / type safety loss]
E --> E1[Type-safe]
E --> E2[Compile-time checking]
E --> E3[No runtime dispatch overhead]
This article covers:
- Generic subprograms
- Generic packages
- Type, value, and subprogram parameters
- Type categories (
private,range <>,digits <>, and more) - Worked examples: swap, stack, sort, statistics,
Count_If, key-value store - Formal package parameters for higher-order generics
- Ada’s contract model and practical design considerations
This is the sixth installment in our series, following “The Appeal of the Ada Language”, “SPARK Formal Verification Intro”, “Safe Concurrency with Ada”, and “Real-Time Systems Programming in Ada”. Here we explore Ada’s “types speak design” philosophy through the lens of generics.
2. Roadmap
Ada’s generics are more than “functions that take a type parameter.” Depending on what you want to reuse, you combine subprograms, packages, subprogram parameters, value parameters, and formal package parameters.
mindmap
root((Ada Generics))
Generic Subprogram
Swap
Count_If
Sort
Generic Package
Stack
Statistics
KV Store
Formal Parameters
Type
private
limited private
range box
mod box
digits box
delta box
discrete box
Object
Max_Size
Threshold
Subprogram
Less function
Equals function
Predicate
Package
with package P is new Generic
Design Ideas
Contract Model
Static Checking
Zero-Cost Abstraction
Separate Specification and Body
The first half covers syntax; the second half addresses design judgment. If you’re new to Ada, don’t worry about memorizing syntax — focus on what is being passed as a formal parameter and what operations that parameter guarantees.
3. Development Environment
All code in this article targets GNAT 15.x or later. You can install GNAT via Alire (Ada’s package manager) with alr install gnat_native gprbuild and ensure the binaries are on your PATH.
gnat --version
# GNAT 15.2.1
The sample layout for this article:
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]
Multi-unit files must be split with gnatchop before compilation with gnatmake:
mkdir work
cd work
gnatchop ../src/snippets/01_swap.ada
gnatmake -gnata swap_demo
./swap_demo
The -gnata flag enables assertion checks. Not required for generics themselves, but helpful for learning when you want to verify contracts and boundary conditions.
sequenceDiagram
participant Dev as Developer
participant Chop as gnatchop
participant Build as gnatmake
participant Exe as Executable
Dev->>Chop: Pass a single .ada file
Chop-->>Dev: Split into .ads / .adb / main
Dev->>Build: gnatmake -gnata main
Build-->>Dev: Bind and link
Dev->>Exe: ./main
Exe-->>Dev: Output
4. The Basic Model of Ada Generics
Ada generics boil down to three steps:
- Write a generic unit
- Declare formal parameters in the
genericsection - Instantiate with
newat the point of use
flowchart LR
G[generic declaration] --> F[Formal parameters]
F --> B[Generic body]
B --> I[Instantiation via new]
I --> U[Used as a normal subprogram or package]
F --> F1[Types]
F --> F2[Values]
F --> F3[Subprograms]
F --> F4[Packages]
For example, a procedure that swaps two values only needs the element type as a formal parameter:
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
At this point, Generic_Swap is not callable. It is a template for a swap operation that works for any Element type. It becomes a real procedure only after we supply a concrete type:
procedure Swap_Integer is new Generic_Swap (Integer);
The relationship:
flowchart TB
Template[Generic_Swap<br/>type Element is private] -->|Pass Integer| SwapInt[Swap_Integer]
Template -->|Pass Character| SwapChar[Swap_Character]
Template -->|Pass My_Record| SwapRecord[Swap_My_Record]
SwapInt --> ICall[Swaps Integer variables]
SwapChar --> CCall[Swaps Character variables]
SwapRecord --> RCall[Swaps My_Record variables]
The crucial point: the template body is written using only the operations available on Element. type Element is private; grants assignment and equality, but not arithmetic or comparison. The generic declaration itself says “here is what this component may assume.”
5. Kinds of Formal Parameters — Ada’s Generic Vocabulary
Ada generics accept more than just types. This is a major departure from typical generics in C# or Java.
flowchart TB
P[Generic formal parameters] --> T[Type parameters]
P --> O[Object / value parameters]
P --> S[Subprogram parameters]
P --> PKG[Package parameters]
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 ...]
A summary table:
| Kind | Example | Meaning |
|---|---|---|
| Type parameter | type Element is private; |
Any definite nonlimited type |
| Limited type parameter | type Element is limited private; |
Types that cannot be copied |
| Discrete type | type Index is (<>); |
Integer or enumeration — usable as array indices |
| Signed integer type | type Count is range <>; |
+, -, comparison etc. assumed available |
| Modular integer type | type Word is mod <>; |
Bitwise operations, modular arithmetic |
| Floating-point type | type Real is digits <>; |
Float, Long_Float, user-defined floating-point |
| Fixed-point type | type Money is delta <>; |
Fixed-point arithmetic |
| Value parameter | Max_Size : Positive; |
Size or threshold fixed per instance |
| Subprogram parameter | with function Predicate (...) return Boolean; |
Inject comparison, predicate, output behavior |
| Package parameter | with package P is new Some_Generic (<>); |
Accept a generic package instance as a component |
With this vocabulary, Ada lets you write “accept only types that support these operations” rather than “accept anything and hope for the best.”
6. Generic Subprograms — Understanding the Minimum via Generic_Swap
6.1 Specification
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
The section after generic is the formal parameter list. Element is a type parameter. is private means the generic body does not know the internal representation of Element.
Two things are clear from this declaration:
Generic_Swapworks for anyElementtype- The body does not depend on
Element’s internal structure or ordering
6.2 Body
procedure Generic_Swap (A, B : in out Element) is
Temp : constant Element := A;
begin
A := B;
B := Temp;
end Generic_Swap;
The body only uses assignment. No A < B, no A + B. Therefore it naturally works for Integer, Character, record types, enumeration types — anything assignable.
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 Instantiation
Use the new keyword:
procedure Swap_Int is new Generic_Swap (Integer);
procedure Swap_Char is new Generic_Swap (Character);
Now Swap_Int and Swap_Char are normal callable procedures.
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;
Output:
Before: X= 10, Y= 20
After : X= 20, Y= 10
You cannot pass a Float to Swap_Int — it was instantiated specifically for Integer. Generics do not create “holes that accept anything”; they create type-safe concretizations.
7. Generic Packages — Parameterizing Types and Values Together
When you need multiple operations and internal state — not just a single function — use a generic package. The classic example is a stack.
A stack’s logic is the same regardless of element type and capacity:
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 Specification
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;
Two kinds of formal parameters:
Element_Type— a type parameterMax_Size— a value parameter
Since Max_Size is Positive, you cannot instantiate with zero or negative capacity. Value parameters carry their own type constraints.
7.2 Body
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 and Top are created separately for each instance:
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
Both are generated from the same template, but their internal state is independent.
flowchart LR
Template[Generic_Stack] --> IntStack[Int_Stack]
Template --> FloatStack[Float_Stack]
subgraph I[Int_Stack State]
ITop[Top]
IData[Data : Integer array]
end
subgraph F[Float_Stack State]
FTop[Top]
FData[Data : Float array]
end
IntStack --> I
FloatStack --> F
7.3 Stack State Machine
The stack is easy to understand as a state machine:
stateDiagram-v2
[*] --> Empty
Empty --> NonEmpty: Push
NonEmpty --> NonEmpty: Push / Pop
NonEmpty --> Empty: Pop removes last element
NonEmpty --> Full: Push reaches Max_Size
Full --> NonEmpty: Pop
Full --> Overflow: Push
Empty --> Underflow: Pop
7.4 Usage
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;
In practice, generic packages shine for small containers, fixed-size buffers, ring buffers, logging queues, and hardware abstraction layers. Ada’s preference for statically fixed sizes (via type and value parameters) pairs well with high-integrity system design.
8. Formal Subprogram Parameters — Injecting Behavior
Types alone cannot express everything. A sort, for instance, needs not just the element type but also the comparison logic — which element comes first.
In Ada, the comparison function itself can be a formal parameter:
flowchart LR
A[Generic_Insertion_Sort] --> T[Item_Type]
A --> I[Index]
A --> ARR[Item_Array]
A --> CMP[Comparison function]
CMP --> ASC[Use standard comparison]
CMP --> DESC[Pass Greater for descending]
CMP --> CUSTOM[Pass a custom ordering]
8.1 Specification
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);
Four formal parameters:
Item_Type— the element typeIndex— the array index typeItem_Array— the actual array type"<"— the comparison function
type Index is (<>) accepts any discrete type — not just Positive, but also enumerations like Day or Color. This is distinctly Ada.
with function "<" ... is <>; — the is <> means: if the caller omits this argument, use whatever < is visible at the instantiation site. For Integer, the standard < works without explicit mention.
8.2 Body
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;
Insertion sort is not optimal for large arrays, but it’s ideal for illustrating generics — the same loop structure works for ascending or descending order just by swapping the comparison.
flowchart TB
Start[Unsorted array] --> Pick[Pick Key from the left]
Pick --> Compare{Key < previous element?}
Compare -->|Yes| Shift[Shift previous element right]
Shift --> Compare
Compare -->|No| Insert[Insert Key]
Insert --> Done{Finished?}
Done -->|No| Pick
Done -->|Yes| End[Sorted array]
8.3 Ascending and Descending from the Same Body
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 uses the standard <. Sort_Desc substitutes Greater as "<".
flowchart LR
Data[99, 3, 47, 12] --> A[Sort_Asc<br/>Standard comparison]
Data --> D[Sort_Desc<br/>Greater passed as comparison]
A --> AO[3, 12, 47, 99]
D --> DO[99, 47, 12, 3]
This approach resembles passing a function object as a C++ template argument, or a Rust Ord trait bound. Ada makes it explicit: “this generic expects a function with this signature.”
9. Type Categories — Writing More Specific Contracts Than private
type T is private; is convenient but limited. You cannot assume arithmetic or comparison on a private type. Ada lets you specify a type category in the formal parameter.
flowchart TB
FormalType[Formal Type] --> Private[private]
FormalType --> Limited[limited private]
FormalType --> Discrete[discrete box: discrete]
FormalType --> Signed[range box: signed integer]
FormalType --> Modular[mod box: modular integer]
FormalType --> Float[digits box: floating-point]
FormalType --> Fixed[delta box: fixed-point]
FormalType --> Access[access type]
Discrete --> Enum[Enumeration]
Discrete --> Int[Integer]
Float --> F1[Float]
Float --> F2[Long_Float]
Float --> F3[User-defined float]
9.1 Why Category Specification Matters
To compute mean and variance, you need addition, subtraction, multiplication, and division. A private type cannot guarantee these. So we restrict to floating-point types:
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 <>; tells the compiler (and the reader): Real is a floating-point type. The body can safely use +, -, *, /.
9.2 Body
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 Using Float and 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);
Same statistical logic, different floating-point precisions.
flowchart LR
Stats[Generic_Statistics<br/>Real is digits box] --> FS[Float_Stats]
Stats --> LS[Long_Stats]
Stats --> MS[My_Real_Stats]
FS --> FCalc[Float Mean / Variance]
LS --> LCalc[Long_Float Mean / Variance]
MS --> MCalc[User-defined Real Mean / Variance]
9.4 Categories Are Type-Level Specifications
Category specifications are not just syntax to satisfy the compiler. They communicate to the reader what this component requires.
| Operation you want to write | Suitable formal type | Why |
|---|---|---|
| Swap, store, retrieve | private |
Assignment is sufficient |
| Manage non-copyable resources | limited private |
Assumes no assignment |
| Array indexing, enumerate states | (<>) |
First, Last, Succ, Pred available |
| Integer sum, counter | range <> |
Integer arithmetic assumed |
| Bit masks, cyclic counters | mod <> |
Modular arithmetic assumed |
| Mean, variance, numeric computation | digits <> |
Floating-point arithmetic assumed |
| Currency, fixed-precision control | delta <> |
Fixed-point arithmetic assumed |
10. Predicate Injection — Count_If the Ada Way
Formal subprogram parameters work for predicates too — functions that take a value and return Boolean.
In C# this is Func<T, bool>, in Java Predicate<T>, in C++ a lambda or function object. In Ada, it’s a generic formal subprogram.
10.1 Specification
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;
No is <> on Predicate — there is no natural default predicate, so the caller must always supply one.
10.2 Body
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[Array] --> Loop[Iterate over elements]
Loop --> P{Predicate(Item)?}
P -->|True| Inc[Increment Count]
P -->|False| Skip[Skip]
Inc --> Next[Next element]
Skip --> Next
Next --> Result[Return Count]
10.3 Even Count and Threshold Count
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);
Same traversal logic, two different counting behaviors.
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[Count of evens]
L --> LR[Count > 50]
The traversal, counter management, and return are all shared. Only what to count is injected as a function. This is Ada’s fundamental form of higher-order design.
11. Composing Multiple Parameters — A Generic Key-Value Store
Real components rarely need just one type parameter. A key-value store needs key type, value type, key comparison, and a capacity limit.
flowchart TB
KV[Generic_KV_Store] --> K[Key_Type]
KV --> V[Value_Type]
KV --> EQ[Key equality function]
KV --> M[Max_Entries]
KV --> Ops[Put / Get / Contains]
Ops --> Use1[Configuration store]
Ops --> Use2[Small cache]
Ops --> Use3[Embedded fixed-size dictionary]
11.1 Specification
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;
Four formal parameters:
| Parameter | Kind | Role |
|---|---|---|
Key_Type |
Type | Key type |
Value_Type |
Type | Value type |
"=" |
Subprogram | Key equality test |
Max_Entries |
Value | Maximum entries |
Max_Entries has a default of 50 — specifying it is optional.
11.2 Body
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;
Linear search — not for large datasets. But the combination of fixed size, no dynamic allocation, and bounded execution time fits its intended niche.
sequenceDiagram
participant App as Caller
participant Store as Generic_KV_Store instance
participant Data as Keys/Values/Used
App->>Store: Put(Key, Value)
Store->>Data: Find_Index(Key)
alt Existing key
Store->>Data: Values(Pos) := Value
else New key
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 Instantiation
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) returns the value
null;
end if;
end KV_Demo;
"=" is omitted — Integer has a visible standard equality, so is <> resolves it. For case-insensitive string keys, you could supply a custom equality:
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. Formal Package Parameters — Generics as Building Blocks
Ada generics allow a package to be a formal parameter. This lets you pass a generic package instance into another generic.
flowchart LR
GS[Generic_Stack] --> IS[Int_Stack]
IS --> Logger[Generic_Stack_Logger]
Logger --> Logged[Int_Stack_Logger_Instance]
12.1 A Logger That Accepts a Stack
Suppose we want a logger that prints the current size of any Generic_Stack instance:
generic
with package Stack is new Generic_Stack (<>);
package Generic_Stack_Logger is
procedure Print_Size;
end Generic_Stack_Logger;
Body:
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;
Usage — first create a stack, then pass it to the 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);
This pattern chains generic components together:
flowchart TB
subgraph Layer1[Step 1]
T1[Generic_Stack] --> I1[Int_Stack]
end
subgraph Layer2[Step 2]
T2[Generic_Stack_Logger] --> I2[Int_Stack_Logger]
end
I1 --> T2
I2 --> API[Print_Size]
Similar in spirit to C++ template template parameters, but Ada expresses it explicitly: “accept a specific instance of this generic package.” In large Ada codebases this pattern is useful for separating containers, algorithms, logging, inspection, and test helpers.
13. The Contract Model — The Most Important Idea in Ada Generics
The contract model is central to understanding Ada generics.
A generic body must be written using only the operations promised by its formal parameters. If you declare type Element is private;, you cannot use < on Element. To use <, you must either add it as a formal subprogram or tighten the type category.
flowchart TB
Spec[Generic formal part = contract] --> Body[Generic body: implemented within the contract]
Body --> Check1[Body type-checked independently]
Spec --> Inst[Instantiation]
Actual[Actual parameters: types, functions, values] --> Inst
Inst --> Check2[Verify actuals satisfy contract]
Check2 --> Instance[Result: a normal package or subprogram]
This design protects both the user and the author of the generic.
13.1 How It Differs from C++ Templates
C++ templates are powerful, but historically errors only surface at instantiation time. C++20 concepts improve this dramatically. Ada’s generics, however, have required explicit contracts from the start.
flowchart LR
subgraph Ada[Ada]
A1[Write contract in formal part] --> A2[Body checked within contract]
A2 --> A3[Instantiation: verify actuals]
end
subgraph CPP[C++ templates]
C1[Write template body] --> C2[Requirements materialize at instantiation]
C2 --> C3[Concepts can express constraints explicitly]
end
Java and C# generics center on reference types, bounds, type erasure, and runtime representations. Ada’s generics lean toward compile-time concretization.
| Aspect | Ada | C++ | Java | Rust |
|---|---|---|---|---|
| How contracts are expressed | Formal part: types, values, functions, packages | Templates / concepts | Type parameters with bounds | Trait bounds |
| Body checking | Checked within formal parameter contract | Primarily at instantiation | Checked within bounds | Checked within trait bounds |
| Runtime cost | Static resolution by default | Static generation by default | Affected by type erasure | Monomorphization by default |
| Value parameters | Yes | Yes | Limited | Const generics |
| Subprograms as formal parameters | Yes | Via function objects etc. | Lambda / functional interface | Closure / function / trait |
| Packages as formal parameters | Yes | Template template etc. | No | Separate from module structure |
The languages differ in detail, but Ada’s signature characteristic is writing the contract as syntax first.
14. Practical Design Judgment — What Should Be Generic?
Generics are powerful, but not everything should be generic. Here is a decision flow:
flowchart TB
Start[You have reusable logic] --> Q1{Only the type varies?}
Q1 -->|Yes| GType[Consider a type parameter]
Q1 -->|No| Q2{Size or threshold also varies?}
Q2 -->|Yes| GObject[Add a value parameter]
Q2 -->|No| Q3{Comparison or predicate behavior varies?}
Q3 -->|Yes| GSubp[Add a formal subprogram]
Q3 -->|No| Q4{Need to bundle state and API?}
Q4 -->|Yes| GPackage[Generic package]
Q4 -->|No| Normal[A normal subprogram is enough]
14.1 When Generic Subprograms Fit
Generic subprograms suit stateless algorithms:
SwapSortCount_IfFind- Map-like transformations
Min/Max
When the body is short and input/output is clear, a subprogram is more readable than a package.
14.2 When Generic Packages Fit
Generic packages suit cases where you need multiple operations plus internal state tied to a type:
- Fixed-size stack
- Ring buffer
- Small dictionary
- Statistics suite
- Per-device I/O abstraction
- Unit-of-measure numeric types
Because Ada separates package specification (public API) from package body (implementation), a generic package is effectively a type-safe module template.
flowchart LR
Spec[Package spec = public API] --> User[Caller]
Body[Package body = internal implementation] -.hidden.-> User
Formal[Generic formal part = contract for types, values, functions] --> Spec
Formal --> Body
14.3 Start with Fewer Formal Parameters
Too many formal parameters make instantiations hard to read. Start minimal and add parameters only when you have a concrete reason to swap something out.
-- Hard to read
package X is new Generic_Foo
(A, B, C, D, E, F, G);
-- Named association preserves intent
package X is new Generic_Foo
(Element_Type => Integer,
Index_Type => Positive,
Buffer_Size => 128,
"<" => Less);
Ada supports named association at instantiation. Because the important design decisions are visible at the instantiation site, named association improves long-term maintainability.
15. Common Pitfalls
15.1 You Cannot Compare private Types
This will not compile:
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 -- Error here
return A;
else
return B;
end if;
end Bad_Min;
Element is only private — no < is guaranteed. To compare, add it to the contract:
generic
type Element is private;
with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
flowchart LR
Need[Body needs comparison] --> Contract[Add comparison to formal part]
Contract --> OK[Instantiation verifies comparability]
Need --> NoContract[private only]
NoContract --> Error[Generic body fails to compile]
15.2 is <> Is Not “Automatic Inference”
is <> is convenient, but not magic. At the instantiation site, a matching operator or subprogram must be visible. If your custom comparison lives in a separate package, make sure it is withed and used, or pass it explicitly by name:
procedure Sort_By_Age is new Generic_Insertion_Sort
(Item_Type => Person,
Index => Positive,
Item_Array => Person_Array,
"<" => Younger_Than);
15.3 Exceptions Are Distinct Per Instance
When a generic package declares an exception, each instance gets its own copy:
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
Int_Stack.Stack_Overflow and Float_Stack.Stack_Overflow are different exceptions. If you need a shared exception, define it outside the generic.
flowchart TB
Generic[Generic_Stack<br/>declares Stack_Overflow] --> I[Int_Stack.Stack_Overflow]
Generic --> F[Float_Stack.Stack_Overflow]
I -.distinct exception.-> F
15.4 Code Size Can Grow
Generics avoid runtime indirection, but each distinct instantiation produces its own machine code. Many instantiations mean larger binaries.
This is the same trade-off seen in C++ templates and Rust monomorphization. In high-integrity, embedded, or real-time development, the choice is: eliminate runtime uncertainty in exchange for managing build output size.
flowchart LR
Generic[One generic body] --> I1[Integer variant]
Generic --> I2[Float variant]
Generic --> I3[Long_Float variant]
Generic --> I4[My_Type variant]
I1 --> Code[Generated code]
I2 --> Code
I3 --> Code
I4 --> Code
Code --> Pros[Avoids runtime type checks and boxing]
Code --> Cons[More instantiations = larger binary]
15.5 When to Use limited private
type Element is private; assumes assignment. For file handles, locks, and device handles — things that should not be copied — use limited private:
generic
type Resource is limited private;
with procedure Close (R : in out Resource);
procedure Generic_Use_And_Close (R : in out Resource);
When working with non-copyable types, algorithms that apply procedures or explicitly manage references are safer than value-storing containers.
16. Small Design Pattern Collection
16.1 Provide Min Only for Comparable Types
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[Requires comparison]
C --> M[Generic_Min]
M --> R[Returns the smaller]
16.2 Threshold as a Value Parameter
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;
Value parameters suit quantities you want fixed as an instance property, not a runtime argument.
16.3 Injecting Output Behavior
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;
Swap the Put parameter to target console, log, or test buffer:
flowchart TB
Print[Generic_Print_Twice] --> Put[Put as formal subprogram]
Put --> Console[Console output]
Put --> Log[Log output]
Put --> Test[Test buffer]
16.4 Don’t Hardcode Array Index Types
In Ada, the index type of an array is significant type information. Instead of hardcoding Positive, make the index type a formal parameter for maximum reuse:
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);
Now it works with Positive-indexed arrays, enumeration-indexed arrays, and custom integer-indexed arrays.
flowchart LR
Index[Index is discrete] --> Positive[Positive range]
Index --> Day[Day enumeration]
Index --> State[State enumeration]
Index --> Slot[Custom integer type]
17. An Ada-Idiomatic API Checklist
When writing a generic, review it against these points:
flowchart TB
C[Generic design check] --> C1[Are formal parameters minimal?]
C --> C2[Are required operations explicit in formal part?]
C --> C3[Are categories private/range/digits appropriate?]
C --> C4[Can instantiations use named association?]
C --> C5[Are per-instance state and exceptions understood?]
C --> C6[Is code size increase acceptable?]
C --> C7[Do you have test instances ready?]
In summary:
- Every operation used in the body must be visible as part of the formal parameter contract.
- Use
privatewhen it suffices. Userange <>ordigits <>when arithmetic is needed. - Comparison, hashing, output, and conversion — anything that varies by type — should be a formal subprogram.
- If a size or threshold defines the nature of an instance, make it a value parameter.
- Stateful → generic package. Stateless → generic subprogram.
- Many formal parameters → named association at instantiation.
- Design with the understanding that exceptions and internal state are per-instance.
18. Sample Code Organization
The sample code for this article is organized as follows:
flowchart TB
Root[ada-generic-programming] --> Src[src]
Src --> Snippets[snippets]
Snippets --> Swap[01_swap.ada]
Snippets --> Stack[02_stack.ada]
Snippets --> Sort[03_sort.ada]
Snippets --> Stats[04_statistics.ada]
Snippets --> Filter[05_filter.ada]
Snippets --> KV[06_kv_store.ada]
For an article, bundling multiple compilation units into a single .ada file and splitting with gnatchop keeps things compact. In production code, separating specifications (.ads) and bodies (.adb) is the Ada convention.
All six snippets in the companion repository have been verified to compile and run correctly with GNAT 15.2.1 on Windows.
19. Conclusion — Types Define the Boundaries of Reuse
Generic programming in Ada is about more than “writing type-independent code.” Its essence is making explicit what a reusable component requires, through a contract of types, subprograms, and values.
flowchart LR
Contract[Write the contract] --> Generic[Write the generic body]
Generic --> Instance[Pass types, values, functions — instantiate]
Instance --> Safe[Use type-safely]
Safe --> Reuse[Reuse without copying]
As we have seen, Ada generics accept these as formal parameters:
- Types
- Values
- Subprograms
- Packages
For types, you can specify fine-grained categories: private, limited private, range <>, mod <>, digits <>, delta <>, (<>). The generic body is then written using only the operations guaranteed by the contract — no guesswork, no runtime uncertainty.
In legacy C or older C++ codebases, reuse often means macros, void*, function pointers, and hand-written type dispatch. Ada’s generics replace most of those patterns with a type-safe, readable alternative. In long-lived, embedded, real-time, and high-integrity software, this “decide boundaries at compile time” approach delivers concrete value.
Related Areas
KomuraSoft LLC handles Windows application development, legacy code investigation and modernization, COM/ActiveX/32-bit/64-bit boundary management, and technical consulting and design review. While Ada suits high-integrity static-typing designs, we also work extensively with existing C/C++, C#, VB6, MFC, and COM assets — organizing, extending, and migrating them.
References
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
The Appeal of the Ada Language — Expressing Design Through Types and Powering Software That Runs for Decades
An introduction to the appeal of the Ada language: strong typing, range constraints, separation of specification and implementation via p...
Introduction to Formal Verification with SPARK — From Ada Contracts to Mathematical Proof
A practical introduction to formal verification using the SPARK subset of Ada. Covers how to step up from contracts (Pre/Post) to proofs,...
Safe Concurrency with Ada — A Practical Guide to Tasks and Protected Objects
A practical introduction to Ada's built-in concurrency model — tasks, rendezvous, and protected objects. Covers the rendezvous pattern (e...
Real-Time Systems Programming in Ada — Priorities, Periodic Execution, and CPU Time Control in Practice
A practical deep dive into Ada's Annex D real-time features — task priorities, the Ceiling_Locking protocol, drift-free periodic executio...
Fable Is Gone — Don't Give Up: OpenRouter Fusion + Chinese LLMs + Review Layer
Fable is nowhere near replaceable. But combine OpenRouter Fusion with 5 Chinese LLMs, then add a review layer (GPT-5.5-Pro or Codex PR re...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.
Public links