Capsule : Sous-programme, procédure et fonction

Introduction

Du point de vue d’un utilisateur, un programme informatique est une machine avec une entrée et une sortie. À l’entrée, on fournit des données à traiter à travers la ligne de commande ou de manière interactive, et à la sortie, on reçoit le résultat du traitement sous la forme de nouvelles données affichées à l’écran sous forme de texte ou de graphique, stockées dans un fichier, etc.

Du point de vue du programmeur, il s’agit d’une séquence d’instructions qui permet de réaliser le traitement. Toutefois, à y regarder de plus près, on constate que certaines instructions ressemblent elles-mêmes beaucoup à des programmes. Par exemple, il est assez facile de se rendre compte que la simple ligne System.out.println("Hello world !") qui écrit la chaîne "Hello world !" dans la sortie standard, n’est pas le fin mot de l’histoire. En effet, l’affichage d’un texte à l’écran est une opération complexe qui nécessite l’exécution de plusieurs centaines d’instructions élémentaires par le processeur et les différents contrôleurs de périphériques. De même, l’instruction x = module * Math.cos(alpha) qui calcul le cosinus d’un angle, bien que moins complexe, nécessite typiquement quelques dizaines d’instructions élémentaires.

Il est donc raisonnable de considérer que System.out.println et Math.cos sont des programmes dont l’effet est, respectivement, d’afficher une chaîne de caractère à l’écran et de calculer le cosinus d’un angle, et dont la chaîne "Hello world !" et la valeur de la variable alpha sont les données d’entrée. Puisque ces programmes sont utilisés pour réaliser des programmes, on les nomme sous-programme (subprogram). Enfin, on peut remarquer que l’appel de System.out.println modifie l’état du système — l’écran affiche un texte qui n’y était pas avant — alors que l’appel de Math.cos ne fait que produire une valeur. Dans le premier cas, l’appel du sous-programme est une instruction et le sous-programme est une procédure (procedure). Dans le second cas, l’appel du sous-programme est une expression et le sous-programme est une fonction (function).

Le nom d’un sous-programme permet d’identifier ce sous-programme, il est donc impératif que ce nom soit unique. Pour faciliter la gestion des noms, les sous-programmes sont regroupés dans des modules. Dès lors, il suffit que le nom d’un sous-programme soit unique au sein du module. Les modules étant également identifiés par leur nom, ils sont eux-mêmes regroupés dans des espaces de noms. Avec un langage orienté objet, les modules sont réalisés avec des classes.

Les sections qui suivent abordent ces notions de manière plus détaillée, les modules et les espaces de nom pour commencer, puis la notion de sous-programmes et les diverses notions qui lui sont liées.

Module et espace de noms

Un module est à la fois une unité de compilation, c’est-à-dire une portion de code que l’on peut compiler individuellement, et un moyen de regrouper des sous-programmes « qui vont bien ensemble ». Dans ce sens, une classe Java est toujours un module. Par exemple, la classe Math est un module qui regroupe les fonctions mathématiques d’usage courant.

En Java, le nom d’une classe est généralement un groupe nominal sans déterminant (le nom d’une chose sans article) et s’écrit en « upper camel case ». Par exemple, une classe qui représente une « chouette messagère » se nomme ChouetteMessagere ou, en anglais, MessengerOwl (l’absence d’accent donne un avantage à l’anglais). À chaque classe, doit correspondre exactement un fichier dont le nom est identique à celui de la classe. Par exemple, si le nom de la classe est App alors cette classe doit impérativement se trouver dans un fichier App.java, si le nom de la classe est MessengerOwl alors cette classe doit impérativement se trouver dans un fichier MessengerOwl.java. Il est important de noter que les majuscules et les minuscules sont significatives.

Lorsque l’on a un grand nombre de classes et surtout lorsque l’on a recours à des classes de parties tierces, il peut devenir difficile de trouver des noms de classe uniques. La manière usuelle d’éviter ce problème en informatique consiste à définir un espace de noms différent pour chaque partie. En Java, on réalise cela avec des packages qui permettent d’organiser les fichiers de classe dans une structure de répertoires hiérarchique. Le nom complet d’une classe (fully qualified name) se compose du nom du package et du nom de la classe séparés par un point. Par exemple, ch.epai.ict.m404.exercice1.App identifie la classe App qui se trouve dans le package ch.epai.ict.m404.exercice1. Le graphique de la figure 1 illustre la structure de répertoires correspondante.

Fig. 1 — Structure de répertoires
Fig. 1 — Structure de répertoires

Le code de la figure 2 montre la définition de la classe App qui doit obligatoirement se trouver dans un fichier nommé App.java (voir plus haut). La première ligne d’un fichier .java est toujours la déclaration du package qui doit correspondre précisément à structure de répertoire dans lequel le fichier se trouve. La compilation est impossible si le nom du fichier ou la structure de répertoire ne correspondent pas au nom de la classe et la déclaration du package.

1
2
3
4
5
6
7
8
9
10
11
// Déclaration du package
package ch.epai.ict.m404.exercice1;

// Définition de la classe App
public class App {

    // Ajouter la procédure main s’il s’agit de la classe principale

    // Ajouter les sous-programmes du module

}
Fig. 2 — Contenu du ficher App.java

Définition d’un sous-programme

D’une manière générale, définir un sous-programme consiste à déclarer son nom, déclarer ses paramètres formels et écrire son implémentation (le corps du sous-programme).

En Java, le nom d’un sous-programme s’écrit en « lower camel case » et est généralement un groupe verbal (un verbe et un ou plusieurs compléments) à l’infinitif. Par exemple, le groupe verbal « résoudre l’équation du second degré » s’écrit resoudreEquationDuSecondDegre ou, en anglais, solveSecondDegreeEquation.

 public static Type SubProgramName ( FormalParameters ) InstructionBlock
Fig. 3 – Définition d’un sous-programme

Dans le diagramme de la figure 3 qui décrit la définition d’un sous-programme en Java, le symbole non terminal « FormalParameters » fait référence à la déclaration des paramètres formels dont la syntaxe est décrite par la figure 4.

Les paramètres permettent à un sous-programme appelant (p. ex. la procédure principale) de passer des données au sous-programme appelé. Du point de vue du sous-programme, les paramètres formels sont en tout point identiques à des variables locales dont la valeur est définie au moment de l’appel.

Type ParameterName ,
Fig. 4 – Déclaration des paramètres formels (FormalParameters)

Le dernier symbole non terminal (InstructionBlock) du diagramme de la figure 3 représente le corps du sous-programme. Il s’agit d’une séquence d’instructions entre des accolades comme le montre le diagramme syntaxique de la figure 5.

{ Instruction LocalVariableDeclaration }
Fig. 5 – Bloc d’instructions (InstructionBlock)

Signature d’un sous-programme, encapsulation et abstraction

La signature d’un sous-programme est constituée du type de retour (dans le cas d’une fonction), du nom du sous-programme et de la liste des paramètres formels. Dans le code de la figure 6, la signature de la procédure principale est : static void main(String[] args).

L’encapsulation désigne le fait qu’un sous-programme permet d’isoler (encapsuler) une séquence d’instruction pour se concentrer sur ce qu’elle doit faire sans avoir à se soucier de la manière dont elle sera utilisée. Le terme fait référence à l’idée que la fonction est une capsule — dans le sens d’un emballage — dans laquelle est enfermée l’implémentation.

L’abstraction désigne à la fois l’action de construire une représentation d’une chose en ne considérant que certaines de ses propriétés, et le résultat de cette action (la représentation obtenue). Dans le cas d’un sous-programme, la propriété retenue est sa signature. Pour utiliser un sous-programme, il suffit de connaître sa signature; il n’est pas utile de connaître son implémentation. En ce sens, la signature est une abstraction du sous-programme.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Déclaration du package
package ch.epai.ict.m404.exercice1;

// Définition de la classe App
public class App {

    // Définition de la procédure principale
    public static void main(String[] args) {

        // Faire quelque chose d’utile
        System.out.println("Hello world !");
    }

}
Fig. 6 — Définition de la procédure principale (*main*)

