Licence CC BY-NC-SA

Procédures et fonctions I

Nous avons vu les variables et leurs différents types et opérations, les conditions, les boucles… autant d'éléments essentiels à l'Algorithmique et à tout langage, que ce soit l'Ada, le C, le C++, le Python… Nous allons maintenant voir comment organiser et hiérarchiser notre code à l'aide des fonctions et des procédures (présentes dans tous les langages elles aussi). Ces éléments nous seront essentiels pour la suite du cours. Mais, rassurez-vous, ce n'est pas bien compliqué (au début en tous cas ^^ ).

Les procédures

Procédure sans paramètre

Vous avez déjà vu maintes fois le terme procédure. Allons, souvenez-vous :

1
2
3
4
5
6
7
8
with Ada.Text_IO ; 
use Ada.Text_IO ;

procedure Main is
   ...
begin
   ...
end Main ;

Et oui ! Votre programme est constitué en fait d'une grande procédure (plus les packages en en-tête, c'est vrai :) ). Donc vous savez dors et déjà comment on la déclare :

1
2
3
4
5
Procedure Nom_De_Votre_Procedure is
   --Partie pour déclarer les variables
begin
   --Partie exécutée par le programme
end Nom_De_Votre_Procedure ;

Quel est donc l'intérêt d'en parler si on sait déjà ce que c'est ? o_O

Eh bien pour l'instant, notre programme ne comporte qu'une seule procédure qui, au fil des modifications, ne cessera de s'allonger. Et il est fort probable que nous allons être amenés à recopier plusieurs fois les mêmes bouts de code. C'est à ce moment qu'il devient intéressant de faire appel à de nouvelles procédures afin de clarifier et de structurer notre code. Pour mieux comprendre, nous allons reprendre l'exemple vu pour les boucles : dessiner un rectangle.

Voici le code de notre programme :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
with Ada.Text_IO, Ada.Integer_Text_IO ; 
use Ada.Text_IO, Ada.Integer_Text_IO ; 

procedure Figure is
   nb_lignes : natural ;                                 --nombre de lignes
begin
   Put("Combien de lignes voulez-vous dessiner ?") ;     --on demande le nombre de lignes
   get(nb_lignes) ; skip_line ; 
   for i in 1..nb_lignes loop                            --on exécute deux boucles pour afficher notre rectangle
      for j in 1..5 loop
         put('#') ; 
      end loop ; 
      new_line ;
   end loop ; 
end Figure ;

Comme vous pouvez le voir, ce programme Figure propose d'afficher un rectangle de dièses. Chaque rectangle comporte 5 colonnes et autant de lignes que l'utilisateur le souhaite.

1
2
3
4
5
6
7
8
9
Combien de lignes voulez-vous dessiner ? 7
#####
#####
#####
#####
#####
#####
#####
_

Nous allons remplacer la deuxième boucle (celle qui affiche les dièses) par une procédure que nous appellerons Affich_Ligne dont voici le code :

1
2
3
4
5
6
7
Procedure Affich_Ligne is
begin
   for j in 1..5 loop
      put('#') ; 
   end loop ; 
   new_line ;
end Affich_Ligne ;

Euh, c'est bien gentil tout ça, mais je l'écris où ?

C'est très simple. Le code ci-dessus permet de déclarer au compilateur la sous-procédure Affich_Ligne. Puisqu'il s'agit d'une déclaration, il faut écrire ce code dans la partie réservée aux déclarations de notre procédure principale (avec la déclaration de la variable nb_lignes). Voici donc le résultat :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
with Ada.Text_IO, Ada.Integer_Text_IO ; 
use Ada.Text_IO, Ada.Integer_Text_IO ; 

procedure Figure is
   
   Procedure Affich_Ligne is
   begin
      for j in 1..5 loop
         put('#') ; 
      end loop ; 
      new_line ;
   end Affich_Ligne ; 

   nb_lignes : natural ; 

begin
   Put("Combien de lignes voulez-vous dessiner ?") ; 
   get(nb_lignes ) ; skip_line ; 
   for i in 1..nb_lignes  loop
      Affich_Ligne;
   end loop ; 
