Функциональные коллекции в Java с Vavr: обзор и применение

от автора

Приветствую всех, кто устал от бесконечных проверок на null, громоздких блоков try-catch и мутирующих коллекций. Если вы когда-нибудь мечтали о том, чтобы привнести в Java немного функциональности, то я рад рассказать вам о библиотеке Vavr.

С появлением Java 8 мы наконец-то получили лямбда-выражения и Stream API. Это было как глоток свежего воздуха после долгих лет императивного программирования. Однако, по сравнению с другими ЯП, вроде Scala или Haskell, Java всё ещё ощущается как язык, созданный для ООП, а не для функционального программирования.

Функциональное программирование предлагает нам:

  • Неизменяемость: объекты не меняют своего состояния после создания.

  • Чистые функции: результат функции зависит только от её входных данных и не имеет побочных эффектов.

  • Функции как объекты первого класса: функции можно передавать, возвращать и хранить в переменных.

Vavr стремится привнести эти концепции в Java.

Установка

Для Maven:

Добавляем вpom.xml следующую зависимость:

<dependencies>     <dependency>         <groupId>io.vavr</groupId>         <artifactId>vavr</artifactId>         <version>0.10.4</version>     </dependency> </dependencies> 

Для Gradle:

В build.gradle добавьте:

dependencies {     implementation "io.vavr:vavr:0.10.4" }

Обзор синтаксиса Vavr

Кортежи

Кортежи позволяют объединять несколько значений различных типов в одну неизменяемую структуру без необходимости создавать отдельный класс.

import io.vavr.Tuple; import io.vavr.Tuple2;  Tuple2<String, Integer> user = Tuple.of("Alice", 30);  // Доступ к элементам String name = user._1; Integer age = user._2;

Можно создавать кортежи с количеством элементов до 8 Tuple8.

Функции: композиция, каррирование, мемоизация

Vavr расширяет функциональные интерфейсы Java, предоставляя функции с арностью до 8 Function8 и добавляя некоторые полезные методы.

Композиция функций позволяет объединять функции в цепочку, где выход одного метода становится входом другого:

import io.vavr.Function1;  Function1<Integer, Integer> multiplyBy2 = x -> x * 2; Function1<Integer, Integer> subtract5 = x -> x - 5;  Function1<Integer, Integer> combined = multiplyBy2.andThen(subtract5);  int result = combined.apply(10); // (10 * 2) - 5 = 15

Каррирование превращает функцию с несколькими аргументами в последовательность функций с одним аргументом:

import io.vavr.Function3;  Function3<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c; Function1<Integer, Function1<Integer, Function1<Integer, Integer>>> curriedSum = sum.curried();  int result = curriedSum.apply(1).apply(2).apply(3); // 6

Мемоизация кэширует результат функции для определённых аргументов, что может повысить производительность при повторных вызовах:

import io.vavr.Function1;  Function1<Integer, Integer> factorial = Function1.of(this::computeFactorial).memoized();  int result1 = factorial.apply(5); // Вычисляет и кэширует результат int result2 = factorial.apply(5); // Возвращает кэшированный результат  // Реализация функции факториала private int computeFactorial(int n) {     if (n == 0) return 1;     return n * computeFactorial(n - 1); }

Функциональные типы

Option заменяет использование null, представляя значение, которое может быть присутствующим Some или отсутствующим None:

import io.vavr.control.Option;  Option<String> maybeUsername = getUsername();  maybeUsername.map(String::toUpperCase)              .peek(name -> System.out.println("Hello, " + name))              .onEmpty(() -> System.out.println("No user logged in")); 

Try позволяет обрабатывать операции, которые могут выбросить исключение, в функциональном стиле:

import io.vavr.control.Try;  Try<Integer> parsedNumber = Try.of(() -> Integer.parseInt("123"));  parsedNumber.onSuccess(num -> System.out.println("Parsed number: " + num))             .onFailure(ex -> System.err.println("Failed to parse number: " + ex.getMessage()));

Lazy обеспечивает ленивое вычисление и кэширование результата:

import io.vavr.Lazy;  Lazy<Double> randomValue = Lazy.of(Math::random);  System.out.println(randomValue.isEvaluated()); // false double value = randomValue.get(); // Вычисляет и возвращает значение System.out.println(randomValue.isEvaluated()); // true 

Either представляет значение одного из двух возможных типов: Left (обычно ошибка) или Right (обычно успешный результат):

import io.vavr.control.Either;  Either<String, Integer> divisionResult = divide(10, 2);  divisionResult.peek(result -> System.out.println("Result: " + result))               .peekLeft(error -> System.err.println("Error: " + error));  // Реализация метода divide public Either<String, Integer> divide(int dividend, int divisor) {     if (divisor == 0) {         return Either.left("Cannot divide by zero");     } else {         return Either.right(dividend / divisor);     } }

Future используется для асинхронных операций, позволяя работать с их результатами в функциональном стиле:

import io.vavr.concurrent.Future;  Future<String> futureResult = Future.of(() -> longRunningOperation());  futureResult.onSuccess(result -> System.out.println("Operation completed: " + result))             .onFailure(ex -> System.err.println("Operation failed: " + ex.getMessage()));

Validation используется для накопления ошибок при валидации данных, вместо остановки после первой ошибки:

