3. Une présentation java.Time ?
Qui connaît ?
Qui utilise ?
Equivalent du jdk8 et plus de la librairie JodaTime,
java.Time adresse enfin correctement les problématiques
de fuseau horaire, décalage, date sans horaire et autres
horaires sans date, le tout avec ce qu’il faut d’utilitaires et
de conversions pour un usage courant au top :)
Qui aimerait une présentation ?
4. Une présentation Lombok ?
Qui connaît ?
Qui utilise ?
Lombok : génération de code lors de la compilation via des
annotations, éviter de toujours coder/maintenir soit même
les accesseurs, toString, equals, logger…
Notion très pratique d’objets de données, via un
regroupement de plein de choses utiles dans une même
implémentation : @Data
Qui aimerait une présentation ?
5. Une présentation jOOQ ?
Qui connaît ?
Qui utilise ?
JOOQ considère que le SQL est un langage très riche et
intéressant.
Aussi, pour faciliter son usage depuis Java il propose un
DSL pour écrire du SQL mettant en avant toutes les
possibilités de SQL.
Ainsi on apprend toujours quelque chose mais surtout notre
SQL est fortement type et peut aller d’un select à un
create table…
Dès que possible jOOQ implémente les fonctionnalités de
façon sur plusieurs implémentations de SQL, sinon il fait
quand même et précise les limites de portabilité (en
permettant de s’assurer de la compatibilité via annotation
si besoin).
JOOQ peut s’utiliser seul ou via Hibernate.
Qui aimerait une présentation ?
6. Présentation forte en lambda...
Si vous ne savez pas lire du code contenant des lambdas,
ça va être dur. Révisez un coup puis revenez ^^
8. Informaticiens Protecteurs des Animaux
En anglais : "Yeeepa !"
C’est débile clairement, mais c’est pour le contexte
fonctionnel ^^
9. Good code?
Le code doit être comme un bon chien. Du good code quoi.
Certains disent Clean code, mais on lave tous nos
animaux.
Anyway, c'est quoi du good code?
10. Un bon chien aide son propriétaire, maintenant et
dans le futur.
Le good code, c'est comme un bon chien.
11. Permettre les développements futurs.
Voir même les rendre plus aisés.
Plus on avance, plus on doit aller vite.
Idée folle ?
12. objectif : enregistrement des propriétaires
d'animaux
nom et date de naissance
(doivent être adultes)
Commençons par la date de naissance
13. LocalDate parse(String date){
return LocalDate.parse(
date,
DateTimeFormatter.ofPattern("yyyy/MM/dd"));
}
Un problème?
Est ce du good code?
Quid d’une erreur de parsing ? Exception !
14. LocalDate parse(String date){
try {
return LocalDate.parse(
date,
DateTimeFormatter.ofPattern("yyyy/MM/dd"));
} catch (Exception e) {
return ???;
}
}
Rethrow
- si unchecked, c'est l'origine du problème
- si checked, c'est un autre problème… Et personne n’a
recopié cette fonctionnalité Java, les exceptions vérifiées,
depuis son invention… Passons donc !
Bref, que retourner ?
15. Option to the rescue!
Option<LocalDate> parse(String date){
try {
return Option.of(
LocalDate.parse(
date,
DateTimeFormatter
.ofPattern("yyyy/MM/dd")));
} catch (Exception e) {
return Option.none();
}
}
Pour l’instant, disons que l’Option présente dans le code
fait la même chose que l’Optional de la librairie standard
Java.
Et l’essentiel est que le boulot est fait !
16. Option<Integer> validateAge(String birthdate) {
return parse(birthdate)
.map(
d ->
Period.between(
d,
LocalDate.now())
.getYears())
.filter(age -> age >= MIN_AGE);
}
All good?
I may want to log the exception...
Optional is not enough, let's keep the exception.
En fait, il s'agit d'un essai de parsing n'est ce pas ?
17. Try parse(String date) {
try {
return new Success(
LocalDate.parse(
date,
DateTimeFormatter
.ofPattern("yyyy/MM/dd")));
} catch (Exception t) {
return new Failure(t);
}
}
Success et Failure implémentent tous deux une interface
Try reprenant l'essentiel d'Option(al).
On stocke désormais l'exception si une est présente et on
indique qu'il s'agit d'un échec.
Et on permet d'accéder au contenu de l'erreur.
18. Try onFailure(Consumer<? super Throwable> action)
{
Objects.requireNonNull(
action,
"action is null");
if (isFailure()) {
action.accept(getCause());
}
return this;
}
onFailure retourne le try lui même, permettant de chaîner
Et si pas failure l’action communiquée n’est pas exécutée.
Facile !
19. parse(birthdate)
.onFailure(
t -> log.debug("Parsing failure for
birthdate '" + birthdate + "'", t))
.map(
d -> Period.between(d,
LocalDate.now()).getYears())
.filter(age -> age >= MIN_AGE);
// gives a Try
Le code reste grosso modo le même : on ne gère que ce
qui se passe bien. On ajoute juste le log. Une ligne. Pas
de if.
How great is that ?
20. Lancer des exceptions est fréquent!
Division (par zéro)
Integer.parseInt(string)
Code business...
File...
21. static <T> Try<T> of(
CheckedFunction0<? extends T> supplier) {
Objects.requireNonNull(
supplier,
"supplier is null");
try {
return new Success<>(supplier.apply());
} catch (Throwable t) {
return new Failure<>(t);
}
}
Le Try de vavr :
- générique pour un usage plus large :)
- un supplier pour passer l’action pouvant lancer une
exception :)
22. Try<T> onSuccess(Consumer<? super T> action)
<U> Try<U> flatMap(
Function<? super T,? extends Try<? extends U>>
mapper)
Try<T> andFinally(Runnable runnable)
...
Bien plus dans le Try de vavr !
onSuccess, le pendant d'onFailure
flatMap : permet de gérer un autre bout de code pouvant
lancer des exceptions et permet de tenter autre chose si
réussite du premier Try
andFinally pour une action toujours réalisée quelque soit le
résutlat du Try
23. List<T> toJavaList()
Optional<T> toJavaOptional()
List<T> toList()
Option<T> toOption()
...
Et en bonus plein de fonctions de conversions, juste un
échantillon là.
Et oui vavr a sa propre List et Option, on en parle plus
tard :)
24. Try<LocalDate> parse(String date) {
return Try.of(
() ->
LocalDate.parse(
date,
DateTimeFormatter
.ofPattern("yyyy/MM/dd")));
}
Parse avec Try de vavr
25. Try<Integer> validateAge(String birthdate) {
return parse(birthdate)
.onFailure(
t -> log.debug("Parsing failure for
birthdate " + birthdate, t))
.map(d -> Period.between(
d,
LocalDate.now()).getYears())
.filter(age -> age >= MIN_AGE);
}
Validation de l’âge avec le Try de vavr
27. Contrôle du nom
/**
* Retourne un message si erreur.
*/
Option<String> checkName(String name)
On doit utiliser une méthode pré existante qui retourne (ou
pas) une erreur
28. Option<String> nomValide = Option.of(nom)
.filter(n -> !n.trim().isEmpty())
.filter(n -> checkName(n).isEmpty();
Problème dans le code ci dessus : on perd les erreurs. Dur
de les remonter à l’utilisateur !
29. Tuple2<String, String> t =
Tuple.of("foo","bar");
t._1;// foo
t._2;// bar
Un tuple associe plusieurs valeurs, comme une Entry de
Map en Java mais en plus souple (on peut créer soit
même des entrées).
Il existe de Tuple0 à Tuple8
Très pratique en plein d'occasions
30. Tuple2<String, Option<String>> validateNom(
String nom) {
return Option.of(nom)
.filter(n -> !n.trim().isEmpty())
.map(n -> Tuple.of(n, Option.<String>none()))
.getOrElse(
Tuple
.of(
nom,
Option.of("Le nom doit être
renseigné avec des caractères alpha
numériques")));
}
On peut retourner la valeur et les éventuelles erreurs :)
Mais… pas forcément évident de distinguer les deux…
Et puis l’erreur à la fin est sacrément décorrélée de son
origine...
31. Either<L,R>
R pour Right : valeur correcte
L pour Left : alternative(s)
Transporte la bonne valeur, Right, ainsi que les éventuelles
erreurs ou messages associés.
32. Option<Either<L, R>> filter(Predicate<? super R>
predicate)
<U> Either<L, U> map(Function<? super R, ? extends U>
mapper)
<U> Either<U, R> mapLeft(
Function<? super L, ? extends U> leftMapper)
<U> Either<L, U> flatMap(
Function<
? super R,
? extends Either<L, ? extendsU>> mapper)
Les signatures complexes ne doivent pas effrayer, il s’agit
toujours des bonnes vieilles mêmes méthodes : filter,
map et flatMap.
Seule mapLeft est un peu différente, afin de mapper sur la
valeur « left ».
33. static <L, R> Either<L, R> right(R right)
static <L, R> Either<L, R> left(L left)
On a également des constructeurs pour créer directement
la valeur droite ou gauche
35. Either<String, String> validateNom(String nom) {
return Option.of(nom)
.filter(n -> !n.trim().isEmpty())
.toEither("Le nom doit être renseigné avec
des caractères alpha numériques")
.flatMap(s -> {
Option<String> errors = checkName(s);
return errors.toLeft(s);
});
}
On utilise flatMap afin de pouvoir utiliser la valeur Right
pour déduire la nouvelle valeur Left
Comme précédemment, ce flatMap n’est appelé que si
Either a la valeur Right. A défaut rien ne se passe et on
demeure sur la valeur Left définie plus haut, à savoir « Le
nom doit être renseigné avec des ... »
36. Either<String, Integer> validateAge(String
Birthdate) {
return parse(birthdate)
.onFailure(t -> log.debug("Parsing failure
for birthdate " + birthdate, t))
.toEither("La date de naissance doit être au
format " + DATE_PATTERN + ".")
.map(d -> Period.between(
d,
LocalDate.now()).getYears())
.flatMap(age ->
(age >= MIN_AGE) ?
Either.right(age) :
Either.left("Il faut avoir plus de " +
MIN_AGE));
}
Ici le flatMap se fait sur une either, vu qu’on a besoin
de l’age pour déterminer s’il est correct.
37. Either<String, Person> validatePerson(
String name,
String birthdate) {
return validateNom(name)
.flatMap(nomValide ->
validateAge(birthdate)
.map(age ->
new Person(nomValide, age)));
}
Là on ne lève pas de nouveaux cas d’erreurs, du
coup on se contente du flux normal. Joli non ?
A noter qu’on peut aussi se servir d’Either pour une
alternative (indépendamment de toute gestion
d’erreur).
Par exemple « Either<Cat,Dog> ». Autre usage
souvent pratique :)
38. Option, Try et Either
Beaucoup de ressemblances
Plusieurs fois le même principe: une ou des valeurs sur
lesquelles on applique des opérations.
Expliciter le comportement du code
39. Option → Absence
Try → Exception
Either → Alternative
Et tout cela à côté du cas normal
Option, Try et Either explicitent des situations parfois
cachées dans notre code
Ainsi le code devient bien plus clair.
Qui plus est, la façon d’encapsuler les éléments mis en
avant, cad de n’appliquer les map ou flatMap que si la
valeur est ok (cad non absente, sans exception et sans
l’alternative) permet de faire un code très lisible, concis et
sans if imbriqués. Du bonheur pour le lecteur :)
40. Monade
un état
des opérations dans la monade
des opérations de sortie de la monade
La ressemblance est plus profonde que juste des
similitudes de comportement, ces 3 notions sont trois
déclinaisons d’un même concept : la monade.
- un état (un élément ou plusieurs)
- des opérations "dans" la monade (map, filter....)
-- "happy path" dans les monades vues jusqu'ici, on ne
décrit que le "chemin nominal", on ignore soit les autres
soit ils sont un état à part embarqué en même temps
-- permet de différer le traitement des erreurs quand on
veut, une fois à la fin généralement
- des opérations de sortie de la monade (get...)
42. IO Monad :)
En programmation fonctionnelle, on utilise une monade
pour décrire les opérations d'IO : description dans la
monade des opérations, déclenchement à la demande en
sortant de la monade
43. Plus proche de nous,
d’autre monades se cachent…
Avez vous connaissance d’une autre Monade que vous
utilisez au quotidien ?
En Java..
45. Outils de programmation fonctionnelle... en Java!
Avec vavr, c’est java à l’envers. Littéralement : inverser le
logo !
On reste toutefois à des niveaux « simples » de PF, du fait
notamment des limites du langage
Mais c’est déjà beaucoup et permet d’améliorer
sensiblement notre code.
46. Inspiré de Scala... en mieux!
Découle de la lib standard Scala
L'API de collection Scala se décline en version mutable ou
immuable pour une même interface
API à mi chemin, implémentation partiellement partagée
source de bugs...
Vavr ayant choisi de ne faire que de l'immuable, tout est
grandement simplifié
47. Précédemment javaslang...
Actuellement en 0.9.2
Changement de nom car pb de copyright, Java étant la
marque d’Oracle.
Le nom Vavr a été trouvé en regardant le reflet d’un sticker
« javaslang ».
En profite pour un "reset", pas mal de modif non
rétrocompatible prévues pour la 1.0.0
50. Transformer null en None toujours possible
flatMap(x -> Option.of(x))
Déjà, retourner les nulls n’empêche pas d’atteindre le
même résultat qu’Optional si on le désire
51. map() retournant les nulls
uniforme
null != None
associatif
Mais au-delà, transformer les null en None serait étrange
dans un Try ou un Either… Sans parler d’une List où null
est une valeur valide !
D’ailleurs, dans une Map, avoir une clé avec une valeur
nulle est différente de l’absence de valeur. Optional perd
donc de l’information.
Cette perte d’information est en fait dramatique : suivant
l’ordre dans lequel on applique les map, Optional peut
avoir différents résultats. En effet, nombreuses sont les
méthodes pouvant retourner un résultat pour une valeur
nulle. Or celles ci ne seront jamais appelée dans le cas
d’Optional.
Cet aspect, l’associativité, est une des lois des monades.
52. map() retournant les nulls
Risque de NullPointerException ?
On risque toujours des NPE, même avec Optional. Au sein
d’un map on met du code. Et là le risque de NPE
persiste. Null est bel et bien une valeur possible en Java,
Optional ne pourra rien y changer.
A propos, qui a tenté de retourner null dans le flatMap
d’Optional ?
Du coup le gain de transformer les nulls en empty est très
faible, alors que les pertes sont énormes.
53. Retour au sujet
Où en étions nous ?
Pas simple de revenir sur un sujet après une distraction
hein ?
Vrai problème...
54. Quid d’une doc pour avoir un récap ?
Une petite doc dans l’idée devrait bien faire le job non ?
On n’a pas 200 règles, on pourrait facilement avoir un
aperçu…
55. Une doc toujours à jour ?
Seul hic, 99 % des docs ne sont pas à jour…
Et le 1 % restant est souvent infâme de relents de
procédures qualité ^^
Mais ce serait tellement bien...
56. Une doc interactive ?
Encore mieux qu’à jour, quid d’une doc qu’on pourrait
manipuler ?
Ajouter des cas, voir ce qu’ils retournent… A la volée,
comme ça, parce qu’on le veut ?
57. Informaticiens Protecteurs des Animaux
On sait aussi teaser hein ?
Vous pensez que c’est du rêve tout ça ? Vous voulez la
réponse ?
58. Elements validés en vert, éléments en erreur en rouge,
avec résultats effectifs affichés et ceux erronés barrés.
59. Spécifications par l’exemple
L’idée des spécifications par l’exemple est qu’un exemple
vaut beaucoup d’explications.
C’est en discutant d’exemples concrets que l’on parvient à
affiner. Il est donc important de matérialiser et mettre en
conserver ces exemples.
60. Affichage via html ou markdown
Java ou .Net
L’html (ou le markdown) donne une liberté de présentation
infinie fortement appréciable, que ce soit en termes de
contenu (choix de la langue) ou présentation.
61. Communication possible de l’html ou du
markdown
Avec ou sans exécution du test
On peut passer les fichiers bruts à n’importe qui.
Là l’html a un avantage à mon sens : tout le monde a un
navigateur a même de l’ouvrir, un outil pour visualiser du
markdown est plus compliqué à avoir.
62. Je me suis occupé de la mise en forme…
Bien sûr qu’on en passe l’html d’origine sans exécution du
test, on ne sait pas ce qui valide ou pas.
Ceci dit, cela ne devrait casser que dans des feature
branchs, pas en prod ou develop. Au développeur de
s’assurer qu’il passe la bonne version.
On peut aussi directement accéder à ces fichiers, en tant
que non codeurs, via un outil de navigation de repo
(github…).
64. <table concordion:execute="#result =
validate(#name,#birthdate)">
<tr>
<th concordion:set="#name">Nom</th>
<th concordion:set="#birthdate">Date de
naissance <br/>(yyyy/MM/dd)</th>
<th concordion:assert-
equals="#result">Résultat</th>
</tr>
<tr>
<td>Dupont</td>
<td>1999/03/21</td>
<td>Person(Dupont, 18)</td>
</tr>
</table>
Coté htlm, on ajoute des éléments qui ne perturbent pas
l’affichage, indiquant :
- ce qu’on capture
- ce qu’on exécute
- ce qu’on vérifie.
Dans le cas d’un tableau, on définit une fois le
comportement au niveau de l’header. Cela s’applique
alors sur chaque ligne.
65. @RunWith(ConcordionRunner.class)
public class PersonValidatorTest {
public String validate(
String name,
String birthdate) {
Either<String, String> strings =
new PersonValidator()
.validatePerson(name, birthdate)
.map(Person::toString);
return strings
.getOrElse(() -> strings.getLeft());
}
}
La classe de test fait le lien entre les méthodes appelées
depuis l’affichage et le code testé.
Ici on « aplatit » les sorties pour les passer toutes en String.
66. Exécution avec les tests unitaires
Embarqué avec le code
Le coût d’exécution d’un concordion est minime. On peut
les exécuter en même temps que les tests unitaires (ce
qui est le comportement par défaut).
Le fait que tout soit avec le code est génial. En effet, si on
utilise un gestionnaire de sources alors on a l’historique
des changements et on peut adapter les concordion dans
une feature branch quand cela s’impose, sans impacter
les autres branches.
67. Exemples hors tables possibles
Également intéressant, mais plus limité en portée et plus
coûteux en instrumentation.
69. Documentation vivante !
Une documentation vivante :
- s’adapte aux changements (captures actualisées)
- dit si elle est périmée (tests qui plantent)
- s’adapte aux différentes versions (est stockée par
branche, avec le code)
70. Du (good) code
Attention toutefois, la facilité d’instrumentation, via du code,
peut parfois conduire à des usines à gaz, comme tout
code.
Toujours penser à limiter le scope, à refactorer, à découper
si cela devient trop complexe.
On est vite embarqué dans un cycle perfectionniste de
l’affichage, mais ce n’est pas une fin en soi !
71. Jouons un peu avec les spécifications
Souvent en ajoutant des exemples on découvre des
choses...
73. Validation<E, T>
<E> value type in the case of invalid
<T> value type in the case of valid
Très utile pour toutes les validations, notamment d’une
IHM.
74. Validation<String, String> nomValidation(
String nom) {
return Option.of(nom)
.filter(n -> !n.trim().isEmpty())
.toValidation("Le nom doit être renseigné
avec des caractères alpha numériques")
.flatMap(s -> checkName(s).toInvalid(s));
}
On indique le type d’erreur à remonter, là on a pris String,
pour l’exemple, mais dans la vraie vie ça peut être bien
plus évolué.
On transforme l’état stocké dans la monade, ici le nom
validé..
On décide alors s l’état de la monade est valide ou non puis
on fournit l’autre information pour faire la validation.
75. Validation<Seq<String>, Person>
personValidation(
String name,
String birthdate) {
return Validation
.combine(
nomValidation(name),
ageValidation(birthdate))
.ap(Person::new);
}
A plus haut niveau, soit tout est valide et on construit une
instance de ce qui est validé, soit on retourne les erreurs.
77. Conversion en validation possible pour toutes les
monades
<E> Validation<E, T> toValid(E error)
<U> Validation<T, U> toInvalid(U value)
Du moins en vavr ;)
78. Validation via du code
Si besoin de différentes validations suivant le contexte,
easy :)
82. Partage de Stream de Java 8 possible ?
- Oui, mais le premier qui collecte plante le Stream
pour tous les autres consommateurs!
- Oh, ce partage est il thread safe?
Plein de choses à avoir à l’esprit !
83. Vivre avec java.util.ArrayList<E>...
Risque de changement dans la liste sans qu'on le
sache...
Que ce soit dans le code écrit dès à présent ou ...
dans tout code écrit dans le futur!!
Retour à la définition de code propre : Est ce le cas
avec une list muable? Si oui, pouvez vous en être
sûrs?
Bien sûr, des palliatifs sont possibles: copies
défensives, Collections.immutableList...
Mettez vous toujours en oeuvre ces palliatifs ? A quel
coût ?
A propos, les éléments d'une immutableList peuvent
ils être modifiés ?
84. Immuabilité == tranquillité
Quand tout est immuable, pas d’inquiétude. On peut
passer son objet à tout le monde, ils ne pourront
pas faire de mal. Contexte multi threadé ou non.
Plus besoin de programmation défensive, plus de
bug de folies dus à un changement non prévu.
Que du bonheur :)
86. Persistent data structure
Ensemble d’algorithmes pour rendre efficace les structures
immuables, que ce soit en coût de garbage collection ou
rapidité de modification/consultation.
87. List<Integer> list1 = List.of(1, 2, 3);
// List(1, 2, 3)
List<Integer> list2 = list1.tail().prepend(0);
// List(0, 2, 3)
On parle là de « structural sharing » : partage de données.
On évite ainsi de créer de nouveaux tableaux et de faire
pleins de nouvelles références à chaque action, voir
même de dupliquer les objets référencés « par
précaution »..
89. SortedSet<Integer> xs = TreeSet.of(6, 1, 3, 2, 4,
7, 8);
// TreeSet(1, 2, 3, 4, 6, 7, 8)
SortedSet<Integer> ys = xs.add(5);
// TreeSet(1, 2, 3, 4, 5, 6, 7, 8)
La structure en arbre permet toujours le « structural
sharing » mais en plus des mises à jour et parcours
rapides.
Lors des changements on change juste les « branches »
affectées, le reste peut être réutilisé :)
90. Utilisation de Tree
Accès en temps constant
Performances !
Au delà du partage de structure, des algoritmes tels que le
Red/Black Tree permet de s’assurer de l’accès à chaque
donnée en 4 sauts maximum, soit un temps d’accès
constant quelque soit la taille du Set…
D’ailleurs ces techniques ont été repris dans pour les
collections de java.util.concurrent depuis java 7.
Il y a même des discussions pour passer la taille d’une
ConcurrentHashMap à long : désormais int et ses 4
milliards peuvent être atteints sans problème.
91. Y a pas que les perfs !
Et l’IPA ?
Au delà du partage de structure, des algoritmes tels que le
Red/Black Tree permet de s’assurer de l’accès à chaque
donnée en 4 sauts maximum, soit un temps d’accès
constant quelque soit la taille du Set…
D’ailleurs ces techniques ont été repris dans pour les
collections de java.util.concurrent depuis java 7.
Il y a même des discussions pour passer la taille d’une
ConcurrentHashMap à long : désormais int et ses 4
milliards peuvent être atteints sans problème.
92. List<Integer> ints = List.of(1, 22);
// List(1, 22)
ints.head();
// 1
ints.headOption();
// Some(1)
tail();
// List(22)
ints.tailOption();
// Some(List(22))
En termes d’accesseurs, on peut aisément avoir le
premier élément, head, et les suivants, tail.
93. ints;
// List(1, 22)
ints.map(i -> "Mon int est " + i);
// List(Mon int est 1, Mon int est 22)
ints.flatMap(i -> List.of(i));
// List(1, 22)
Bien sûr, map et flatMap sont toujours là :)
94. ints;
// toujours List(1, 22)!
ints.fold(0, (acc, val) -> acc + val);
// 23
ints.foldLeft("acc", (acc, val) -> acc + val);
// acc122
ints.foldRight("acc", (val, acc) -> acc + val);
// acc221
Fold permet de parcourir tous les éléments de la liste
et d’accumuler le résultat au fur et à mesure
FoldLeft parcours la liste en commençant par la
gauche
FoldRight parcours la liste en commençant par la
droite
Ces deux derniers folds permettent d’avoir un
accumulateur de type différent que les éléments.
95. ints;
// toujours List(1, 22)!
ints.zipWithIndex();
// List((1, 0), (22, 1))
ints.intersperse(5);
// List(1, 5, 22)
ints.distinct();
// List(1, 22)
ints.find(i -> i > 10);
// Some(22)
Avec les folds et les méthodes précédentes, on
devine vite qu’on peut faire plein de choses, mais
vavr a la gentillesse de nous proposer les plus
courantes et/ou les plus pratiques !
De façon générale, rares sont les cas où vavr n’a pas
de quoi grandement simplifier notre quotidien :)