Ada pêche aux moules

Introduction

Le langage Ada offre, aux travers de la généricité, un mécanisme puissant permettant de créer des unités (sous-programmes ou paquetages) pouvant être utilisées avec différents types de données. Ce mécanisme permet d’avoir un code réutilisable, flexible et maintenable.

Cet article a pour objet de montrer, dans les grandes lignes, ce qu’est la « généricité en Ada », et comment elle doit être utilisée.

Justification

Considérons le code suivant permettant de trier un vecteur de vitesses :

with Ada.Text_IO;
procedure Test_Generic is

  type Vitesse_T is range 0 .. 130; -- km/h
  type Vecteur_Vitesse_T is array (Positive range <>) of Vitesse_T;

  procedure Trier (Vecteur : in out Vecteur_Vitesse_T) is
  begin
     for I in Vecteur'First .. Positive'Pred (Vecteur'Last) loop
        for J in Positive'Succ (I) .. Vecteur'Last loop
           if Vecteur (J) < Vecteur (I) then
              Vecteur := [Vecteur with delta I => Vecteur (J),
                                             J => Vecteur (I)];
           end if;
        end loop;
     end loop;
  end Trier;

  Vecteur_Vitesse : Vecteur_Vitesse_T := [20, 0, 130, 50, 10, 90, 30, 80, 110, 120];
begin
  Trier (Vecteur_Vitesse);
  Ada.Text_IO.Put_Line (Vecteur_Vitesse'Image);
end Test_Generic;

Ce code affichera :

[0, 10, 20, 30, 50, 80, 90, 110, 120, 130]

Mais que ferions-nous si nous voulions aussi trier un vecteur de couleurs ?

Sans généricité, nous devrions dupliquer le code de la procédure Trier :

with Ada.Text_IO;
procedure Test_Generic is

  type Vitesse_T is range 0 .. 130; -- km/h
  type Vecteur_Vitesse_T is array (Positive range <>) of Vitesse_T;

  procedure Trier (Vecteur : in out Vecteur_Vitesse_T) is
  begin
     for I in Vecteur'First .. Positive'Pred (Vecteur'Last) loop
        for J in Positive'Succ (I) .. Vecteur'Last loop
           if Vecteur (J) < Vecteur (I) then
              Vecteur := [Vecteur with delta I => Vecteur (J),
                                             J => Vecteur (I)];
           end if;
        end loop;
     end loop;
  end Trier;

  type Couleur_T is (Rouge, Orange, Jaune, Vert, Bleu, Violet);
  type Vecteur_Couleur_T is array (Positive range <>) of Couleur_T;

  procedure Trier (Vecteur : in out Vecteur_Couleur_T) is
  begin
     for I in Vecteur'First .. Positive'Pred (Vecteur'Last) loop
        for J in Positive'Succ (I) .. Vecteur'Last loop
           if Vecteur (J) < Vecteur (I) then
              Vecteur := [Vecteur with delta I => Vecteur (J),
                                             J => Vecteur (I)];
           end if;
        end loop;
     end loop;
  end Trier;

  Vecteur_Vitesse : Vecteur_Vitesse_T := [20, 0, 130, 50, 10, 90, 30, 80, 110, 120];
  Vecteur_Couleur : Vecteur_Couleur_T := [Bleu, Jaune, Orange, Jaune, Violet, Rouge, Vert];
begin
  Trier (Vecteur_Vitesse);
  Trier (Vecteur_Couleur);
  Ada.Text_IO.Put_Line (Vecteur_Vitesse'Image);
  Ada.Text_IO.Put_Line (Vecteur_Couleur'Image);
end Test_Generic;

Ce code affichera :

[0, 10, 20, 30, 50, 80, 90, 110, 120, 130]
[ROUGE, ORANGE, JAUNE, JAUNE, VERT, BLEU, VIOLET]

Manifestement ce n’est pas la bonne solution, car, qui dit duplication dit :

  • Risque d’erreurs.
  • Non-factorisation du code.
  • Multiplication des tests unitaires.
  • Mauvaise maintenabilité.
  • etc.

C’est dans ce cadre que la généricité prend tout son sens !

Filons la métaphore

Une façon de voir un générique est de considérer celui-ci comme un moule. En fonction de ce que l’on versera dans ce dernier, on obtiendra, et à titre d’exemple, soit une pièce en or soit une pièce en chocolat !

En Ada c’est la même chose !

Le moule sera l’implémentation du générique, ce que l’on verse dans le moule sera décrit au travers des paramètres formels d’instanciation et la pièce sera obtenue lors de l’instanciation du générique.

Concernant les paramètres formels d’instanciation, il y en a de quatre sortes :

  1. Les types
  2. Les objets
  3. Les sous-programmes
  4. Les paquetages

Dans le cadre de cet article, nous ne présenterons que les « types » et les « sous-programmes ».

Exemple

Nous allons maintenant écrire notre procédure Trier en tant que procédure générique afin de pouvoir aussi bien trier des vecteurs de vitesses que des vecteurs de couleurs.

Nous allons déjà décrire ce que l’on veut pouvoir verser dans notre moule au travers de deux paramètres formels d’instanciation. Nous changerons aussi l’identificateur de notre procédure en G_Trier.

generic
   type Element_T is (<>); -- type discret
   type Vecteur_T is array (Positive range <>) of Element_T;
procedure G_Trier (Vecteur : in out Vecteur_T);

On exprime ainsi que l’on veut pouvoir verser dans notre moule des vecteurs dont les bornes de type Positive ne sont pas connues. Ces vecteurs contiendront uniquement des types discrets que sont par exemple les types Vitesse_T et Couleur_T.

Afin de ne pas être dépendants du type de l’index, on peut rajouter ce dernier en tant que paramètre formel d’instanciation :

generic
   type Element_T is (<>); -- type discret
   type Index_T is (<>);   -- type discret
   type Vecteur_T is array (Index_T range <>) of Element_T;
procedure G_Trier (Vecteur : in out Vecteur_T);

Nous avons donc défini ce que l’on peut mettre dans notre moule. Attaquons-nous maintenant au moule proprement dit, c’est-à-dire à l’implémentation de notre procédure G_Trier. Aussi surprenant que cela puisse paraitre, il n’y a presque rien à faire ! Seul un ajustement concernant le type de l’index de notre vecteur est à faire :

procedure G_Trier (Vecteur : in out Vecteur_T) is
begin
   for I in Vecteur'First .. Index_T'Pred (Vecteur'Last) loop
      for J in Index_T'Succ (I) .. Vecteur'Last loop
         if Vecteur (J) < Vecteur (I) then
            Vecteur := [Vecteur with delta I => Vecteur (J),
                                           J => Vecteur (I)];
         end if;
      end loop;
   end loop;
end G_Trier;

Reste maintenant à créer nos procédures Trier en « versant » ce qu’il faut dans notre moule G_Trier. Pour ce faire, nous instancions notre procédure générique G_Trier en lui fournissant des paramètres effectifs d’instanciation.

procedure Trier is new G_Trier (Element_T => Vitesse_T,
                                Index_T   => Positive,
                                Vecteur_T => Vecteur_Vitesse_T);

procedure Trier is new G_Trier (Element_T => Couleur_T,
                                Index_T   => Positive,
                                Vecteur_T => Vecteur_Couleur_T);

À partir de ce point, nous avons deux procédures Trier que l’on peut utiliser et qui donneront les mêmes sorties que précédemment.

Au final notre code sera :

with Ada.Text_IO;
procedure Test_Generic is

  type Vitesse_T is range 0 .. 130; -- km/h
  type Vecteur_Vitesse_T is array (Positive range <>) of Vitesse_T;

  type Couleur_T is (Rouge, Orange, Jaune, Vert, Bleu, Violet);
  type Vecteur_Couleur_T is array (Positive range <>) of Couleur_T;

  generic
     type Element_T is (<>); -- type discret
     type Index_T is (<>);   -- type discret
     type Vecteur_T is array (Index_T range <>) of Element_T;
  procedure G_Trier (Vecteur : in out Vecteur_T);

  procedure G_Trier (Vecteur : in out Vecteur_T) is
  begin
     for I in Vecteur'First .. Index_T'Pred (Vecteur'Last) loop
        for J in Index_T'Succ (I) .. Vecteur'Last loop
           if Vecteur (J) < Vecteur (I) then
              Vecteur := [Vecteur with delta I => Vecteur (J),
                                             J => Vecteur (I)];
           end if;
        end loop;
     end loop;
  end G_Trier;

  procedure Trier is new G_Trier (Element_T => Vitesse_T,
                                  Index_T   => Positive,
                                  Vecteur_T => Vecteur_Vitesse_T);

  procedure Trier is new G_Trier (Element_T => Couleur_T,
                                  Index_T   => Positive,
                                  Vecteur_T => Vecteur_Couleur_T);

  Vecteur_Vitesse : Vecteur_Vitesse_T := [20, 0, 130, 50, 10, 90, 30, 80, 110, 120];
  Vecteur_Couleur : Vecteur_Couleur_T := [Bleu, Jaune, Orange, Jaune, Violet, Rouge, Vert];
begin
  Trier (Vecteur_Vitesse);
  Trier (Vecteur_Couleur);
  Ada.Text_IO.Put_Line (Vecteur_Vitesse'Image);
  Ada.Text_IO.Put_Line (Vecteur_Couleur'Image);
end Test_Generic;

Et il affichera bien :

[0, 10, 20, 30, 50, 80, 90, 110, 120, 130]
[ROUGE, ORANGE, JAUNE, JAUNE, VERT, BLEU, VIOLET]

Il est important de noter que le compilateur va vérifier que :

  1. Lors de l’instanciation, les paramètres effectifs sont conformes aux paramètres formels (exemple : le type Couleur_T est bien un type discret).
  2. Les paramètres formels disposent au minimum des opérations requises (exemple : l’opérateur « < » existe bien pour le type Element_T).
  3. Les liens existant entre les paramètres formels sont reconduits sur les paramètres effectifs (exemple : Vector_T est bien un tableau non contraint indexé sur des Index_T et contenant des Element_T).

Vouloir le beurre et l’argent du beurre

Maintenant, nous souhaiterions avoir un moule (que nous nommerons maintenant générique) permettant de créer des procédures capables de trier des vecteurs de « presque » n’importe quoi et non pas des vecteurs d’éléments discrets.

Dans les paramètres formels d’instanciations, le fait d’indiquer que le type Element_T est un type discret renseigne le compilateur sur les opérations disponibles pour ce type. Pour les types discrets, l’opération « < », nécessaire à l’implémentation de tri est disponible. Il est à noter que notre implémentation ne permet de trier que de façon croissante.

Si on souhaite avoir des vecteurs de « presque » n’importe quoi, il faut définir le type Element_T comme étant privé.

Ce qui revient à écrire :

   generic
      type Element_T is private; -- "presque" tous les types
      type Index_T is (<>);      -- type discret
      type Vecteur_T is array (Index_T range <>) of Element_T;
   procedure G_Trier (Vecteur : in out Vecteur_T);

Le problème est que le compilateur ne connait maintenant plus la nature du type Element_T et donc il ne peut plus préjuger de l’existence de l’opérateur « < » pour ce type. Ce code ne compilera donc pas !

Nous nous retrouvons donc devant un dilemme.

  • Soit on est « très » générique et on dispose de peu, voire d’aucune opération sur le type.
  • Soit on est peu générique et on dispose d’un certain nombre d’opérations sur le type.

C’est donc la locution « vouloir le beurre et l’argent du beurre » qui s’applique.

Heureusement en Ada, il est possible d’avoir le beurre, l’argent du beurre et le sourire du crémier ou de la crémière !

Pour cela, il faut indiquer au compilateur, par le biais des « paramètres formels génériques de type sous-programme », que l’on fournira comment faire cette opération de comparaison entre deux Element_T.

Cela revient à écrire :

   generic
      type Element_T is private; -- "presque" tous les types
      type Index_T is (<>);      -- type discret
      type Vecteur_T is array (Index_T range <>) of Element_T;
      with function Cmp (G, D : in Element_T) return Boolean;
   procedure G_Trier (Vecteur : in out Vecteur_T);

Dans l’implémentation du générique, on ne fera plus appel à « > » mais à Cmp :

procedure G_Trier (Vecteur : in out Vecteur_T) is
begin
   for I in Vecteur'First .. Index_T'Pred (Vecteur'Last) loop
      for J in Index_T'Succ (I) .. Vecteur'Last loop
         if Cmp (Vecteur (J), Vecteur (I)) then
            Vecteur := [Vecteur with delta I => Vecteur (J),
                                           J => Vecteur (I)];
         end if;
      end loop;
   end loop;
end G_Trier;

Attention

Maintenant, l’implémentation laisse libre cours sur la façon de trier notre vecteur.

Nous allons donc pouvoir trier un vecteur de presque n’importe quoi. Pour notre exemple, nous allons créer un vecteur d’Unbounded_String :

with Ada.Text_IO,
     Ada.Strings.Unbounded;
use Ada.Strings.Unbounded;
procedure Test_Generic is

   type Vecteur_US_T is array (Positive range <>) of Unbounded_String;

   generic
      type Element_T is private; -- "presque" tous les types
      type Index_T is (<>);      -- type discret
      type Vecteur_T is array (Index_T range <>) of Element_T;
      with function Cmp (G, D : in Element_T) return Boolean;
   procedure G_Trier (Vecteur : in out Vecteur_T);

   procedure G_Trier (Vecteur : in out Vecteur_T) is
   begin
      for I in Vecteur'First .. Index_T'Pred (Vecteur'Last) loop
         for J in Index_T'Succ (I) .. Vecteur'Last loop
            if Cmp (Vecteur (J), Vecteur (I)) then
               Vecteur := [Vecteur with delta I => Vecteur (J),
                                              J => Vecteur (I)];
            end if;
         end loop;
      end loop;
   end G_Trier;

   procedure Trier is new G_Trier (Element_T => Unbounded_String,
                                   Index_T   => Positive,
                                   Vecteur_T => Vecteur_US_T,
                                   Cmp       => "<"); -- disponible dans Ada.Strings.Unbounded

   Vecteur_US : Vecteur_US_T := [1 => To_Unbounded_String ("pêche aux moules"),
                                 2 => To_Unbounded_String ("Ada")];
begin
   Ada.Text_IO.Put_Line (Vecteur_US'Image);
   Trier (Vecteur_US);
   Ada.Text_IO.Put_Line (Vecteur_US'Image);
end Test_Generic;

Et il affichera :

["pêche aux moules", "Ada"]
["Ada", "pêche aux moules"]

Conclusion

Le modèle des génériques en Ada est réputé pour sa robustesse. Celle-ci s’appuie sur :

  • Un typage fort et statique : le compilateur vérifie que les types utilisés lors de l’instanciation sont compatibles avec les contraintes spécifiées.

  • Des contraintes explicites : des contraintes sur les types peuvent être définies. Ces contraintes peuvent inclure la présence de certaines opérations (dans notre exemple : un opérateur de comparaison) ou des propriétés spécifiques aux types. Cela garantit que les composants génériques sont utilisés de manière cohérente et que les erreurs potentielles sont détectées lors de la compilation.

  • Une instanciation explicite : l’instanciation d’un générique est explicite évitant ainsi toutes ambiguïtés et garantissant que les génériques sont utilisés de façon appropriée.

Ces caractéristiques contribuent donc à la fiabilité et à la sécurité des logiciels développés en Ada.

Référence

[1] : https://learn.adacore.com/courses/intro-to-ada/chapters/generics.html