Maîtriser l'instruction de retour des fonctions

Pour terminer une fonction, JavaScript utilise l'instruction return. L'instruction return est très simple. C'est d'ailleurs une des premières que l'on voit dans les cours de programmation.

Mais il ne faudrait pas sous-estimer sa capacité à nous aider à écrire un code plus propre et performant (et inversement lorsqu'on ne la maîtrise pas suffisamment).

J'utilise du code JavaScript pour illustrer mes propos dans cet article, mais les principes expliqués s'appliquent également aux autres langages de programmation.

Comment fonctionne l'instruction return⁣ ?

Nous utilisons les fonctions pour regrouper des instructions qui vont contextuellement ensemble. C'est-à-dire qu'elles servent à accomplir la même tâche.

Première règle que l'on s'efforcera de respecter : une fonction = une responsabilité. Jamais plus d'une responsabilité. Cela améliorera nettement notre code.

function addOneTo(a) {
    const result = a + 1
    
    return result
}

La fonction addOneTo(a) ajoute 1 à la valeur contenue dans la variable a. J'explique les variables en JavaScript un peu plus en détail dans un autre article, pour ceux qui ne sont pas encore très à l'aise avec le sujet.

L'instruction de retour nous permet de retourner le résultat de l'addition de cette façon : return result.

En d'autres termes, l'instruction return nous laisse indiquer clairement à l'interpréteur JavaScript ce que la fonction va retourner comme valeur. Dans notre exemple, il y a deux variables utilisées. Nous ne voulons pas retourner a mais uniquement result.

Qu'en est-il des fonctions sans instruction de retour ?

Ceux qui, comme moi, ont suivi des cours d'algorithmie se le rappelleront peut-être : une fonction doit toujours retourner quelque chose. C'est une règle immuable. JavaScript suit la même règle, les fonctions retournent donc toujours quelque chose. Dans les cas où nous ne souhaitons pas retourner de valeur, une autre structure existe. Ce sont les procédures.

Le problème, c'est qu'il n'existe pas de procédures en JavaScript.

Pour résoudre ce problème, JavaScript nous permet de retourner une valeur "indéfinie". Elle n'est pas définie. On ne sait pas ce qu'elle vaut. On peut dire qu'elle ne vaut rien.

function prepareObject(obj) {
    obj.connect({
      // ...
    })
    obj.use({
      // ...
    })
    
    return undefined
}

De cette façon, il est possible d'utiliser les fonctions comme des procédures, c'est-à-dire des rassemblements d'instructions réutilisables qui ne retournent pas de résultats définis.

La fonction prepareObject exécute ses instructions et à la fin, nous indiquons que la fonction retourne la valeur undefined. Ce qui pour nous veut dire que nous ne retournons rien, mais l'interpréteur est content parce que nous avons quand même retourné une valeur.

Si vous faites le test d'exécuter la fonction prepareObject vous verrez que celle-ci retourne bien la valeur undefined.

Rassurez-vous, il y a plusieurs manières de simplifier ce code. Tout d'abord, omettre la valeur comme ceci :

function prepareObject(obj) {
    obj.connect({
      // ...
    })
    obj.use({
      // ...
    })
    
    return
}

Comme nous ne retournons "rien", l'interpréteur de JavaScript comprend que vous souhaitez en fait retourner la valeur undefined et fait automatiquement le remplacement.

Sinon vous pouvez aussi omettre toute l'instruction, de cette façon :

function prepareObject(obj) {
    obj.connect({
      // ...
    })
    obj.use({
      // ...
    })
}

Si vous vous dites que finalement il s'agit d'une procédure, c'est bien vu, mais c'est faux. Faites le test d'exécuter la fonction prepareObject vous verrez que celle-ci continue de retourner la valeur undefined.

L'instruction de retour est une instruction "bloquante"

Le fait que return soit une instruction classique nous permet de la placer n'importe où dans le code d'une fonction.

function doComplicatedStuff(obj) {
    obj.connect({
      // ...
    })
    
    return obj
    
    obj.use({
      // ...
    })
}

Le code précédent est tout à fait valide. La seule chose dont il faut se rappeler ici, c'est que return est une instruction "bloquante" dans le scope de la fonction parent.

Ce qui veut dire qu'il indique l'arrêt de la fonction dans laquelle il se trouve. C'est important à comprendre, car cela signifie que le code situé après une instruction return ne sera jamais exécuté.

Le code situé après une instruction return est ce que l'on appelle du "code mort".

En fait, l'instruction return veut littéralement dire : "cette fonction a fini de s'exécuter correctement, voici le résultat que tu peux remonter au contexte parent. Inutile de continuer à lire après.".

Mieux utiliser l'instruction return

Si vous vous êtes demandés pourquoi on utiliserait return; au lieu de juste omettre totalement l'instruction, ou si vous vous êtes demandés pourquoi on écrirait du code après un return, cette partie de l'article est pour vous.

Une fois que vous avez assimilé ces deux idées, il devient possible pour vous d'améliorer votre utilisation de l'instruction return. Une bonne utilisation du return permet d'améliorer la lisibilité du code.

Sortir le plus tôt possible de la fonction

Lorsque nous lisons du code, que ce soit notre code ou celui de quelqu'un d'autre, plus il y a de données à prendre en compte plus c'est compliqué à comprendre pour notre cerveau. Les sauts de contextes d'exécution font partie des données à considérer et chaque fonction est un nouveau contexte d'exécution.

Ceci dit, nous ne souhaitons pas utiliser moins les fonctions, car elles apportent beaucoup plus d'avantages que d'inconvénients (ré-utilisabilité, structuration du code, etc.)

Un moyen de réduire la complexité d'un code est de réduire le nombre de données "à prendre en compte". L'instruction return peut nous aider à faire cela. Prenons un exemple.

function byOrder(a, b) {
    let result = null
    
    if (a.order > b.order) {
        result = 1
    }
    
    if (a.order < b.order) {
        result = -1
    }
    
    if (a.order == b.order) {
        result = 0
    }
    
    return result
}

Cette fonction sert à indiquer à la fonction array.sort de JavaScript comment trier un tableau. Elle s'utilise de la manière suivante array.sort(byOrder).

Lisons la fonction pour la comprendre. Détaillons le travail de notre cerveau pendant le débogage pour illustrer le point. a.order veut 10 et b.order vaut 0.

  1. Tout d'abord, je vois qu'il y a une variable result initialisée avec la valeur null, mais sa valeur va changer, car on utilise l'instruction let. Pour en savoir plus sur cette étape, lire l'article détaillé sur les variables en JavaScript.
  2. Une condition : si l'ordre de a est supérieur à celui de b. C'est bien notre cas, on modifie la valeur de result par 1.
  3. Une nouvelle condition : si l'ordre de a est inférieur à celui de b. Ce n'est pas notre cas, pas la peine de rentrer dans le scope de cette condition.
  4. Une nouvelle condition : si l'ordre de a est égale à celui de b. Ce n'est pas notre cas, pas la peine de rentrer dans le scope de cette condition.
  5. On rencontre l'instruction return qui veut retourner la valeur de la variable result. La fonction retourne 1 car on est rentré dans le scope de la première condition.

Nous avons dû lire (et comprendre) beaucoup plus de code que nécessaire pour être sûr de comprendre ce que fait cette fonction. Les points 3 et 4 ci-dessus n'étaient pas indispensables mais nous avons quand même dû les exécuter dans notre cerveau. De plus, nous avons dû nous rappeler ce qui s'était passé avant ces étapes pour savoir quoi retourner.

Essayons maintenant de réécrire le code de byOrder de façon à sortir le plus tôt possible grâce aux propriétés de return. Ensuite, nous referons le même exercice pour comparer la complexité du code.

function byOrder(a, b) {
    if (a.order > b.order) {
        return 1
    }
    
    if (a.order < b.order) {
        return -1
    }
    
    return 0
}

Cette nouvelle fonction fait exactement la même chose. Elle a la même signature que la première fonction. Essayons de la lire maintenant (pour rappel, a.order veut 10 et b.order vaut 0) :

  1. Tout d'abord, j'entre dans la fonction et il y a directement une condition : si l'ordre de a est supérieur à celui de b. C'est bien notre cas, on retourne la valeur 1.

Voilà. Comme l'instruction return est bloquante et qu'elle arrête l'exécution de la fonction, dans notre cas spécifique, le reste du code est du code "mort". Cela ne sert à rien de le lire. Nous n'avons pas besoin de comprendre le reste de la fonction pour comprendre ce qu'elle fait dans notre cas.

Il y a donc beaucoup moins de paramètres à retenir pour le cerveau pendant la lecture. La complexité est réduite significativement. D'ailleurs, cela me fait penser au fonctionnement du mot-clé guard en Swift.

Et, cela est vrai dans quasiment tous les cas. Essayez de refaire l'exercice pour a.order === b.order, même s'il s'agit de la dernière des conditions testées, la deuxième fonction reste moins complexe.

Rendre l'utilisation du else plus utile

Une autre façon d'améliorer le code de byOrder est d'utiliser les mot-clés else et else...if.

function byOrder(a, b) {
    let result = null
    
    if (a.order > b.order) {
        result = 1
    } else if (a.order < b.order) {
        result = -1
    } else {
        result = 0
    }
    
    return result
}

On reproduit ici le comportement de la solution avec les return puisque les conditions suivantes ne sont pas exécutées dès qu'une condition correspondante est trouvée.

Par contre, la solution ne me convient pas, car je considère que cela reste chargé et cela me demande quand même plus de concentration que la solution des return exposée plus haut.

Certains pallient ce problème en utilisant des return, justement. Sauf qu'ils oublient l'intérêt des else.

function byOrder(a, b) {
    if (a.order > b.order) {
        return 1
    } else if (a.order < b.order) {
        return -1
    } else {
        return 0
    }
}

Dans ce cas précis, il faut choisir : soit les else, soit les return. L'utilisation des return supprime tout l'intérêt des else et, accessoirement, vient complexifier encore plus le code. Ce code n'a pas d'autre utilité que de montrer que celui qui l'a écrit n'avait pas bien compris les subtilités des instructions else et des instructions return. Ne faites pas ça.

En utilisant uniquement les return dans cette fonction, vous supprimez quelques mots. Mais, surtout vous supprimez complètement un scope (le dernier else) et vous supprimez un niveau d'indentation, ce qui, nous l'avons vu plus haut, contribue grandement à réduire la complexité de la fonction.

Personnellement, je n'utilise presque plus les else dans mon code parce que j'arrive dans la plupart des cas à écrire une fonction beaucoup plus simple à lire et comprendre en utilisant exclusivement les return.

Après, ne me faites pas dire ce que je n'ai pas dit, le else et le else...if restent des structures très utiles que je continue d'utiliser lorsque j'en ai besoin. Mon seul point ici est qu'ils ne sont vraiment utiles que dans certaines situations. Il ne sert à rien de les utiliser à tout-va s'il existe d'autres mots-clés ou d'autres structures de code mieux adaptés à la situation. Et ce conseil vaut pour à peu près tout ce qui existe en programmation.


En résumé, l'instruction de retour (return) permet de retourner une valeur, définie ou non, au contexte parent de la fonction. Lorsque l'instruction de retour est rencontrée, elle met fin à l'exécution de la fonction courante.

Ceci aide à optimiser notre façon d'écrire du code afin de réduire le nombre de scopes et niveaux d'indentation différents au sein d'une même fonction, et par ailleurs de nous éviter d'avoir à tenir compte de trop d'informations lors du débogage.