Programmation Java fonctionnelle avec Vavr

Beaucoup ont entendu parler de langages fonctionnels tels que Haskell et Clojure. Mais il y a des langues comme Scala, par exemple. Il combine à la fois la POO et une approche fonctionnelle. Et le bon vieux Java? Est-il possible d'écrire des programmes dans un style fonctionnel et à quel point cela peut faire mal? Oui, il y a Java 8 et lambdas avec des flux. C'est un grand pas pour la langue, mais ce n'est toujours pas suffisant. Est-il possible de trouver quelque chose dans cette situation? Il s'avère que oui.



Pour commencer, essayons de déterminer ce que signifie l'écriture de code dans un style fonctionnel. Premièrement, nous devons opérer non pas avec des variables et des manipulations avec elles, mais avec des chaînes de calculs. Essentiellement, une séquence de fonctions. De plus, nous devons avoir des structures de données spéciales. Par exemple, les collections Java standard ne conviennent pas. Il sera bientôt clair pourquoi.

Examinons plus en détail les structures fonctionnelles. Une telle structure doit satisfaire au moins deux conditions:

  • immuable - la structure doit être immuable. Cela signifie que nous fixons l'état de l'objet au stade de la création et le laissons tel quel jusqu'à la fin de son existence. Un exemple clair d'une violation de condition: ArrayList standard.
  • persistante - la structure doit être stockée en mémoire aussi longtemps que possible. Si nous avons créé un objet, au lieu d'en créer un nouveau avec le même état, nous devrions utiliser celui qui est prêt. Plus formellement, ces structures conservent tous leurs états précédents lors de la modification. Les références à ces conditions doivent rester pleinement opérationnelles.

De toute évidence, nous avons besoin d'une sorte de solution tierce. Et il existe une telle solution: la bibliothèque Vavr . Aujourd'hui, c'est la bibliothèque Java la plus populaire pour travailler dans un style fonctionnel. Ensuite, je décrirai les principales fonctionnalités de la bibliothèque. De nombreux, mais en aucun cas tous, exemples et descriptions ont été tirés de la documentation officielle.

Les principales structures de données de la bibliothèque vavr


Tuple


Les tuples sont l'une des structures de données fonctionnelles les plus simples et les plus simples. Un tuple est un ensemble ordonné de longueur fixe. Contrairement aux listes, un tuple peut contenir des données de tout type.

Tuple tuple = Tuple.of(1, "blablabla", .0, 42L); // (1, blablabla, 0.0, 42) 

Obtenir l'élément souhaité provient de l'appel du champ avec le numéro d'article dans le tuple.

 ((Tuple4) tuple)._1 // 1 

Remarque: l'indexation de tuple commence à 1! De plus, pour obtenir l'élément souhaité, nous devons convertir notre objet en le type souhaité avec l'ensemble de méthodes approprié. Dans l'exemple ci-dessus, nous avons utilisé un tuple de 4 éléments, ce qui signifie que la conversion doit être de type Tuple4 . En fait, personne ne nous empêche de faire initialement le bon type.

 Tuple4 tuple = Tuple.of(1, "blablabla", .0, 42L); // (1, blablabla, 0.0, 42) System.out.println(tuple._1); // 1 

Top 3 des collections Vavr


Liste


Créer une liste avec vavr est très simple. Encore plus facile que sans vavr .

 List.of(1, 2, 3) 

Que pouvons-nous faire avec une telle liste? Eh bien, tout d'abord, nous pouvons le transformer en une liste java standard.

 final boolean containThree = List.of(1, 2, 3) .asJava() .stream() .anyMatch(x -> x == 3); 

Mais en fait, ce n'est pas très nécessaire, car nous pouvons faire, par exemple, comme ceci:

 final boolean containThree = List.of(1, 2, 3) .find(x -> x == 1) .isDefined(); 

En général, la liste de bibliothèques vavr standard contient de nombreuses méthodes utiles. Par exemple, il existe une fonction de convolution assez puissante qui vous permet de combiner une liste de valeurs par une règle et un élément neutre.

 //   final int zero = 0; //   final BiFunction<Integer, Integer, Integer> combine = (x, y) -> x + y; //   final int sum = List.of(1, 2, 3) .fold(zero, combine); //   

Il convient de noter ici un point important. Nous avons des structures de données fonctionnelles, ce qui signifie que nous ne pouvons pas changer leur état. Comment notre liste est-elle mise en œuvre? Les tableaux ne nous conviennent tout simplement pas.

Liste liée comme liste par défaut

Faisons une liste simplement liée avec des objets immuables. Cela ressemblera à ceci:

image

Exemple de code
 List list = List.of(1, 2, 3); 


