5 - Le développement 5.4 - La programmation

Chapitre 5.4.6Les fonctions

Des briques de logique réutilisables.
Temps de lecture : 5 minutes

On sait maintenant stocker des valeurs, les typer, prendre des décisions avec les conditions et répéter des traitements avec les boucles. De quoi écrire un programme qui fonctionne. Le problème, c'est qu'à ce stade, tout s'empile dans un seul bloc, et plus le programme grossit, moins on s'y retrouve.

Une fonction, c'est un bloc de code réutilisable auquel on donne un nom. On lui fournit des données en entrée, elle fait un traitement et renvoie un résultat. C'est la brique avec laquelle on organise un programme.

Pourquoi des fonctions ?

Sans fonctions, le code serait une longue suite d'instructions sans organisation. Imagine un livre sans chapitres, sans paragraphes, sans titres. Les fonctions permettent de :

  • Réutiliser du code : au lieu de recopier 20 fois le calcul de la TVA, on écrit une fonction calculerTVA et on l'appelle partout où on en a besoin.
  • Nommer les choses : un bon nom de fonction remplace un commentaire. envoyerEmailDeConfirmation(commande) est bien plus clair qu'un bloc de 30 lignes sans contexte.
  • Isoler la complexité : chaque fonction fait une seule chose et la fait bien. On peut comprendre un programme en lisant les noms des fonctions sans plonger dans les détails.

Ce dernier point mérite un exemple, car c'est de cette façon qu'un programme s'organise. Une fonction en appelle d'autres, déléguant le détail à celles du niveau en dessous.

Une fonction qui en appelle d'autres
// verifierStock, debiterCarte et envoyerEmailDeConfirmation
// sont définies ailleurs dans le programme.
function traiterCommande(commande) {
    verifierStock(commande)
    debiterCarte(commande)
    envoyerEmailDeConfirmation(commande)
}

En trois lignes, n'importe qui comprend le déroulé d'une commande sans savoir comment le stock est vérifié ni comment la carte est débitée.

Déclarer une fonction

En JavaScript, on déclare une fonction avec le mot-clé function, suivi de son nom et de ses paramètres entre parenthèses :

Dans la console de ton navigateur
function additionner(a, b) {
    return a + b
}

console.log(additionner(3, 7))  // 10
console.log(additionner(100, 50))  // 150

La fonction additionner prend deux paramètres en entrée (a et b) et retourne leur somme grâce au mot-clé return. On peut ensuite l'appeler autant de fois qu'on veut avec des valeurs différentes.

Une nuance de vocabulaire au passage : a et b sont les "paramètres", les cases vides prévues à la déclaration. Les 3 et 7 passés au moment de l'appel sont les "arguments", les valeurs qui viennent remplir ces cases.

Paramètres et retour

Une fonction peut prendre des paramètres en entrée et retourner un résultat. Mais ce n'est pas obligatoire :

Différentes formes de fonctions
// Pas de paramètres, pas de retour
function direBonjour() {
    console.log("Bonjour !")
}

// Paramètres sans retour
function afficherPrix(nom, prix) {
    console.log(nom + " coûte " + prix + " euros")
}

// Paramètres avec retour
function estMajeur(age) {
    return age >= 18
}

direBonjour()  // Bonjour !
afficherPrix("Clavier", 49)  // Clavier coûte 49 euros
console.log(estMajeur(25))  // true
console.log(estMajeur(12))  // false

Le mot-clé return met fin à l'exécution de la fonction et renvoie la valeur à celui qui l'a appelée. Le résultat peut être n'importe quoi : un nombre, du texte, un booléen, un tableau...

Les développeurs essaient de garder leurs fonctions courtes et focalisées. Une fonction de 200 lignes est un signe qu'il faut la découper en plusieurs fonctions plus petites. On entend souvent la règle, un peu extrême mais parlante : "une fonction ne devrait pas dépasser 10 lignes".

Un paramètre peut aussi recevoir une valeur par défaut, utilisée quand l'appelant ne fournit rien. Il devient alors optionnel, ce qui permet d'enrichir une fonction sans toucher aux appels existants.

Un paramètre optionnel
function calculerTTC(prixHT, taux = 0.20) {
    return prixHT * (1 + taux)
}

console.log(calculerTTC(100))         // 120, le taux normal s'applique
console.log(calculerTTC(100, 0.055))  // 105.5, taux réduit sur l'alimentaire

Les fonctions de rappel (callbacks)

On peut passer une fonction en paramètre d'une autre fonction. C'est ce qu'on appelle un callback (fonction de rappel).

On l'a déjà croisé dans le chapitre sur les boucles avec forEach :

Un callback en action
let clients = ["Alice", "Bob", "Charlie"]

clients.forEach(function(client) {
    console.log("Bienvenue, " + client + " !")
})

Ici, la fonction passée à forEach est anonyme : elle n'a pas de nom. C'est forEach qui décide quand et combien de fois l'appeler.

Les callbacks sont au coeur de la programmation moderne. Ils permettent de créer du code flexible : on écrit une mécanique générale, et c'est celui qui l'utilise qui choisit le comportement spécifique.

Exemple :

Imagine une fonction trierTableau qui trie un tableau. Le tri lui-même est toujours la même mécanique, mais le critère de tri change : par prix croissant ? Par nom alphabétique ? Par date ? Le callback permet de personnaliser le critère sans réécrire la fonction de tri.

Les fonctions fléchées

Si tu jettes un oeil à du JavaScript écrit ces dernières années, tu vas tomber sur une autre écriture, avec une flèche =>. On parle de fonction fléchée, et c'est exactement la même chose en plus court.

Deux écritures pour la même fonction
// Écriture classique
clients.forEach(function(client) {
    console.log("Bienvenue, " + client + " !")
})

// Fonction fléchée
clients.forEach(client => {
    console.log("Bienvenue, " + client + " !")
})

Le mot function disparaît, remplacé par une flèche placée après les paramètres. Quand la fonction tient en une seule instruction, on peut même se passer des accolades et du return.

Encore plus court
const doubler = nombre => nombre * 2

console.log(doubler(21))  // 42

Cette écriture s'est imposée parce que les callbacks sont partout, et que répéter function à chaque fois finissait par noyer l'intention sous la syntaxe. Il existe quelques différences techniques entre les deux formes, mais pour lire du code et suivre une discussion entre développeurs, retiens simplement que la flèche est un raccourci d'écriture.

Tu entendras aussi parler de lambda, le terme générique venu des mathématiques pour désigner une fonction définie sur place et sans nom. C'est un mot-clé en Python, et on parle d'expressions lambda en Java. En JavaScript, on l'emploie souvent comme synonyme de fonction fléchée.

La portée des variables

Une variable déclarée à l'intérieur d'une fonction n'existe que dans cette fonction. C'est ce qu'on appelle la portée (ou "scope") :

La portée
function calculer() {
    let resultat = 42
    console.log(resultat)  // 42
}

calculer()
console.log(resultat)  // Erreur ! resultat n'existe pas ici

C'est un mécanisme de protection. Chaque fonction est un espace isolé : les variables qu'elle crée ne "fuient" pas vers l'extérieur. Ça évite qu'une fonction modifie accidentellement les données d'une autre.

En revanche, en JavaScript, une fonction lit naturellement les variables déclarées autour d'elle :

Accès aux variables externes
let tva = 0.20

function prixTTC(prixHT) {
    return prixHT * (1 + tva)
}

console.log(prixTTC(100))  // 120

La variable tva est accessible partout : on dit qu'elle est globale. Les développeurs évitent au maximum les variables globales, car elles rendent le code plus difficile à comprendre et à maintenir. On préfère passer les valeurs en paramètres pour rendre les dépendances explicites.

Sur ce point, les langages se répartissent en trois familles. Certains, comme JavaScript ou Python, laissent la fonction lire librement ce qui l'entoure comme dans l'exemple ci-dessus. D'autres, comme PHP, ne lui montrent rien tant qu'on ne l'y a pas explicitement autorisée. D'autres enfin, comme le C ou Rust, l'interdisent purement et simplement, une fonction n'ayant jamais accès aux variables locales d'une autre. Il faut alors les lui passer en paramètre, ce qui est de toute façon la bonne pratique partout ailleurs.

Ne pas tout écrire soi-même

Les développeurs ne codent pas eux-mêmes toutes les fonctions qu'ils utilisent, loin de là. Réinventer la roue est même l'un des reproches les plus fréquents qu'ils se font entre eux, parce que ce qu'on réécrit dans son coin existe presque toujours ailleurs.

Une partie vient du langage lui-même. Depuis le début de cette section, tu as croisé console.log pour afficher quelque chose, forEach, map et reduce pour parcourir une liste. Ces fonctions sont fournies d'office.

Le reste vient de bibliothèques, des ensembles de fonctions écrites par d'autres et que l'on installe dans son projet. Manipuler des dates, générer un PDF, dessiner un graphique, envoyer un email, valider un numéro de carte bancaire, il existe une bibliothèque pour chacun de ces besoins. Chaque écosystème a son magasin, npm pour JavaScript, Composer pour PHP, pip pour Python, Maven pour Java.

La quasi-totalité de ces bibliothèques sont open source, c'est-à-dire que leur code est public, consultable et modifiable par tous, le plus souvent gratuitement. Un projet moderne s'appuie ainsi sur des centaines de milliers d'heures de travail offertes par d'autres développeurs.

Mais l'utilisation de bibliothèques n'est pas sans conséquences. Chaque bibliothèque installée est une dépendance, donc un morceau de ton produit que tu ne maîtrises pas. Si une faille de sécurité y est découverte, tu dois mettre à jour. Si son auteur cesse de la maintenir, il faudra en changer un jour. Et ces bibliothèques en utilisent elles-mêmes d'autres, si bien qu'un projet en compte souvent plusieurs centaines sans que personne ne les ait toutes choisies. C'est la raison pour laquelle les équipes techniques réclament régulièrement du temps pour "mettre à jour les dépendances", une tâche invisible côté produit mais qui évite de se réveiller un matin avec une faille béante.