Introduction à Vavr

1. Vue d’ensemble

Dans cet article, nous allons explorer exactement ce que Vavr est, pourquoi nous en avons besoin et comment l’utiliser dans nos projets.

Vavr est une bibliothèque fonctionnelle pour Java 8 fournissant des types de données et des structures de contrôle fonctionnelles immuables.

1.1. Dépendance Maven

Pour utiliser Vavr, vous devez ajouter la dépendance:

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.0</version>
</dependency>

Il est recommandé de toujours utiliser la dernière version. Vous pouvez l’obtenir en suivant this link .

2. Option

L’objectif principal d’Option est d’éliminer les contrôles nuls dans notre code en utilisant le système de type Java.

Option est un conteneur d’objets dans Vavr avec un objectif final similaire à celui de Facultatif en Java 8. L’option Option de Vavr implémente Serializable, Iterable, et possède une API plus riche _. _

Comme toute référence d’objet en Java peut avoir une valeur null , nous devons généralement vérifier la nullité avec les instructions if avant de l’utiliser. Ces contrôles rendent le code robuste et stable:

@Test
public void givenValue__whenNullCheckNeeded__thenCorrect() {
    Object possibleNullObj = null;
    if (possibleNullObj == null) {
        possibleNullObj = "someDefaultValue";
    }
    assertNotNull(possibleNullObj);
}

Sans contrôles, l’application peut tomber en panne à cause d’un simple NPE:

@Test(expected = NullPointerException.class)
public void givenValue__whenNullCheckNeeded__thenCorrect2() {
    Object possibleNullObj = null;
    assertEquals("somevalue", possibleNullObj.toString());
}

Cependant, les contrôles rendent le code verbeux et moins lisible , en particulier lorsque les instructions if finissent par être imbriquées plusieurs fois.

Option résout ce problème en éliminant totalement nulls et en les remplaçant par une référence d’objet valide pour chaque scénario possible.

Avec Option , une valeur null sera évaluée comme une instance de None , tandis qu’une valeur non-nulle sera évaluée comme une instance de Some :

@Test
public void givenValue__whenCreatesOption__thenCorrect() {
    Option<Object> noneOption = Option.of(null);
    Option<Object> someOption = Option.of("val");

    assertEquals("None", noneOption.toString());
    assertEquals("Some(val)", someOption.toString());
}

Par conséquent, au lieu d’utiliser directement les valeurs d’objet, il est conseillé de les envelopper dans une instance de Option comme indiqué ci-dessus.

Notez que nous n’avions pas besoin de vérifier avant d’appeler toString , mais nous n’avions pas à gérer une NullPointerException comme nous l’avions fait auparavant. ToString de l’option nous renvoie des valeurs significatives pour chaque appel.

Dans le deuxième extrait de cette section, nous avions besoin d’une vérification null , dans laquelle nous attribuerions une valeur par défaut à la variable, avant de tenter de l’utiliser. Option peut traiter cela en une seule ligne, même s’il y a un zéro:

@Test
public void givenNull__whenCreatesOption__thenCorrect() {
    String name = null;
    Option<String> nameOption = Option.of(name);

    assertEquals("baeldung", nameOption.getOrElse("baeldung"));
}

Ou un non-nul:

@Test
public void givenNonNull__whenCreatesOption__thenCorrect() {
    String name = "baeldung";
    Option<String> nameOption = Option.of(name);

    assertEquals("baeldung", nameOption.getOrElse("notbaeldung"));
}

Remarquez comment, sans null , nous pouvons obtenir une valeur ou renvoyer une valeur par défaut sur une seule ligne.

3. Tuple

Il n’y a pas d’équivalent direct d’une structure de données de tuples en Java. Un tuple est un concept commun dans les langages de programmation fonctionnels. Les tuples sont immuables et peuvent contenir plusieurs objets de types différents de manière sécurisée.

Vavr apporte des nuplets à Java 8. Les queues sont de type Tuple1, Tuple2 à Tuple8 , en fonction du nombre d’éléments à prendre.

Il existe actuellement une limite supérieure de huit éléments. Nous accédons aux éléments d’un tuple comme tuple. n n__ est similaire à la notion d’index dans les tableaux:

public void whenCreatesTuple__thenCorrect1() {
    Tuple2<String, Integer> java8 = Tuple.of("Java", 8);
    String element1 = java8.__1;
    int element2 = java8.__2();

    assertEquals("Java", element1);
    assertEquals(8, element2);
}

Notez que le premier élément est récupéré avec n == 1 . Donc, un tuple n’utilise pas une base zéro comme un tableau. Les types d’éléments qui seront stockés dans le tuple doivent être déclarés dans sa déclaration de type comme indiqué ci-dessus et ci-dessous:

@Test
public void whenCreatesTuple__thenCorrect2() {
    Tuple3<String, Integer, Double> java8 = Tuple.of("Java", 8, 1.8);
    String element1 = java8.__1;
    int element2 = java8.__2();
    double element3 = java8.__3();

    assertEquals("Java", element1);
    assertEquals(8, element2);
    assertEquals(1.8, element3, 0.1);
}

La place d’un tuple consiste à stocker un groupe fixe d’objets de tous types mieux traités comme une unité et pouvant être échangés. Un cas d’utilisation plus évident renvoie le retour de plusieurs objets d’une fonction ou d’une méthode en Java.

4. Essayer

Dans Vavr, Try est un conteneur pour un calcul pouvant entraîner une exception.

Comme Option encapsule un objet nullable afin que nous n’ayons pas à nous occuper explicitement de nulls avec if contrôles, Try encapsule un calcul afin que nous n’ayons pas à nous occuper explicitement des exceptions avec les blocs try-catch .

Prenez le code suivant par exemple:

@Test(expected = ArithmeticException.class)
public void givenBadCode__whenThrowsException__thenCorrect() {
    int i = 1/0;
}

Sans les blocs try-catch , l’application se bloquerait. Afin d’éviter cela, vous devez envelopper la déclaration dans un bloc try-catch .

Avec Vavr, nous pouvons envelopper le même code dans une instance de Try et obtenir un résultat:

@Test
public void givenBadCode__whenTryHandles__thenCorrect() {
    Try<Integer> result = Try.of(() -> 1/0);

    assertTrue(result.isFailure());
}

Que le calcul soit réussi ou non peut alors être inspecté par choix à tout moment dans le code.

Dans l’extrait ci-dessus, nous avons choisi de simplement vérifier le succès ou l’échec. Nous pouvons également choisir de renvoyer une valeur par défaut:

@Test
public void givenBadCode__whenTryHandles__thenCorrect2() {
    Try<Integer> computation = Try.of(() -> 1/0);
    int errorSentinel = result.getOrElse(-1);

    assertEquals(-1, errorSentinel);
}

Ou même de jeter explicitement une exception de notre choix:

@Test(expected = ArithmeticException.class)
public void givenBadCode__whenTryHandles__thenCorrect3() {
    Try<Integer> result = Try.of(() -> 1/0);
    result.getOrElseThrow(ArithmeticException::new);
}

Dans tous les cas ci-dessus, nous avons le contrôle sur ce qui se passe après le calcul, grâce à la fonction Try

5. Interfaces fonctionnelles

Avec l’arrivée de Java 8, les interfaces interfaces fonctionnelles sont intégrées et faciles à utiliser, en particulier lorsqu’elles sont combinées à lambdas.

Cependant, Java 8 ne fournit que deux fonctions de base. On ne prend qu’un seul paramètre et produit un résultat:

@Test
public void givenJava8Function__whenWorks__thenCorrect() {
    Function<Integer, Integer> square = (num) -> num **  num;
    int result = square.apply(2);

    assertEquals(4, result);
}

La seconde ne prend que deux paramètres et produit un résultat:

@Test
public void givenJava8BiFunction__whenWorks__thenCorrect() {
    BiFunction<Integer, Integer, Integer> sum =
      (num1, num2) -> num1 + num2;
    int result = sum.apply(5, 7);

    assertEquals(12, result);
}

D’un autre côté, Vavr étend l’idée d’interfaces fonctionnelles en Java en prenant en charge un maximum de huit paramètres et en optimisant l’API avec des méthodes de mémorisation, de composition et de curry.

Comme les tuples, ces interfaces fonctionnelles sont nommées en fonction du nombre de paramètres qu’elles prennent: Function0 , Function1 , Function2 etc.

@Test
public void givenVavrFunction__whenWorks__thenCorrect() {
    Function1<Integer, Integer> square = (num) -> num **  num;
    int result = square.apply(2);

    assertEquals(4, result);
}

et ça:

@Test
public void givenVavrBiFunction__whenWorks__thenCorrect() {
    Function2<Integer, Integer, Integer> sum =
      (num1, num2) -> num1 + num2;
    int result = sum.apply(5, 7);

    assertEquals(12, result);
}

Lorsqu’il n’y a pas de paramètre mais que nous avons toujours besoin d’une sortie, Java 8 nécessite l’utilisation d’un type Consumer . Dans Vavr, Function0 est là pour aider:

@Test
public void whenCreatesFunction__thenCorrect0() {
    Function0<String> getClazzName = () -> this.getClass().getName();
    String clazzName = getClazzName.apply();

    assertEquals("com.baeldung.vavr.VavrTest", clazzName);
}

Pourquoi ne pas utiliser une fonction à cinq paramètres?

@Test
public void whenCreatesFunction__thenCorrect5() {
    Function5<String, String, String, String, String, String> concat =
      (a, b, c, d, e) -> a + b + c + d + e;
    String finalString = concat.apply(
      "Hello ", "world", "! ", "Learn ", "Vavr");

    assertEquals("Hello world! Learn Vavr", finalString);
}

Nous pouvons également combiner la méthode de fabrique statique FunctionN.of pour n’importe laquelle des fonctions afin de créer une fonction Vavr à partir d’une référence de méthode. Comme si nous avions la méthode sum suivante:

public int sum(int a, int b) {
    return a + b;
}

Nous pouvons créer une fonction comme celle-ci:

@Test
public void whenCreatesFunctionFromMethodRef__thenCorrect() {
    Function2<Integer, Integer, Integer> sum = Function2.of(this::sum);
    int summed = sum.apply(5, 6);

    assertEquals(11, summed);
}

6. Collections

L’équipe Vavr a déployé beaucoup d’efforts pour concevoir une nouvelle API de collections qui réponde aux exigences de la programmation fonctionnelle, à savoir la persistance, l’immuabilité.

  • Les collections Java sont modifiables, ce qui en fait une excellente source d’échec de programme ** , surtout en présence de simultanéité L’interface Collection fournit des méthodes telles que:

interface Collection<E> {
    void clear();
}

Cette méthode supprime tous les éléments d’une collection (produisant un effet secondaire) et ne renvoie rien. Des classes telles que ConcurrentHashMap ont été créées pour traiter les problèmes déjà créés.

Une telle classe n’apporte pas seulement zéro avantage marginal, mais dégrade également les performances de la classe dont elle tente de combler les lacunes.

  • Avec l’immutabilité, nous bénéficions de la sécurité des threads gratuitement ** : inutile d’écrire de nouvelles classes pour traiter un problème qui ne devrait pas être là en premier lieu.

D’autres tactiques existantes pour ajouter de l’immuabilité aux collections en Java créent encore plus de problèmes, à savoir des exceptions:

@Test(expected = UnsupportedOperationException.class)
public void whenImmutableCollectionThrows__thenCorrect() {
    java.util.List<String> wordList = Arrays.asList("abracadabra");
    java.util.List<String> list = Collections.unmodifiableList(wordList);
    list.add("boom");
}

Tous les problèmes ci-dessus n’existent pas dans les collections Vavr.

Pour créer une liste dans Vavr:

@Test
public void whenCreatesVavrList__thenCorrect() {
    List<Integer> intList = List.of(1, 2, 3);

    assertEquals(3, intList.length());
    assertEquals(new Integer(1), intList.get(0));
    assertEquals(new Integer(2), intList.get(1));
    assertEquals(new Integer(3), intList.get(2));
}

Des API sont également disponibles pour effectuer des calculs sur la liste en place:

@Test
public void whenSumsVavrList__thenCorrect() {
    int sum = List.of(1, 2, 3).sum().intValue();

    assertEquals(6, sum);
}

Les collections Vavr offrent la plupart des classes courantes présentes dans Java Collections Framework et, en réalité, toutes les fonctionnalités sont implémentées.

La solution à retenir est l’immuabilité , la suppression des types de retour à vide et des API produisant des effets secondaires , un ensemble plus riche de fonctions permettant d’opérer sur les éléments sous-jacents , très court, robuste et compact par rapport à la collection de Java. opérations.

Une couverture complète des collections Vavr dépasse le cadre de cet article.

7. Validation

Vavr apporte le concept de Applicative Functor à Java à partir du monde de la programmation fonctionnelle. En termes simples, un Functor__applicatif nous permet d’effectuer une séquence d’actions tout en accumulant les résultats .

La classe vavr.control.Validation facilite l’accumulation d’erreurs. Rappelez-vous que, généralement, un programme se termine dès qu’une erreur est rencontrée.

Cependant, Validation continue à traiter et à accumuler les erreurs pour que le programme les traite en tant que lot.

Considérez que nous enregistrons les utilisateurs par name et age et que nous souhaitons commencer par toutes les entrées et décider de créer une instance Person ou de renvoyer une liste d’erreurs. Voici notre classe Person :

public class Person {
    private String name;
    private int age;

   //standard constructors, setters and getters, toString
}

Ensuite, nous créons une classe appelée PersonValidator . Chaque champ sera validé par une méthode et une autre méthode peut être utilisée pour combiner tous les résultats dans une instance Validation :

class PersonValidator {
    String NAME__ERR = "Invalid characters in name: ";
    String AGE__ERR = "Age must be at least 0";

    public Validation<Seq<String>, Person> validatePerson(
      String name, int age) {
        return Validation.combine(
          validateName(name), validateAge(age)).ap(Person::new);
    }

    private Validation<String, String> validateName(String name) {
        String invalidChars = name.replaceAll("[a-zA-Z]", "");
        return invalidChars.isEmpty() ?
          Validation.valid(name)
            : Validation.invalid(NAME__ERR + invalidChars);
    }

    private Validation<String, Integer> validateAge(int age) {
        return age < 0 ? Validation.invalid(AGE__ERR)
          : Validation.valid(age);
    }
}

La règle pour age est qu’il doit s’agir d’un entier supérieur à 0 et la règle pour name est qu’il ne doit contenir aucun caractère spécial:

@Test
public void whenValidationWorks__thenCorrect() {
    PersonValidator personValidator = new PersonValidator();

    Validation<List<String>, Person> valid =
      personValidator.validatePerson("John Doe", 30);

    Validation<List<String>, Person> invalid =
      personValidator.validatePerson("John? Doe!4", -1);

    assertEquals(
      "Valid(Person[name=John Doe, age=30])",
        valid.toString());

    assertEquals(
      "Invalid(List(Invalid characters in name: ?!4,
        Age must be at least 0))",
          invalid.toString());
}
  • Une valeur valide est contenue dans une instance Validation.Valid , une liste d’erreurs de validation est contenue dans une instance Validation.Invalid ** . Toute méthode de validation doit donc renvoyer l’un des deux.

Inside Validation.Valid est une instance de Person alors que dans Validation.Invalid est une liste d’erreurs.

8. Paresseux

Lazy est un conteneur qui représente une valeur calculée paresseusement, c.-à-d.

le calcul est différé jusqu’à ce que le résultat soit requis. De plus, la valeur évaluée est mise en cache ou mémo et renvoyée encore et encore chaque fois que cela est nécessaire sans répéter le calcul:

@Test
public void givenFunction__whenEvaluatesWithLazy__thenCorrect() {
    Lazy<Double> lazy = Lazy.of(Math::random);
    assertFalse(lazy.isEvaluated());

    double val1 = lazy.get();
    assertTrue(lazy.isEvaluated());

    double val2 = lazy.get();
    assertEquals(val1, val2, 0.1);
}

Dans l’exemple ci-dessus, la fonction que nous évaluons est Math.random .

Notez que, dans la deuxième ligne, nous vérifions la valeur et réalisons que la fonction n’a pas encore été exécutée. En effet, nous n’avons toujours pas montré d’intérêt pour la valeur de retour.