Chaque élément de la liste a deux méthodes principales: obtenir l'élément head (head) et tous les autres (tail).

Exemple de code
 list.head(); // 1 list.tail(); // List(2, 3) 


Maintenant, si nous voulons changer le premier élément de la liste (de 1 à 0), alors nous devons créer une nouvelle liste avec la réutilisation des pièces finies.

image
Exemple de code
 final List tailList = list.tail(); //    tailList.prepend(0); //      


Et c'est tout! Étant donné que nos objets dans la feuille de calcul sont immuables, nous obtenons une collection thread-safe et réutilisable. Les éléments de notre liste peuvent être appliqués n'importe où dans l'application et c'est complètement sûr!

File d'attente


Une autre structure de données extrêmement utile est la file d'attente. Comment faire une file d'attente pour construire des programmes efficaces et fiables dans un style fonctionnel? Par exemple, nous pouvons prendre des structures de données que nous connaissons déjà: deux listes et un tuple.

image

Exemple de code
 Queue<Integer> queue = Queue.of(1, 2, 3) .enqueue(4) .enqueue(5); 


Lorsque le premier se termine, nous développons le second et l'utilisons pour la lecture.

image

image

Il est important de se rappeler que la file d'attente doit être inchangée, comme toutes les autres structures. Mais à quoi sert une file d'attente qui ne change pas? En fait, il y a une astuce. En tant que valeur acceptée de la file d'attente, nous obtenons un tuple de deux éléments. Premièrement: l'élément de file d'attente souhaité, deuxièmement: ce qui est arrivé à la file d'attente sans cet élément.

 System.out.println(queue); // Queue(1, 2, 3, 4, 5) Tuple2<Integer, Queue<Integer>> tuple2 = queue.dequeue(); System.out.println(tuple2._1); // 1 System.out.println(tuple2._2); // Queue(2, 3, 4, 5) 

Streams


La prochaine structure de données importante est le flux. Un flux est un flux d'exécution de certaines actions sur un certain ensemble de valeurs, souvent abstrait.

Quelqu'un peut dire que Java 8 a déjà des flux à part entière et nous n'en avons pas besoin du tout. En est-il ainsi?

Pour commencer, assurons-nous que le flux java n'est pas une structure de données fonctionnelle. Vérifiez la structure pour la mutabilité. Pour ce faire, créez un si petit flux:
 IntStream standardStream = IntStream.range(1, 10); 

Nous allons trier tous les éléments du flux:

 standardStream.forEach(System.out::print); 

En réponse, nous obtenons la sortie sur la console: 123456789 . Répétons l'opération de force brute:

 standardStream.forEach(System.out::print); 

Oups, l'erreur suivante s'est produite:

 java.lang.IllegalStateException: stream has already been operated upon or closed 

Le fait est que les flux standard ne sont qu'une sorte d'abstraction sur un itérateur. Bien que les flux extérieurs semblent extrêmement indépendants et puissants, les inconvénients des itérateurs n'ont pas disparu.

Par exemple, la définition d'un flux ne dit rien sur la limitation du nombre d'éléments. Malheureusement, il existe dans l'itérateur, ce qui signifie qu'il se trouve dans les flux standard.

Heureusement, la bibliothèque vavr résout ces problèmes. Assurez-vous de ceci:

 Stream stream = Stream.range(1, 10); stream.forEach(System.out::print); stream.forEach(System.out::print); 

En réponse, nous obtenons 123456789123456789 . Ce qui signifie que la première opération n'a pas «gâché» notre flux.

Essayons de créer un flux sans fin:

Stream infiniteStream = Stream.from (1);
System.out.println (infiniteStream); // Stream (1,?)

Remarque: lors de l'impression d'un objet, nous n'obtenons pas une structure infinie, mais le premier élément et un point d'interrogation. Le fait est que chaque élément suivant du flux est généré à la volée. Cette approche est appelée initialisation paresseuse. C'est lui qui vous permet de travailler en toute sécurité avec de telles structures.

Si vous n'avez jamais travaillé avec des structures de données infinies, alors vous pensez probablement: pourquoi est-ce nécessaire? Mais ils peuvent être extrêmement pratiques. Nous écrivons un flux qui renvoie un nombre arbitraire de nombres impairs, les convertit en chaîne et ajoute un espace:

 Stream oddNumbers = Stream .from(1, 2) //  1   2 .map(x -> x + " "); //  //   oddNumbers.take(5) .forEach(System.out::print); // 1 3 5 7 9 oddNumbers.take(10) .forEach(System.out::print); // 1 3 5 7 9 11 13 15 17 19 

Si simple.

Structure générale des collections


Après avoir discuté des structures de base, il est temps de regarder l'architecture générale des collections fonctionnelles vavr :



