La programmation modulaire III : Généricité

Cela fait déjà plusieurs chapitres que ce terme revient : générique. Les packages Ada.Containers.Doubly_Linked_Lists ou Ada.Containers.Vectors étaient génériques. Mais bien avant cela, le package Ada.Numerics.Discrete_Random qui nous permettait de générer des nombres aléatoirement était générique ! Même pour libérer un pointeur, nous avions utilisé une procédure générique : Ada.Unchecked_Deallocation(). Nous côtoyons cette notion depuis la partie II déjà, sans avoir pris beaucoup de temps pour l'expliquer : c'est que cette notion est très présente en Ada. Et pour cause, la norme Ada83 permettait déjà la généricité. Il est donc temps de lever le voile avant de revenir à la programmation orientée objet et à l'héritage.

Généricité : les grandes lignes

Que veut-on faire ?

Avant de parler de packages génériques, il serait bon d'évoquer le problème qui a mené à cette notion de généricité. Vous avez du remarquer au cours de ce tutoriel que vous étiez souvent amenés à réécrire les mêmes bouts de code, les mêmes petites fonctions ou procédures qui servent sans arrêt. Par exemple, des procédures pour échanger les valeurs de deux variables, pour afficher le contenu d'un tableau, en extraire le plus petit élément ou encore en effectuer la somme… Bref, ce genre de sous-programme revient régulièrement et les packages ne suffisent pas à résoudre ce problème : notre bon vieux package P_Integer_Array ne résolvait certains de ces soucis que pour des tableaux contenant des Integer, mais pas pour des tableaux de float, de character, de types personnalisés… De même, nous avons créé des packages P_Pile et P_File uniquement pour des Integer, et pour disposer d'une pile de Float, il faudrait jouer du copier-coller : c'est idiot !

D'où la nécessité de créer des programmes génériques, c'est-à-dire pouvant traiter «toute sorte» de types de données. A-t-on vraiment besoin de connaître le type de deux variables pour les intervertir ? Non bien sûr, tant qu'elles ont le même type ! L'idée est donc venue de proposer aux programmeurs la possibilité de ne rédiger un code qu'une seule fois pour ensuite le réemployer rapidement selon les types de données rencontrées.

Le langage Ada utilise le terme (suffisamment explicite je pense) de GENERIC pour indiquer au compilateur quels sont les types ou variables génériques utilisés par les sous-programmes. D'autres langages parlent en revanche de modèle et utilisent le terme de template : on indique au compilateur que le sous-programme est un «cas-général», un «moule», une sorte de plan de montage de sous-programme en kit. Même difficulté, même approche mais des termes radicalement différents.

Plan de bataille

Il est possible de créer des fonctions génériques, des procédures génériques ou encore des packages génériques : nous parlerons d'unités de programmes génériques. Ces unités de programmes ne seront génériques que parce qu'elles accepteront des paramètres eux-mêmes génériques. Ces paramètres sont en général des types, mais il est également possible d'avoir comme paramètre de généricité une variable ou une autre unité de programme ! Ce dernier cas (plus complexe) sera vu à la fin de ce chapitre. Ces paramètres seront appelés soit paramètres génériques soit paramètres formels. Pour l'instant, nous allons retenir que pour réaliser une unité de programme générique il faut avant-tout déclarer le (ou les) type(s) générique(s) qui sera (seront) utilisé(s) par la suite. D'où un premier schéma de déclaration.

1
2
3
DÉCLARATION DU (OU DES) TYPE(S) GÉNÉRIQUE(S)
SPÉCIFICATION DE L’UNITÉ DE PROGRAMME GÉNÉRIQUE
CORPS DE L’UNITÉ DE PROGRAMME GÉNÉRIQUE

PLAN

Notez bien ceci : il est OBLIGATOIRE d'écrire les spécificationsdes unités de programme, même pour les fonctions et procédures! ! ! Et si j'utilise le gras-rouge, ce n'est pas pour rien. :colere2:

Ensuite, une unité de programme générique, seule, ne sert à rien. Il n'est pas possible de l'utiliser directement avec un type concret comme Integer ou Float (on parle de types effectifs). Vous devrez préalablement recréer une nouvelle unité de programme spécifique au type désiré. Rassurez-vous, cette étape se fera très rapidement : c'est ce que l'on appelle l'instanciation. D'où un plan de bataille modifié :

1
2
3
4
DÉCLARATION DU (OU DES) TYPE(S) GÉNÉRIQUE(S)
SPÉCIFICATION DE L’UNITÉ DE PROGRAMME GÉNÉRIQUE
CORPS DE L’UNITÉ DE PROGRAMME GÉNÉRIQUE
INSTANCIATION D'UNE UNITÉ DE PROGRAMME SPÉCIFIQUE

PLAN

Un dernier point de vocabulaire

Bon ça y est, on commence ? :)

Pas encore, nous avons un petit point de théorie à éclaircir. Vous l'aurez compris, nous allons pouvoir définir des types génériques, seulement les types peuvent être classés en plusieurs catégories qu'il n'est pas toujours évident de distinguer : on dit qu'il existe plusieurs classes de types (tiens, encore ce mot «classe» ;) ).

Nous avons ainsi les types tableaux, les types flottants, les types entiers, les types pointeurs… mais aussi les types discrets. De quoi s'agit-il ? Ce ne sont pas des types qui ne font pas de bruit ! :p Non, le terme discret est un terme mathématique signifiant que chaque élément (sauf les extrémités) a un prédécesseur et un successeur. Le type Natural par exemple est un type discret : si vous prenez un élément N au hasard parmi le type Natural (175 par exemple), vous pouvez lui trouver un successeur avec l'instruction Natural'succ(N) (c'est 176) et un prédécesseur avec l'instruction Natural'pred(N) (c'est 174). Et cela marchera pour tous les Natural hormis 0 qui n'a pas de prédécesseur et Natural'last qui n'a pas de successeur. Il en va de même pour les tous les types entiers comme Positive ou Integer, pour les types Character, Boolean ou même les types énumérés.

En revanche, le type Float n'est pas un type discret. Prenez un flottant X au hasard (175.0 par exemple). Quel est son successeur ? 176.0 ou 175.1 ? Et pourquoi pas 175.0002 ou 175.0000001 ? De même, les tableaux ou les pointeurs ne sont évidemment pas des types discrets.

Types discrets ou non

Pourquoi vous parlez de cela ? Eh bien parce qu'avant de déclarer des types génériques, il est important de savoir à quelle classe de type il appartiendra : intervertir deux variables ne pose pas de soucis, mais effectuer une addition par 1 ne peut se faire qu'avec un type entier et pas flottant, connaître le successeur d'une variable ne peut se faire qu'avec des types discrets, connaître le nème élément ne peut se faire qu'avec un type tableau… Bref, faire de la généricité, ce n'est pas faire n'importe quoi : il est important d'indiquer au compilateur quels types seront acceptables pour nos unités de programmes génériques.

Créer et utiliser une méthode générique

Créer une méthode générique

Bon ! Il est temps désormais de voir un cas concret ! Nous allons créer une procédure qui échange deux éléments (nous l'appellerons Swap, c'est le terme anglais pour Échanger). Voilà à quoi elle ressemblerait :

1
2
3
4
5
6
7
procedure swap(a,b : in out Positive) is
   c : Positive ; 
begin
   c:=a ; 
   a:=b ; 
   b:=c ; 
end swap ;

Mais elle est conçue pour échanger des Positive : pas des Natural ou des Integer, non ! Seulement des Positive ! Peut-être pourrions-nous élargir son champ d'action à tous les types entiers, au moins ?

Créer un type générique

Nous allons donc créer un type générique appelé T_Entier. Attention, ce type n'existera pas réellement, il ne servira qu'à la réalisation d'une procédure générique (et une seule). Pour cela nous allons devoir ouvrir un bloc GENERIC dans la partie réservée aux déclarations :

1
2
generic
   type T_Entier is range <> ;

Notez bien le « RANGE <> » ! Le diamant (<>) est le symbole indiquant que les informations nécessaires ne seront transmises que plus tard. La combinaison de RANGE et du diamant indique plus précisément que le type attendu est un type entier (Integer, Natural, Positive, Long_Long_integer…) et pas flottant ou discret ou que-sais-je encore !

Notez également qu'il n'y a pas d'instruction « END GENERIC » ! Pourquoi ? Tout simplement parce que ce type générique ne va servir qu'une seule fois et ce sera pour l'unité de programme que l'on va déclarer ensuite. Ainsi, c'est le terme FUNCTION, PROCEDURE ou PACKAGE qui jouera le rôle du END GENERIC et mettra un terme aux déclarations génériques. Le type T_Entier doit donc être vu comme un paramètre de l'unité de programme générique qui suivra.

Créer une procédure générique

Comme nous l'avions dit précédemment, la déclaration du type T_Entier doit être immédiatement suivie de la spécification de la procédure générique, ce qui nous donnera le code suivant :

1
2
3
generic
   type T_Entier is range <> ; 
procedure Generic_Swap(a,b : in out T_Entier) ;

Comme vous le constater, notre procédure utilise désormais le type T_Entier déclaré précédemment (d'où son nouveau nom Generic_Swap). Cette procédure prend en paramètres deux variables a et b indiquées entre parenthèses mais aussi un type T_Entier indiqué dans sa partie GENERIC.

Ne reste plus désormais qu'à rédiger le corps de notre procédure un peu plus loin dans notre programme :

1
2
3
4
5
6
7
procedure Generic_Swap(a,b : in out T_Entier) is
   c : T_Entier ; 
begin
   c:=a ; 
   a:=b ; 
   b:=c ; 
end Generic_Swap ;

Utiliser une méthode générique

Instanciation

Mais, j'essaye le code suivant depuis tout à l'heure, et il ne marche pas !

1
2
3
4
5
6
...
   n : Integer := 3 ; 
   m : Integer := 4 ; 
BEGIN
   Generic_Swap(n,m) ;
...

C'est normal, votre procédure Generic_Swap() est faite pour un type générique seulement. Pas pour les Integer spécifiquement. Le travail que nous avons fait consistait simplement à rédiger une sorte de «plan de montage», mais nous n'avons pas encore réellement «monté notre meuble». En programmation, cette étape s'appelle l'instanciation : nous allons devoir créer une instance de Generic_Swap, c'est-à-dire une nouvelle procédure appelée Swap_Integer. Et cela se fait grâce à l'instruction NEW :

1
procedure swap_integer is new Generic_Swap(Integer) ;

Le schéma d'instanciation est relativement simple et sera systématiquement le-même pour toutes les unités de programmes :

procedure

Nom_Unite_Specifique

is new

Nom_Unite_Generique

(Types spécifiques désirés)

function

Nom_Unite_Specifique

is new

Nom_Unite_Generique

(Types spécifiques désirés)

package

Nom_Unite_Specifique

is new

Nom_Unite_Generique

(Types spécifiques désirés)

Surcharge

Ainsi, le compilateur se chargera de générer lui-même le code de notre procédure swap_integer, vous laissant d'avantage de temps pour vous concentrer sur votre programme. Et puis, si vous aviez besoin de disposer de procédures swap_long_long_integer ou swap_positive_integer, il vous suffirait simplement d'écrire :

1
2
3
procedure swap_integer is new Generic_Swap(Integer) ;
procedure swap_long_long_integer is new Generic_Swap(long_long_integer) ;
procedure swap_positive is new Generic_Swap(Positive) ;

Hop ! Trois procédures générées en seulement trois lignes ! Pas mal non ? Et nous pourrions faire encore mieux, en générant trois instances de Generic_Swap portant toutes trois le même nom : swap ! Ainsi, en surchargeant la procédure, nous économisons de nombreux caractères à taper ainsi que la nécessité de systématiquement réfléchir aux types employés :

1
2
3
procedure swap is new Generic_Swap(Integer) ;
procedure swap is new Generic_Swap(long_long_integer) ;
procedure swap is new Generic_Swap(Positive) ;

Votre procédure générique ne doit pas porter le même nom que ses instances, car le compilateur ne saurait pas faire la distinction (d'où l'ajout du préfixe "Generic_").

Et désormais, nous pourrons effectuer des inversions à notre guise :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
...
   generic
      type T_Entier is range <> ; 
   procedure Generic_Swap(a,b : in out T_Entier) ;

   procedure Generic_Swap(a,b : in out T_Entier) is
      c : T_Entier ; 
   begin
      c:=a ; 
      a:=b ; 
      b:=c ; 
   end swap ;

   procedure swap is new Generic_Swap(Integer) ;

   n : integer := 3 ; 
   m : integer := 4 ; 
BEGIN
   swap(n,m) ; 
...

Paramètres génériques de types simples et privés

Types génériques simples

Bien, vous connaissez désormais les rudiments de la généricité en Ada. Toutefois, notre déclaration du type T_Entier implique que vous ne pourrez pas utiliser cette procédure pour des character !

Et si je veux qu'elle fonctionne pour les character aussi, comment je fais ?

Eh bien il va falloir modifier notre classe de type générique :

  • Pour couvrir tous les types discrets (entiers mais aussi character, boolean ou énumérés), on utilisera le diamant seul :

type T_Discret is (<>) ; - Pour couvrir les types réels à virgule flottante (comme Float, Long_Float, Long_Long_Float ou des types flottants personnalisés), on utilisera la combinaison de DIGITS et du diamant :

type T_Flottant is digits <> ; - Pour couvrir les types réels à virgule fixe (comme Duration, Time ou les types réels à virgule fixe personnalisés), on utilisera la combinaison de DELTA et du diamant, voire de DELTA, DIGITS et du diamant (revoir le chapitre Variables III : Gestion des données si besoin) :

type T_Fixe is delta <> ; type T_Fixe is delta <> digits <> ; - Enfin, pour couvrir les types modulaires (revoir le chapitre Créer vos propres objets si besoin), on utilisera la combinaison de MOD et du diamant :

type T_Modulaire is mod <> ;

Types génériques privés

Mais, on ne peut pas faire de procédure encore plus générique ? Comme une procédure qui manipulerait des types discrets mais aussi flottants ?

Si, c'est possible. Pour que votre type générique soit «le plus générique possible», il vous reste deux possibilités :

  • Si vous souhaitez que votre type bénéficie au moins de l'affectation et de la comparaison, il suffira de le déclarer ainsi :

type T_Generique is private ; - Et si vous souhaitez un type le plus générique possible, écrivez :

type T_Super_Generique is limited private ;

Si votre type générique est déclaré comme LIMITED PRIVATE, votre procédure générique ne devra employer ni la comparaison ni l'affectation, ce qui est gênant dans le cas d'un échange de variables.

Paramètres génériques de types composites et programmes

Tableaux génériques

Un premier cas

Pour déclarer un type tableau, nous avons toujours besoin de spécifier les indices : le tableau est-il indexé de 0 à 10, de 3 à 456, de 'a' à 'z', de JANVIER à FÉVRIER… ? Par conséquent, pour déclarer un tableau générique, il faut au minimum deux types génériques : le type (nécessairement discret) des indices et le type du tableau.

Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
generic
   type T_Indice is (<>) ; 
   type T_Tableau is array(T_Indice) of Float ; 
procedure Generic_Swap(T : in out T_Tableau ; i,j : T_Indice) ;

procedure Generic_Swap(T : in out T_Tableau ; i,j : T_Indice) is
   tmp : Float ; 
begin
   tmp := T(i) ; 
   T(i) := T(j) ; 
   T(j) := tmp ; 
end Generic_Swap ;

Notre procédure Generic_Swap dispose alors de deux paramètres formels (T_Indice et T_Tableau) qu'il faudra spécifier à l'instanciation dans l'ordre de leur déclaration :

1
2
3
4
5
6
7
8
...
   subtype MesIndices is integer range 1..10 ; 
   type MesTableaux is array(MesIndices) of Float ; 
   procedure swap is new Generic_Swap(MesIndices,MesTableaux) ; 
   T : MesTableaux := (9.0, 8.1, 7.2, 6.3, 5.4, 4.5, 3.6, 2.7, 1.8, 0.9) ; 
begin
   swap(T,3,5) ; 
...

Avec un tableau non contraint

Une première amélioration peut être apportée à ce code. Déclaré ainsi, notre type de tableaux est nécessairement contraint : tous les tableaux de type MesTableaux sont obligatoirement indexés de 1 à 10. Pour lever cette restriction, il faut tout d'abord modifier notre type formel :

1
2
3
4
5
generic
   type T_Indice is (<>) ; 
   type T_Tableau is array(T_Indice range <>) of Float ;
      --l'ajout de "range <>" nous laisse la liberté de contraindre nos tableaux plus tard
procedure Generic_Swap(T : in out T_Tableau ; i,j : T_Indice) ;

Notez bien l'ajout de « RANGE <> » après T_Indice à la ligne 3 ! On laisse une liberté sur l'intervalle d'indexage des tableaux. Puis, nous pourrons modifier les types effectifs (ceux réellement utilisés par votre programme) :

1
2
3
4
5
6
7
8
9
...
   type MesTableaux is array(Integer range <>) of Float ; 
      --on réécrit "range <>" pour bénéficier d'un type non contraint !
   procedure swap is new Generic_Swap(Integer,MesTableaux) ; 
   T : MesTableaux(1..6) := (7.2, 6.3, 5.4, 4.5, 3.6, 2.7) ; 
      --Cette fois, on contraint notre tableau en l'indexant de 1 à 6 ! Étape obligatoire ! 
begin
   swap(T,3,5) ; 
...

Un tableau entièrement générique

Seconde amélioration : au lieu de disposer d'un type T_Tableau contenant des Float, nous pourrions créer un type contenant toute sorte d'élément. Cela impliquera d'avoir un troisième paramètre formel : un type T_Element :

1
2
3
4
5
6
generic
   type T_Indice is (<>) ; 
   type T_Element is private ;
      --Nous aurons besoin de l'affectation pour la procédure Generic_Swap donc T_Element ne peut être limited
   type T_Tableau is array(T_Indice range <>) of T_Element;
procedure Generic_Swap(T : in out T_Tableau ; i,j : T_Indice) ;

Je ne vous propose pas le code du corps de Generic_Swap, j'espère que vous serez capable de le modifier par vous-même (ce n'est pas bien compliqué). En revanche, l'instanciation devra à son tour être modifiée :

1
2
3
4
type MesTableaux is array(Integer range <>) of Float ; 
   --Jusque là pas de grosse différence
procedure swap is new Generic_Swap(Integer,Float,MesTableaux) ;
   --Il faut indiquer le type des indices + le type des éléments + le type du tableau

L'inconvénient de cette écriture c'est qu'elle n'est pas claire : le type T_Tableau doit-il être écrit en premier, en deuxième ou en troisième ? Les éléments du tableau sont des Integer ou des Float ? Bref, on s'emmêle les pinceaux et il faut régulièrement regarder les spécifications de notre procédure générique pour s'y retrouver. On préfèrera donc l'écriture suivante.

1
2
3
4
5
procedure swap is new Generic_Swap(T_Indice  =>  Integer,
                                   T_Element => Float,
                                   T_Tableau => MesTableaux) ;
      --RAPPEL : L'ordre n'a alors plus d'importance ! ! ! 
      --CONSEIL : Indentez correctement votre code pour plus de lisibilité

Pointeurs génériques

De la même manière, il est possible de créer des types pointeurs génériques. Et comme pour déclarer un type pointeur, il faut absolument connaître le type pointé, cela impliquera d'avoir deux paramètres formels.

Exemple :

1
2
3
4
generic
   type T_Element is private ; 
   type T_Pointeur is access T_Element ; 
procedure Access_Swap(P,Q : in out T_Pointeur) ;

D'où l'instanciation suivante :

1
2
3
4
5
type MesPointeursPersos is access character ; 
procedure swap is new Access_Swap(character,MesPointeursPersos) ; 
         --   OU MIEUX :
procedure swap is new Access_Swap(T_Pointeur => MesPointeursPersos, 
                                  T_Element  => Character) ;

Paramètre de type programme : le cas d'un paramètre LIMITED PRIVATE

Bon, j'ai compris pour les différents types tableaux, pointeurs, etc. Mais quel intérêt de créer un type LIMITED PRIVATE ET GENERIC ? On ne pourra rien faire ! Ça ne sert à rien pour notre procédure d'échange ! o_O

En effet, nous sommes face à un problème : les types LIMITED PRIVATE ne nous autorisent pas les affectations, or nous aurions bien besoin d'un sous-programme pour effectuer cette affectation. Prenons un exemple : nous allons réaliser un package appelé P_Point qui saisit, lit, affiche… des points et leurs coordonnées. Voici quelle pourrait être sa spécification :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package P_Point is 

   type T_Point is limited private ; 

   procedure set_x(P : out T_Point ; x : in float) ; 
   procedure set_y(P : out T_Point ; y : in float) ; 
   function get_x(P : in T_Point) return float ; 
   function get_y(P : in T_Point) return float ; 
   procedure put(P : in T_Point) ; 
   procedure copy(From : in T_Point ; To : out T_Point) ; 

private
   type T_Point is record
      x,y : Float ; 
   end record ; 
end P_Point ;

Ce type T_Point est bien LIMITED PRIVATE mais son package nous fournit une procédure pour réaliser une copie d'un point dans un autre. Reprenons désormais notre procédure générique :

1
2
3
4
generic
   type T_Element is limited private ; 
   with procedure copier_A_vers_B(a : in T_Element ; b : out T_Element) ; 
procedure Generic_Swap(a,b : in out T_Element) ;

Nous ajoutons un nouveau paramètre formel : une procédure appelée copier_A_vers_B et qu'il faudra préciser à l'instanciation. Mais revenons avant cela au code source de la procédure Generic_Swap. Si elle ne peut pas utiliser le symbole d'affectation :=, elle pourra toutefois utiliser cette nouvelle procédure générique copier_A_vers_B :

1
2
3
4
5
6
7
procedure Generic_Swap(a,b : in out T_Element) is
   c : T_Element ; 
begin
   copier_A_vers_B(b,c) ; 
   copier_A_vers_B(a,b) ; 
   copier_A_vers_B(c,a) ; 
end generic_swap ;

Désormais, si vous désirez une procédure pour échanger deux points, vous devrez préciser, à l'instanciation, le nom de la procédure qui effectuera cette copie de A vers B :

1
2
3
4
procedure swap is new generic_swap(T_Point,copy) ; 
      --   OU BIEN
procedure swap is new generic_swap(T_Element       => T_Point,
                                   Copier_A_vers_B => copy) ;

Si à l'instanciation vous ne fournissez pas une procédure effectuant une copie exacte, vous vous exposez à un comportement erratique de votre programme.

Il est possible d'avoir plusieurs procédures ou fonctions comme paramètres formels de généricité. Il est même possible de transmettre des opérateurs comme "+", "*", "<"

Une dernière remarque : si vous ne souhaitez pas être obligé de spécifier le nom de la procédure de copie à l'instanciation, il suffit de modifier la ligne :

1
with procedure copier_A_vers_B(a : in T_Element ; b : out T_Element) ;

En y ajoutant un diamant (mais n'oubliez pas de modifier le code source de Generic_Swap en conséquence). L'opération d'affectation standard sera alors utilisée.

1
with procedure copy(a : in T_Element ; b : out T_Element) is <> ;

Packages génériques

Exercice

Vous avez dors et déjà appris l'essentiel de ce qu'il y a à savoir sur la généricité en Ada. Mais pour plus de clarté, nous n'avons utilisé qu'une procédure générique. Or, la plupart du temps, vous ne créerez pas un seul programme générique, mais bien plusieurs, de manière à offrir toute une palette d'outil allant de paire avec l'objet générique que vous proposerez. Prenez l'exemple de nos packages P_Pile et P_File (encore et toujours eux) : quel intérêt y a-t-il à ne proposer qu'une seule procédure générique alors que vous disposez de plusieurs primitives ? Dans 95% des cas, vous devrez donc créer un package générique.

Pour illustrer cette dernière (et courte) partie, et en guise d'exercice final, vous allez donc modifier le package P_Pile pour qu'il soit non seulement limité privé, mais également générique ! Ce n'est pas bien compliqué : retenez le schéma que je vous avais donné en début de chapitre.

1
2
3
4
DÉCLARATION DU (OU DES) TYPE(S) GÉNÉRIQUE(S)
SPÉCIFICATION DE L’UNITÉ DE PROGRAMME GÉNÉRIQUE
CORPS DE L’UNITÉ DE PROGRAMME GÉNÉRIQUE
INSTANCIATION D'UNE UNITÉ DE PROGRAMME SPÉCIFIQUE

La différence, c'est que le tout se fait dans des fichiers séparés :

1
2
DÉCLARATION DU (OU DES) TYPE(S) GÉNÉRIQUE(S)
SPÉCIFICATION DU PACKAGE GÉNÉRIQUE

P_Pile.ads

1
CORPS DU PACKAGE GÉNÉRIQUE

P_Pile.adb

1
INSTANCIATION D'UN PACKAGE SPÉCIFIQUE

VotreProgramme.adb

La structure de votre fichier ads va donc devoir encore évoluer :

Nouvelle organisation des packages

Vous êtes prêts pour le grand plongeon ? Alors allez-y ! ^^

Solution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
GENERIC
   TYPE T_Element IS PRIVATE ; 
PACKAGE P_Pile IS
   TYPE T_Pile IS LIMITED PRIVATE ; 
   PROCEDURE Push (P : IN OUT T_Pile; N : IN T_Element ) ;
   PROCEDURE Pop (P : IN OUT T_Pile ; N : OUT T_Element ) ;
   FUNCTION Empty (P : IN T_Pile) RETURN Boolean ;
   FUNCTION Length(P : T_Pile) RETURN Integer ;
   FUNCTION First(P : T_Pile) RETURN T_Element ;
PRIVATE
   TYPE T_Cellule;
   TYPE T_Pile IS ACCESS ALL T_Cellule;
   TYPE T_Cellule IS
      RECORD
         Valeur  : T_Element ; --On crée une pile générique
         Index   : Integer;
         Suivant : T_Pile; 
      END RECORD;
END P_Pile;

Fichier P_Pile.ads

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
PACKAGE BODY P_Pile IS

   PROCEDURE Push (
         P : IN OUT T_Pile;
         N : IN     T_Element ) IS
      Cell : T_Cellule;
   BEGIN
      Cell.Valeur := N ;
      IF P /= NULL
            THEN
         Cell.Index := P.All.Index + 1 ;
         Cell.Suivant := P.All'ACCESS ;
      ELSE
         Cell.Index := 1 ;
      END IF ;
      P := NEW T_Cellule'(Cell) ;
   END Push ;


   PROCEDURE Pop (
         P : IN OUT T_Pile;
         N :    OUT T_Element ) IS
   BEGIN
      N := P.All.Valeur ;     --ou P.valeur
      --P.all est censé exister, ce sera au programmeur final de le vérifier
      IF P.All.Suivant /= NULL
            THEN
         P := P.Suivant ;
      ELSE
         P := NULL ;
      END IF ;
   END Pop ;

   FUNCTION Empty (
         P : IN     T_Pile)
     RETURN Boolean IS
   BEGIN
      IF P=NULL
            THEN
         RETURN True ;
      ELSE
         RETURN False ;
      END IF ;
   END Empty ;

   FUNCTION Length(P : T_Pile) RETURN Integer IS
   BEGIN
      IF P = NULL
         THEN RETURN 0 ;
      ELSE RETURN P.Index ;
      END IF ;
   END Length ;

   FUNCTION First(P : T_Pile) RETURN T_Element IS
   BEGIN
      RETURN P.Valeur ;
   END First ;

END P_Pile;

Fichier P_Pile.adb

Application

Vous pouvez désormais créer des piles de Float, de tableaux, de boolean… Il vous suffira juste d'instancier votre package :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
With P_Pile ;      --Pas de clause Use, cela n'aurait aucun sens car P_Pile est générique

procedure MonProgramme is

   package P_Pile_Character is new P_Pile(Character) ;
   use P_Pile_Character ; 

   P : T_Pile ; 

begin
   push(P,'Z') ; 
   push(P,'#') ; 
   ...
end MonProgramme ;

Si vous réaliser deux instanciations du même package P_Pile, des confusions sont possibles que le compilateur n'hésitera pas à vous faire remarquer. Regardez l'exemple suivant.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
With P_Pile ; 

procedure MonProgramme is

   package P_Pile_Character is new P_Pile(Character) ;
   use P_Pile_Character ; 
   package P_Pile_Float is new P_Pile(Float) ;
   use P_Pile_Float ; 

   P : T_Pile ;    --Est-ce une pile de Float ou une pile de Character ? 

begin
   ...
end MonProgramme ;

Privilégiez alors l'écriture pointée afin de préciser l'origine de vos objets et méthodes :

1
P : P_Pile_Float.T_Pile ;    --Plus de confusion possible

En résumé :

  • La généricité permet d'élaborer des outils complexes (comme les piles ou les files) disponibles pour toute une panoplie de type.
  • Une unité de programme générique peut être : une fonction, une procédure ou un package.
  • Pour créer une unité de programme générique, il suffit d'écrire le mot-clé GENERIC juste avant FUNCTION, PROCEDURE ou PACKAGE.
  • Tout type générique doit être déclaré après le mot GENERIC et juste avant FUNCTION, PROCEDURE ou PACKAGE.
  • Une unité de programme générique est indiquée avec les termes « WITH FUNCTION… », « WITH PROCEDURE… » ou « WITH PACKAGE… ».
  • Il est important de bien classer votre type : ajouter 1 n'est possible qu'avec un type entier, utiliser les attributs 'first, 'last, 'succ ou 'pred n'est possible qu'avec un type discret…
  • Pour utiliser une unité de programme générique vous devez créer une instance avec NEW et fournir les types que vous souhaitez réellement utiliser.