Dans la troisième ligne de code, nous montrons de l’intérêt pour la valeur de calcul en appelant Lazy.get . À ce stade, la fonction s’exécute et Lazy.evaluated renvoie true.

Nous allons également confirmer le bit de mémorisation de Lazy en essayant de get de nouveau la valeur. Si la fonction que nous fournissions était exécutée à nouveau, nous recevrions certainement un nombre aléatoire différent.

Cependant, Lazy renvoie à nouveau paresseusement la valeur calculée initialement lors de la confirmation de l’assertion finale.

9. Correspondance de modèle

La correspondance de modèle est un concept natif dans presque tous les langages de programmation fonctionnels. Il n’y a rien de tel en Java pour le moment.

À la place, chaque fois que nous souhaitons effectuer un calcul ou renvoyer une valeur en fonction des entrées reçues, nous utilisons plusieurs instructions if pour résoudre le code à exécuter:

@Test
public void whenIfWorksAsMatcher__thenCorrect() {
    int input = 3;
    String output;
    if (input == 0) {
        output = "zero";
    }
    if (input == 1) {
        output = "one";
    }
    if (input == 2) {
        output = "two";
    }
    if (input == 3) {
        output = "three";
    }
    else {
        output = "unknown";
    }

    assertEquals("three", output);
}

Nous pouvons voir soudain le code s’étendre sur plusieurs lignes en ne vérifiant que trois cas. Chaque contrôle prend trois lignes de code. Et si nous devions vérifier jusqu’à une centaine de cas, cela ferait environ 300 lignes, pas bien!

Une autre alternative consiste à utiliser une instruction switch :

@Test
public void whenSwitchWorksAsMatcher__thenCorrect() {
    int input = 2;
    String output;
    switch (input) {
    case 0:
        output = "zero";
        break;
    case 1:
        output = "one";
        break;
    case 2:
        output = "two";
        break;
    case 3:
        output = "three";
        break;
    default:
        output = "unknown";
        break;
    }

    assertEquals("two", output);
}

Pas mieux. Nous sommes toujours en moyenne 3 lignes par chèque. Beaucoup de confusion et potentiel de bugs. L’oubli d’une clause break n’est pas un problème au moment de la compilation, mais peut entraîner par la suite des bogues difficiles à détecter.

Dans Vavr, nous remplaçons le bloc entier switch par une méthode Match .

Chaque instruction case ou if est remplacée par un appel à la méthode Case .

Enfin, des modèles atomiques tels que $ () remplacent la condition qui évalue ensuite une expression ou une valeur. Nous fournissons également ceci comme second paramètre à Case :

@Test
public void whenMatchworks__thenCorrect() {
    int input = 2;
    String output = Match(input).of(
      Case($(1), "one"),
      Case($(2), "two"),
      Case($(3), "three"),
      Case($(), "?"));

    assertEquals("two", output);
}

Notez à quel point le code est compact, avec en moyenne une ligne par contrôle. L’API de correspondance de modèles est bien plus puissante que cela et peut effectuer des tâches plus complexes.

Par exemple, nous pouvons remplacer les expressions atomiques par un prédicat.

Imaginons que nous analysions une commande de console pour help et version flags:

Match(arg).of(
    Case($(isIn("-h", "--help")), o -> run(this::displayHelp)),
    Case($(isIn("-v", "--version")), o -> run(this::displayVersion)),
    Case($(), o -> run(() -> {
        throw new IllegalArgumentException(arg);
    }))
);

Certains utilisateurs sont peut-être plus familiarisés avec la version abrégée (-v) alors que d’autres, avec la version complète (–version). Un bon designer doit considérer tous ces cas.

Sans la nécessité de plusieurs déclarations if , nous avons pris en charge plusieurs conditions. Nous en apprendrons davantage sur les prédicats, les conditions multiples et les effets secondaires de la correspondance de modèles dans un article séparé.

10. Conclusion

Dans cet article, nous avons présenté Vavr, la bibliothèque de programmation fonctionnelle populaire pour Java 8. Nous avons abordé les principales fonctionnalités que nous pouvons rapidement adapter pour améliorer notre code.

Le code source complet de cet article est disponible dans le projet Github