end Figure ;

Vous remarquerez que dans la partie de la procédure principale contenant les instructions, la deuxième boucle, son contenu et le new_line ont disparu et ont laissé la place à un simple «Affich_Ligne ;». Cette partie devient ainsi plus lisible et plus compacte.

Mouais… Bah, écrire tout ça pour gagner 3 lignes dans la procédure principale, moi je dis que c'est pas folichon. :colere2:

N'en jetez plus ! >_< En effet, cela ne semble pas être une opération très rentable, mais je vous invite à continuer la lecture car vous allez vite comprendre l'intérêt des sous-procédures.

Procédure avec un paramètre (ou argument)

Notre programme est pour l'heure très limité : l'utilisateur ne peut pas choisir le nombre de colonnes ! Nous savons facilement (à l'aide de Put et Get) obtenir un nombre de colonnes mais comment faire pour que notre procédure Affich_Ligne affiche le nombre de dièses désiré ? Nous ne pouvons pas effectuer la saisie de la variable Nb_Colonnes dans la procédure Affich_Ligne, car la boucle de la procédure principale demanderait plusieurs fois le nombre de colonnes désiré.

Ah ! C'est malin ! Avec tes histoires, je me suis cassé la tête et en plus je ne peux plus amélioré mon programme ! :colere:

Attendez ! Il y a une solution ! Nous allons demander le nombre de colonnes en même temps que le nombre de lignes et nous allons modifier notre procédure Affich_Ligne pour qu'elle puisse écrire autant de dièses qu'on le souhaite (pas forcément 5). Voici le nouveau code de notre sous-procédure :

1
2
3
4
5
6
7
Procedure Affich_Ligne(nb natural) is
begin
   for j in 1..nb loop
      put('#') ; 
   end loop ; 
   new_line ;
end Affich_Ligne ;

Nous avons remplacé « j in 1..5 » par « j in 1..Nb ». La variable nb correspondra bien évidemment au nombre de dièses désirés. Mais surtout, le nom de notre procédure est maintenant suivi de parenthèses à l'intérieur desquelles on a déclaré la fameuse variable nb.

o_O J'ai deux questions : Pourquoi on la déclare pas dans la zone prévue à cet effet, entre IS et BEGIN ? Et est-ce normal que ta variable nb n'ait jamais été initialisée ?

Cette variable nb est ce que l'on appelle un paramètre. Elle n'est pas déclarée dans la «zone prévue à cet effet», ni initialisée, parce qu'elle vient «de l'extérieur». Petite explication : toute variable déclarée entre IS et BEGIN a une durée de vie qui est liée à la durée de vie de la procédure. Lorsque la procédure commence, la variable est créée ; lorsque la procédure se termine, la variable est détruite : plus moyen d'y accéder ! Cela va même plus loin ! Les variables déclarées entre IS et BEGIN sont inaccessibles en dehors de leur propre procédure ! Elles n'ont aucune existence possible en dehors. Le seul moyen de communication offert est à travers ce paramètre : il permet à la procédure principale de copier et d'envoyer le contenu d'une variable dans la sous-procédure.

Les variables ne sont utilisables que dans la procédure où elles sont déclarées

Comme l'indique le schéma ci-dessus, il existe deux variables n°1 : une pour la procédure principale et une pour la sous-procédure, mais elles n'ont rien à voir ! Chacune fait sa petite vie dans sa propre procédure. Il serait bien sûr impossible de déclarer dans la même procédure deux variables de même nom. Qui plus est, si une variable n°2 est envoyée comme paramètre dans une sous-procédure, alors elle formera une nouvelle variable, appelée paramètre, distincte de la variable n°2 initiale. La variable n°2 et le paramètre auront la même valeur mais constitueront deux variables distinctes, la seconde n'étant qu'une copie de la première.

Voilà pourquoi la variable nb est déclarée entre parenthèses : c'est un paramètre. Quant à sa valeur, elle sera déterminée dans la procédure principale avant d'être «envoyée» dans la procédure «Affich_Ligne()».

Mais revenons à notre procédure principale : Figure. Voilà ce que devient son code :

 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
with Ada.Text_IO, Ada.Integer_Text_IO ; 
use Ada.Text_IO, Ada.Integer_Text_IO ; 

procedure Figure is
   
   Procedure Affich_Ligne(nb natural) is
   begin
      for j in 1..nb loop
         put('#') ; 
      end loop ; 
      new_line ;
   end Affich_Ligne ; 

   nb_lignes, nb_colonnes : natural; 

begin

   Put("Combien de lignes voulez-vous dessiner ? ") ; 
   get(nb_lignes ) ; skip_line ; 
   Put("Combien de colonnes voulez-vous dessiner ? ") ; 
   get(nb_colonnes ) ; skip_line ; 

   for i in 1..nb_lignes  loop
      Affich_Ligne(nb_colonnes);
   end loop ; 

end Figure ;

Nous avons ajouté deux lignes pour saisir le nombre de colonnes voulues. L'instruction «Affich_ligne» est devenue «Affich_Ligne(nb_colonnes)». Et voilà ! Notre problème initial est résolu ! :p

Procédure avec plusieurs paramètres (ou arguments)

Et bah moi je continue à penser que c'était bien plus simple sans sous-procédure ! Quel intérêt de saucissonner notre code ? :colere2:

Toujours pas convaincu ? :o Et bien voilà qui devrait mettre fin à vos réticences. Nous allons perfectionner notre programme : il proposera d'afficher soit un rectangle soit un triangle rectangle isocèle (un demi-carré pour les mathophobes ;) ).

Il va donc falloir épurer notre code en créant deux procédures supplémentaires : une pour créer un rectangle (Affich_Rect) et une pour créer le triangle rectangle isocèle (Affich_Tri). Ainsi, notre procédure principale (Figure) se chargera de poser les questions à l'utilisateur, puis elle réorientera vers l'une ou l'autre de ces deux sous-procédures.

La procédure Affich_Rect prendra deux paramètres : le nombre de colonnes et le nombre de lignes.

1
2
3
4
5
6
Procedure Affich_Rect(nb_lignes natural ; nb_colonnes natural) is
begin
   for i in 1..nb_lignes  loop
      Affich_Ligne(nb_colonnes);
   end loop ; 
end Affich_Rect ;

Ici, les deux paramètres sont séparés par un « ; » ! Cela permet d'insérer un retour à la ligne au milieu des paramètres pour rendre plus lisible notre code.

Pour fonctionner, Affich_Rect a besoin de la procédure Affich_Ligne, il faut donc que Affich_Ligne soit déclarée avant Affich_Rect !

Et maintenant notre procédure Affich_Tri :

1
2
3
4
5
6
Procedure Affich_Tri(nb natural) is
begin
   for i in 1..nb  loop
      Affich_Ligne(i);
   end loop ; 
end Affich_Tri ;

L'utilité des procédures commence à apparaître clairement : il a suffit d'écrire «Affich_Ligne(i)» pour que le programme se charge d'afficher une ligne comportant le nombre de dièses voulus, sans que l'on ait à réécrire notre boucle et à se creuser la tête pour savoir comment la modifier pour qu'elle n'affiche pas toujours le même nombre de dièses. Alors, convaincu cette fois ? Je pense que oui. Pour les récalcitrants, je vous conseille d'ajouter des procédures Affich_Carre, Affich_Tri_Isocele, Affich_Carre_Vide… et quand vous en aurez assez de recopier la boucle d'affichage des dièses, alors vous adopterez les procédures. ;)

