review and application

Greetings to all who are tired of endless checks on nullbulky blocks try-catch and mutating collections. If you've ever dreamed of bringing some functionality to Java, I'm happy to introduce you to the Vavr library.

With Java 8, we finally got lambda expressions and the Stream API. It was a breath of fresh air after years of imperative programming. However, compared to other languages ​​like Scala or Haskell, Java still feels like a language made for OOP rather than functional programming.

Functional programming offers us:

  • Immutability: objects do not change their state after creation.

  • Pure functions: the result of a function depends only on its input data and has no side effects.

  • Functions as first-class objects: functions can be passed, returned, and stored in variables.

Vavr aims to bring these concepts to Java.

Installation

For Maven:

Add inpom.xml the following dependency:

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

For Gradle:

IN build.gradle add:

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

Vavr Syntax Overview

Corteges

Tuples allow you to combine multiple values ​​of different types into a single immutable structure without having to create a separate class.

import io.vavr.Tuple;
import io.vavr.Tuple2;

Tuple2<String, Integer> user = Tuple.of("Alice", 30);

// Доступ к элементам
String name = user._1;
Integer age = user._2;

You can create tuples with up to 8 elements. Tuple8.

Functions: composition, currying, memoization

Vavr extends Java functional interfaces by providing functions with arity up to 8 Function8 and adding some useful methods.

Function composition allows you to chain functions together, where the output of one method becomes the input of another:

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

Currying turns a function with multiple arguments into a sequence of functions with one argument:

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

Memoization caches the function result for certain arguments, which can improve performance on repeated calls:

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);
}

Functional types

Option replaces the use nullrepresenting a value that may be present Some or absent 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 allows you to handle operations that may throw an exception in a functional style:

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 provides lazy evaluation and caching of the result:

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 represents a value of one of two possible types: Left (usually a mistake) or Right (usually successful outcome):

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 is used for asynchronous operations, allowing you to work with their results in a functional style:

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 used to accumulate errors during data validation, instead of stopping after the first error:

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");
}

Functional collections

Vavr provides immutable collections that extend Iterable and offer a rich functional API.

List

Immutable list with functional methods:

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

A lazy sequence that can be infinite:

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

Immutable associative array:

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

Immutable 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 Usage Examples

Handling Errors with Try and Either

Situation: there is a method that can throw an exception, and you want to handle it without using 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()));

Or using Either for more explicit error handling:

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 for dealing with potentially missing values

Let's say we get a value from an external source, which can be 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 for asynchronous computations

Let's say you need to perform several independent asynchronous operations and wait for their results:

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()));

Pattern Matching in Java with Vavr

Pattern matching allows you to process different data options:

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);

With Vavr you can significantly improve the quality of your code and make development more enjoyable.

  • Start small: use Option instead of null, Try instead of try-catch.

  • Introduce functional collections gradually: Replace mutable collections with immutable counterparts from Vavr.

Additional resources:


For all Java newbies, I recommend joining this open lesson where participants will learn Java using the example of ping-pong. The game project will help you better understand the relationship between writing code and the result of its execution, even if you have never programmed before. You can sign up on the course page.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *