Aujourd'hui j'ai relu un cours de @Kysofer qu'il prépare sur la bonne utilisation des monades en programmation fonctionnelle et ça m'a donné envie de le résumer en un cas d'école clair et simple.
Pour la petite histoire et depuis Java 8, je vois apparaître des Optionals
partout mais 99% des développeurs n'ont pas compris comment s'en servir (c'est pour cette raison que je dis souvent que la programmation fonctionnelle est un cancer métastasé, car une fois ces mauvaises pratiques installées, le code devient alors incurable). #Sadness
Bref, c'est partie pour une explication claire et courte... Notre cas d'école sera le suivant :
Une
Personne
possède uneDate de Naissance
, cette date contient elle-même uneAnnée de Naissance
qui contient à son tour une valeur (logée dans un Integer).L'objectif est d'imprimer l'année de naissance si elle existe sans jamais écrire le moindre 'if' pour l'exécution ci-après déclarée dans la méthode
main()
.
La méthode main()
:
class Main {
public static void main(String[] args) {
optionalPrintln(Optional.ofNullable(new Person(new BirthDay(new YearOfBirth(2000)))));
optionalPrintln(Optional.ofNullable(new Person(new BirthDay(null))));
optionalPrintln(Optional.ofNullable(new Person(null)));
}
}
La sortie attendue dans la console doit être celle-ci
2000
Étape 1 - Les classes/structures de données Person, BirthDay et YearOfBirth
class Person {
private final BirthDay birthDay;
public Person(BirthDay birthDay) {
this.birthDay = birthDay;
}
public Optional<BirthDay> getBirthDay() {
return Optional.ofNullable(birthDay);
}
}
class BirthDay {
private final YearOfBirth year;
public BirthDay(YearOfBirth year) {
this.year = year;
}
public Optional<YearOfBirth> getYearOfBirth() {
return Optional.ofNullable(year);
}
}
class YearOfBirth {
private final int year;
public YearOfBirth(int year) {
this.year = year;
}
public Optional<Integer> getValue() {
return Optional.of(year);
}
}
Étape 2 - Ce qu'il ne faut pas faire
Les 9-10ième des développeurs Java que je côtoie écriront ce genre de code pour la optionalPrintln(Optional<Person> option)
:
class Main {
// ...
public static void optionalPrintln(Optional<Person> option) {
if (option.isPresent()) {
Person person = option.get();
Optional<BirthDay> birthDay = person.getBirthDay();
if (birthDay.isPresent()) {
Optional<YearOfBirth> yearOfBirth = birthDay.get().getYearOfBirth();
if (yearOfBirth.isPresent()) {
Optional<Integer> year = yearOfBirth.get().getValue();
if (year.isPresent()) {
System.out.println(year.get());
}
}
}
}
}
}
C'est très moche, c'est très compliqué et ça n'est pas fonctionnel du tout ! Pire encore, certains développeurs écriront ceci :
year.ifPresent(it -> System.out.println(it));
à la place de ce dernier 'if' :
if (year.isPresent()) {
System.out.println(year.get());
}
tout en croyant coder en fonctionnel, or ça n'est pas le cas du tout non plus ! Croire que l'on fait bien alors que l'on fait mal, c'est en ce sens que la chose est pire AMHA.
Étape 3 - Ce qu'il faut faire
La programmation fonctionnelle ne cherche pas à représenter des instructions, c'est-à-dire une succession d'actions techniques qui s'enchaînent mais a contrario, elle cherche à représenter un flux de conversion, c'est-à-dire le fait de passer d'un type A à un type B.
L'API permettant une telle prouesse réside dans le map
de la stream API, or cette fonctionnalité n'est pas applicable à un objet composé comme c'est le cas du Optional
puisqu'il s'agit d'un arbre à deux niveaux sur une seule branche.
Dans ce cas, il faut utiliser la méthode flatMap()
de la même API pour supprimer le niveau superflue de l'arbre et accéder directement à la donnée si elle n'est pas null
.
Ce qui donne le code suivant :
public static void optionalPrintln(Optional<Person> option) {
option.flatMap(it -> it.getBirthDay())
.flatMap(it -> it.getYearOfBirth())
.flatMap(it -> it.getValue())
.ifPresent(it -> System.out.println(it));
}
Explication
A chaque invocation de flatMap()
, cette méthode va jouer pour nous un optional.ifPresent(it -> ...)
afin de nous en décharger. Si rien n'est présent, alors l'évaluation du flatMap()
lui succédant ne se fera tout simplement pas car le flux d'instances à convertir d'un type à un autre type sera dépourvu de toute instance à convertir. #Malin
Et c'est comme cela que l'on code en fonctionnel avec les Optionals ! Le véritable but étant de ne jamais s'en servir explicitement.
Remarque :
La programmation fonctionnelle est extrêmement difficile car elle oblige les développeurs à passer du stade "je dis à la machine comment faire" au stade "je décris à la machine quoi transformer et vers quoi d'autre".
Depuis ces 20 dernières années, j'ai toujours constaté que moins de 5% des développeurs étaient capables de penser dans ce paradigme et parmi ce petit pourcentage, à peine la moitié s'en servira dans sa vie professionnelle.