Приветствую всех, кто устал от бесконечных проверок на 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 на примере пинг-понга. Игровой проект поможет вам лучше понять связь между написанием кода и результатом его выполнения, даже если вы никогда не программировали. Записаться можно на странице курса.

Комментарии (3)


  1. iboltaev
    20.09.2024 05:38
    +3

    Для тех, кто хочет привнести в Java немного функциональности, существует Scala.


  1. shirshov79
    20.09.2024 05:38
    +1

    Субъективное мнение, чем глубже человек погружается в функциональщину, тем геморройнее его код читать и поддерживать. Также автор на определенном этапе сам уже не понимает, что написано и уже и ему требуется вдумчивый анализ, что там наворочено. Поэтому я бы рекомендовал оставить java проекты в покое и нужна функциональщина, то пишите на scala или kotlin.


  1. aleksandy
    20.09.2024 05:38
    +2

    Статья опоздала лет эдак на несколько. Удобные of-методы и в стандартных коллекциях есть и они так же неизменяемыми получаются.

    Имена классов сторонней библиотеки, совпадающие с общепринятыми именами из стандартной библиотеки, - полный трешак, как, видя в коде List.of(1, 2, 3) понимать чей это список: вавровский или стандартный?

    используйте Option

    Зачем, если есть стандартный Optional?

    заменяйте мутабельные коллекции на неизменяемые аналоги из Vavr.

    Спасибо, не надо.