Vavr User Guide
Vavr User Guide
5, 2024-12-06
1. Introduction
1.1. Functional Data Structures in Java 8 with Vavr
1.2. Functional Programming
1.2.1. Side-E�ects
1.2.2. Referential Transparency
1.2.3. Thinking in Values
1.3. Data Structures in a Nutshell
1.3.1. Mutable Data Structures
1.3.2. Immutable Data Structures
1.3.3. Persistent Data Structures
1.4. Functional Data Structures
1.4.1. Linked List
1.4.2. Queue
1.4.3. Sorted Set
1.5. State of the Collections
1.5.1. Seq
1.5.2. Set and Map
2. Getting started
2.1. Gradle
2.2. Maven
2.3. Standalone
2.4. Snapshots
2.4.1. Gradle
2.4.2. Maven
3. Usage Guide
3.1. Tuples
3.1.1. Create a tuple
3.1.2. Map a tuple component-wise
3.1.3. Map a tuple using one mapper
3.1.4. Transform a tuple
3.2. Functions
3.2.1. Composition
3.2.2. Lifting
3.2.3. Partial application
3.2.4. Currying
3.2.5. Memoization
3.3. Values
3.3.1. Option
3.3.2. Try
3.3.3. Lazy
3.3.4. Either
3.3.5. Future
3.3.6. Validation
3.4. Collections
3.4.1. List
3.4.2. Stream
3.4.3. Performance Characteristics
3.5. Property Checking
3.6. Pattern Matching
3.6.1. The Basics of Match for Java
3.6.2. Patterns
4. License
Vavr (formerly called Javaslang) is a functional library for Java 8+ that provides persistent data types and functional
control structures.
Vavr (http://vavr.io/) leveraged lambdas to create various new features based on functional patterns. One of them is a
functional collection library that is intended to be a replacement for Java’s standard collections.
(This is just a bird’s view, you will find a human-readable version below.)
Before we deep-dive into the details about the data structures I want to talk about some basics. This will make it clear
why I created Vavr and specifically new Java collections.
For example, if a function throws an exception and this exception is interpreted, it is considered as side-effect that
affects our program. Furthermore exceptions are like non-local goto-statements
(http://c2.com/cgi/wiki?DontUseExceptionsForFlowControl). They break the normal control-flow. However, real-world
applications do perform side-effects.
JAVA
int divide(int dividend, int divisor) {
// throws if divisor is zero
return dividend / divisor;
}
In a functional setting we are in the favorable situation to encapsulate the side-effect in a Try:
JAVA
// = Success(result) or Failure(exception)
Try<Integer> divide(Integer dividend, Integer divisor) {
return Try.of(() -> dividend / divisor);
}
This version of divide does not throw any exception anymore. We made the possible failure explicit by using the type
Try.
JAVA
// not referentially transparent
Math.random();
// referentially transparent
Math.max(1, 2);
Rich Hickey, the creator of Clojure, gave a great talk about The Value of Values
(https://www.youtube.com/watch?v=-6BsiVyC1kM). The most interesting values are immutable
(https://en.wikipedia.org/wiki/Immutable_object) values. The main reason is that immutable values
• are stable regarding equals and hashCode and thus are reliable hash keys
Vavr’s collection library comprises of a rich set of functional data structures built on top of lambdas. The only interface
they share with Java’s original collections is Iterable. The main reason is that the mutator methods of Java’s collection
interfaces do not return an object of the underlying collection type.
We will see why this is so essential by taking a look at the different types of data structures.
Java is an object-oriented programming language. We encapsulate state in objects to achieve data hiding and provide
mutator methods to control the state. The Java collections framework (JCF)
(https://en.wikipedia.org/wiki/Java_collections_framework) is built upon this idea.
JAVA
interface Collection<E> {
// removes all elements from this collection
void clear();
}
Immutable (https://en.wikipedia.org/wiki/Immutable_object) data structures cannot be modified after their creation. In the
context of Java they are widely used in the form of collection wrappers.
JAVA
List<String> list = Collections.unmodifiableList(otherList);
// Boom!
list.add("why not?");
There are various libraries that provide us with similar utility methods. The result is always an unmodifiable view of
the specific collection. Typically it will throw at runtime when we call a mutator method.
A persistent data structure (https://en.wikipedia.org/wiki/Persistent_data_structure) does preserve the previous version of itself
when being modified and is therefore effectively immutable. Fully persistent data structures allow both updates and
queries on any version.
Many operations perform only small changes. Just copying the previous version wouldn’t be efficient. To save time and
memory, it is crucial to identify similarities between two versions and share as much data as possible.
This model does not impose any implementation details. Here come functional data structures into play.
Also known as purely functional data structures (https://en.wikipedia.org/wiki/Purely_functional), these are immutable and
persistent. The methods of functional data structures are referentially transparent.
Vavr features a wide range of the most-commonly used functional data structures. The following examples are
explained in-depth.
One of the most popular and also simplest functional data structures is the (singly) linked List
(https://en.wikipedia.org/wiki/Linked_list). It has a head element and a tail List. A linked List behaves like a Stack which
follows the last in, first out (LIFO) (https://en.wikipedia.org/wiki/Stack_(abstract_data_type)) method.
Each of the List elements forms a separate List node. The tail of the last element is Nil, the empty List.
JAVA
// = List(0, 2, 3)
List<Integer> list2 = list1.tail().prepend(0);
The new head element 0 is linked to the tail of the original List. The original List remains unmodified.
These operations take place in constant time, in other words they are independent of the List size. Most of the other
operations take linear time. In Vavr this is expressed by the interface LinearSeq, which we may already know from
Scala.
If we need data structures that are queryable in constant time, Vavr offers Array and Vector. Both have random access
(https://en.wikipedia.org/wiki/Random_access) capabilities.
The Array type is backed by a Java array of objects. Insert and remove operations take linear time. Vector is in-between
Array and List. It performs well in both areas, random access and modification.
In fact the linked List can also be used to implement a Queue data structure.
A very efficient functional Queue can be implemented based on two linked Lists. The front List holds the elements that
are dequeued, the rear List holds the elements that are enqueued. Both operations enqueue and dequeue perform in
O(1).
JAVA
Queue<Integer> queue = Queue.of(1, 2, 3)
.enqueue(4)
.enqueue(5);
The initial Queue is created of three elements. Two elements are enqueued on the rear List.
If the front List runs out of elements when dequeueing, the rear List is reversed and becomes the new front List.
When dequeueing an element we get a pair of the first element and the remaining Queue. It is necessary to return the
new version of the Queue because functional data structures are immutable and persistent. The original Queue is not
affected.
JAVA
Queue<Integer> queue = Queue.of(1, 2, 3);
What happens when the Queue is empty? Then dequeue() will throw a NoSuchElementException. To do it the functional
way we would rather expect an optional result.
JAVA
// = Some((1, Queue()))
Queue.of(1).dequeueOption();
// = None
Queue.empty().dequeueOption();
JAVA
// = Queue(1)
Queue<Integer> queue = Queue.of(1);
// = Some((1, Queue()))
Option<Tuple2<Integer, Queue<Integer>>> dequeued =
queue.dequeueOption();
// = Some(1)
Option<Integer> element = dequeued.map(Tuple2::_1);
// = Some(Queue())
Option<Queue<Integer>> remaining =
dequeued.map(Tuple2::_2);
Sorted Sets are data structures that are more frequently used than Queues. We use binary search trees to model them
in a functional way. These trees consist of nodes with up to two children and values at each node.
We build binary search trees in the presence of an ordering, represented by an element Comparator. All values of the
left subtree of any given node are strictly less than the value of the given node. All values of the right subtree are
strictly greater.
JAVA
// = TreeSet(1, 2, 3, 4, 6, 7, 8)
SortedSet<Integer> xs = TreeSet.of(6, 1, 3, 2, 4, 7, 8);
Searches on such trees run in O(log n) time. We start the search at the root and decide if we found the element. Because
of the total ordering of the values we know where to search next, in the left or in the right branch of the current tree.
JAVA
// = TreeSet(1, 2, 3);
SortedSet<Integer> set = TreeSet.of(2, 3, 1, 2);
// = TreeSet(3, 2, 1);
Comparator<Integer> c = (a, b) -> b - a;
SortedSet<Integer> reversed = TreeSet.of(c, 2, 3, 1, 2);
Most tree operations are inherently recursive (https://en.wikipedia.org/wiki/Recursion). The insert function behaves
similarly to the search function. When the end of a search path is reached, a new node is created and the whole path is
reconstructed up to the root. Existing child nodes are referenced whenever possible. Hence the insert operation takes
O(log n) time and space.
JAVA
// = TreeSet(1, 2, 3, 4, 5, 6, 7, 8)
SortedSet<Integer> ys = xs.add(5);
In order to maintain the performance characteristics of a binary search tree it needs to be kept balanced. All paths
from the root to a leaf need to have roughly the same length.
Generally we are observing a convergence of programming languages. Good features make it, other disappear. But Java
is different, it is bound forever to be backward compatible. That is a strength but also slows down evolution.
Lambda brought Java and Scala closer together, yet they are still so different. Martin Odersky, the creator of Scala,
recently mentioned in his BDSBTB 2015 keynote (https://www.youtube.com/watch?v=NW5h8d_ZyOs) the state of the Java 8
collections.
He described Java’s Stream as a fancy form of an Iterator. The Java 8 Stream API is an example of a lifted collection.
What it does is to define a computation and link it to a specific collection in another explicit step.
JAVA
// i + 1
i.prepareForAddition()
.add(1)
.mapBackToInteger(Mappers.toInteger())
This is how the new Java 8 Stream API works. It is a computational layer above the well known Java collections.
JAVA
// = ["1", "2", "3"] in Java 8
Arrays.asList(1, 2, 3)
.stream()
.map(Object::toString)
.collect(Collectors.toList())
Vavr is greatly inspired by Scala. This is how the above example should have been in Java 8.
JAVA
// = Stream("1", "2", "3") in Vavr
Stream.of(1, 2, 3).map(Object::toString)
Within the last year we put much effort into implementing the Vavr collection library. It comprises the most widely
used collection types.
We started our journey by implementing sequential types. We already described the linked List above. Stream, a lazy
linked List, followed. It allows us to process possibly infinite long sequences of elements.
All collections are Iterable and hence could be used in enhanced for-statements.
JAVA
for (String s : List.of("Java", "Advent")) {
// side effects and mutation
}
We could accomplish the same by internalizing the loop and injecting the behavior using a lambda.
JAVA
List.of("Java", "Advent").forEach(s -> {
// side effects and mutation
});
Anyway, as we previously saw we prefer expressions that return a value over statements that return nothing. By
looking at a simple example, soon we will recognize that statements add noise and divide what belongs together.
JAVA
String join(String... words) {
StringBuilder builder = new StringBuilder();
for(String s : words) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(s);
}
return builder.toString();
}
The Vavr collections provide us with many functions to operate on the underlying elements. This allows us to express
things in a very concise way.
JAVA
String join(String... words) {
return List.of(words)
.intersperse(", ")
.foldLeft(new StringBuilder(), StringBuilder::append)
.toString();
}
Most goals can be accomplished in various ways using Vavr. Here we reduced the whole method body to fluent function
calls on a List instance. We could even remove the whole method and directly use our List to obtain the computation
result.
JAVA
List.of(words).mkString(", ");
In a real world application we are now able to drastically reduce the number of lines of code and hence lower the risk
of bugs.
Sequences are great. But to be complete, a collection library also needs different types of Sets and Maps.
We described how to model sorted Sets with binary tree structures. A sorted Map is nothing else than a sorted Set
containing key-value pairs and having an ordering for the keys.
Our Map does not have a special Entry type to represent key-value pairs. Instead we use Tuple2 which is already part of
Vavr. The fields of a Tuple are enumerated.
JAVA
// = (1, "A")
Tuple2<Integer, String> entry = Tuple.of(1, "A");
Maps and Tuples are used throughout Vavr. Tuples are inevitable to handle multi-valued return types in a general way.
JAVA
// = HashMap((0, List(2, 4)), (1, List(1, 3)))
List.of(1, 2, 3, 4).groupBy(i -> i % 2);
At Vavr, we explore and test our library by implementing the 99 Euler Problems (https://projecteuler.net/archives). It is a
great proof of concept. Please don’t hesitate to send pull requests.
GROOVY
dependencies {
compile "io.vavr:vavr:0.10.5"
}
Gradle 7+
GROOVY
dependencies {
implementation "io.vavr:vavr:0.10.5"
}
XML
<dependencies>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.5</version>
</dependency>
</dependencies>
Because Vavr does not depend on any libraries (other than the JVM) you can easily add it as standalone .jar to your
classpath.
GROOVY
repositories {
(...)
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
}
XML
<profiles>
<profile>
<id>allow-snapshots</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<repositories>
<repository>
<id>snapshots-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
Vavr comes along with well-designed representations of some of the most basic types which apparently are missing or
rudimentary in Java: Tuple , Value and λ .
In Vavr, everything is built upon these three basic building blocks:
Java is missing a general notion of tuples. A Tuple combines a fixed number of elements together so that they can be
passed around as a whole. Unlike an array or list, a tuple can hold objects with different types, but they are also
immutable.
Tuples are of type Tuple1, Tuple2, Tuple3 and so on. There currently is an upper limit of 8 elements. To access elements
of a tuple t , you can use method t._1 to access the first element, t._2 to access the second, and so on.
JAVA
// (Java, 8)
Tuple2<String, Integer> java8 = Tuple.of("Java", 8);
// "Java"
String s = java8._1;
// 8
Integer i = java8._2;
The component-wise map evaluates a function per element in the tuple, returning another tuple.
JAVA
// (vavr, 1)
Tuple2<String, Integer> that = java8.map(
s -> s.substring(2) + "vr",
i -> i / 8
);
JAVA
// (vavr, 1)
Tuple2<String, Integer> that = java8.map(
(s, i) -> Tuple.of(s.substring(2) + "vr", i / 8)
);
JAVA
// "vavr 1"
String that = java8.apply(
(s, i) -> s.substring(2) + "vr " + i / 8
);
Functional programming is all about values and transformation of values using functions. Java 8 just provides a
Function which accepts one parameter and a BiFunction which accepts two parameters. Vavr provides functions
up to a limit of 8 parameters. The functional interfaces are of called Function0, Function1, Function2, Function3
and so on. If you need a function which throws a checked exception you can use CheckedFunction1,
CheckedFunction2 and so on.
The following lambda expression creates a function to sum two integers:
JAVA
// sum.apply(1, 2) = 3
Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
JAVA
Function2<Integer, Integer, Integer> sum = new Function2<Integer, Integer, Integer>() {
@Override
public Integer apply(Integer a, Integer b) {
return a + b;
}
};
You can also use the static factory method Function3.of(…) to a create a function from any method reference.
JAVA
Function3<String, String, String, String> function3 =
Function3.of(this::methodWhichAccepts3Parameters);
In fact Vavr functional interfaces are Java 8 functional interfaces on steroids. They also provide features like:
• Composition
• Lifting
• Currying
• Memoization
You can compose functions. In mathematics, function composition is the application of one function to the result of
another to produce a third function. For instance, the functions f : X → Y and g : Y → Z can be composed to yield a
function h: g(f(x)) which maps X → Z.
You can use either andThen :
JAVA
Function1<Integer, Integer> plusOne = a -> a + 1;
Function1<Integer, Integer> multiplyByTwo = a -> a * 2;
then(add1AndMultiplyBy2.apply(2)).isEqualTo(6);
or compose :
JAVA
Function1<Integer, Integer> add1AndMultiplyBy2 = multiplyByTwo.compose(plusOne);
then(add1AndMultiplyBy2.apply(2)).isEqualTo(6);
You can lift a partial function into a total function that returns an Option result. The term partial function comes from
mathematics. A partial function from X to Y is a function f: X′ → Y, for some subset X′ of X. It generalizes the concept of a
function f: X → Y by not forcing f to map every element of X to an element of Y. That means a partial function works
properly only for some input values. If the function is called with a disallowed input value, it will typically throw an
exception.
The following method divide is a partial function that only accepts non-zero divisors.
JAVA
Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
We use lift to turn divide into a total function that is defined for all inputs.
JAVA
Function2<Integer, Integer, Option<Integer>> safeDivide = Function2.lift(divide);
// = None
Option<Integer> i1 = safeDivide.apply(1, 0);
// = Some(2)
Option<Integer> i2 = safeDivide.apply(4, 2);
A lifted function returns None instead of throwing an exception, if the function is invoked with disallowed
input values.
A lifted function returns Some , if the function is invoked with allowed input values.
The following method sum is a partial function that only accepts positive input values.
JAVA
int sum(int first, int second) {
if (first < 0 || second < 0) {
throw new IllegalArgumentException("Only positive integers are allowed");
}
return first + second;
}
JAVA
Function2<Integer, Integer, Option<Integer>> sum = Function2.lift(this::sum);
// = None
Option<Integer> optionalResult = sum.apply(-1, 2);
Partial application allows you to derive a new function from an existing one by fixing some values. You can fix one or
more parameters, and the number of fixed parameters defines the arity of the new function such that new arity =
(original arity - fixed parameters) . The parameters are bound from left to right.
JAVA
Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
Function1<Integer, Integer> add2 = sum.apply(2);
then(add2.apply(4)).isEqualTo(6);
This can be demonstrated by fixing the first three parameters of a Function5 , resulting in a Function2 .
JAVA
Function5<Integer, Integer, Integer, Integer, Integer, Integer> sum = (a, b, c, d, e) -> a + b + c + d + e;
Function2<Integer, Integer, Integer> add6 = sum.apply(2, 3, 1);
then(add6.apply(4, 3)).isEqualTo(13);
Partial application differs from Currying, as will be explored in the relevant section.
Currying is a technique to partially apply a function by fixing a value for one of the parameters, resulting in a
Function1 function that returns a Function1 .
When a Function2 is curried, the result is indistinguishable from the partial application of a Function2 because both
result in a 1-arity function.
JAVA
Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
Function1<Integer, Integer> add2 = sum.curried().apply(2);
then(add2.apply(4)).isEqualTo(6);
You might notice that, apart from the use of .curried() , this code is identical to the 2-arity given example in Partial
application. With higher-arity functions, the difference becomes clear.
JAVA
Function3<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
final Function1<Integer, Function1<Integer, Integer>> add2 = sum.curried().apply(2);
then(add2.apply(4).apply(3)).isEqualTo(9);
Memoization is a form of caching. A memoized function executes only once and then returns the result from a cache.
The following example calculates a random number on the first invocation and returns the cached number on the
second invocation.
JAVA
Function0<Double> hashCache =
Function0.of(Math::random).memoized();
then(randomValue1).isEqualTo(randomValue2);
Option is a monadic container type which represents an optional value. Instances of Option are either an instance of
Some or the None .
JAVA
// optional *value*, no more nulls
Option<T> option = Option.of(...);
If you’re coming to Vavr after using Java’s Optional class, there is a crucial difference. In Optional , a call to .map
that results in a null will result in an empty Optional . In Vavr, it would result in a Some(null) that can then lead to a
NullPointerException .
JAVA
Optional<String> maybeFoo = Optional.of("foo");
then(maybeFoo.get()).isEqualTo("foo");
Optional<String> maybeFooBar = maybeFoo.map(s -> (String)null)
.map(s -> s.toUpperCase() + "bar");
then(maybeFooBar.isPresent()).isFalse();
JAVA
Option<String> maybeFoo = Option.of("foo");
then(maybeFoo.get()).isEqualTo("foo");
try {
maybeFoo.map(s -> (String)null)
.map(s -> s.toUpperCase() + "bar");
Assert.fail();
} catch (NullPointerException e) {
// this is clearly not the correct approach
}
This seems like Vavr’s implementation is broken, but in fact it’s not - rather, it adheres to the requirement of a monad to
maintain computational context when calling .map . In terms of an Option , this means that calling .map on a Some
will result in a Some , and calling .map on a None will result in a None . In the Java Optional example above, that
context changed from a Some to a None .
This may seem to make Option useless, but it actually forces you to pay attention to possible occurrences of null and
deal with them accordingly instead of unknowingly accepting them. The correct way to deal with occurrences of null
is to use flatMap .
JAVA
Option<String> maybeFoo = Option.of("foo");
then(maybeFoo.get()).isEqualTo("foo");
Option<String> maybeFooBar = maybeFoo.map(s -> (String)null)
.flatMap(s -> Option.of(s)
.map(t -> t.toUpperCase() + "bar"));
then(maybeFooBar.isEmpty()).isTrue();
Alternatively, move the .flatMap to be co-located with the the possibly null value.
JAVA
Option<String> maybeFoo = Option.of("foo");
then(maybeFoo.get()).isEqualTo("foo");
Option<String> maybeFooBar = maybeFoo.flatMap(s -> Option.of((String)null))
.map(s -> s.toUpperCase() + "bar");
then(maybeFooBar.isEmpty()).isTrue();
Try is a monadic container type which represents a computation that may either result in an exception, or return a
successfully computed value. It’s similar to, but semantically different from Either . Instances of Try, are either an
instance of Success or Failure .
JAVA
// no need to handle exceptions
Try.of(() -> bunchOfWork()).getOrElse(other);
JAVA
import static io.vavr.API.*; // $, Case, Match
import static io.vavr.Predicates.*; // instanceOf
A result = Try.of(this::bunchOfWork)
.recover(x -> Match(x).of(
Case($(instanceOf(Exception_1.class)), t -> somethingWithException(t)),
Case($(instanceOf(Exception_2.class)), t -> somethingWithException(t)),
Case($(instanceOf(Exception_n.class)), t -> somethingWithException(t))
))
.getOrElse(other);
Lazy is a monadic container type which represents a lazy evaluated value. Compared to a Supplier, Lazy is memoizing,
i.e. it evaluates only once and therefore is referentially transparent.
JAVA
Lazy<Double> lazy = Lazy.of(Math::random);
lazy.isEvaluated(); // = false
lazy.get(); // = 0.123 (random generated)
lazy.isEvaluated(); // = true
lazy.get(); // = 0.123 (memoized)
You may also create a real lazy value (works only with interfaces):
JAVA
CharSequence chars = Lazy.val(() -> "Yay!", CharSequence.class);
Either represents a value of two possible types. An Either is either a Left or a Right. If the given Either is a Right and
projected to a Left, the Left operations have no effect on the Right value. If the given Either is a Left and projected to a
Right, the Right operations have no effect on the Left value. If a Left is projected to a Left or a Right is projected to a
Right, the operations have an effect.
Example: A compute() function, which results either in an Integer value (in the case of success) or in an error message
of type String (in the case of failure). By convention the success case is Right and the failure is Left.
JAVA
Either<String,Integer> value = compute().right().map(i -> i * 2).toEither();
A Future is a computation result that becomes available at some point. All operations provided are non-blocking. The
underlying ExecutorService is used to execute asynchronous handlers, e.g. via onComplete(…).
The computation finished successfully with a result, failed with an exception or was cancelled.
Callbacks may be registered on a Future at each point of time. These actions are performed as soon as the Future is
completed. An action which is registered on a completed Future is immediately performed. The action may run on a
separate Thread, depending on the underlying ExecutorService. Actions which are registered on a cancelled Future are
performed with the failed result.
JAVA
// future *value*, result of an async calculation
Future<T> future = Future.of(...);
The Validation control is an applicative functor and facilitates accumulating errors. When trying to compose Monads,
the combination process will short circuit at the first encountered error. But 'Validation' will continue processing the
combining functions, accumulating all errors. This is especially useful when doing validation of multiple fields, say a
web form, and you want to know all errors encountered, instead of one at a time.
Example: We get the fields 'name' and 'age' from a web form and want to create either a valid Person instance, or
return the list of validation errors.
JAVA
PersonValidator personValidator = new PersonValidator();
// Invalid(List(Name contains invalid characters: '!4?', Age must be greater than 0))
Validation<Seq<String>, Person> invalid = personValidator.validatePerson("John? Doe!4", -1);
The following validator is used to combine different validation results to one Validation instance.
class PersonValidator {
If the validation succeeds, i.e. the input data is valid, then an instance of Person is created of the given fields name
and age .
class Person {
@Override
public String toString() {
return "Person(" + name + ", " + age + ")";
}
Much effort has been put into designing an all-new collection library for Java which meets the requirements of
functional programming, namely immutability.
Java’s Stream lifts a computation to a different layer and links to a specific collection in another explicit step. With Vavr
we don’t need all this additional boilerplate.
JAVA
// 1000 random numbers
for (double random : Stream.continually(Math::random).take(1000)) {
...
}
TraversableOnce has a huge amount of useful functions to operate on the collection. Its API is similar to
java.util.stream.Stream (http://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html) but more mature.
Vavr’s List is an immutable linked list. Mutations create new instances. Most operations are performed in linear time.
Consequent operations are executed one by one.
JAVA
Arrays.asList(1, 2, 3).stream().reduce((i, j) -> i + j);
JAVA
IntStream.of(1, 2, 3).sum();
JAVA
// io.vavr.collection.List
List.of(1, 2, 3).sum();
The io.vavr.collection.Stream implementation is a lazy linked list. Values are computed only when needed.
Because of its laziness, most operations are performed in constant time. Operations are intermediate in general and
executed in a single pass.
The stunning thing about streams is that we can use them to represent sequences that are (theoretically) infinitely long.
JAVA
// 2, 4, 6, ...
Stream.from(1).filter(i -> i % 2 == 0);
Vector consteff consteff const eff const eff const eff const eff
Legend:
• consteff — effectively constant time, depending on assumptions like distribution of hash keys
Property checking (also known as property testing (http://en.wikipedia.org/wiki/Property_testing)) is a truly powerful way to
test properties of our code in a functional way. It is based on generated random data, which is passed to a user defined
check function.
Vavr has property testing support in its io.vavr:vavr-test module, so make sure to include that in order to use it in
your tests.
JAVA
Arbitrary<Integer> ints = Arbitrary.integer();
Scala has native pattern matching, one of the advantages over plain Java. The basic syntax is close to Java’s switch:
JAVA
val s = i match {
case 1 => "one"
case 2 => "two"
case _ => "?"
}
Pattern matching is a great feature that saves us from writing stacks of if-then-else branches. It reduces the amount of
code while focusing on the relevant parts.
Vavr provides a match API that is close to Scala’s match. It is enabled by adding the following import to our application:
JAVA
import static io.vavr.API.*;
Having the static methods Match, Case and the atomic patterns
JAVA
String s = Match(i).of(
Case($(1), "one"),
Case($(2), "two"),
Case($(), "?")
);
We use uniform upper-case method names because 'case' is a keyword in Java. This makes the API special.
The last wildcard pattern $() saves us from a MatchError which is thrown if no case matches.
Because we can’t perform exhaustiveness checks like the Scala compiler, we provide the possibility to return an
optional result:
JAVA
Option<String> s = Match(i).option(
Case($(0), "zero")
);
JAVA
Case($(predicate), ...)
JAVA
import static io.vavr.Predicates.*;
JAVA
String s = Match(i).of(
Case($(is(1)), "one"),
Case($(is(2)), "two"),
Case($(), "?")
);
JAVA
Case($(isIn("-h", "--help")), ...)
Match acts like an expression, it results in a value. In order to perform side-effects we need to use the helper function
run which returns Void :
JAVA
Match(arg).of(
Case($(isIn("-h", "--help")), o -> run(this::displayHelp)),
Case($(isIn("-v", "--version")), o -> run(this::displayVersion)),
Case($(), o -> run(() -> {
throw new IllegalArgumentException(arg);
}))
);
run is used to get around ambiguities and because void isn’t a valid return value in Java.
run must not be used as direct return value, i.e. outside of a lambda body:
JAVA
// Wrong!
Case($(isIn("-h", "--help")), run(this::displayHelp))
Otherwise the Cases will be eagerly evaluated before the patterns are matched, which breaks the whole Match
expression. Instead we use it within a lambda body:
JAVA
// Ok
Case($(isIn("-h", "--help")), o -> run(this::displayHelp))
As we can see, run is error prone if not used right. Be careful. We consider deprecating it in a future release and
maybe we will also provide a better API for performing side-effects.
JAVA
Number plusOne = Match(obj).of(
Case($(instanceOf(Integer.class)), i -> i + 1),
Case($(instanceOf(Double.class)), d -> d + 1),
Case($(), o -> { throw new NumberFormatException(); })
);
So far we directly matched values using atomic patterns. If an atomic pattern matches, the right type of the matched
object is inferred from the context of the pattern.
Next, we will take a look at recursive patterns that are able to match object graphs of (theoretically) arbitrary depth.
In Java we use constructors to instantiate classes. We understand object decomposition as destruction of objects into
their parts.
While a constructor is a function which is applied to arguments and returns a new instance, a deconstructor is a
function which takes an instance and returns the parts. We say an object is unapplied.
Object destruction is not necessarily a unique operation. For example, a LocalDate can be decomposed to
• the long value representing the epoch milliseconds of the corresponding Instant
• etc.
In Vavr we use patterns to define how an instance of a specific type is deconstructed. These patterns can be used in
conjunction with the Match API.
For many Vavr types there already exist match patterns. They are imported via
JAVA
import static io.vavr.Patterns.*;
JAVA
Match(_try).of(
Case($Success($()), value -> ...),
Case($Failure($()), x -> ...)
);
A first prototype of Vavr’s Match API allowed to extract a user-defined selection of objects from a match pattern.
Without proper compiler support this isn’t practicable because the number of generated methods exploded
exponentially. The current API makes the compromise that all patterns are matched but only the root patterns are
decomposed.
JAVA
Match(_try).of(
Case($Success($Tuple2($("a"), $())), tuple2 -> ...),
Case($Failure($(instanceOf(Error.class))), error -> ...)
);
Here the root patterns are Success and Failure. They are decomposed to Tuple2 and Error, having the correct generic
types.
Deeply nested types are inferred according to the Match argument and not according to the matched patterns.
It is essential to be able to unapply arbitrary objects, including instances of final classes. Vavr does this in a declarative
style by providing the compile time annotations @Patterns and @Unapply .
Note: Of course the patterns can be implemented directly without using the code generator. For more information
take a look at the generated source.
JAVA
import io.vavr.match.annotation.*;
@Patterns
class My {
@Unapply
static <T> Tuple1<T> Optional(java.util.Optional<T> optional) {
return Tuple.of(optional.orElse(null));
}
}
The annotation processor places a file MyPatterns in the same package (by default in target/generated-sources). Inner
classes are also supported. Special case: if the class name is $, the generated class name is just Patterns, without prefix.
Now we are able to match Optionals using guards.
JAVA
Match(optional).of(
Case($Optional($(v -> v != null)), "defined"),
Case($Optional($(v -> v == null)), "empty")
);
And yes, extracting null is weird. Instead of using Java’s Optional give Vavr’s Option a try!
JAVA
Match(option).of(
Case($Some($()), "defined"),
Case($None(), "empty")
);
http://www.apache.org/licenses/LICENSE-2.0
Version 0.10.5
Last updated 2024-12-06 17:38:04 UTC