Aller au contenu

Et si on arrêtait de se mentir ! Soyons purs !

Lorsque nous programmons, il est important d'améliorer la charge cognitive à comprendre notre code. Découvrons comment écrire un code facile à utiliser, à tester mais surtout à comprendre.

Photo by DALL-E

Introduction

Nous avons souvent l’habitude d’utiliser ou d’écrire un code qui ressemble à celui-ci :

public static Character firstChar(CharSequence str) {
  return str.charAt(0);
}

Fonction utilitaire extractant le premier charactère

A première vue la fonction (méthode) utilitaire firstChar, nous permet d’avoir le premier caractère contenu dans notre paramètre d’entrée str. Aussi, nous pouvons penser que si ce dernier est une chaîne vide, alors nous aurons une valeur null ou vide. Avons-nous raison pour cette première intuition sur le service rendu ? Voyons ensemble.

Vérification du service rendu

Étant développeurs et ayant l’obsession de tout automatiser, un seul moyen pour vérifier le service rendu : tester afin de lever le doute 🙂.

@Test
void firstCharShouldSucceed() {
  assertEquals(firstChar("Functional Programming"), 'F');
  assertEquals(firstChar(""), '\u0000');
  assertNull(firstChar(null));
}

Test unitaire pour la fonction utilitaire

En exécutant ce simple test unitaire, nous allons constater que l’exécution dépasse la première (1) assertion qui confirme l’attendu (caractère ‘F’ en retour) mais lève une exception à l’exécution de la deuxièmes assertion à la ligne deux (2) : java.lang.StringIndexOutOfBoundsException: Index 0 out of bounds for length 0.

En effet, au lieu de recevoir le premier caractère, nous avons à la place une exception car il n’existe pas de premier caractère pour une chaîne vide ("").
En revanche, ce comportement n’était pas ce qu’on attendait en ayant uniquement la signature de la fonction à notre disposition et notre intuition sans se plonger dans la documentation (pas toujours existante ou à jour) ou l’implémentation de la méthode (nous sommes censés l’utiliser comme une boîte noire). Ce comportement inattendu est aussi valable pour une valeur null du paramètre d’entrée qui en résulte en une NullPointerException. Comment pouvons-nous être plus clairs par rapport au service rendu et expliciter l’absence de valeur au lieu de lever une exception ?

Optional à la rescousse

Afin de lever toute ambiguïté et limiter toute interprétation, nous pourrons être plus explicites dans notre définition de la fonction en encapsulant le retour de celle-ci dans un Optional (introduit par Java 8) comme ceci :

public static Optional<Character> firstChar(CharSequence str) {
  return Optional.ofNullable(str)
            .filter(Predicate.not(CharSequence::isEmpty))
            .map(value -> value.charAt(0));;
}

Représentation de l'absence de résultat

Avec cette nouvelle définition, nous pouvons déjà avoir une idée plus précise sur le service rendu par la fonction. En effet, nous pouvons remarquer que le retour est un Optional qui signifie qu’on pourrait avoir une absence de valeur dûe à l’absence de résultat représentée par la valeur Optional.empty() en lieu et place d'un null. Jetant un oeil à cette nouvelle implémentation et faisant un petit rappel sur le Optional :

  1. Optional.ofNullable() : permet d’encapsuler dans un optionnel le paramètre str s’il n’est pas null, sinon retourne un Optional.empty() représentant une valeur vide.
  2. Opérateur filter : permet d’appliquer un filtre (un prédicat) sur la valeur encapsulée par l’Optional. Ici, nous nous intéressons uniquement aux chaînes de caractères non vides.
  3. Opérateur map : permet de transformer la valeur encapsulée par l’Optional et dans ce cas récupérer le premier caractère du paramètre str. Avec le filtre juste avant, nous avons la certitude que celui-ci n’est jamais vide.

Adaptant le test ci-dessus après utilisation de Optional et vérifiant le résultat:

@Test
void firstCharShouldSucceed() {
  assertEquals(firstChar("Functional Programming"), Optional.of('F'));
  assertEquals(firstChar(""), Optional.empty());
  assertEquals(firstChar(null), Optional.empty());
}

Adaptation du test unitaire

Nous constatons que toutes les assertions sont vérifiées et qu’aucune exception n’est levée. Avec simplement l’enveloppe Optional nous avons pu rendre la signature de notre fonction plus explicite sur ce qu’elle nous rend comme résultat ou service. Aussi, à son utilisation, nous sommes pas contraints de vérifier la nullabilité de la valeur retournée ce qui rend notre code plus lisible et plus robuste. Nous appellerons ce type de fonction qui ne ment pas sur son résultat :  une fonction pure.

Fonction pure

Une fonction pure est un concept qu’on retrouve dans le paradigme de programmation méconnu : la programmation fonctionnelle, abrégé PF. Celui-ci est un style de programmation mettant en avant, entre autres, l’utilisation de fonctions comme élément de première classe, en anglais : first class citizen. Une fonction pure répond à trois critères :

  1. Utiliser uniquement ses paramètres pour calculer son résultat.
  2. Retourner une seule valeur.
  3. Prémunir d’opérations à effet de bord : ne modifie pas de valeurs existantes, ne lit ou écrit dans aucun support et ne lance aucune exception intentionnellement pour représenter un échec.

Nous pouvons constater que la nouvelle définition de la fonction firstChar que nous manipulons depuis le début de cet article est une fonction pure qui répond aux trois exigences :

  1. Utiliser uniquement son paramètre str afin de calculer le premier caractère et le retourner.
  2. Retourne toujours une valeur Optional<Character> : elle retourne le premier caractère de son paramètre str s’il n’est pas null ou vide, sinon retourne la valeur vide Optional.empty().
  3. N’utilise aucune opération à effet de bord de lecture/écriture dans un support, muter une valeur existante ou lancer une expcetion.

Conclusion

Une fonction pure est une fonction qui ne ment pas sur son fonctionnement et retourne toujours une valeur calculée à partir de ses paramètres uniquement sans opérations à effet de bord. Cette appellation est issue de la programmation fonctionnelle (PF) qui met l’accent sur l’utilisation des fonctions comme citoyen de première classe, ou, en anglais first class citizen.

Aussi, afin d’implémenter notre fonction pure : firstChar, nous avons fait recours aux Optional introduits en Java 8 qui nous permettent de toujours retourner une valeur. Ce concept lui aussi issu  de la PF permet d’éviter la NullPointerException connue pour être “The billion Dollar Mistake”.

Avec les fonctions pures et donc la PF nous avons rendu notre code plus clair, robuste, concis à l’utilisation et enfin plus propre.

Dernier