Chaque élément de la structure peut être utilisé comme itérable:

 StringBuilder builder = new StringBuilder(); for (String word : List.of("one", "two", "tree")) { if (builder.length() > 0) { builder.append(", "); } builder.append(word); } System.out.println(builder.toString()); // one, two, tree 

Mais vous devriez réfléchir à deux fois et voir le dock avant d'utiliser pour. La bibliothèque vous permet de faciliter les choses familières.

 System.out.println(List.of("one", "two", "tree").mkString(", ")); // one, two, tree 

Travailler avec des fonctions


La bibliothèque a un certain nombre de fonctions (8 pièces) et des méthodes utiles pour travailler avec elles. Ce sont des interfaces fonctionnelles ordinaires avec de nombreuses méthodes intéressantes. Le nom des fonctions dépend du nombre d'arguments acceptés (de 0 à 8). Par exemple, Function0 ne prend aucun argument, Function1 prend un argument, Function2 en prend deux, etc.

 Function2<String, String, String> combineName = (lastName, firstName) -> firstName + " " + lastName; System.out.println(combineName.apply("Griffin", "Peter")); // Peter Griffin 

Dans les fonctions de la bibliothèque vavr, nous pouvons faire beaucoup de choses sympas. En termes de fonctionnalité, ils dépassent de loin la fonction standard, la bi-fonction, etc. Par exemple, le curry. Le curry est la construction de fonctions en plusieurs parties. Regardons un exemple:

 //    Function2<String, String, String> combineName = (lastName, firstName) -> firstName + " " + lastName; //           Function1<String, String> makeGriffinName = combineName .curried() .apply("Griffin"); //      System.out.println(makeGriffinName.apply("Peter")); // Peter Griffin System.out.println(makeGriffinName.apply("Lois")); // Lois Griffin 

Comme vous pouvez le voir, très succinctement. La méthode au curry est extrêmement simple, mais peut être très utile.

Implémentation de la méthode au curry
 @Override default Function1<T1, Function1<T2, R>> curried() { return t1 -> t2 -> apply(t1, t2); } 


Il existe de nombreuses autres méthodes utiles dans le jeu de fonctions . Par exemple, vous pouvez mettre en cache le résultat de retour d'une fonction:

 Function0<Double> hashCache = Function0.of(Math::random).memoized(); double randomValue1 = hashCache.apply(); double randomValue2 = hashCache.apply(); System.out.println(randomValue1 == randomValue2); // true 


Lutte contre les exceptions


Comme nous l'avons dit plus tôt, le processus de programmation doit être sûr. Pour cela, il faut éviter divers effets étrangers. Les exceptions sont leurs générateurs explicites.

Vous pouvez utiliser la classe Try pour gérer en toute sécurité les exceptions dans un style fonctionnel. En fait, c'est une monade typique. Il n'est pas nécessaire de se plonger dans la théorie de l'utilisation. Regardez simplement un exemple simple:

 Try.of(() -> 4 / 0) .onFailure(System.out::println) .onSuccess(System.out::println); 

Comme vous pouvez le voir sur l'exemple, tout est assez simple. Nous suspendons simplement l'événement à une erreur potentielle et ne le dépassons pas des limites du calcul.

Correspondance de motifs


Il arrive souvent une situation dans laquelle nous devons vérifier la valeur d'une variable et modéliser le comportement du programme en fonction du résultat. Dans de telles situations, un merveilleux moteur de recherche de modèles vient à la rescousse. Vous n'avez plus besoin d'écrire un tas de sinon , configurez simplement toute la logique en un seul endroit.

 import static io.vavr.API.*; import static io.vavr.Predicates.*; public class PatternMatchingDemo { public static void main(String[] args) { String s = Match(1993).of( Case($(42), () -> "one"), Case($(anyOf(isIn(1990, 1991, 1992), is(1993))), "two"), Case($(), "?") ); System.out.println(s); // two } } 

Veuillez noter que le cas est en majuscule, comme case est un mot-clé et est déjà pris.

Conclusion


À mon avis, la bibliothèque est très cool, mais cela vaut la peine de l'utiliser très soigneusement. Elle peut exceller dans le développement événementiel . Cependant, son utilisation excessive et irréfléchie dans la programmation impérative standard basée sur un pool de threads peut apporter beaucoup de maux de tête. De plus, souvent dans nos projets, nous utilisons Spring et Hibernate, qui ne sont pas toujours prêts pour une telle application. Avant d'importer une bibliothèque dans votre projet, vous devez comprendre clairement comment et pourquoi elle sera utilisée. Ce dont je parlerai dans un de mes prochains articles.

Source: https://habr.com/ru/post/fr421839/


All Articles