Pour utiliser un sous-programme, il suffit d’en connaître la ou les signatures (voir encadré). Par exemple, il n’est pas nécessaire de savoir comment est réalisée la procédure println pour l’utiliser. Il suffit de savoir que ce sous-programme est une procédure (il ne renvoie pas de valeur), qu’il s’appelle println et qu’il a les signatures suivantes :

  • void println​()
  • void println​(boolean x)
  • void println​(char x)
  • void println​(char[] x)
  • void println​(double x)
  • void println​(float x)
  • void println​(int x)
  • void println​(long x)
  • void println​(Object x)
  • void println​(String x)

De même pour utiliser la fonction Math.cos, il suffit de savoir que la fonction prend comme paramètre, une valeur de type double qui représente l’angle en radian, renvoie une valeur, de type double également, qui représente une approximation du cosinus de cet angle :

  • static double cos​(double a)

Procédure

Une procédure est un sous-programme qui ne renvoie pas de valeur et qui a un effet sur son environnement, c’est-à-dire qu’il modifie l’état du système dans lequel il est exécuté. L’effet peut être, par exemple, l’affectation d’une valeur à variable globale, l’affichage d’un texte à l’écran ou encore l’écriture de données dans un fichier. Un appel de procédure est une instruction.

La déclaration d’une procédure en Java se distingue de la déclaration d’une fonction par son type qui est toujours void (vide) comme dans la définition de la procédure main.

Fig. 7 — Procédure
Fig. 7 — Procédure

On voit assez fréquemment des exemples de code dans lesquels un sous-programme qui devrait être une procédure utilise la valeur de retour pour signaler une erreur. Cette pratique n’est acceptable que dans le cas où le langage utilisé n’a pas de support pour les exceptions (p. ex. le langage C). Mais les exceptions sont désormais supportées par tous les langages courants ; Java, C#, C++, PHP et JavaScript supportent tous les exceptions.

Bien que cela soit rarement utile, il est possible et permis pour une procédure de n’avoir aucun paramètre ; l’effet peut dépendre de l’état du système. Pour terminer l’exécution d’une procédure et revenir au sous-programme appelant, on peut utiliser l’instruction return.

Fonction

Une fonction est, dans l’idéal, un sous-programme qui renvoie une valeur qui ne dépend que des valeurs de ses paramètres. Si ce critère est rempli, on parle d’une fonction pure, dans le cas contraire, on parle d’une fonction avec effet de bord (voir plus bas). Un appel de fonction est une expression dont le type est celui de la fonction.

Fig. 8 — Fonction pure
Fig. 8 — Fonction pure

En Java, pour renvoyer une valeur au sous-programme appelant, on utilise l’instruction return avec une expression. L’expression renvoyée doit être du même type que la fonction. Le code de la figure 10 montre une implémentation possible d’une fonction de type int qui renvoie la plus petite de deux valeurs et son utilisation dans diverses expressions.

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
// Déclaration du package
package ch.epai.ict.m404.exercices;

// Définition de la classe App
public class App {

    // Définition de la procédure principale
    public static void main(String[] args) {

        int a = 10;
        int b = 25;

        // Invoque la fonction dans une expression
        int res = 4 * minOfTwo(a * 2, b) + 4;

        // Invoque la fonction dans une deuxième expression
        if (minOfTwo(b, a) < 20) {
            // Invoque encore la fonction dans une troisième expression
            System.out.printf("Le résultat est : %d%n", minOfTwo(res / 2, 43));
        } else {
            System.out.printf("Le résultat est : %d%n", res);
        }
    }

    // Définition de la fonction minOfTwo
    public static int minOfTwo(int val1, int val2) {
        // Initialize min avec val1 par hypothèse
        int min = val1;

        // Si val2 est plus petit que min, change
        // la valeur de min.
        if (val2 < min) {
            min = val2;
        }

        // Renvoie la valeur de min
        return min;
    }
}
Fig. 10 — Exemple de fonction

Une fonction doit obligatoirement avoir une instruction return pour chaque chemin d’exécution possible. Dans l’exemple précédent, cette condition est bien remplie puisque le return est la dernière instruction. Le code de la figure 11 montre une autre implémentation possible de la même fonction. Dans cette version, on commence par tester si val1 est plus petit que val2 et si c’est le cas, on renvoie immédiatement val1. Dans un tel cas, il faut s’assurer que l’autre chemin d’exécution passe également par une instruction return. Si ce n’est pas le cas, Java refuse de compiler le programme.

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
// Déclaration du package
package ch.epai.ict.m404.exercices;

// Définition de la classe App
public class App {

    // Définition de la procédure principale
    public static void main(String[] args) {
        int a = 21;
        int b = 11;

        int res = minOfTwo(a, b * 2) * 2;

        System.out.printf("Le résultat est : %d%n", res);
    }

    // Définition de la fonction minOfTwo
    public static int minOfTwo(int val1, int val2) {
        if (val1 < val2) {
            return val1;
        } else {
            return val2;
        }
    }
}
Fig. 11 — Fonction avec plusieurs instruction return

Enfin, comme dans le cas des procédures, la valeur de retour d’une fonction ne doit en aucun cas être utilisée pour signaler une erreur. Pour cela, il convient d’utiliser les exceptions.

Fonction avec effet de bord

Les fonctions pures ont beaucoup d’avantages – elles sont notamment plus faciles à tester –, mais avec un langage de programmation impératif, les fonctions avec effet de bord sont inévitables. Une fonction avec effet de bord est une fonction dont la valeur renvoyée dépend de l’état du système ou qui modifie l’état du système. Deux appels successifs d’une fonction avec effet de bord ne renvoient pas nécessairement la même valeur.

Par exemple, la fonction nextInt de la classe Scanner « consomme » un nombre entier dans flux d’entrée (InputStream) et renvoie ce nombre à l’appelant. Après l’exécution de la fonction, l’entier « consommé » ne se trouve plus dans le flux. L’effet de bord est la modification du flux d’entrée. Un autre exemple de fonction avec effet de bord est la fonction now de la classe LocalDateTime qui renvoie la date et l’heure courante dont la valeur dépend de l’instant auquel elle est appelée.

Fig. 9 — Fonction impure
Fig. 9 — Fonction impure

Dans ces deux exemples, la valeur de la fonction ne dépend pas uniquement des valeurs de ses paramètres. Dans les deux cas, la fonction à un effet de bord.

Opération avec effet de bord

Certaines opérations – que l’on peut voir comme des cas particuliers de fonctions – sont des opérations avec effet de bord. C’est notamment le cas de l’opération d’instanciation (création) d’objets et de tableaux avec l’opérateur new. Considérons, par exemple, le code suivant :

1
2
3
4
        ...
        double[] array1 = new double[2];
        double[] array2 = new double[2];
        ...

L’expression de la partie droite (new double[2]) est la même pour les deux affectations, mais la valeur de la variable array1 n’est pas la même que la valeur de la variable de array2. En effet, dans cette expression, l’opérateur new crée un nouveau tableau et renvoie l’adresse (la position) à laquelle se trouve ce tableau dans la mémoire de l’ordinateur. La création du tableau est l’effet (de bord) de l’opération. Comme deux tableaux distincts ne peuvent pas occuper la même portion de mémoire, leurs adresses sont nécessairement différentes.

Utilisation d’expressions avec effet de bord

Du point de vue du langage, une expression avec effet de bord est une expression tout à fait ordinaire. Toutefois, pour éviter des erreurs particulièrement difficiles à diagnostiquer, on s’impose le principe suivant :

Si une expression a un effet de bord (appel d’une fonction avec effet de bord, création d’un objet ou d’un tableau, etc.), alors cette expression doit être la partie droite d’une affectation.

En particulier, l’utilisation d’une fonction avec effet de bord dans l’expression booléenne d’une structure de contrôle ou comme paramètre effectif dans l’appel d’un sous-programme est une très mauvaise pratique qu’il est facile d’éviter. Le respect de cette règle justifie, en outre, l’abandon des opérations d’incrémentation et de décrémentation (++ et --). En effet, puisque ces opérations ont un effet de bord, une expression qui en fait usage doit, en vertu de ce principe, être la partie droite d’une affectation, ce qui n’a aucun sens. En Java, ces opérateurs peuvent être avantageusement remplacés par += 1 et -= 1.