Bien ! Il nous reste encore à modifier notre procédure principale. En voici pour l'instant une partie :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
begin

   Put_line("Voulez-vous afficher : ") ;              --On propose les deux choix : rectangle ou triangle ? 
   Put_line("1-Un rectangle ?") ; 
   Put_line("2-Un triangle ?") ; 
   Get(choix) ; skip_line; 

   Put("Combien de lignes voulez-vous dessiner ? ") ; --on saisit tout de suite le nombre de lignes
   get(nb_lignes ) ; skip_line ; 

   if choix=1                                         --selon le choix on procède à l'affichage de la figure
      then Put("Combien de colonnes voulez-vous dessiner ? ") ; 
           get(nb_colonnes ) ; skip_line ; 
           Affich_Rect(nb_lignes, nb_colonnes) ; 
      else Affich_Tri(nb_lignes) ; 
   end if ; 

end Figure ;

Notre code est tout de même beaucoup plus lisible que s'il comportait de nombreuses boucles imbriquées !

Mais ? Il y a une erreur ! :waw: Pour Affich_Rect, les paramètres doivent être séparés par un point virgule et pas par une virgule ! La honte ! :diable:

Et bien non ! Voici une erreur que le compilateur ne vous pardonnerait pas. On utilise le point virgule «;» uniquement lors de la déclaration de la procédure. Lorsque l'on fait appel à la procédure «pour de vrai», on n'utilise que la virgule «,» !

