Generic Programming in Ada — Contracts, Types, and Zero-Cost Reuse

· · 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:

  1. Write a generic unit
  2. Declare formal parameters in the generic section
  3. Instantiate with new at 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_Swap works for any Element type
  • 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 parameter
  • Max_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:

  1. Item_Type — the element type
  2. Index — the array index type
  3. Item_Array — the actual array type
  4. "<" — 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:

  • Swap
  • Sort
  • Count_If
  • Find
  • 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 private when it suffices. Use range <> or digits <> 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.

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

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

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.

Back to the Blog