import io.vavr.collection.Seq; import io.vavr.control.Validation;  Validation<Seq<String>, User> userValidation = Validation.combine(     validateName(""),     validateAge(-5) ).ap(User::new);  if (userValidation.isValid()) {     User user = userValidation.get(); } else {     Seq<String> errors = userValidation.getError();     errors.forEach(System.err::println); }  // Реализация методов валидации public Validation<String, String> validateName(String name) {     return (name != null && !name.trim().isEmpty())         ? Validation.valid(name)         : Validation.invalid("Name cannot be empty"); }  public Validation<String, Integer> validateAge(int age) {     return (age > 0)         ? Validation.valid(age)         : Validation.invalid("Age must be positive"); }

Функциональные коллекции

Vavr предоставляет неизменяемые коллекции, которые расширяют Iterable и предлагают богатый функциональный API.

List

Неизменяемый список с функциональными методами:

import io.vavr.collection.List;  List<String> fruits = List.of("apple", "banana", "orange");  List<String> uppercaseFruits = fruits.map(String::toUpperCase);  System.out.println(uppercaseFruits); // [APPLE, BANANA, ORANGE]

Stream

Ленивая последовательность, которая может быть бесконечной:

import io.vavr.collection.Stream;  Stream<Integer> naturalNumbers = Stream.from(1);  Stream<Integer> evenNumbers = naturalNumbers.filter(n -> n % 2 == 0);  evenNumbers.take(5).forEach(System.out::println); // 2, 4, 6, 8, 10

Map

Неизменяемый ассоциативный массив:

import io.vavr.collection.HashMap;  HashMap<String, Integer> wordCounts = HashMap.of("hello", 1, "world", 2);  wordCounts = wordCounts.put("hello", wordCounts.get("hello").get() + 1);  System.out.println(wordCounts); // HashMap((hello, 2), (world, 2))

Set

Неизменяемое множество:

import io.vavr.collection.HashSet;  HashSet<String> colors = HashSet.of("red", "green", "blue");  HashSet<String> moreColors = colors.add("yellow").remove("green");  System.out.println(moreColors); // HashSet(red, blue, yellow)

Примеры использования Vavr

Обработка ошибок с помощью Try и Either

Ситуация: есть метод, который может выбросить исключение, и хочется обработать его без использования try-catch:

import io.vavr.control.Try;  Try<String> fileContent = Try.of(() -> readFile("path/to/file.txt"));  fileContent.onSuccess(content -> System.out.println("File content: " + content))            .onFailure(ex -> System.err.println("Error reading file: " + ex.getMessage()));

Или с использованием Either для более явной обработки ошибок:

import io.vavr.control.Either;  Either<String, String> result = readFile("path/to/file.txt");  result.peek(content -> System.out.println("File content: " + content))       .peekLeft(error -> System.err.println("Error: " + error));  // Реализация метода readFile public Either<String, String> readFile(String path) {     try {         String content = new String(Files.readAllBytes(Paths.get(path)));         return Either.right(content);     } catch (IOException e) {         return Either.left("Failed to read file: " + e.getMessage());     } }

Option для работы с потенциально отсутствующими значениями

Допустим, мы получаем значение из внешнего источника, которое может быть null:

Option<String> maybeEmail = Option.of(getUserEmail());  maybeEmail.filter(email -> email.contains("@"))           .peek(email -> System.out.println("Valid email: " + email))           .onEmpty(() -> System.out.println("Invalid or missing email"));

Future для асинхронных вычислений

Допустим, нужно выполнить несколько независимых асинхронных операций и дождаться их результатов:

Future<String> future1 = Future.of(() -> fetchDataFromService1()); Future<String> future2 = Future.of(() -> fetchDataFromService2());  Future<List<String>> combinedFuture = Future.sequence(List.of(future1, future2));  combinedFuture.onSuccess(results -> {     String result1 = results.get(0);     String result2 = results.get(1);     System.out.println("Results: " + result1 + ", " + result2); }).onFailure(ex -> System.err.println("Error fetching data: " + ex.getMessage()));

Паттерн-матчинг в Java с Vavr

Паттерн-матчинг позволяет обрабатывать разные варианты данных:

import static io.vavr.API.*; import static io.vavr.Predicates.*;  Object input = getInput();  String output = Match(input).of(     Case($(instanceOf(Integer.class).and(i -> (Integer) i > 0)), "Positive integer"),     Case($(instanceOf(Integer.class).and(i -> (Integer) i < 0)), "Negative integer"),     Case($(instanceOf(String.class)), str -> "String: " + str),     Case($(), "Unknown type") );  System.out.println(output);

С Vavr можно существенно улучшить качество кода и сделать разработку более приятной.

  • Начните с малого: используйте Option вместо null, Try вместо try-catch.

  • Постепенно вводите функциональные коллекции: заменяйте мутабельные коллекции на неизменяемые аналоги из Vavr.

Доп.ресурсы:

  • Официальная документация Vavr: vavr.io

  • GitHub репозиторий Vavr: github.com/vavr-io/vavr

  • Книга: «Functional Programming in Java» Венката Субраманиама


Всем новичкам в Java рекомендую присоединиться к открытому уроку, на котором участники познакомятся с Java на примере пинг-понга. Игровой проект поможет вам лучше понять связь между написанием кода и результатом его выполнения, даже если вы никогда не программировали. Записаться можно на странице курса.


ссылка на оригинал статьи https://habr.com/ru/articles/843352/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *