Différences entre les enums Option et Result en Rust.
Il n'y a pas à dire, à chaque fois que je code et où le langage m'oblige à utiliser des Optional, je me dis que c'est un langage pourri. Et ceci inclus Rust malgré tout le bien que je pense de lui 😤 !
La meilleure façon de gérer les cas de nullité se trouve dans Kotlin 🥰, TypeScript et Groovy. Le type Optional est une technique archaïque et verbeuse quand on vient du null-check de Kotlin.
Mais bon j'ai déjà expliqué par le passé en quoi le paradigme fonctionnel pure était un cancer métastasé 🤮. Et entre C++ et Rust pour de la programmation système il n'y a plus vraiment de discussion à cette heure, donc faisons-nous violence avec les Optional.
Mon rêve serait un langage natif reprenant la syntaxe de Kotlin (sauf tout ce qui touche aux getter/setter) avec le borrow checker de Rust et qui produise des binaires natifs. Je pense que je peux rêver encore longtemps 🥲
En ce moment je dois expliquer les défauts de Protobuf v2 et pourquoi il est préférable de s'appuyer sur Protobuf v3 à la place.
En substance, c'est à cause des mots-clefs optional et required.
Je m'explique, une fois qu'un champ est flagué à required, que votre format se sera diffusé auprès d'autres systèmes et que forcément ces mêmes systèmes vont se parler entre-eux avec ce format Protobuf, alors il ne vous sera plus jamais possible de supprimer ce champ.
D'où le required is forever...
Protobuf v3 supprime les notions d'optional et required, ce qui fait que tous les champs deviennent optionnels de manière implicite (à l'image de JSON que je préfère largement à Protobuf).
Citation tirée de la doc officielle :
Required Is Forever You should be very careful about marking fields as required. If at some point you wish to stop writing or sending a required field, it will be problematic to change the field to an optional field – old readers will consider messages without this field to be incomplete and may reject or drop them unintentionally. You should consider writing application-specific custom validation routines for your buffers instead. Within Google, required fields are strongly disfavored; most messages defined in proto2 syntax use optional and repeated only. (Proto3 does not support required fields at all.)
@Riduidel : la réponse à ta question est dans ce post (un peu long à lire désolée).
Un objet Iterable implique que l'on traite les données à partir d'un paradigme de programmation procédurale et impératif, c'est-à-dire ni objet, ni fonctionnel. Or les Optionals sont justement des éléments prévus spécialement pour penser et coder dans un style fonctionnel parmi les plus purs.
Vouloir que la classe Optional implémente Itérable, c'est un marqueur fort permettant d'identifier qu'une personne n'a pas encore appris et compris le paradigme fonctionnel où :
- Les instructions if/else/switch ont été remplacés par du pattern-matching.
- Les boucles for/while/do-while ont été remplacées par des appels récursifs.
- Les références ont été remplacées par des lambdas.
Enfin, le fait que Scala ait implémenté Iterable dans les Optionals est justement ce qui fait de Scala un mauvais langage fonctionnel à mon avis, car si c'est génial d'être multi-paradigme, c'est par contre très mal de mélanger les paradigmes dans les éléments constituants l'API du langage car cela créé un code extrêmement incohérent où il devient difficile de savoir comment s'exprimer ; mais c'est mon côté software craftswomanship qui parle #JavaDuchesses
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.
Edit la conclusion est fausse. Si l'on veut que le module A soit présent au runtime comme au compile-time, il faudra l'ajouter explicitement.
Les <optional>true</optional>
ne sont utiles que si l'on se sert de frameworks comme Spring qui passent leur temps à faire de la détection de chargement au runtime.
Je recherchais un moyen de reproduire avec Maven le comportement du implementation
de Gradle, j'en parlais ici. Pour ceux qui ne connaîtraient pas l'idée est la suivante :
- Un module B tire en tant que dépendance un module A.
- Si un module C tire à son tour le module B en tant que dépendance, et puisque tout est transitif par défaut dans Maven, alors les classes de C pourront importer les classes de A
Ceci est dangereux car rien ne lie explicitement C à A et il devient impossible de supprimer B en tant que dépendance dans le pom de C.
La solution, déclarer le module A en tant que dépendance optionnelle de B de cette manière :
<dependency>
<groupId>com.a</groupId>
<artifactId>module-a</artifactId>
<version>1.0.0</version>
<optional>true</optional>
</dependency>
Si C tire B, alors les classes de A ne seront plus visiblent par C dans le classpath mais elles resteront présentent au runtime.
Mes amis.. à vos pom !