Capsule : sous-programme, procédure et fonction

Introduction

Du point de vue du programmeur, un programme est une entité qui porte un nom et qui est constitué d’une séquence d’instructions écrite avec un langage de programmation. Ces instructions décrivent à la fois l’algorithme, c’est-à-dire la méthode qui permet de réaliser le traitement, et la forme des données d’entrée et des données de sortie. Les mots qui constituent le vocabulaire de base du langage de programmation permettent de décrire n’importe quel traitement et est donc peu spécifique. Un programme qui ne serait écrit qu’avec ce vocabulaire de base serait, de fait, difficile à comprendre. L’une des solutions à ce problème est l’utilisation de commentaire. Il est possible d’ajouter dans le code du programme, des commentaires en français ou dans n’importe quelle autre langue naturelle, pour expliquer à quoi sert telle instruction ou tel groupe d’instructions. Toutefois, lorsque les commentaires ne font que répéter en français, ce que disent les instructions en code, cette solution souffre des problèmes liés à la redondance des données. En particulier, il existe le risque que le code change sans que les commentaires correspondants soient mis à jour. Si les commentaires et le code ne disent plus la même chose, le code devient alors encore plus difficile à comprendre.

Une meilleure solution consiste à ajouter un vocabulaire plus spécifique au langage de programmation et l’une des manières de le faire est de définir des sous‑programmes, l’objet de cette capsule de théorie. Comme un programme, un sous‑programme a un nom parlant qui indique clairement ce qu’il fait, et dispose d’un moyen de recevoir et de renvoyer des données. Contrairement à un programme, l’exécution d’un sous‑programme n’est pas lancée directement par un utilisateur final, mais par un appel de sous‑programme dans le code source d’un programme.

Dans les sections qui suivent, nous commençons par examiner les notions de module, d’espace de noms et de bibliothèque en général et de bibliothèque standard en particulier. Comme nous le verrons, la bibliothèque standard de Java nous est déjà en partie connue. Nous définissons ensuite de manière plus détaillée la notion de sous‑programme avant de nous intéresser à leur réalisation en Java.

Notion de module et d’espace de noms

En programmation, un module peut être défini comme une unité d’organisation du code, c’est-à-dire un moyen de regrouper différentes parties du code « qui vont bien ensemble ». La notion de module ne renvoie donc pas à une chose en particulier, mais à n’importe quel moyen permettant d’organiser du code. Dans le cadre de ce cours, nous parlerons de module pour désigner un moyen qui permet de regrouper un ensemble de sous‑programmes et de nommer cet ensemble; en Java, il s’agit d’une classe. Par exemple, la classe Math est un module qui regroupe les fonctions mathématiques d’usage courant comme les fonctions sin, cos, sqrt, pow ou encore round.

En Java, le nom d’une classe est généralement le nom d’une chose sans article (un groupe nominal sans déterminant) et s’écrit en « upper camel case » sans accents. 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). Une classe doit se trouver dans un fichier dont l’extension est .java dont le correspond exactement à 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.

Le nom d’une classe doit être unique. Toutefois, lorsqu’il y beaucoup de classes et en particulier quand on utilise à des classes écrites par des tiers, il peut devenir difficile de trouver un nom de classe qui n’a jamais été utilisé. 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. Comme une classe permet de rassembler des sous‑programmes, un package qui permet de rassembler des classes. De plus, les packages peuvent être organisés sous une forme hiérarchique; un package peut contenir d’autres packages. Concrètement, un package correspond à un répertoire qui peut contenir des fichiers .java (classe) et d’autres répertoires (packages). Le nom complet d’une classe (fully qualified name) se compose de la juxtaposition du nom de chaque package de la hiérarchie 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 exercice1 qui se trouve dans le package m404 qui se trouve dans le package ict, etc. 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 et 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

Bibliothèse et bibliothèque standard

Lorsque des modules sont utilisables dans plusieurs programmes différents, on peut rassembler ces modules dans des bibliothèques (library, attention, faux-amis). En Java, une bibliothèque prend généralement la forme d’un fichier JAR. Un fichier JAR (Java archive) est un fichier dont l’extension est « .jar » et qui contient une archive au format ZIP. L’archive contient une structure de répertoire correspondant aux packages et des fichiers « .class » qui correspondent aux classes compilées.

Une bibliothèque standard est une bibliothèque mise à disposition directement par le langage de programmation. En Java, la bibliothèque standard s’appelle Java Class Library ou JCL qui contient environ 4000 classes. Parmi celles-ci, on peut mentionner la class System qui permet d’avoir accès à l’entrée et à la sortie standard (System.in et System.out), la class Math dont nous avons déjà parlé, la classe String qui représente une chaîne de caractère, ou encore la classe ArrayList qui représente une liste d’objet.

Notion de sous‑programme

La notion de sous‑programme renvoie à une construction d’un langage de programmation qui permet de donner un nom à une séquence d’instructions qui constitue une partie d’un programme. Un sous‑programme peut soit avoir un effet, c’est-à-dire que l’exécution du sous‑programme modifie l’état du système, soit produire une valeur. Dans le premier cas, on parle de procédure et, dans le second, de fonction. L’appel d’une procédure est une instruction et l’appel d’un fonction est une expression. Un sous‑programme peut avoir des paramètres qui permettent à l’appelant de lui passer des données.

On distingue l’appel et la définition d’un sous-programme. Avant de pouvoir appeler (utiliser) un sous‑programme, celui-ci doit être défini. Définir un sous‑programme consiste à déclarer sa signature composée d’un nom et d’une liste des paramètres formels (éventuellement vide), et à lui associer les instructions qui constituent le corps du sous‑programme.

En Java, le nom d’un sous‑programme s’écrit en « lower camel case » et se compose généralement d’un verbe à l’infinitif et d’un complément (groupe verbal). Par exemple, le groupe verbal « résoudre l’équation du second degré » s’écrit resoudreEquationDuSecondDegre ou, en anglais, solveSecondDegreeEquation. Là encore, l’anglais a l’avantage par rapport au français.

 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é. Par exemple, l’effet peut être 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 devrait pas ê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.