Vavr: Revolutionizing Your Perception of Java

6 min
AI 总结
$
|
Article Summary Image
Article Summary Image

Vavr Core is a lightweight yet comprehensive functional programming library. It introduces immutable collections, functional control structures, and various semantic containers, enabling Java engineers to enjoy the expressiveness of the functional world while maintaining runtime compatibility.

What is Vavr

  • Immutable Data Structures: Provides a complete set of immutable collections including List, Map, Stream, etc., naturally thread-safe.
  • Functional Syntax Sugar: Functional interfaces covering 0~8 parameters, with additional tools for currying, partial application, composition, etc.
  • Container Models: Types like Option, Try, Either, Validation make error handling and flow control more explicit.
  • Syntax Enhancements: Features like Property Checking and pattern matching fill gaps in native Java.

Adding Dependencies

Maven

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

Gradle (Groovy DSL)

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

Testing features require additional dependency io.vavr:vavr-test, see Property Checking section for details.

1. Tuples

1.1 Tuple Concept

In Vavr, Tuple1 ~ Tuple8 represent fixed-length, immutable combinations that can hold different types.

  • Indexing starts from 1, accessed via _1, _2, etc.
  • Typically used for returning multiple values or holding intermediate state, avoiding extra DTO declarations.

1.2 Creating Tuples

final Tuple2<Integer, String> user = Tuple.of(1, "Eli auk");
Assert.equals(user._1, 1);
Assert.equals(user._2, "Eli auk");

Use Tuple.of() static factory to create, type is automatically inferred based on element count.

1.3 Mapping Tuple Components Individually

final Tuple2<Integer, String> mapped = user.map(
        a -> a + 1,
        b -> b + "1"
);
Assert.equals(mapped._1, 2);
Assert.equals(mapped._2, "Eli auk1");

1.4 Using a Single Mapping Function

final Tuple2<Integer, String> mapped = user.map((a, b) -> Tuple.of(a + 1, b + "1"));
Assert.equals(mapped._1, 2);
Assert.equals(mapped._2, "Eli auk1");

When both fields need to be transformed together, you can directly return a new Tuple, avoiding multiple iterations.

1.5 Transforming Tuples

final String result = user.apply((a, b) -> b.substring(2) + a);
Assert.equals(result, "i auk1");

apply() directly uses each element as function parameters, returning any type of result, perfect for string concatenation or computing derived values.

2. Function

2.1 Functional Interfaces

Java 8 only has built-in functional interfaces with at most two parameters. Vavr extends this to Function0 ~ Function8, providing methods like memoized, curried, andThen, making it convenient to organize business logic in DSL style.

2.2 Creating Functions

Anonymous Class

Function2<Integer, Integer, Integer> sum = new Function2<>() {
    @Override
    public Integer apply(Integer a, Integer b) {
        return a + b;
    }
};

Lambda

Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;

Static Factory

final Function1<Integer, Integer> inc = Function1.of(a -> a + 1);

2.3 Composition

andThen: Current First, Then Parameter

Function1<Integer, Integer> plusOne = a -> a + 1;
Function1<Integer, Integer> multiply = a -> a * 2;
Function1<Integer, Integer> pipeline = plusOne.andThen(multiply);
Assert.equals(pipeline.apply(2), 6);

compose: Parameter First, Then Current

Function1<Integer, Integer> pipeline = plusOne.compose(multiply);
Assert.equals(pipeline.apply(2), 5);

Memory tip: andThen is “me first, you second”, compose is “you first, me second”.

2.4 Lifting

Partial functions throw exceptions on invalid input. Using lift() can wrap them into total functions:

Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
Option<Integer> safeDivide = Function2.lift(divide).apply(1, 0);
Assert.equals(safeDivide.isEmpty(), true);

2.5 Partial Application

Fix some parameters to get a new function, useful for presetting context.

Function2<Integer, Integer, Integer> sum = Integer::sum;
Function1<Integer, Integer> add1 = sum.apply(1);
Function5<Integer, Integer, Integer, Integer, Integer, Integer> sum5 = (a, b, c, d, e) -> a + b + c + d + e;
Function2<Integer, Integer, Integer> add6 = sum5.apply(2, 3, 1);
Assert.equals(add1.apply(2), 3);
Assert.equals(add6.apply(4).apply(5), 15);

Currying breaks FunctionN into a chain of single-parameter functions:

Function1<Integer, Integer> add1Curried = sum.curried().apply(1);
Function1<Integer, Function1<Integer, Function1<Integer, Function1<Integer, Integer>>>> curried = sum5.curried().apply(1);
Assert.equals(add1Curried.apply(2), 3);
Assert.equals(curried.apply(2).apply(3).apply(4).apply(5), 15);

Difference Summary:

  1. Syntax: Partial application uses .apply() to specify multiple parameters, currying uses .curried() first then .apply() one by one.
  2. Return Type: Partial application directly gets a new FunctionK, currying always returns a chain of single-parameter functions.
  3. Flexibility: Partial application can bind multiple parameters at once, currying is better for chaining composition.

2.6 Memoization

Memoized functions compute only once, subsequent calls read from cache; compared to storing variables directly, functions are lazily executed and composable.

Function0<Integer> randomGenerator = Function0.of(() -> new Random().nextInt()).memoized();
Integer first = randomGenerator.apply();
Integer second = randomGenerator.apply();
Assert.equals(first, second);

Common scenarios: caching expensive computations, lazy loading resources, composing multiple logic segments, or mocking dependencies in unit tests.

3. Value

3.1 Option

Encapsulates potentially null return values. API style is similar to Stream-Query’s Opp, supports chaining map, getOrElse, avoiding explicit null pointer checks.

3.2 Try

Captures exceptions and converts them to Success / Failure. You can also refer to Opp.ofTry in Stream-Query, quickly handling errors through methods like recover, onFailure.

3.3 Lazy

A lazy evaluation container with built-in memoization capability.

Lazy<Double> lazy = Lazy.of(Math::random);
lazy.isEvaluated(); // false
lazy.get();         // 0.123...
lazy.isEvaluated(); // true
Assert.equals(lazy.get(), lazy.get());

Compared to Supplier, Lazy executes only once, while Supplier recomputes every time.

3.4 Either

Left value typically represents error, right value represents success.

public Either<String, Integer> compute() {
    if (RandomUtil.randomBoolean()) {
        return Either.right(42);
    }
    return Either.left("Computation failed");
}

Either<String, Integer> value = compute().map(i -> i * 2);
if (value.isRight()) {
    System.out.println("Success: " + value.get());
} else {
    System.out.println("Failure: " + value.getLeft());
}

3.5 Validation

Validation is an applicative functor, suitable for aggregating errors from multiple fields.

class Person {
    final String name;
    final int age;
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

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) {
    return name == null || name.trim().isEmpty()
            ? Validation.invalid("Name cannot be empty")
            : Validation.valid(name);
}

private Validation<String, Integer> validateAge(int age) {
    return age < 0 || age > 150
            ? Validation.invalid("Age must be between 0 and 150")
            : Validation.valid(age);
}

Validation<Seq<String>, Person> validPerson = validatePerson("John", 30);
Assert.equals(validPerson.get().age, 30);
Assert.equals(validPerson.get().name, "John");

Validation<Seq<String>, Person> invalidPerson = validatePerson(" ", -1);
Assert.equals(
        invalidPerson.getError().asJava(),
        List.of("Name cannot be empty", "Age must be between 0 and 150").asJava()
);

4. Collection

Vavr’s collections are immutable by default, with chaining APIs that bring less boilerplate code.

4.1 List

Optional<Integer> reduce = Stream.of(1, 2, 3).reduce(Integer::sum);
int sum = IntStream.of(1, 2, 3).sum();
Number listSum = List.of(1, 2, 3).sum();
Assert.equals(listSum.intValue(), sum);

4.2 Stream

Because of built-in tuples, Stream’s transformation capabilities are more friendly than the JDK version.

java.util.Map<Integer, Character> vanilla = Stream.of(1, 2, 3)
        .collect(Collectors.toMap(a -> a, b -> (char) (b + 64)));
Map<Integer, Character> vavrMap = List.of(1, 2, 3).toMap(a -> a, b -> (char) (b + 64));
Assert.equals(vavrMap.get(1).get(), vanilla.get(1));

HashMap<Integer, Character> javaMap = List.of(1, 2, 3)
        .toJavaMap(HashMap::new, a -> Tuple.of(a, (char) (a + 64)));
Assert.equals(javaMap.get(1), 'A');

5. Property Checking

Requires additional dependency:

<dependency>
  <groupId>io.vavr</groupId>
  <artifactId>vavr-test</artifactId>
  <version>0.10.4</version>
</dependency>

Example:

Arbitrary<Integer> ints = Arbitrary.integer();

Property.def("square(int) >= 0")
        .forAll(ints)
        .suchThat(i -> i * i >= 0)
        .check()
        .assertIsSatisfied();

6. Pattern Matching

6.1 Basic Matching

String s = Match(1).of(
        Case($(1), "one"),
        Case($(2), "two"),
        Case($(), "?")
);
Assert.equals(s, "one");

String opt = Match(null).of(
        Case($(1), "one"),
        Case($(2), "two"),
        Case($(), "?")
);
Assert.equals(opt, "?");

6.2 Multi-Condition Matching

int input = 5;
String output = Match(input).of(
        Case($(n -> n > 0 && n < 3), "Between 1 and 2"),
        Case($(n -> n > 3 && n < 6), "Between 4 and 5"),
        Case($(), "Other")
);
Assert.equals(output, "Between 4 and 5");

6.3 Predicate Matching

String s = Match(2).of(
        Case($(a -> a == 1), "one"),
        Case($(a -> a == 2), "two"),
        Case($(), "?")
);
Assert.equals(s, "two");

Built-in predicates (is(), isIn(), isNotNull(), isZero(), etc.) can make conditions more semantic:

String opt = Match(null).of(
        Case($(is(1)), "one"),
        Case($(is(2)), "two"),
        Case($(), "?")
);
Assert.equals(opt, "?");

Through Vavr, you can embrace immutable models and functional expressions while maintaining Java ecosystem compatibility, making business code more concise and safer.