Les fonctions

Qu'est-ce qu'une fonction ? C'est peu ou prou la même chose que les procédures. La différence, c'est que les fonctions renvoient un résultat.

Une bête fonction

Pour mieux comprendre, nous allons créer une fonction qui calculera l'aire de notre rectangle. On rappelle (au cas où certains auraient oublié leur cours de 6ème) que l'aire du rectangle se calcule en multipliant sa longueur par sa largeur !

Déclaration

Commençons par déclarer notre fonction (au même endroit que l'on déclare variables et procédures) :

1
2
3
4
5
6
function A_Rect(larg natural ; long natural) return natural is
   A : natural ; 
begin
   A:= larg * long ; 
   return A ; 
end A_Rect ;

Globalement, cela ressemble beaucoup à une procédure à part que l'on utilise le mot FUNCTION au lieu de PROCEDURE. Une nouveauté toutefois : l'instruction RETURN. Par RETURN, il faut comprendre «résultat». Dans la première ligne, « RETURN Natural IS » indique que le résultat de la fonction sera de type natural. En revanche, le « RETURN A » de la 5ème ligne a deux actions :

  • Il stoppe le déroulement de la fonction (un peu comme EXIT pour une boucle).
  • Il renvoie comme résultat la valeur contenue dans la variable A.

À noter qu'il est possible de condenser le code ainsi :

1
2
3
4
function A_Rect(larg natural ; long natural) return natural is
begin
   return larg * long ; 
end A_Rect ;

Utilisation

Nous allons donc déclarer une variable Aire dans notre procédure principale. Puis, nous allons changer la condition de la manière suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
   if choix=1
      then Put("Combien de colonnes voulez-vous dessiner ? ") ; 
           get(nb_colonnes ) ; skip_line ; 
           Affich_Rect(nb_lignes, nb_colonnes) ; 
           Aire := A_Rect(nb_lignes, nb_colonnes) ; 
           Put("L'aire du rectangle est de ") ; Put(Aire) ; put_line(" dieses.") ; 
      else Affich_Tri(nb_lignes) ; 
   end if ; 
...

Notre variable Aire va ainsi prendre comme valeur le résultat de la fonction A_Rect(). N'oubliez pas le signe d'affectation := ! D'ailleurs, cela ne vous rappelle rien ? Souvenez-vous des attributs !

1
c := character'val(153) ;

Ces attributs agissaient à la manière des fonctions !

Une fonction un peu moins bête (optionnel)

Et l'aire du triangle ?

C'est un peu plus compliqué. Normalement, pour calculer l'aire d'un triangle rectangle isocèle, on multiplie les côtés de l'angle droit entre eux puis on divise par 2. Donc pour un triangle de côté 5, cela fait ${5 \times 5 \over 2} = 12,5$ dièses !?! Bizarre. Et puis, si l'on fait un exemple :

1
2
3
4
5
#
##
###
####
#####

Il y a en fait 15 dièses ! ! Pour quelle raison ? Et bien, ce que nous avons dessiné n'est pas un «vrai» triangle. On ne peut avoir de demi-dièses, de quart de dièses… La solution est donc un peu plus calculatoire. Remarquez qu'à chaque ligne, le nombre de dièses est incrémenté (augmenté de 1). Donc si notre triangle a pour côté $N$, pour connaître l'aire, il faut effectuer le calcul : $1 + 2 + 3 + ... + (N-1) + N$. Les points de suspension remplaçant les nombres que je n'ai pas envie d'écrire. Comment trouver le résultat ? Astuce de mathématicien. Posons le calcul dans les deux sens :

Calcul n°1

1

+

2

+

3

+

+

N-2

+

N-1

+

N

Calcul n°2

N

+

N-1

+

N-2

+

+

3

+

2

+

1

Somme des termes

N+1

+

N+1

+

N+1

+

+

N+1

+

N+1

+

N+1

En additionnant les termes deux à deux, on remarque que l'on trouve toujours $N+1$. Et comme il y a $N$ termes cela nous fait un total de : $N \times (N+1)$. Mais n'oublions pas que nous avons effectuer deux fois notre calcul. Le résultat est donc : $N \times (N+1) \over 2$.

Pour un triangle de côté 5, cela fait ${5 \times 6 \over 2} = 15$ dièses ! :soleil: Bon, il est temps de créer notre fonction A_Triangle !

1
2
3
4
5
6
function A_Triangle(N natural) return natural is
   A : natural ; 
begin
   A:= N * (N+1) / 2 ; 
return A ; 
end A_Triangle ;

Et voilà ce que donne le code général :

 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
60
61
62
63
64
65
with Ada.Text_IO, Ada.Integer_Text_IO ; 
use Ada.Text_IO, Ada.Integer_Text_IO ; 

procedure Figure is
   
   Procedure Affich_Ligne(nb natural) is
   begin
      for j in 1..nb loop
         put('#') ; 
      end loop ; 
      new_line ;
   end Affich_Ligne ; 

   Procedure Affich_Rect(nb_lignes natural ; nb_colonnes natural) is
   begin
      for i in 1..nb_lignes  loop
         Affich_Ligne(nb_colonnes);
      end loop ; 
   end Affich_Rect ; 

   Procedure Affich_Tri(nb natural) is
   begin
      for i in 1..nb  loop
         Affich_Ligne(i);
      end loop ; 
   end Affich_Tri ; 

   function A_Rect(larg natural; long natural) return natural is
      A : natural ; 
   begin
      A:= larg * long ; 
   return A ; 
   end A_Rect ; 

   function A_Triangle(N natural) return natural is
      A : natural ; 
   begin
      A:= N * (N+1) / 2 ; 
   return A ; 
   end A_Triangle ;

   nb_lignes, nb_colonnes,choix,Aire : natural; 

begin

   Put_line("Voulez-vous afficher : ") ; 
   Put_line("1-Un rectangle ?") ; 
   Put_line("2-Un triangle ?") ; 
   Get(choix) ; skip_line; 

   Put("Combien de lignes voulez-vous dessiner ? ") ; 
   get(nb_lignes ) ; skip_line ; 

   if choix=1
      then Put("Combien de colonnes voulez-vous dessiner ? ") ; 
           get(nb_colonnes ) ; skip_line ; 
           Affich_Rect(nb_lignes, nb_colonnes) ; 
           Aire := A_Rect(nb_lignes, nb_colonnes) ; 
           Put("L'aire du rectangle est de ") ; Put(Aire) ; put_line(" dieses.") ; 
      else Affich_Tri(nb_lignes) ; 
           Aire := A_Triangle(nb_lignes) ; 
           Put("L'aire du triangle est de ") ; Put(Aire) ; put_line(" dieses.") ; 
   end if ; 

end Figure ;

Bilan

Rendu à ce stade, vous avez du remarquer que le code devient très chargé. Voici donc ce que je vous propose : COMMENTEZ VOTRE CODE ! Exemple :

 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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
with Ada.Text_IO, Ada.Integer_Text_IO ; 
use Ada.Text_IO, Ada.Integer_Text_IO ; 

procedure Figure is

         --------------
         --PROCEDURES--
         --------------

   Procedure Affich_Ligne(nb natural) is
   begin
      for j in 1..nb loop
         put('#') ; 
      end loop ; 
      new_line ;
   end Affich_Ligne ; 

   Procedure Affich_Rect(nb_lignes natural ; nb_colonnes natural) is
   begin
      for i in 1..nb_lignes  loop
         Affich_Ligne(nb_colonnes);
      end loop ; 
   end Affich_Rect ; 

   Procedure Affich_Tri(nb natural) is
   begin
      for i in 1..nb  loop
         Affich_Ligne(i);
      end loop ; 
   end Affich_Tri ; 

         -------------
         --FONCTIONS--
         -------------

   function A_Rect(larg natural; long natural) return natural is
   begin
      return larg * long ; 
   end A_Rect ; 

   function A_Triangle(N natural) return natural is
   begin
      return N * (N+1) / 2 ; 
   end A_Triangle ;

         -------------
         --VARIABLES--
         -------------

   nb_lignes, nb_colonnes,choix : natural; 

         ------------------------
         --PROCEDURE PRINCIPALE--
         ------------------------

begin

   Put_line("Voulez-vous afficher : ") ; 
   Put_line("1-Un rectangle ?") ; 
   Put_line("2-Un triangle ?") ; 
   Get(choix) ; skip_line; 
   Put("Combien de lignes voulez-vous dessiner ? ") ; 
   get(nb_lignes ) ; skip_line ; 

   if choix=1
      then Put("Combien de colonnes voulez-vous dessiner ? ") ; 
           get(nb_colonnes ) ; skip_line ; 
           Affich_Rect(nb_lignes, nb_colonnes) ; 
           Put("L'aire du rectangle est de ") ; Put(A_Rect(nb_lignes, nb_colonnes)) ; put_line(" dieses.") ; 
      else Affich_Tri(nb_lignes) ; 
           Put("L'aire du triangle est de ") ; Put(A_Triangle(nb_lignes)) ; put_line(" dieses.") ; 
   end if ; 

end Figure ;

J'utilise, pour ma part, de gros commentaires afin de structurer mon code. Nous verrons bien plus tard comment créer des packages afin de libérer notre procédure principale. Autre remarque, dans cet exemple j'ai condensé mon code. J'ai notamment écrit : «Put(A_Rect(nb_lignes, nb_colonnes)) ;» ! Il est en effet possible d'imbriquer plusieurs fonctions (ou/et une procédure) les unes dans les autres pour «compacter» votre code. Faites toutefois attention à ce que les types soient compatibles !

Prédéfinir ses paramètres

Il est possible de donner une valeur par défaut aux paramètres d'une procédure ou d'une fonction. Par exemple :

1
Procedure Affich_Ligne(nb : natural := 1) is

Ainsi, il sera possible de ne pas spécifier de paramètre. En tapant «Affich_Ligne», le compilateur comprendra tout seul «Affich_Ligne(1)». Cette astuce est surtout utile pour des fonctions exigeant de nombreux paramètres dont certains seraient optionnels. Imaginons par exemple une fonction Affich_Rect définie comme suit :

1
2
3
4
5
6
procedure Affich_Rect(nb_lignes   natural ;
                      nb_colonnes natural ;
                      symbole     character ; 
                      plein       boolean ; 
                      affich_a    boolean ; 
                      affich_p    boolean) ;

On dit qu'il s'agit due la spécification de la procédure Affich_Rect (dans d'autres langages, comme le C, on parlerait plutôt de prototype ). On se moque ici de la façon dont la procédure est codée, on se contente d'une simple déclaration. Nous reverrons les spécifications lors du chapitre sur les packages et nous expliquerons alors pourquoi le IS a été bizarrement remplacé par un point virgule.

Expliquons plutôt les différents paramètres :

  • nb_lignes et nb_colonnes jouent le même rôle que lors des exemples précédents.
  • symbole est le caractère qu'il faut afficher pour constituer le rectangle. Auparavant, ce caractère était le dièse (#).
  • plein est un booléen qui indique si le rectangle est creux (plein vaut FALSE) ou plein (plein vaut TRUE).
  • affich_a et affich_p sont deux booléens. Si affich_a vaut TRUE, alors le programme affichera l'aire du rectangle. S'il vaut FALSE, alors le programme n'affichera rien. Le paramètre affich_p joue le même rôle mais avec le périmètre au lieu de l'aire.

Nous avons ainsi une procédure vraiment complète. Le seul inconvénient c'est que lorsque l'on veut afficher deux simples rectangles formés de dièses, il faut taper :

1
2
Affich_Rect(5,8,'#',true,false,false) ;
Affich_Rect(7,3,'#',true,false,false) ;

Hormis les deux premiers, on ne changera pas souvent les paramètres. La procédure est complète, mais un peu trop : il faudra retaper souvent la même chose ('#', TRUE, FALSE, FALSE) en prenant garde de ne pas s'emmêler les pinceaux dans l'ordre des variables (très important l'ordre des variables !).

Eh bien moi, c'est certain ! Je ne ferai jamais de procédure trop compliquée ! :-°

Allons ! Cette procédure est très bien, il faut juste identifier les paramètres qui seront optionnels et leur donner une valeur par défaut. Il serait donc plus judicieux de déclarer notre procédure ainsi :

1
2
3
4
5
6
procedure Affich_Rect(nb_lignes   natural ;
                      nb_colonnes natural ;
                      symbole     character := '#'; 
                      plein       boolean   := true ; 
                      affich_a    boolean   := false ; 
                      affich_p    boolean   := false ) ;

Ainsi, nous pourrons l'appeler plus simplement :

1
2
Affich_Rect(5,8) ; --Un simple rectangle 5 par 8
Affich_Rect(7,3,'@') ; --un rectangle 7 par 3 avec des @ à la place des # !

Ah oui, c'est effectivement intéressant. :D Mais si je veux un rectangle 5 par 8 classique en affichant le périmètre, je peux taper Affich_Rect(5,8,true) ; ?

NON ! Surtout pas ! Le troisième paramètre est sensé être un caractère, pas un booléen !

Mais je fais quoi alors ? :'(

C'est vrai que dans ce cas, on a un souci. Il nous faut respecter l'ordre des paramètres mais certains ne sont pas utiles. Heureusement, il y a un solution !

1
Affich_Rect(5,8,affich_p => true) ;

Il suffit de spécifier le paramètre auquel on attribue une valeur ! Attention, on n'utilise pas le symbole « := » mais la flèche « => ». Ainsi, l'ordre n'a plus d'importance, on pourrait tout aussi bien noter :

1
2
Affich_Rect(5,affich_p => true,nb_colonnes => 8) ;
Affich_Rect(nb_colonnes => 8, affich_p => true, nb_lignes => 5) ;

D'où l'intérêt de bien choisir ses noms de variables (et de paramètres en l'occurrence).

In, Out, In Out

Nous avons vu précédemment que les paramètres d'une fonction ou d'une procédure ne peuvent pas (et ne doivent pas) être modifiés durant le déroulement de la fonction ou de la procédure. Eh bien… c'est faux ! Enfin non ! C'est vrai, mais disons qu'il y a un moyen de contourner cela pour les procédures.

Paramètres de procédure

Mode in

Lorsque l'on écrit :

1
procedure Affich_Rect(nb_lignes : natural, nb_colonnes : natural)is

Nous écrivons, implicitement, ceci :

1
procedure Affich_Rect(nb_lignes : in natural, nb_colonnes : in natural)is

Les instructions IN indiquent que nos paramètres sont uniquement des paramètres d'entrée. Les emplacements-mémoire qui contiennent leur valeur peuvent être lus mais il vous est interdit d'y écrire : ils ne sont accessibles qu'en lecture. Donc impossible d'écrire « nb_lignes := 5 » au cours de cette procédure.

Mode out

En revanche, en écrivant ceci :

1
procedure Affich_Rect(nb_lignes : out natural, nb_colonnes : out natural) is

… vos paramètres deviendront des paramètres de sortie. Il sera possible d'écrire dans les emplacements-mémoire réservés à nb_lignes et nb_colonnes ! Génial non ? Sauf qu'il vous sera impossible de lire la valeur initiale, donc impossible d'effectuer des tests par exemple. :( Vos paramètres ne sont accessibles qu'en écriture.

Mode in out

Heureusement, il est possible de mélanger les deux :

1
procedure Affich_Rect(nb_lignes : in out natural, nb_colonnes : in out natural) is

Nous avons alors des paramètres d'entrée et de sortie, accessibles en lecture ET en écriture ! Tout nous est permis ! :p

Pourquoi tous les paramètres ne sont-ils pas en mode in out par défaut ?

Réfléchissons à notre procédure Affich_Rect : est-il judicieux que nos paramètres nb_lignes et nb_colonnes soient accessibles en écriture ? Je ne pense pas. Notre procédure n'a pas besoin de les modifier et il ne serait pas bon qu'elle les modifie. Les laisser en mode IN, c'est prendre l'assurance que nos paramètres ne seront pas modifiés (sinon, le rigoureux compilateur GNAT se chargera de vous rappeler à l'ordre :pirate: ). Et réciproquement, des paramètres en mode OUT n'ont pas à être lu.

Un exemple ? Imaginez une procédure dont l'un des paramètres s'appellerait Ca_Marche et servirait à indiquer si il y a eu un échec du programme ou pas. Notre procédure n'a pas à lire cette variable, cela n'aurait pas de sens. Elle doit seulement lui affecter une valeur. C'est pourquoi vous devrez prendre quelques secondes de réflexion avant de choisir entre ces différents modes et notamment pour le mode IN OUT.

Paramètres de fonction

Pourquoi ne parles-tu que des procédures ? Qu'en est-il des fonctions ?

Pour les fonctions, tout dépend de votre compilateur. Pour les normes Ada83, Ada95 et Ada2005, seul le mode IN est accessible pour les fonctions. Ce qui est logique puisqu'une fonction renvoie déjà un résultat calculé à partir de paramètres. Il n'y a donc pas de raison pour que certains paramètres constituent des résultats.

Mais cela peut parfois se révéler fort restrictif. C'est pourquoi la norme Ada2012 permet, entre autres améliorations, que les fonctions utilisent des paramètres en mode IN OUT. Le mode OUT n'est pas accessible : comment calculer le résultat si les paramètres ne sont pas lisibles.

Nous savons maintenant comment mieux organiser nos codes en Ada. Cela nous permettra de créer des programmes de plus en plus complexes sans perdre en lisibilité. Nous reviendrons plus tard sur les fonctions et procédures pour aborder une méthode très utile en programmation : la récursivité. Mais cette partie étant bien plus compliquée à comprendre, nous allons pour l'heure laisser reposer ce que nous venons de voir.

Lors du prochain chapitre théorique, nous aborderons un nouveau type : les tableaux ! Nous entrerons alors dans un nouvel univers (et une nouvelle partie) : celui des types composites ! Je conseille donc à ceux qui ne seraient pas encore au point de revoir les chapitres de la seconde partie. Avant tout cela, il est l'heure pour vous de mettre en application ce que nous avons vu. Le chapitre qui suit est un chapitre pratique : un TP !


En résumé :

  • Structurez votre code source en le découpant en sous-programmes, voire en sous-sous-programmes.
  • Un sous-programme est généralement une PROCEDURE. Mais si vous souhaitez calculer un résultat, il sera préférable d'utiliser les FUNCTION.
  • La plupart des sous-programmes utilisent des paramètres : pensez à réfléchir à leur mode (lecture et/ou écriture) et, si besoin, à leur attribuer une valeur par défaut pour simplifier leur utilisation ultérieure.
  • Si un sous-programme A a besoin d'un sous-programme B, alors il est nécessaire que B soit déclaré avant A, sinon le compilateur ne pourra le voir.