La programmation générique en Ada ── Écrire des contrats par les types, réaliser une réutilisation à coût nul
· Mis à jour le: · Go Komura · Ada, Langage de Programmation, Génériques, Système de Types, Typage Statique, Modèle de Contrat, Abstraction à Coût Nul, GNAT, Alire, Haute Fiabilité, Réutilisation de Code
1. Introduction ── Pas « tout accepter », mais « ce que l’on promet »
Dès que l’on essaie d’écrire du code réutilisable dans un langage à typage statique, on se heurte rapidement au même problème. On voudrait qu’une pile écrite pour les entiers fonctionne aussi avec des chaînes de caractères. On voudrait appliquer le même traitement statistique à un tableau de flottants. On voudrait réutiliser la logique d’un tri croissant pour un tri décroissant. Mais si l’on copie le même code pour chaque type, des oublis de correction finissent par apparaître. À l’inverse, si l’on conçoit un système qui accepte n’importe quoi via void* ou des transtypages (casts), la sûreté de typage s’effondre.
La réponse d’Ada est la généricité (generic units).
Les génériques Ada ne sont pas une simple substitution de texte. Ils reçoivent des types, des valeurs, des sous-programmes, et même des paquets eux-mêmes comme paramètres formels, et sont vérifiés statiquement au moment de l’instanciation. Autrement dit, ce n’est pas au moment de l’exécution que l’on vérifie « ce type convient-il vraiment ? », mais à la compilation que l’on détermine « ce composant satisfait-il ce contrat ? ».
flowchart LR
A[Traitement à réutiliser] --> B{Comment le réutiliser ?}
B --> C[Copier-coller]
B --> D[void* / Object / transtypage]
B --> E[Génériques Ada]
C --> C1[Oublis de correction fréquents]
D --> D1[Erreurs à l'exécution ou pertes de sûreté de type fréquentes]
E --> E1[Sûreté de type]
E --> E2[Vérification à la compilation]
E --> E3[Aucune répartition dynamique superflue à l'exécution]
Cet article organise la programmation générique en Ada selon le plan suivant :
- Sous-programmes génériques
- Paquets génériques
- Paramètres de type, paramètres de valeur, paramètres de sous-programme
- Catégories de types telles que
private,range <>,digits <> - Exemples d’implémentation : tri, pile, traitement statistique,
Count_If, magasin clé-valeur - Génériques d’ordre supérieur via les paramètres formels de paquet
- Le contract model d’Ada et les principes de conception pratiques
Ce thème s’inscrit dans la continuité de notre série « L’attrait du langage Ada », « Introduction à la vérification formelle avec SPARK », « Une concurrence sûre » et « Systèmes temps réel ». Nous approfondissons ici la philosophie d’Ada consistant à « exprimer la conception par les types », sous l’angle de la généricité.
2. Plan de cet article
Commençons par saisir la vue d’ensemble à travers un schéma. Comprendre les génériques Ada comme une simple « fonctionnalité qui prend un type en argument » donne une vision bien trop étroite. En réalité, selon l’unité que l’on souhaite réutiliser, on combine sous-programmes, paquets, paramètres de sous-programme, paramètres de valeur et paramètres formels de paquet.
mindmap
root((Génériques Ada))
Sous-programme générique
Swap
Count_If
Sort
Paquet générique
Stack
Statistics
KV Store
Paramètres formels
Type
private
limited private
range box
mod box
digits box
delta box
discrete box
Objet
Max_Size
Threshold
Sous-programme
Fonction Less
Fonction Equals
Predicate
Paquet
with package P is new Generic
Idées de conception
Modèle de contrat
Vérification statique
Abstraction à coût nul
Séparation spécification et corps
Ce qui compte dans la lecture de cet article est simple. La première moitié suit la syntaxe, la seconde traite des décisions de conception. Si vous découvrez Ada, n’essayez pas d’emblée de mémoriser les détails syntaxiques : concentrez-vous sur « ce qui est pris comme paramètre formel » et « quelles opérations ce paramètre formel autorise ».
3. Environnement d’exécution et méthode de compilation
Le code de cet article suppose GNAT 15.x ou une version ultérieure. GNAT est le compilateur Ada de référence, installable via Alire. Alire est le gestionnaire de paquets d’Ada / SPARK ; il sert aussi à gérer la chaîne d’outils et à construire les projets.
gnat --version
# GNAT 15.2.1
Installez GNAT depuis Alire (le gestionnaire de paquets d’Ada) avec alr install gnat_native gprbuild, puis ajoutez-le à votre PATH.
Les exemples traités dans cet article sont supposés placés ainsi dans le dépôt :
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]
Les échantillons regroupant plusieurs unités de compilation dans un seul fichier doivent être découpés avec gnatchop avant d’être compilés avec gnatmake.
mkdir work
cd work
gnatchop ../src/snippets/01_swap.ada
gnatmake -gnata swap_demo
./swap_demo
-gnata est une option qui active les assertions. Elle n’est pas indispensable pour utiliser la généricité elle-même, mais elle facilite la vérification des contrats et des conditions limites dans les exemples pédagogiques.
sequenceDiagram
participant Dev as Développeur
participant Chop as gnatchop
participant Build as gnatmake
participant Exe as Exécutable
Dev->>Chop: Fournit un seul fichier .ada
Chop-->>Dev: Découpe en .ads / .adb / main
Dev->>Build: gnatmake -gnata main
Build-->>Dev: Exécute la liaison (bind) et l'édition de liens
Dev->>Exe: ./main
Exe-->>Dev: Résultat d'exécution
4. Le modèle de base des génériques Ada
Les génériques Ada se comprennent globalement en trois étapes :
- Écrire une unité générique
- Écrire les paramètres formels dans la partie
generic - Instancier avec
newdu côté utilisateur
flowchart LR
G[Déclaration generic] --> F[Paramètres formels]
F --> B[Corps générique]
B --> I[Instanciation via new]
I --> U[Utilisé comme sous-programme ou paquet normal]
F --> F1[Type]
F --> F2[Valeur]
F --> F3[Sous-programme]
F --> F4[Paquet]
Par exemple, si l’on rend générique le traitement qui échange deux valeurs, seul le type peut être pris comme paramètre formel.
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
À ce stade, Generic_Swap ne peut pas encore être appelé. C’est un « modèle d’échange utilisable pour n’importe quel type Element ». Il ne devient une procédure normale qu’une fois qu’on lui fournit un type concret.
procedure Swap_Integer is new Generic_Swap (Integer);
En schéma, la relation est la suivante :
flowchart TB
Template[Generic_Swap<br/>type Element is private] -->|Passer Integer| SwapInt[Swap_Integer]
Template -->|Passer Character| SwapChar[Swap_Character]
Template -->|Passer My_Record| SwapRecord[Swap_My_Record]
SwapInt --> ICall[Échange des variables Integer]
SwapChar --> CCall[Échange des variables Character]
SwapRecord --> RCall[Échange des variables My_Record]
Ce qui compte, c’est que le corps du modèle n’est écrit qu’avec les opérations utilisables sur Element. Si l’on déclare type Element is private;, les opérations de base comme l’affectation ou la comparaison d’égalité sont utilisables, mais pas la comparaison d’ordre ni les opérations arithmétiques. Autrement dit, la déclaration générique elle-même exprime « ce que ce composant a le droit de présumer ».
5. Les types de paramètres formels ── le vocabulaire des génériques Ada
Ce que les génériques Ada peuvent recevoir ne se limite pas aux types. C’est là un point qui les distingue nettement des génériques classiques de C# ou de Java.
flowchart TB
P[Paramètres formels génériques] --> T[Paramètres de type]
P --> O[Objet / paramètres de valeur]
P --> S[Paramètres de sous-programme]
P --> PKG[Paramètres de paquet]
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 ...]
En tableau, les paramètres formels typiques se présentent ainsi :
| Type | Exemple | Signification |
|---|---|---|
| Paramètre de type | type Element is private; |
Forme de base : reçoit tout type definite non limited |
| Paramètre de type limited | type Element is limited private; |
Reçoit aussi les types non copiables |
| Type discret | type Index is (<>); |
Type entier ou énuméré, utilisable comme indice de tableau |
| Type entier signé | type Count is range <>; |
Présume +, -, la comparaison d’ordre et d’autres opérations entières |
| Type entier modulaire | type Word is mod <>; |
Traite les opérations bit à bit et l’arithmétique modulaire |
| Type flottant | type Real is digits <>; |
Float, Long_Float, type flottant défini par l’utilisateur, etc. |
| Type virgule fixe | type Money is delta <>; |
Traite l’arithmétique en virgule fixe |
| Paramètre de valeur | Max_Size : Positive; |
Fixe une taille ou un seuil pour chaque instance |
| Sous-programme | with function Predicate (...) return Boolean; |
Injecte un comportement tel qu’une fonction de comparaison ou un prédicat |
| Paquet | with package P is new Some_Generic (<>); |
Reçoit comme composant une instance déjà créée d’un paquet générique |
Grâce à ce vocabulaire, Ada permet naturellement d’écrire « on ne reçoit que les types capables de ces opérations », plutôt que « on reçoit n’importe quoi, quitte à faire des choses dangereuses à l’intérieur ».
6. Sous-programmes génériques ── comprendre la structure minimale avec Generic_Swap
Comme premier exemple, examinons Generic_Swap, qui échange deux variables d’un type quelconque.
6.1 Spécification
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
La partie qui suit generic constitue les paramètres formels. Ici, on reçoit un type nommé Element. is private signifie que, du point de vue du corps générique, on ne connaît pas la représentation interne de ce type.
Cette déclaration nous apprend deux choses :
Generic_Swapfonctionne pour n’importe quel typeElement- Le corps ne dépend ni de la structure interne d’
Element, ni de sa comparaison d’ordre
6.2 Corps
procedure Generic_Swap (A, B : in out Element) is
Temp : constant Element := A;
begin
A := B;
B := Temp;
end Generic_Swap;
Ce corps n’utilise que l’affectation sur Element. Ni A < B ni A + B n’apparaissent. Il fonctionne donc naturellement pour Integer, Character, un type enregistrement, un type énuméré — tout type affectable.
flowchart LR
subgraph Before[Avant l'appel]
A1[A = 10]
B1[B = 20]
end
A1 --> T[Temp = A]
B1 --> A2[A = B]
T --> B2[B = Temp]
subgraph After[Après l'appel]
A2[A = 20]
B2[B = 10]
end
6.3 Instanciation
Côté utilisateur, on utilise new.
procedure Swap_Int is new Generic_Swap (Integer);
procedure Swap_Char is new Generic_Swap (Character);
Swap_Int et Swap_Char deviennent alors des procédures normales, appelables directement.
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;
L’exécution donne le résultat suivant :
Before: X= 10, Y= 20
After : X= 20, Y= 10
On ne peut pas passer de variable Float à Swap_Int (X, Y); : Swap_Int est une procédure normale instanciée spécifiquement pour Integer. Il est plus facile de comprendre la généricité non pas comme « un trou où tout peut entrer », mais comme « un mécanisme qui fabrique, pour chaque type, un artefact concret et sûr ».
7. Paquets génériques ── paramétrer types et valeurs
Lorsqu’on veut réutiliser non pas un seul sous-programme mais un ensemble d’opérations avec un état interne, on utilise un paquet générique. L’exemple classique est la pile.
Pour une pile, tant que seuls le type d’élément et la taille maximale changent, la logique de base reste la même.
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 Spécification
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;
Ici, deux types de paramètres formels sont utilisés.
Element_Typeest un paramètre de typeMax_Sizeest un paramètre de valeur
Max_Size étant de type Positive, on ne peut pas instancier avec une taille inférieure ou égale à 0. Un paramètre de valeur peut ainsi porter, lui aussi, une contrainte de type.
7.2 Corps
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;
Ce qui compte dans ce corps de paquet, c’est que Data et Top sont créés séparément pour chaque instance.
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
Ces deux paquets sont créés à partir du même modèle, mais ne partagent pas leur état interne.
flowchart LR
Template[Generic_Stack] --> IntStack[Int_Stack]
Template --> FloatStack[Float_Stack]
subgraph I[État de Int_Stack]
ITop[Top]
IData[Data : tableau Integer]
end
subgraph F[État de Float_Stack]
FTop[Top]
FData[Data : tableau Float]
end
IntStack --> I
FloatStack --> F
7.3 Transitions d’état de la pile
Il est plus facile de comprendre la pile en la voyant comme une machine à états.
stateDiagram-v2
[*] --> Empty
Empty --> NonEmpty: Push
NonEmpty --> NonEmpty: Push / Pop
NonEmpty --> Empty: Pop retire le dernier élément
NonEmpty --> Full: Push atteint Max_Size
Full --> NonEmpty: Pop
Full --> Overflow: Push
Empty --> Underflow: Pop
7.4 Exemple d’utilisation
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;
En pratique, les paquets génériques sont particulièrement efficaces pour les « petits conteneurs », les « tampons de taille fixe », les « tampons circulaires (ring buffers) », les « files pour la journalisation », ou les « couches d’abstraction matérielle ». En Ada en particulier, fixer statiquement la taille via des paramètres de type et de valeur, plutôt que de la faire varier à l’exécution, s’accorde bien avec la conception de systèmes à haute fiabilité.
8. Paramètres formels de sous-programme ── injecter du comportement
Recevoir uniquement un type ne suffit pas toujours à tout exprimer. Pour un tri, par exemple, il ne faut pas seulement le type d’élément, mais aussi la logique de comparaison qui détermine « lequel placer en premier ».
En Ada, cette fonction de comparaison peut elle-même devenir un paramètre formel du générique.
flowchart LR
A[Generic_Insertion_Sort] --> T[Item_Type]
A --> I[Index]
A --> ARR[Item_Array]
A --> CMP[Fonction de comparaison]
CMP --> ASC[Utilise la comparaison standard]
CMP --> DESC[Passe Greater pour un tri décroissant]
CMP --> CUSTOM[Passe un ordre personnalisé]
8.1 Spécification
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);
Il y a ici quatre paramètres formels.
Item_Type: le type des éléments du tableauIndex: le type des indices du tableauItem_Array: le type de tableau réel"<": la fonction de comparaison
type Index is (<>); reçoit un type discret. Cela inclut non seulement les types entiers, mais aussi les types énumérés. Pouvoir utiliser, en plus de Positive, un type énuméré comme Day pour l’indice de tableau, voilà ce qui est typiquement Ada.
Dans with function "<" ... is <>;, le is <> signifie que si le paramètre effectif est omis, on utilise l’opérateur standard visible ou une fonction correspondante. Autrement dit, pour un type qui possède déjà un <, comme Integer, la fonction de comparaison est utilisable sans être explicitée.
8.2 Corps
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;
Le tri par insertion ne convient pas aux grands tableaux, mais il se prête bien à l’explication de la généricité. En ne changeant que la fonction de comparaison, la même structure de boucle peut servir aussi bien pour un tri croissant que décroissant.
flowchart TB
Start[Tableau non trié] --> Pick[Extraire Key en partant de la gauche]
Pick --> Compare{Key précède-t-il l'élément précédent ?}
Compare -->|Oui| Shift[Décaler l'élément précédent vers la droite]
Shift --> Compare
Compare -->|Non| Insert[Insérer Key]
Insert --> Done{Traitement terminé ?}
Done -->|Non| Pick
Done -->|Oui| End[Tableau trié]
8.3 Créer un ordre croissant et décroissant à partir du même corps
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 utilise le < standard. Sort_Desc, lui, substitue la fonction de comparaison en écrivant "<" => Greater.
flowchart LR
Data[99, 3, 47, 12] --> A[Sort_Asc<br/>Comparaison standard]
Data --> D[Sort_Desc<br/>Greater passé comme comparaison]
A --> AO[3, 12, 47, 99]
D --> DO[99, 47, 12, 3]
Ce mécanisme se rapproche de la conception qui consiste, en C++, à passer un objet fonction de comparaison en argument de template, ou, en Rust, à exiger un ordre via une borne de trait. En Ada toutefois, on explicite clairement, en tant que paramètre formel de sous-programme, « quelle forme de fonction on passe ».
9. Catégories de types ── écrire des contrats plus précis que private
type T is private; est pratique, mais ne permet pas tout. Pour un type private, on ne peut pas naturellement utiliser les opérations arithmétiques ou la comparaison d’ordre. C’est pourquoi Ada permet de spécifier une catégorie sur le paramètre de type formel.
flowchart TB
FormalType[Type formel] --> Private[private]
FormalType --> Limited[limited private]
FormalType --> Discrete[discrete box : type discret]
FormalType --> Signed[range box : entier signé]
FormalType --> Modular[mod box : entier modulaire]
FormalType --> Float[digits box : virgule flottante]
FormalType --> Fixed[delta box : virgule fixe]
FormalType --> Access[type access]
Discrete --> Enum[Type énuméré]
Discrete --> Int[Type entier]
Float --> F1[Float]
Float --> F2[Long_Float]
Float --> F3[Type flottant défini par l'utilisateur]
9.1 Pourquoi spécifier une catégorie apporte-t-il un bénéfice ?
Calculer une moyenne ou une variance requiert l’addition, la soustraction, la multiplication et la division. Un type private ne permet pas de présumer ces opérations. On restreint donc le paramètre à un type flottant.
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;
Grâce à type Real is digits <>;, on sait que Real est un type flottant. Le corps générique peut donc utiliser +, -, *, /.
9.2 Corps
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 Utilisation avec Float et 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);
Le même traitement statistique peut être réutilisé pour des types flottants de précisions différentes.
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 avec Float]
LS --> LCalc[Mean / Variance avec Long_Float]
MS --> MCalc[Mean / Variance avec un Real défini par l'utilisateur]
9.4 La spécification de catégorie est un « cahier des charges au niveau du type »
La spécification de catégorie n’est pas qu’une simple syntaxe destinée à satisfaire le compilateur. Elle constitue aussi, pour le lecteur, un cahier des charges indiquant « ce que ce composant exige ».
| Traitement souhaité | Type formel adapté | Raison |
|---|---|---|
| Échanger, stocker, récupérer | private |
L’affectation suffit |
| Gérer une ressource non copiable | limited private |
Ne présume pas l’affectation |
| Indexer un tableau, parcourir des états énumérés | (<>) |
First, Last, Succ, Pred sont utilisables |
| Sommer des entiers, compter | range <> |
Présume l’arithmétique entière |
| Masque de bits, compteur cyclique | mod <> |
Présume l’arithmétique modulaire |
| Moyenne, variance, calcul numérique | digits <> |
Présume l’arithmétique flottante |
| Montants, grandeurs de contrôle à précision fixe | delta <> |
Présume l’arithmétique en virgule fixe |
10. Injection de prédicats ── écrire Count_If à la manière Ada
Les paramètres formels de sous-programme ne servent pas qu’aux fonctions de comparaison, ils servent aussi aux prédicats. Un prédicat est une fonction qui reçoit une valeur et retourne un Boolean.
Le rôle que jouent Func<T, bool> en C#, Predicate<T> en Java, ou les lambdas et objets fonction en C++, Ada peut l’exprimer par un sous-programme formel de générique.
10.1 Spécification
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;
Ici, is <> n’est pas ajouté à Predicate. Comme il n’existe pas de fonction de prédicat standard visible par défaut, l’appelant doit obligatoirement en fournir une.
10.2 Corps
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;
Le déroulement du traitement est simple.
flowchart LR
Arr[Tableau] --> Loop[Parcourt chaque élément]
Loop --> P{"Predicate(Item) ?"}
P -->|Vrai| Inc[Incrémente Count]
P -->|Faux| Skip[Ne fait rien]
Inc --> Next[Élément suivant]
Skip --> Next
Next --> Result[Retourne Count]
10.3 Compter les nombres pairs et compter au-delà d’un seuil
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);
À partir de la même logique de parcours, on peut créer deux fonctions qui ne diffèrent que par leur condition.
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[Nombre de valeurs paires]
L --> LR[Nombre de valeurs supérieures à 50]
Dans cet exemple, le parcours du tableau, la gestion du compteur et le retour du résultat sont entièrement mutualisés. Seul « ce que l’on compte » est injecté comme fonction. C’est la forme fondamentale de conception d’ordre supérieur en Ada.
11. Composer plusieurs paramètres ── un magasin clé-valeur générique
Dans un composant réel, il est rare de se contenter d’un seul paramètre de type. Le type de la clé, le type de la valeur, la méthode de comparaison des clés, le nombre maximal d’entrées : il faut souvent combiner plusieurs conditions.
Prenons ici l’exemple d’un magasin clé-valeur simple, de taille fixe.
flowchart TB
KV[Generic_KV_Store] --> K[Key_Type]
KV --> V[Value_Type]
KV --> EQ[Fonction de correspondance de clé]
KV --> M[Max_Entries]
KV --> Ops[Put / Get / Contains]
Ops --> Use1[Magasin de valeurs de configuration]
Ops --> Use2[Petit cache]
Ops --> Use3[Dictionnaire de taille fixe pour l'embarqué]
11.1 Spécification
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;
Ce paquet comporte quatre paramètres formels.
| Paramètre | Type | Rôle |
|---|---|---|
Key_Type |
Type | Le type de la clé |
Value_Type |
Type | Le type de la valeur |
"=" |
Sous-programme | Le test d’égalité des clés |
Max_Entries |
Valeur | Le nombre maximal d’entrées |
Max_Entries reçoit une valeur par défaut avec := 50. Sans indication particulière, le magasin comportera donc 50 entrées.
11.2 Corps
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;
Cette implémentation repose sur une recherche linéaire, elle n’est donc pas adaptée aux gros volumes de données. Mais elle reste facile à employer dans les cas où la propriété importante est d’être de taille fixe, de petite échelle, et sans allocation dynamique de mémoire.
sequenceDiagram
participant App as Appelant
participant Store as Instance de Generic_KV_Store
participant Data as Keys/Values/Used
App->>Store: Put(Key, Value)
Store->>Data: Find_Index(Key)
alt Clé existante
Store->>Data: Values(Pos) := Value
else Nouvelle clé
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 Exemple d’instanciation
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) permet de récupérer la valeur
null;
end if;
end KV_Demo;
Le "=" a été omis. Integer possède un opérateur d’égalité standard, et c’est celui-ci que is <> utilise.
Si les clés étaient, par exemple, des chaînes de caractères insensibles à la casse, on pourrait passer une fonction d’égalité personnalisée.
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. Paramètres formels de paquet ── composer les génériques entre eux
Dans les génériques Ada, un paquet lui-même peut devenir un paramètre formel. Cela permet de traiter « une instance créée à partir d’un paquet générique » comme l’entrée d’un autre générique.
flowchart LR
GS[Generic_Stack] --> IS[Int_Stack]
IS --> Logger[Generic_Stack_Logger]
Logger --> Logged[Instance de Int_Stack_Logger]
12.1 Un journal (logger) qui reçoit une pile
Supposons, par exemple, que l’on veuille créer un journal (logger) qui reçoit une instance du Generic_Stack vu précédemment et affiche sa taille.
generic
with package Stack is new Generic_Stack (<>);
package Generic_Stack_Logger is
procedure Print_Size;
end Generic_Stack_Logger;
Le corps se présente ainsi :
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;
Côté utilisateur, on crée d’abord la pile, puis on la passe au journal.
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);
Une telle conception permet de combiner des composants génériques entre eux.
flowchart TB
subgraph Layer1[Première étape]
T1[Generic_Stack] --> I1[Int_Stack]
end
subgraph Layer2[Deuxième étape]
T2[Generic_Stack_Logger] --> I2[Int_Stack_Logger]
end
I1 --> T2
I2 --> API[Print_Size]
Cet usage se rapproche des paramètres de template de template en C++, mais en Ada, on peut expliciter clairement que l’on reçoit « une instance de ce paquet générique ». Dans les grandes bases de code Ada, c’est pratique pour séparer et combiner conteneurs, algorithmes, journalisation, vérification, ou auxiliaires de test.
13. Le contract model ── l’idée la plus importante des génériques Ada
Ce qui importe le plus pour comprendre les génériques Ada, c’est le contract model.
Le corps générique doit être écrit en n’utilisant que les opérations promises par les paramètres formels. Par exemple, si l’on n’a déclaré que type Element is private;, on ne peut pas utiliser < sur Element. Pour utiliser <, il faut soit l’expliciter comme sous-programme formel, soit préciser davantage la catégorie du type.
flowchart TB
Spec[Partie formelle generic<br/>= contrat] --> Body[Corps generic<br/>implémenté dans les limites du contrat]
Body --> Check1[Corps vérifié seul, indépendamment]
Spec --> Inst[Instanciation]
Actual[Paramètres effectifs<br/>types, fonctions, valeurs réels] --> Inst
Inst --> Check2[Vérifie que les paramètres effectifs satisfont le contrat]
Check2 --> Instance[Paquet ou sous-programme normal]
Cette conception protège aussi bien l’utilisateur du générique que celui qui l’écrit.
13.1 Différence de perspective avec les templates C++
Les templates C++ sont puissants, mais ils ont historiquement la particularité de ne révéler leurs erreurs qu’une fois le corps de template instancié. Les concepts de C++20 ont amélioré ce point, mais les génériques Ada adoptent, dès l’origine, un modèle où le contrat est explicite.
flowchart LR
subgraph Ada[Ada]
A1[Le contrat est écrit dans la partie formelle] --> A2[Le corps est vérifié dans les limites du contrat]
A2 --> A3[L'instanciation vérifie le paramètre effectif]
end
subgraph CPP[Templates C++]
C1[Le corps du template est écrit] --> C2[Les expressions requises se concrétisent à l'instanciation]
C2 --> C3[Les concepts permettent d'expliciter les contraintes]
end
Les génériques de Java et de C# placent au centre de leur conception les types référence, les contraintes, l’effacement de type et la relation avec la représentation à l’exécution. Les génériques Ada, eux, penchent vers l’idée de créer des instances concrètes à la compilation.
| Aspect | Ada | C++ | Java | Rust |
|---|---|---|---|---|
| Manière d’écrire le contrat | Types, valeurs, fonctions, paquets dans la partie formelle | Templates / concepts | Paramètres de type et bounds | Trait bounds |
| Vérification du corps | Vérifiée dans les limites du contrat des paramètres formels | Principalement lors de la concrétisation à l’instanciation | Vérifiée dans les limites des bounds | Vérifiée dans les limites des trait bounds |
| Coût à l’exécution | Résolution statique par défaut | Génération statique par défaut | Subit l’effet de l’effacement de type | Monomorphisation par défaut |
| Paramètres de valeur | Oui | Oui | Limité | Const generics |
| Sous-programme comme paramètre formel | Oui | Via des objets fonction, etc. | Lambda / interface fonctionnelle | Closure / fonction / trait |
| Paquet comme paramètre formel | Oui | Template de template, etc. | Non | Distinct de la structure de module |
Les mécanismes détaillés diffèrent d’un langage à l’autre, mais la caractéristique propre à Ada est d’« écrire le contrat comme une syntaxe, en premier ».
14. Décisions de conception en pratique ── que doit-on rendre générique ?
La généricité est pratique, mais tout ne doit pas nécessairement devenir générique. En pratique, le raisonnement suivant limite les mauvaises décisions.
flowchart TB
Start[Il existe un traitement à réutiliser] --> Q1{Seul le type diffère ?}
Q1 -->|Oui| GType[Envisager un paramètre de type]
Q1 -->|Non| Q2{La taille ou le seuil diffèrent aussi ?}
Q2 -->|Oui| GObject[Ajouter un paramètre de valeur]
Q2 -->|Non| Q3{Le comportement de comparaison ou de décision diffère ?}
Q3 -->|Oui| GSubp[Ajouter un sous-programme formel]
Q3 -->|Non| Q4{Souhaite-t-on regrouper l'état interne et l'API ?}
Q4 -->|Oui| GPackage[Paquet générique]
Q4 -->|Non| Normal[Un sous-programme normal suffit]
14.1 Cas où un sous-programme générique convient
Les sous-programmes génériques conviennent aux algorithmes sans état.
SwapSortCount_IfFind- Des transformations de type
Map Min/Max
Lorsque le corps de l’algorithme est court et que l’entrée et la sortie sont claires, un sous-programme se lit plus facilement qu’un paquet.
14.2 Cas où un paquet générique convient
Les paquets génériques conviennent lorsqu’on souhaite regrouper, avec le type, plusieurs opérations ainsi qu’un état interne.
- Pile de taille fixe
- Tampon circulaire (ring buffer)
- Petit dictionnaire
- Ensemble de traitements statistiques
- Abstraction d’E/S propre à chaque périphérique
- Ensemble d’opérations pour des types numériques porteurs d’unité
En Ada en particulier, la spécification du paquet constitue l’API publique et le corps du paquet l’implémentation ; cette séparation fait qu’un paquet générique peut servir de « modèle de module type-sûr ».
flowchart LR
Spec[Spécification du paquet<br/>API publique] --> User[Côté utilisateur]
Body[Corps du paquet<br/>implémentation interne] -.masqué.-> User
Formal[Partie formelle generic<br/>contrat de types, valeurs, fonctions] --> Spec
Formal --> Body
14.3 Commencer avec peu de paramètres formels
Si l’on multiplie trop les paramètres formels, l’instanciation devient difficile à lire. Il est plus sûr de commencer avec un minimum, et de n’en ajouter que lorsqu’un besoin concret de substitution apparaît.
-- Exemple difficile à lire
package X is new Generic_Foo
(A, B, C, D, E, F, G);
-- L'association nommée préserve l'intention
package X is new Generic_Foo
(Element_Type => Integer,
Index_Type => Positive,
Buffer_Size => 128,
"<" => Less);
En Ada, on peut utiliser l’association nommée au moment de l’instanciation. Comme les points importants de la conception apparaissent précisément à l’instanciation d’un générique, écrire avec des noms explicites facilite souvent la maintenance du code en pratique.
15. Pièges fréquents
Les génériques Ada sont puissants, mais présentent des points sur lesquels on trébuche facilement au début.
15.1 Impossible de comparer un type private
Le corps suivant ne peut pas être écrit.
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 -- Erreur ici
return A;
else
return B;
end if;
end Bad_Min;
Element n’est déclaré que comme private, rien ne garantit donc que < soit utilisable. Pour comparer, il faut l’ajouter au contrat, comme ceci :
generic
type Element is private;
with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
flowchart LR
Need[On veut comparer dans le corps] --> Contract[Écrire une fonction de comparaison dans la partie formelle]
Contract --> OK[La possibilité de comparaison est vérifiée à l'instanciation]
Need --> NoContract[private seul]
NoContract --> Error[Erreur de compilation dans le corps générique]
15.2 is <> n’est pas une « inférence automatique universelle »
is <> est pratique, mais ce n’est pas de la magie. Au point d’instanciation, il faut qu’un opérateur ou un sous-programme correspondant soit visible. Si l’on a placé sa propre fonction de comparaison dans un autre paquet, il faut faire un with et un use appropriés, ou bien la passer explicitement par son nom, ce qui est plus sûr.
procedure Sort_By_Age is new Generic_Insertion_Sort
(Item_Type => Person,
Index => Positive,
Item_Array => Person_Array,
"<" => Younger_Than);
15.3 Les exceptions aussi deviennent distinctes par instance
Si l’on déclare une exception dans la spécification d’un paquet générique, elle devient une exception différente pour chaque instance.
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
Dans ce cas, Int_Stack.Stack_Overflow et Float_Stack.Stack_Overflow sont traitées comme deux exceptions distinctes. Si l’on souhaite une exception commune, il faut envisager de la définir en dehors du générique.
flowchart TB
Generic[Generic_Stack<br/>déclare Stack_Overflow] --> I[Int_Stack.Stack_Overflow]
Generic --> F[Float_Stack.Stack_Overflow]
I -.exception distincte.-> F
15.4 La taille du code peut augmenter
La généricité permet d’éviter facilement les indirections superflues à l’exécution, mais comme une instance est créée pour chaque type, un grand nombre d’instances peut augmenter la taille du code.
C’est un compromis que l’on retrouve aussi bien dans les templates C++ que dans la monomorphisation de Rust. Dans les développements orientés haute fiabilité, embarqué ou temps réel, l’approche consiste à réduire l’incertitude à l’exécution en échange d’une gestion de la taille des artefacts produits à la compilation.
flowchart LR
Generic[Un seul corps generic] --> I1[Version Integer]
Generic --> I2[Version Float]
Generic --> I3[Version Long_Float]
Generic --> I4[Version My_Type]
I1 --> Code[Code généré]
I2 --> Code
I3 --> Code
I4 --> Code
Code --> Pros[Facilite l'évitement des vérifications de type et du boxing à l'exécution]
Code --> Cons[Attention à l'augmentation de taille si les instances sont nombreuses]
15.5 Quand utiliser limited private
type Element is private; présuppose l’affectation. Pour des poignées de fichier, des verrous, des poignées de périphérique — des ressources que l’on ne veut pas copier — envisagez limited private.
generic
type Resource is limited private;
with procedure Close (R : in out Resource);
procedure Generic_Use_And_Close (R : in out Resource);
Pour concevoir avec des types non copiables, un algorithme qui applique une procédure ou qui explicite une référence est plus sûr qu’un conteneur qui stocke des valeurs.
16. Petit recueil de patrons de conception
Voici, résumés brièvement, quelques patrons souvent utiles en pratique.
16.1 Ne fournir Min que pour des valeurs comparables
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[Nécessite une fonction de comparaison]
C --> M[Generic_Min]
M --> R[Retourne le plus petit]
16.2 Faire du seuil un paramètre de valeur
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;
Un paramètre de valeur convient à une grandeur que l’on souhaite fixer comme propriété de l’instance, plutôt que comme réglage variable à l’exécution.
16.3 Injecter un moyen de sortie
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;
Sous cette forme, on peut substituer la destination de sortie : sortie standard, journal, tampon de test, etc.
flowchart TB
Print[Generic_Print_Twice] --> Put[Reçoit Put comme sous-programme formel]
Put --> Console[Sortie console]
Put --> Log[Sortie journal]
Put --> Test[Tampon de test]
16.4 Ne pas figer le type d’indice du tableau
En Ada, le type des indices d’un tableau est lui aussi une information de type importante. Plutôt que de figer Positive, faire aussi du type d’indice un paramètre formel, selon le besoin, augmente la réutilisabilité.
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);
Cette conception permet de gérer non seulement les tableaux indexés par Positive, mais aussi les tableaux indexés par un type énuméré.
flowchart LR
Index[Index is discret] --> Positive[Plage Positive]
Index --> Day[Type énuméré Day]
Index --> State[Type énuméré State]
Index --> Slot[Type entier personnalisé]
17. Liste de vérification pour une API à la manière Ada
Lorsqu’on écrit un générique, revoir les points suivants à la fin en améliore la lisibilité.
flowchart TB
C[Vérification de conception du générique] --> C1[Les paramètres formels sont-ils minimaux ?]
C --> C2[Les opérations nécessaires sont-elles explicitées dans la partie formelle ?]
C --> C3[Les catégories private / range / digits sont-elles appropriées ?]
C --> C4[Peut-on instancier de façon lisible avec l'association nommée ?]
C --> C5[L'état et les exceptions propres à chaque instance sont-ils pris en compte ?]
C --> C6[L'augmentation de la taille du code est-elle acceptable ?]
C --> C7[Dispose-t-on d'instances destinées aux tests ?]
En résumé :
- Toute opération utilisée dans le corps doit toujours être visible sous forme de contrat dans les paramètres formels.
- Si
privatesuffit, utiliserprivate. Si l’arithmétique est nécessaire, utiliserrange <>oudigits <>. - Comparaison, hachage, sortie, conversion : tout comportement qui varie selon le type doit devenir un sous-programme formel.
- Si la taille ou le seuil relève d’une propriété de l’instance, en faire un paramètre de valeur.
- S’il y a un état, penser d’abord au paquet générique ; s’il n’y en a pas, penser d’abord au sous-programme générique.
- À l’instanciation, plus les arguments sont nombreux, plus il faut utiliser l’association nommée.
- Concevoir en présumant que les exceptions et l’état interne sont indépendants pour chaque instance.
18. Exemple d’organisation complète des échantillons
Si l’on répartit les exemples de l’article en plusieurs fichiers, une organisation comme celle-ci facilite la lecture.
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]
Pour un petit échantillon d’article, il est pratique de tout regrouper dans un seul fichier puis de le découper avec gnatchop. Mais pour un usage professionnel ou une maintenance à long terme, séparer la spécification .ads du corps .adb donne une organisation plus conforme à l’esprit Ada.
19. Conclusion ── fixer les frontières de la réutilisation par les types
La programmation générique en Ada ne se résume pas à une simple « fonctionnalité permettant d’écrire du code indépendant du type ». Son essence réside plutôt dans le fait de rendre explicite, sous forme de contrat entre types, sous-programmes et valeurs, ce qu’exige un composant réutilisable.
flowchart LR
Contract[Écrire le contrat] --> Generic[Écrire le corps générique]
Generic --> Instance[Instancier en passant types, valeurs, fonctions]
Instance --> Safe[Utiliser de façon type-sûre]
Safe --> Reuse[Réutiliser sans copier]
Comme on l’a vu dans cet article, les génériques Ada peuvent recevoir comme paramètres formels :
- Un type
- Une valeur
- Un sous-programme
- Un paquet
De plus, pour les types, on peut spécifier des catégories assez fines : private, limited private, range <>, mod <>, digits <>, delta <>, (<>). Grâce à cela, le corps générique n’a pas à dépendre d’« opérations dont on ne sait pas si elles sont disponibles » : il peut être implémenté de façon sûre, en n’utilisant que les opérations écrites dans le contrat.
Dans le patrimoine du C ou du C++ ancien, la réutilisation passe parfois par des macros, void*, des pointeurs de fonction, ou des branchements de type écrits à la main. Les génériques Ada permettent de remplacer une grande partie de ces usages par une forme type-sûre et lisible. C’est en particulier dans les logiciels à maintenance longue durée, embarqués, temps réel ou à haute fiabilité que cette approche consistant à « fixer les frontières à la compilation » apporte une valeur considérable.
Domaines de conseil associés
KomuraSoft LLC (合同会社小村ソフト) intervient sur le développement d’applications Windows, l’investigation et la remise à niveau d’actifs existants, la clarification des frontières COM / ActiveX / 32 bits / 64 bits, ainsi que le conseil technique et la revue de conception. Au-delà des conceptions orientées typage statique et haute fiabilité comme celles d’Ada, la question de savoir comment organiser et faire perdurer ou migrer des actifs existants en C/C++, C#, VB6, MFC ou COM constitue elle aussi, en pratique, une thématique proche de notre activité.
- Développement d’applications Windows
- Développement de composants COM
- Remplacement d’applications Windows
- Conseil technique et revue de conception
- Contact
Références
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
L'attrait du langage Ada ── Exprimer la conception par les types, faire fonctionner des logiciels pendant des décennies
Une introduction à l'attrait du langage Ada : le typage fort, les contraintes de plage, la séparation de la spécification et de l'impléme...
Concurrence sécurisée avec Ada ── Guide pratique des tâches et objets protégés
Un article d'introduction aux tâches et objets protégés, la concurrence intégrée au langage Ada. Il couvre le rendez-vous (entry/accept),...
Introduction à la vérification formelle avec SPARK ── Des contrats Ada à la preuve mathématique
Une introduction pratique à la vérification formelle avec SPARK, le sous-ensemble d'Ada. L'article couvre le passage des contrats (Pre/Po...
Programmation de systèmes temps réel avec Ada ── Priorités, exécution périodique et contrôle du temps d'exécution en pratique
Découvrez l'Annexe D d'Ada (systèmes temps réel) à travers 8 exemples de code pratiques. L'article couvre progressivement les priorités d...
Gestion des erreurs et stratégie de nouvelle tentative dans Power Automate — éviter qu'un flux qui fonctionnait ne s'arrête sans que personne ne le remarque
Un ensemble de modèles de conception pour éviter qu'un flux Power Automate ne s'arrête sans que personne ne s'en aperçoive. Nous détaillo...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Qu'est-ce qu'un générique en Ada ?
- C'est un mécanisme de réutilisation qui reçoit des types, des valeurs, des sous-programmes, voire des paquets eux-mêmes comme paramètres formels, et qui est vérifié statiquement au moment de l'instanciation avec new. Ce n'est pas une simple substitution de texte : à la compilation, le compilateur détermine si « ce composant satisfait bien ce contrat ». Un sous-programme générique ne peut pas être appelé tel quel une fois déclaré ; il ne devient une procédure ou une fonction normale qu'après avoir été instancié avec un type concret.
- En quoi les génériques Ada diffèrent-ils des templates C++ ?
- Ada adopte dès l'origine un contract model où le contrat est explicite : le corps du générique n'utilise que les opérations promises par les paramètres formels, et il est vérifié seul, indépendamment de toute instanciation. Les templates C++ ont historiquement la particularité de ne révéler leurs erreurs qu'au moment de l'instanciation, un point amélioré par les concepts de C++20. De plus, en Ada, outre les paramètres de valeur et de sous-programme, un paquet entier peut lui-même être un paramètre formel, ce qui permet de composer des composants génériques entre eux.
- Pourquoi spécifier une catégorie de type pour un paramètre de type formel en Ada ?
- Pour rendre explicites, sous forme de contrat, les opérations utilisables dans le corps du générique. Avec type T is private, on ne peut présumer que des opérations de base comme l'affectation ou la comparaison d'égalité — ni la comparaison d'ordre ni les opérations arithmétiques ne sont disponibles. Si l'on a besoin d'arithmétique entière, on spécifie range <> ; pour l'arithmétique flottante, digits <> ; pour les opérations bit à bit, mod <>. La spécification de catégorie fonctionne aussi comme une spécification au niveau du type, indiquant au lecteur « ce que ce composant exige ».
- Quels sont les points de vigilance avec les génériques Ada ?
- Comme une instance distincte est créée pour chaque type, un grand nombre d'instanciations peut augmenter la taille du code généré. C'est le même compromis que l'on retrouve avec les templates C++ ou la monomorphisation de Rust. De plus, une exception déclarée dans la spécification d'un paquet générique devient une exception distincte pour chaque instance. En pratique, il est recommandé de commencer avec un minimum de paramètres formels, de n'en ajouter que lorsqu'un besoin concret de substitution apparaît, et d'utiliser l'association nommée pour les instanciations comportant de nombreux arguments.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics