Practical Vavr

Read the entire book below

Practical Vavr is a clearly structured overview of basic functional programming concepts with Vavr. There are many examples in this book to help you understand most of the existing Vavr APIs.

I like the pointers Alexandre gives about my design decisions and how he puts topics in a larger context.

This is a book I am recommending to all Vavr users. It is a great introduction to Vavr for beginners and an everyday reference for experts.

Daniel Dietrich, Creator of Vavr

Vavr

Vavr (formerly called Javaslang) is a functional library for Java.

As stated from the official repository of Vavr:

Vavr is an object-functional language extension to Java 8, which aims to reduce the lines of code and increase code quality. It provides persistent collections, functional abstractions for error handling, concurrent programming, pattern matching, and much more.

Vavr fuses the power of object-oriented programming with the elegance and robustness of functional programming. The most interesting part is a feature-rich, persistent collection library that smoothly integrates with Java's standard collections.

Because Vavr does not depend on any libraries (other than the JVM) you can easily add it as standalone .jar to your classpath.

To sum up, it's a Java library that helps to reduce the amount of code and to increase the robustness, using concepts from functional programming, immutable values, and control structures to operate on these values.

Vavr has been originally developed by Daniel Dietrich and was released in March 2014.

Why

Have you heard about Functional Programming, or have you been attracted by it but still need to use Java? Do you want to write more robust code, but in a different way, a more elegant way? Do you want a library that gives you all these well-thought building blocks in a well-organized API?

Well, Vavr is the library you need. It can improve your code, give you confidence in writing it, make you design your business code better, and can prevent you from falling into programming traps and all kinds of exceptions.

How

Using Vavr

The latest released version as the date I'm writing these lines is 0.10.5.

If you are using Maven just add the following dependency to your pom.xml file

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

If you are using Gradle, just add the following dependency to your build.gradle file

dependencies {
    compile "io.vavr:vavr:0.10.5"
}

And finally, if you're not using any build system, you can just download the latest version and drop the JAR in your classpath.

That's it, you're ready to rock some code in your favorite code editor or IDE and benefit instantly from the features of Vavr.

This book

Why

I have been using Vavr before it was called Vavr, I discovered it beginning of 2015, and have been using it pretty extensively since then.

As a Tech Lead and Software Craftsman in my day to day job, I like to promote this library to help developers write less and safer code with better quality, understand functional programming idioms and how to apply them in their work.

I've been teaching this library for multiple years, and developers always tend to have the same set of questions on how to get going with it, when to use it, also how and why to use it.

This book will cover what I think are the best parts of Vavr, how the API works, and what it has to offer in your real world day-to-day programming.

Code samples

Code example will be using Java 15 with preview enabled so that we can benefit from features like the new instanceof syntactic sugar, and from records to avoid Lombok when not necessary.

The code is available on my github/agrison.

About me

My name is Alexandre, I am a Software Engineer from Metz in France and working as a Tech Lead and Software Craftsman in Luxembourg.

In my spare time, I like to code in various languages including Java (with Vavr), Clojure/ClojureScript, Kotlin, JavaScript, Go, Python, OCaml, and a few others. I'm interested in both backend and frontend having practiced them extensively.

From APIs to mobile, through the cloud and databases, preferably using a functional programming language.

Besides coding, I am passionate about my wife Jessica, my daughter Eva and my son William, I also love to travel and practice photography.

You can find me at grison.me and on Twitter at @algrison.

The cover

The book's cover is a picture of Metz at sunrise as seen from the Plan d'eau.

It features the Cathedrale Saint Etienne de Metz on the left, which is the Cathedral having one of the highest naves in the world and the largest expanse of stained glass in the world.

Besides, on the right you'll see a Torii - a traditional Japanese gate - which was installed around 1985 (the year I was born) during a Japanese exposition in Metz.

If you happen to be visiting Metz, don't hesitate to drop me a message :)

Thank you

Thank you to all the readers, especially to Mikalai Zaikin for the great feedback and help he provided regarding typos and sentences improvements.

Table of Content


Tuple

Let's say you want to create a collection of heterogeneous elements. A small collection going from 0 to 8 elements.

Of course, you can create a custom class or record for it, however, sometimes you don't need or want to.

Here comes the Tuple type. Vavr's Tuples are immutable and are of type Tuple0, Tuple1, Tuple2, …, Tuple8.

Creating Tuples

In order to create a Tuple, just use the factory method Tuple.of().

var t = Tuple.of("Alex", 36); // Tuple2

Notice that this Tuple contains two different types, one String and one Integer.

record Customer(String name, int age) {}

var t = Tuple.of(42, "Hello", new Customer("Alex", 36)); // Tuple3
t._1; // 42
t._2; // "Hello"
t._3; // Customer(name = "Alex", age = 36) 

// you can also use _N as a method
t._1(); // 42
t._2(); // "Hello"
t._3(); // Customer(name = "Alex", age = 36) 

In this example, the Tuple contains three elements, one Integer, one String, and one Customer.

Finally, you can also create a Tuple2 from a Map.Entry.

var t = Tuple.of(new SimpleEntry(42, "foo"));

Creating Tuples from Iterables

You can create an instance of Tuple with an Iterable of tuples using the sequenceN method, where N refers to the size of the Tuple.

List<Tuple2<Integer, String>> list = Arrays.asList(
    Tuple.of(1, "foo"), Tuple.of(2, "bar"));

var t = Tuple.sequence2(list); // Tuple2(Seq(1, 2), Seq("foo", "bar"))

Note that it returns a TupleN<Seq<T1>, ...> but implementation related the Seq are of type Stream.

Accessing elements

Each TupleN implementation will provide a _N accessors to let you get the element. You can also use the _N() method to access it.

Mapping a tuple

There are two ways to map a function, providing N functions:

var t = Tuple.of("Alex", 36);
var newTuple = t.map(String::toUpperCase, age -> ++age);
newTuple._1(); // "ALEX"
newTuple._2(); // 37

// t stays unchanged
t._1(); // "Alex"
t._2(); // 36

or one with N-arities.

var t = Tuple.of("Alex", 36);
var newTuple = t.map((name, age) -> Tuple.of(name.toUpperCase(), ++age));

newTuple._1(); // "ALEX"
newTuple._2(); // 37

// t stays unchanged
t._1(); // "Alex"
t._2(); // 36

If you only need to map a specific Tuple element you can use mapN() where N is the element position.

var t = Tuple.of("Alex", 36);
var newTuple = t.map2(age -> ++age); 
newTuple._1(); // "Alex"
newTuple._2(); // 37

// t stays unchanged
t._1(); // "Alex"
t._2(); // 36

Updating a Tuple

Vavr offers an updateN() method, where N is the element position to update.

var t = Tuple.of("Alex", 36);
var newTuple = t.update2(50);

newTuple._1(); // "John"
newTuple._2(); // 50

It is similar to mapN except that mapN takes a Function whereas updateN takes directly the value to update the tuple element with.

Transforming Tuples

If you want to transform a Tuple to a new type, there's the apply method for that.

var t = Tuple.of("Alex", 36)

var hello = t.apply((name, age) -> "My name is " + name + " and I'm " + age + 
    " years old");

System.out.println(hello); 
// "My name is Alex and I'm 36 years old"

Getting the size

If you want to retrieve the size of a Tuple instance, just ust the arity method.

var t = Tuple.of("Alex", 36);
t.arity(); // 2

Growing Tuples

Let's say you want to grow your Tuple from 2 to 3 elements, then use the append method.

record Address(String street, String city) {}

var t = Tuple.of("Alex", 36);
var newTuple = t.append(new Address("rue Serpenoise", "Metz"));
newTuple.arity(); // 3
newTuple._3(); // Address(street = "rue Serpenoise", city = "Metz")

Vavr also offers the possibility to concat tuples, it's like append but with other tuples.

record Address(String street, String city) {}
record Nationality(String nationality, String birthPlace) {}

var t = Tuple.of("Alex", 36);
var p = Tuple.of(new Address("rue Serpenoise", "Metz"),
                 new Nationality("French", "Paris"));
var newTuple = t.concat(p);
newTuple.arity(); // 4
newTuple._3(); // Address(street = "rue Serpenoise", city = "Metz")
newTuple._4(); // Nationality(street="French", birthPlace="Paris")

Converting to a sequence

Instances of Tuple can be converted to a Seq using toSeq() (with a List implementation).

var t = Tuple.of("foo", "bar", "bazz");
t.toSeq(); // Seq(foo, bar, bazz)

Hashing objects

This comes as a practical utility, the Tuple type has a method hash which will compute a hash of all the given objects.

int h = Tuple.hash("foo", "bar"); // ...

Which is equivalent to:

(31 * (31 + Objects.hashCode("foo")) + Objects.hashCode("bar"))

Tuples and Maps

Tuples are used by Vavr to represent and work with entries in a Map collection, thus you can create a Map from a Tuple, iterate on Tuples representing each entry of the Map, get the first and last elements as a Tuple, and much more, but we'll see about this in the Collections chapter.


In its io.vavr.control package, Vavr provide useful values like Option, Either and Try. These values share similar APIs and as such, some operations are valid on these 3 types.

You will then find similarities in the following sections.

Option

The Option type from Vavr is a replacement for Java's Optional. It is referred to as a monadic container type which represents an optional value.

Instances of Option are either an instance of Some or None.

The API is similar to the one of Optional and what can be found in languages like Scala or Haskell.

Creating an Option

While Java offers both of and ofNullable to create an Optional, Vavr offers only of. An Option created with of cannot hold a null, consider Option.of similar to Optional.ofNullable.

If you

var o1 = Option.of("foo"); // Option<String>
var o2 = Option.some("foo"); // Option<String>
o1.equals(o2); // true

You may also want to create an Option from a Java's Optional:

var o = Option.ofOptional(Optional.ofNullable(42));

Conditionally creating an Option

Vavr offers the when method to conditionally create an Option:

var i = 42
var o1 = Option.when(i % 2 == 0, "foo");
o1.isDefined(); // true
var o2 = Option.when(i > 0, () -> "foo");
o2.isDefined(); // true
var o3 = Option.when(i < 0, "bar");
o3.isEmpty(); // true

Creating an Option from a sequence

You can use the sequence method to create an Option from an Iterable of Options.

The resulting Option will hold the given values if all Options were defined, None otherwise.

var o1 = Option.sequence(List.of(Option.of(1), Option.of(2)));
o1.isDefined(); // true
o1.get(); // Seq(1, 2)

var o2 = Option.sequence(List.of(Option.of(1), Option.none(), Option.of(2)));
o2.isDefined(); // false

This is not to be confused with filtering a list for non defined Options.

Vavr also offers the traverse method which does exactly like the sequence method but provides a mapping on the sequence before rejecting them if they contain any empty Option.

// ensure we continue only if all the ints are positive
var o1 = Option.traverse(List.of(1, 2, 3), e -> Option.of(e > 0 ? e : null));
o1.isDefined(); // true
o1.get(); // Seq(1, 2, 3)

var o2 = Option.traverse(List.of(1, 2, -1, 3, -4), e -> Option.of(e > 0 ? e : null));
o2.isDefined(); // false

Checking the status of an Option

There are multiple ways to know if an Option is defined or not.

var o = Option.of("foo");
o.isDefined(); // true
o.isEmpty(); // false

Creating an Option holding null

Well, if you need such a thing (business-related), then you can do it like this:

var o = Option.some(null)
o.isDefined(); // true
o.get(); // null

But be aware of all the NullPointerException that might occurs later on.

Running side effects

You can run side effects when the Option is empty using onEmpty.

var r = new Random();
var o = Option.of(r.nextBoolean() ? r.nextInt() : null);
o.onEmpty(() -> System.out.println("Bad Luck")); // maybe printing "Bad Luck"

The onEmpty method won't run any side effect if the Option is defined.

You may look at the Option API and see that there is no onDefined method, and you're totally right, this method is named peek, the API is different in that onEmpty takes a Runnable whereas peek takes a Consumer<T>, because the Option is defined and so you have to deal with the value it is holding.

var r = new Random();
var o = Option.of(r.nextBoolean() ? r.nextInt() : null);
o.peek(i -> System.out.println("Feeling lucky: " + i)); // may print "Feeling lucky: 42"

Getting the value out of an Option

Try not using get without knowing if the Option is defined, otherwise it will crash on you. Use getOrElse or orElse to do so.

var age = Option.of(42).getOrElse(0); // 42
var age = Option.of(null).getOrElse(0); // 0

var alex = Option.of(null);
var eva = Option.of("Eva");
var cute = alex.orElse(eva); // Option(Eva)

The orElse method also supports a Supplier.

In case an Option being empty is critical and you need to crash, Vavr has got you covered with getOrElseThrow.

var r = new Random();
var o = Option.of(r.nextBoolean() ? r.nextInt() : null);
var luckyNumber = o.getOrElseThrow(() -> 
    new RuntimeException("No losers in this Casino"));

Last option you have to get a value out of an Option is by using fold.

var r = new Random();
var o = Option.of(r.nextBoolean() ? r.nextInt() : null);
var str = o.fold(() -> "Bad Luck", i -> "Feeling lucky: " + i);

The fold methods takes a Supplier and a Function. The Supplier is used to provide a value if the Option is empty, and the Function is used to compute a value from the Option, thus the three following forms are essentially the same:

var o = Option.of(r.nextBoolean() ? r.nextInt() : null);
var s1 = o.getOrElse("Bad Luck");                      // both are
var s2 = o.fold(() -> "Bad Luck", Function::identity); // essentially the same

Use fold if you need to get the value out of a map by transforming it also, it's like doing a map just before a getOrElse.

var o = Option.of(r.nextBoolean() ? r.nextInt() : null);
var s1 = o.map(i -> "Feeling lucky: " + i).getOrElse("Bad Luck"); // both are
var s2 = o.fold(() -> "Bad Luck", i -> "Feeling lucky: " + i));   // the same

Filtering an Option

You may wish to set an Option to None if the value that it holds doesn't satisfy a given Predicate. In such a case, use the filter method.

var o = Option.of(41);
var even = o.filter(i -> i % 2 == 0);
even.isEmpty(); // true

var odd = o.filter(i -> i % 2 == 1);
odd.isDefined(); // true

Mapping an Option

Option is a powerful type to work with, and you may want to keep working with it until it makes sense to get the value out of this magical box. As said in the introduction of the chapter, Option is a monadic container, and as such it provides an API to transform what's inside the container, but it does so safely for you.

Indeed, if an Option is empty, mapping its content will have no effect at all.

var str = Option.of("alex")
              .map(String::toUpperCase)
              .getOrElse("Could not proceed");

In the following example I am using StringUtils from the commons-lang3 library:

<dependency>
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-lang3</artifactId>
           <version>3.11</version>
</dependency>

You can use as many map as you want, and remember, mapping on an empty Option has no effect:

import org.apache.commons.lang3.StringUtils;

var str = Option.of(null)
              .map(StringUtils::reverse)
              .map(StringUtils::lowerCase)
              .map(StringUtils::capitalize)
              .map(s -> s.concat(" is enjoying Vavr"))
              .getOrElse("Could not proceed");
System.out.println(str); // "Could not proceed"

Now imagine that the Option was defined:

var str = Option.of("XELA")
              .map(StringUtils::reverse)
              .map(StringUtils::lowerCase)
              .map(StringUtils::capitalize)
              .map(s -> s.concat(" is enjoying Vavr"))
              .getOrElse("Could not proceed");
System.out.println(str); // "Alex is enjoying Vavr"

Monadic containers help you think in terms of pipeline of transformation, and keeping in mind the happy path, letting you handle the failure case when you need the value.

The code above is equivalent to:

var o = Option.of("XELA");
String s = "Could not proceed";
if (o.isDefined()) { // without Option you would check == null instead
   s = StringUtils.capitalize(
          StringUtils.lowerCase(StringUtils.reverse(o.get())))
       + " is enjoying Vavr";
}
System.out.println(s);

When accustomed to monadic containers, the first example looks better (I think) and conveys much more the business intent of the code which is:

  1. try to reverse the string
  2. then lowercase it
  3. then capitalize it
  4. then concatenate some other string with it
  5. then get the final result
    1. but if any of this fails I want the Could not proceed string instead.

Mapping and nulls or other Option

As said earlier in this chapter, an Option can be either Some or None, but it can be a Some(null), and this can be problematic.

Let's see an example:

var str = Option.of("ALEX")
                .map(String::toLowerCase)
                .map(s -> s.length() < 10 ? null : s)
                .map(s -> s + " size is: " + s.length()) // BOOM: NullPointerException
                .getOrElse("Could not proceed");

Now this is where flatMap comes into action:

var str = Option.of("ALEX")
                .map(String::toLowerCase)
                .flatMap(s -> Option.of(s.length() < 10 ? null : s))
                .map(s -> s + " size is: " + s.length()) // no BOOM :-)
                .getOrElse("Could not proceed");
System.out.println(str); // "Could not proceed"

In the last example we encapsulated the possible null in an Option but to avoid dealing with an Option<Option<T>>, we used flatMap which gives an Option<T> if the inner Option was defined, None otherwise.

Transforming an Option

You can transform an Option to another type using transform.

var length = Option.of("foo").transform(o -> o.getOrElse("").length()); // int = 3

// wich is essentially the same as
var length = Option.of("foo").map(String::length).getOrElse(0);

Using with collectors

Interrop with the Java Collectors is provided via collect()

Option.of("foo").collect(Collectors.toList()); // List(foo)
// IntSummaryStatistics{count=1, sum=3, min=3, average=3, max=3}
Option.of("foo").collect(Collectors.summarizingInt(String::length));

Generally the collect method is available on most of Vavr's types.


Either

The Either object represents a value of two possible types. It's either a Left or a Right.

By convention a Left represents an error, and a Right a computed value.

Like Option, Either is a monadic container, and thus provide ways to transfrom the two branches that it holds, these transformations are safe in that trying to map on Left when the Either is Right will have no effect, same in the other way.

Creating an Either

Use left or right to create an Either instance.

var e1 = Either.left("foo"); // Left<String>
var e2 = Either.some("bar"); // Right<String>

Creating an Either from a sequence

You can use the sequence method to create an Either from an Iterable of Eithers.

If the Iterable contains any Left then sequence will return a Left with all the Lefts provided in the Iterable, otherwise it will return a Right with all the provided Rights.

var e1 = Either.sequence(List.of(
             Either.left("foo"), Either.right(1), Either.left("bar")));
e1.isLeft(); // true
e1.getLeft(); // Vector("foo", "bar")

var e2 = Either.sequence(List.of(
             Either.right(1), Either.right(2)));
e2.isRight(); // true
e2.getRight(); // Vector(1, 2)

Vavr provides also sequenceRight which is similar to sequence except that in case of any Left in the Iterable, it will return a Left containing the first Left value (in iteration order).

var e1 = Either.sequenceRight(List.of(
             Either.left("foo"), Either.right(1), Either.left("bar")));
e1.isLeft(); // true
e1.getLeft(); // "foo"

This is not to be confused with filtering a list of Either.

Vavr also offer the traverse and traverseRight method which does exactly like the sequence method but provides a mapping on the sequence before rejecting them if they contain any Left value.

// ensure we continue only if all the ints are positive
var e1 = Either.traverse(List.of(1, 2, 3),
              e -> e > 0 ? Either.right(e) : Either.left("bad: " + e));
e1.isRight(); // true
e1.getRight(); // Vector(1, 2, 3)

var e2 = Either.traverse(List.of(1, 2, -1, 3, -4),
              e -> e > 0 ? Either.right(e) : Either.left("bad: " + e));
e2.isLeft(); // true
e2.getLeft(); // Vector("bad: -1", "bad: -4")

var e3 = Either.traverseRight(List.of(1, 2, -1, 3, -4),
              e -> e > 0 ? Either.right(e) : Either.left("bad: " + e));
e3.isLeft(); // true
e3.getLeft(); // Vector("bad: -1")

Checking the status of an Either

There are multiple ways to know if an Either is Left or Right, using isLeft or isRight.

var e1 = Either.left("foo");
e1.isLeft(); // true
e1.isRight(); // false

Running side effects

You can run side effects when the Either is either Left or Right using peek and peekLeft.

var r = new Random();
var o = 10 > 0 ? Either.right(r.nextInt()) : Either.left("foo");
o.peek(i -> System.out.println("Feeling lucky:" + i)); // print "Feeling lucky: 42"
o.peekLeft(l -> System.out.println("Bad Luck: " + l)); // print nothing
  • The peek method won't run any side effect if the Either is Left.
  • The peekLeft method won't run any side effect if the Either is Right.

Additionally, Vavr provides the orElseRun method which let you run a side effect if the Either is a Left:

var r = new Random();
var o = 10 < 0 ? Either.right(r.nextInt()) : Either.left("foo");
o.orElseRun(l -> System.out.println("Bad luck:" + l)); // print "Bad luck: foo"

Getting the value out of an Either

Try not using get without knowing if the Either is a Right, otherwise it will crash on you. Use getOrElse, getOrElseGet or orElse to do so.

var handle = Either.right("Alex").getOrElse("Unknown"); // "Alex"
var handle = Either.left("Alex").getOrElse("Unknown"); // "Unknown"

Additionnally you can use fold :

var handle = Either.right("Alex")
                   .fold(l -> "Unknown", 
                         r -> "@" + r.toLowerCase()); // "@alex"

The orElse method supports also giving both an Either or a Supplier.

Either<String, String> isCool(String name) {
    return name.startsWith("E") ?
            Either.right(name) : Either.left("not cool: " + name);
}

var s = isCool("Alex")
           .orElse(isCool("Eva"))
           .fold(l -> "No one is cool",
                 r -> r + " is so cool")); // "Eva is so cool"

In case an Either being a Left is critical and you need to crash, Vavr has got you covered with getOrElseThrow.

isCool("Alex")
    .getOrElseThrow(() -> new RuntimeException("No losers in this Casino"));

Filtering an Either

You may wish to set an Either to Left if the value that an Either holds doesn't satisfy a given Predicate. In such a case, use the filterOrElse method.

var e = Either.right(41);
var even = e.filterOrElse(i -> i % 2 == 0, v -> "nope: " + v);
even.isLeft(); // true

var odd = e.filterOrElse(i -> i % 2 == 1, v -> "nope: " + v);
odd.isRight(); // true

Swaping an Either

You may wish to swap an Either, by making the Left its Right, and the other way:

var e = Either.right(41).swap();
e.isLeft(); // true

Mapping an Either (transforming what's inside)

Like Option, Either is a monadic container, and provides an API to transform what's inside the container, but it does so safely for you. Indeed, if an Either is Left, mapping its content will have no effect at all.

var str = Either.right("alex")
              .map(String::toUpperCase)
              .getOrElse("Could not proceed");

You can use as many map as you want, and remember, mapping on a Left has no effect:

var str = Either.left(null)
              .map(StringUtils::reverse)
              .map(StringUtils::lowerCase)
              .map(StringUtils::capitalize)
              .map(s -> s.concat(" is enjoying Vavr"))
              .getOrElse("Could not proceed");
System.out.println(str); // "Could not proceed"

Now imagine that the Either was Right:

var str = Either.right("XELA")
              .map(StringUtils::reverse)
              .map(StringUtils::lowerCase)
              .map(StringUtils::capitalize)
              .map(s -> s.concat(" is enjoying Vavr"))
              .getOrElse("Could not proceed");
System.out.println(str); // "Alex is enjoying Vavr"

Monadic containers help you think in terms of pipeline of transformation, and keeping in mind the happy path, letting you handle the failure case when you need the value.

The code above is equivalent to:

var e = Either.right("XELA");
String str = "Could not proceed";
if (e.isRight()) { // without Option you would check == null instead
   str = StringUtils.capitalize(
          StringUtils.lowerCase(StringUtils.reverse(o.get())))
       + " is enjoying Vavr";
}
System.out.println(str);

When accustomed to monadic containers, the first example looks better (I think) and conveys much more the business intent of the code which is:

  1. try to reverse the string
  2. then lowercase it
  3. then capitalize it
  4. then concatenate some other string with it
  5. then get the final result
    1. but if any of this fails I want the Could not proceed string instead.

Since Either is like a two sided Option you can also map but on the Left with mapLeft.

Lastly, you can use bimap to map both sides dependently of which side the Either is representing.

Either<Exception, String> e = Either.right("Updated user.");
var status = e.bimap(Throwable::getCause, String::toUpperCase); // UPDATED USER.

Either<Exception, String> e = Either.left(new Exception("Database Error."));
var status = e.bimap(Throwable::getCause, String::toUpperCase); // Database Error.

Like Option, an Either supports flatMap.

Using with collectors

Interrop with the Java Collectors is provided via collect()

Either.left("foo").collect(Collectors.toList()); // List()
Either.right("foo").collect(Collectors.toList()); // List(foo)

Generally the collect method is available on most of Vavr's types.


Try

The Try control gives the developer the ability to write safe code without focusing on try-catch blocks in the presence of exceptions.

You can think of it as an Either where the Left is an Exception, but also using try-catch blocks behind the scene, so that you don't have to.

Try is a really powerful tool that you can use to write better code, right now.

Creating a Try

Use of to create a Try instance.

String crashes() {
    throw new RuntimeException("I like to crash");
}

var t1 = Try.of(() -> "foo"); // Success<String>
var t2 = Try.of(this::crashes); // Failure<RuntimeException>

The of method has different overloads so that you can give it a CheckedFunction0, a Supplier or a Callable.

The difference between a CheckedFunction0 and a Supplier is that the CheckedFunction0 declares that it can throw a Throwable.

Finally, you can create your own instances of Try using the success and failure methods.

var t1 = Try.success("foo"); // Success<String>
var t2 = Try.failure(new RuntimeException("bar")); // Failure<RuntimeException> 

Note that there is also ofCallable and ofSupplier to respectively create a Try from a Callable or a Supplier.

Using a Try to run a procedure

A procedure is a function returning nothing (void, think just printing to the console for example).

You can use Vavr's Try to run these safely also.

void crashes() {
    throw new RuntimeException("I like to crash");
}

var t1 = Try.run(() -> System.out.println("foo")); // Success<Void>
var t2 = Try.run(this::crashes); // Failure<RuntimeException>

Note that you can also use runRunnable to run a Runnable.

Checking the status of a Try

Use isSuccess or isFailure.

var t1 = Try.of(() -> "foo");
t1.isSuccess(); // true
t1.isFailure(); // false

Running side effects

You can run side effects when the Try is either a Success or a Failure using onSuccess, onFailure and peek.

var r = new Random();
var t = Try.of(() -> {
            if (10 > 0) return r.nextInt();
            else throw new RuntimeException("foo");
        });
t.onSuccess(i -> out.println("Feeling lucky:" + i)) // print "Feeling lucky: 42"
 .onFailure(l -> out.println("Bad Luck: " + l)); // print nothing

Notice that onSuccess and onFailure both return the Try instance so that you can continue to chain calls, and use these methods to do useful side effects (like logging for example).

  • The onSuccess method won't run any side effect if the Try is Failure.
  • The onFailure method won't run any side effect if the Try is Success.

Additionally, Vavr provides the orElseRun method which let you run a side effect in case the Try is a Failure:

var r = new Random();
var t = Try.of(() -> {
            if (10 > 0) return r.nextInt();
            else throw new RuntimeException("foo");
        });
t.orElseRun(l -> System.out.println("Bad luck:" + l)); // print "Bad luck: foo"

Chaining side effects

You can chain side effects using andThen and andThenTry.

var t = Try.of(() -> "foo")
           .andThen(Sytem.out::println); // Try<String>
// prints "foo"

Note that the difference between andThen and andThenTry is that andThenTry accepts CheckedConsumer and CheckedRunnable whereas andThen accepts Consumer and Runnable.

The difference between a CheckedConsumer and a Consumer is that the CheckedConsumer declares that the consumer can throws a Throwable whereas the Consumer interface does not.

If the CheckedConsumer was not offered in the API, then you would have to a wrap your method in a try-catch yourself.

private void log(String str) throws Throwable{
    if (new Random().nextBoolean()) {
        throw new IllegalStateException("Bad luck");
    }
    System.out.println("Logging: " + str);
}

// does not compile
Try.of(() -> "foo").andThen(s -> log(s));

// this compile, but it looks ugly
Try.of(() -> "foo")
        .andThen(s -> {
            try {
                log(s);
            } catch (Throwable e) {
                // do something  
            }
        });

// using andThenTry, it works fine
Try.of(() -> "foo").andThenTry(s -> log(s));

The same rules applies for CheckedRunnable versus Runnable.

Finally

Vavr provides a try-finally behavior no matter what the result of the operation is by using andFinally and andFinallyTry.

var t = Try.of(() -> "foo")
           .andFinally(() -> System.out.println("I'm done")); // Success(foo)
// prints "I'm done"

var t = Try.failure(new Exception("boom"))
           .andFinally(() -> System.out.println("I'm done")); // Failure(boom)
// prints "I'm done"

Note that the difference between andFinally and andFinallyTry is that andFinallyTry accepts a CheckedRunnable whereas andFinally accepts a Runnable.

Getting the value out of a Try

Try not using get without knowing if the Try is a Success, otherwise it will crash on you. Use getOrElse, getOrElseGet or orElse to do so.

var handle = Try.of(() -> "Alex").getOrElse("Unknown"); // "Alex"

// ...

var handle = Try.of(() ->  {
    throw new RuntimeException("foo");
}).getOrElse("Unknown"); // "Unknown"

Additionnally you can use fold :

var handle = Try.of(() -> "Alex")
                   .fold(l -> "Unknown", 
                         r -> "@" + r.toLowerCase()); // "@alex"

The orElse method also support both a Try or a Supplier. While orElseTry will take a Supplier and run it safely for us.

String isCool(String name) {
    if (name.startsWith("E"))
        return name;
    throw new RuntimeException("not cool: " + name);
}

var s = Try.of(() -> isCool("Alex"))
        .orElse(Try.of(() -> isCool("Eva")))
        .fold(l -> "No one is cool", r -> r + " is so cool");

// or with orElseTry
var s = Try.of(() -> isCool("Alex"))
        .orElseTry(() -> isCool("Eva"))
        .fold(l -> "No one is cool", r -> r + " is so cool");

In case a Try being a Failure is critical and you need to crash, Vavr has got you covered with getOrElseThrow.

Try.of(() -> isCool("Alex"))
    .getOrElseThrow(() -> new RuntimeException("No losers in this Casino"));

Filtering a Try

You may wish to set a Try to Failure if the value that it holds doesn't satisfy a given Predicate. In such a case, use the filter method.

var t = Try.of(() -> 41);
var even = t.filter(i -> i % 2 == 0,
        v -> new RuntimeException("nope: " + v));
even.isFailure(); // true

var odd = t.filter(i -> i % 2 == 1,
        v -> new RuntimeException("nope: " + v));
odd.isSuccess(); // true

Note that the API provides also a filterTry if you need to use a CheckedPredicate instead of a Predicate. The filter method delegates to filterTry.

Making an Either

You may wish to make an Either out of a Try:

var e = Try.of(() -> 41).toEither();
e.isRight(); // true

Making a Validation

You may wish to make a Validation out of a Try:

var v = Try.of(() -> 41).toValidation(); // Valid(41)
var i = Try.failure(new Exception("foo")).toValidation(); // Invalid(Exception: foo)
var i = Try.failure(new Exception("foo"))
    .toValidation(Throwable::getMessage); // Invalid(foo)

Mapping a Try

Like Option or Either, a Try is a monadic container, and provides an API to transform what's inside the container, but it does so safely for you. Indeed, if a Try is a Failure, mapping its content will have no effect at all.

var str = Try.of(() -> "alex")
           .map(String::toUpperCase)
           .getOrElse("Could not proceed");

You can use as many map as you want to, and remember, mapping on a Failure has no effect:

var str = Try.failure(null)
           .map(StringUtils::reverse)
           .map(StringUtils::lowerCase)
           .map(StringUtils::capitalize)
           .map(s -> s.concat(" is enjoying Vavr"))
           .getOrElse("Could not proceed");
System.out.println(str); // "Could not proceed"

Now imagine that the Try was a Success:

var str = Try.success("XELA")
           .map(StringUtils::reverse)
           .map(StringUtils::lowerCase)
           .map(StringUtils::capitalize)
           .map(s -> s.concat(" is enjoying Vavr"))
           .getOrElse("Could not proceed");
System.out.println(str); // "Alex is enjoying Vavr"

Monadic containers help you think in terms of pipeline of transformation, and keeping in mind the happy path, letting you handle the failure case when you need the value.

The code above is equivalent to:

var t = Try.success("XELA");
String str = "Could not proceed";
if (t.isSuccess()) { // without Try you would use a catch expression
   s = StringUtils.capitalize(
          StringUtils.lowerCase(StringUtils.reverse(s.get())))
       + " is enjoying Vavr";
}
System.out.println(s);

When accustomed to monadic containers, the first example looks better (I think) and conveys much more the business intent of the code which is:

  1. try to reverse the string
  2. then lowercase it
  3. then capitalize it
  4. then concatenate some other string with it
  5. then get the final result
    1. but if any of this fails I want the Could not proceed string instead.

Since Try is like an Either you can also map on the Failure side with mapFailure and the pattern matching API.

var t = Try.failure(new IOException("boom"));
t.mapFailure(
    Case($(instanceOf(IOException.class)), e -> 
        new RuntimeException("Now a runtime error", e))
).getCause(); // RuntimeException(Now a runtime error, IOException(boom))

Note that Vavr provides both map and mapTry, the difference being that mapTry accepts a CheckedFunction1 whereas map accepts a Function.

The difference between a Function and a CheckedFunction is that the CheckedFunction signature declares that it throws a Throwable.

Mapping and null

As said earlier in this chapter, a Try can be a Failure or, and thus it can be problematic, but Try's version of map is safer than the others and will use a try-catch behind the scene so that it doesn't crash.

Let's see an example:

var str = Try.of(() -> "ALEX")
             .map(String::toLowerCase)
             .map(s -> s.length() < 10 ? null : s)
             .map(s -> s + " size is: " + s.length()) // No Boom
             .getOrElse("Could not proceed");
System.out.println(str); // "Could not proceed"

Still, you can use flatMap if you want to:

var str = Try.of(() -> "ALEX")
             .map(String::toLowerCase)
             .flatMap(s -> Option.of(s.length() < 10 ? null : s))
             .map(s -> s + " size is: " + s.length()) // still no BOOM 
             .getOrElse("Could not proceed");
System.out.println(str); // "Could not proceed"

Making a Success from a Failure

When you need to turn a Failure into a Success whose content is the cause of the Failure you can user failed().

var t = Try.failure(new Exception("foo"));
t.getCause(); // java.lang.Exception: foo
t.failed(); // Success(java.lang.Exception: foo)

Recovering from errors

When a Try fails, you have many ways to handle the error. Vavr provides recover and recoverWith to help you dealing with failures.

var t = Try.of(() -> 10/0)     // will boom, but handled by Try
            .recover(ex -> 42); // Success(42)
t.isSuccess(); // true
t.get(); // 42

The recoverWith let you try to recover a failure by trying to evaluate another Try.

var t = Try.of(() -> 10/0)
           .recoverWith(ex -> Try.of(() -> 42)); // Success(42)
t.isSuccess(); // true
t.get(); // 42

Of course if the second Try called by recoverWith fails, the resulting Try will be a failure.

var t = Try.of(() -> 10/0)
           .recoverWith(ex -> Future.of(() -> 42/0)); 
t.isSuccess(); // false
t.get(); // BOOM

Working with resources

If you need to run functions depending on Closeable resources within a Try, you can use Try.withResources(...).of(...).

Try.withResources(() -> new FileInputStream("foo.txt"))
   .of(fooInputStream -> doSomething(fooInputStream))
   .getOrElse("Could not read");

In the example above the FileInputStream will be automatically closed.

You can use as many as 8 resources with the withResources factory method.

Using with collectors

Interrop with the Java Collectors is provided via collect()

Try.failure(new Exception("foo")).collect(Collectors.toList()); // List()
Try.of(() -> "foo").collect(Collectors.toList()); // List(foo)

Generally the collect method is available on most of Vavr's types.


Lazy

Lazy is a functor which represents a lazy evaluation. Unlike a Supplier it is to be noted that a Lazy uses memoization and as such will only evaluate once.

Creating a Lazy

In order to create a Lazy you can use the of factory method.

var lazy = Lazy.of(() -> "foo"); // Lazy<String>

You can also make a value by using the val(Supplier, Class) method, which creates a lazy value of a specific type, backed by a Proxy which delegates to a Lazy instance. Note that the Class parameter must point to an interface.

var str = Lazy.val(() -> { 
    System.out.println("realizing foo...");
    return "foo";
}, CharSequence.class); // CharSequence

System.out.println("Hello");
System.out.println(s + " is bar");
System.out.println(s + " is totally bar");

In the example above the value of str will be realized only when needed, let's see the generated output.

Hello
realizing foo...
foo is bar
foo is totally bar

Notice that the string realizing foo... is only printed one time, and after the first string Hello so just when we needed the value to print foo is bar.

Getting the value

Getting the value out of a Lazy is as easy as calling the get() method on it.

var lazy = Lazy.of(() -> "foo"); // Lazy<String>
var str = lazy.get(); // String

A Lazy can generate a null value, it's not like an Option, and as such, even if the API gives you access to getOrElse and getOrElseThrow via the Value superclass, it is not defined and will not do what you think it does, so avoid using these constructs on lazy.

This is because Lazy's implementation of isEmpty() returns false.

var lazy = Lazy.of(() -> (String) null); // Lazy<String>
var str = lazy.getOrElse("bar"); // null

// or using getOrElse with a provider
var str = lazy.getOrElse(() -> "bar")); // null

// or using getOrElseThrow
var str = lazy.getOrElseThrow(() -> new RuntimeException()); // null (does not throw)

Knowing if a Lazy has been evaluated

Since Lazy is a container, you can declare it and then use it later. Sometimes you need to know if the value a Lazy is supposed to compute has been already computed.

To do this use the isEvaluated() method.

var lazy = Lazy.of(() -> "foo"); // Lazy<String>
lazy.isEvaluated(); // false
var str = lazy.get(); // "foo"
lazy.isEvaluated(); // true

Filtering a Lazy

Vavr lets you filter a Lazy by creating an Option holding or not the evaluated value if it satisfies a predicate. Of course, calling filter on a Lazy which has not been evaluated will force its evaluation.

var lazy = Lazy.of(() -> {
    System.out.println("realizing...");
    return "foo"; 
}); // Lazy<String>
var opt1 = lazy.filter(s -> s.length() > 3); // None
var opt2 = lazy.filter(s -> s.length() == 3); // Some("foo")

In the example above the string realizing... will be printed only one time.

Mapping on a Lazy

Since Lazy is a monadic container, you can transform the value it holds. Of course calling map on a Lazy which has not been evaluated will force its evaluation.

However the returned Lazy will not be evaluated yet!

var lazy = Lazy.of(() -> {
    System.out.println("realizing...");
    return "foo";
}).map(s -> {
    System.out.println("uppercasing...");
    return s.toUpperCase();
});
System.out.println(lazy.get() + " is BAR");
System.out.println(lazy.get() + " is totally BAR");

Will output the following text:

realizing...
uppercasing...
FOO is BAR
FOO is totally BAR

Running a side effect

Like Option, you can run side effects using peek, except that there is no concept of present or absent value. There's always a value, which can be null.

var lazy = Lazy.of(() -> {
    System.out.println("realizing...");
    return "foo";
}).peek(s -> System.out.println("Current state is: " + s))
  .map(s -> {
        System.out.println("uppercasing...");
        return s.toUpperCase();
  }).peek(s -> System.out.println("Current state is: " + s));
System.out.println(lazy.get() + " is BAR");
System.out.println(lazy.get() + " is totally BAR");

Will output the following text:

realizing...
Current state is: foo
uppercasing...
Current state is: FOO
FOO is BAR
FOO is totally BAR

Transforming to another domain

If you need to get the value out of a Lazy and transform it at the same type, you can use the transform method.

var str = Lazy.of(() -> "foo")
        .transform(s -> s.get().toUpperCase() + " is " + s.get().length());
System.out.println(str); // "FOO is 3"

Deprecation

The Lazy API has been marked deprecated since Java is not a lazily evaluated language. Library authors think that the implementation is ineffective because it acts as a simple wrapper and thus doesn't scale well.


Future

The Future is a computation result that becomes eventually available, providing non-blocking operations on it.

A Future has two states:

  1. Pending, which means that the computation is still ongoing and can be cancelled or completed
  2. Completed, which means that the computation has either finished successfully with a result, failed with an exception or was cancelled.

Creating a Future

To create a Future you can use the of factory method.

var future = Future.of(() -> "foo"); // Future<String>

You can also create Future in a defined state using failed or successful.

var future = Future.failed(); 
future.isFailure(); // true
future = Future.failed(new RuntimeException("boom"));
future.isFailure(); // true
future = Future.failed(newSingleThreadExecutor(), () -> "foo");
future.isFailure(); // true

future = Future.successful();
future.isSuccess(); // true
future = Future.successful("foo");
future.isSuccess(); // true

Finally, you can also use ofCallable and ofSupplier to create a Future from either a Callable or a Supplier.

Retrieve a Future state

To know in what state a Future is, the API provides:

  • isCompleted: true if the Future has completed, false otherwise
  • isSuccess: true if the Future has completed successfully, false otherwise
  • isFailure: true if the Future has completed with an error, false otherwise
  • isCancelled: true if the Future has been cancelled.

Retrieving a Future's value

In order to get the value out of a Future you can use either get, getOrElse, getOrElseThrow. Note that getOrElse returns the absent value only for failures, or cancellation, not for missing values like null.

var future = Future.of(() -> "foo");
var str = future.get(); // "foo"

future = Future.of(() -> null);
str = future.getOrElse("bar"); // null, because a Future can return a null

future = Future.of(() -> { throw new RuntimeException("boom");} );
str = future.getOrElse("bar"); // "bar"

str = future.getOrElseThrow(new RuntimeException("blurp")); // Exception: blurp

Since a Future may have not finished to compute the final value when you call get or getOrElse these two methods will block the current thread until the Future has completed.

If you wish not to block the current thread, then use the getValue method which returns an Option<Try<?>>. This Option will be None until the Future's computation has completed.

var future = Future.of(() -> {
    Thread.sleep(50);
    return "foo";
});
for (int i = 0; i < 1_000_000_000; ++i) {
    var opt = future.getValue();
    if (opt.isDefined()) {
        API.println(i + ": future has completed");
        opt.peek(t -> {
            API.println("\tSuccess: " + t.isSuccess());
            API.println("\t  Value: " + t.getOrElse("fallback"));
        });
        break;
    } else if (i % 10_000_000 == 0) {
        API.println(i + ": Waiting...");
    }
}

In the example above we simulate some computation which takes 50ms before returning, and then we loop maximum a billion times, each time getting the Future value into an Option trying to check if it is defined or not, in such a case we print the current iteration and the state and value of the Future, otherwise each ten million times we print Waiting....

On my machine it prints:

0: Waiting...
10000000: Waiting...
20000000: Waiting...
30000000: Waiting...
40000000: Waiting...
40674362: future has completed
	Success: true
	  Value: foo

Eventually the future has returned and a defined Option has been produced by getValue. This Option contains a Try which represents the status of the completion, either a Success or a Failure.

Running side effects

To run side effects on Success or Failure you can do exactly like with the Try monad by using onSuccess and onFailure.

Future.of(() -> "foo")
      .onSuccess(value -> System.out.println("Success: " + value))
      .onFailure(ex -> System.err.println("Error: " + ex))
      .getOrElse("bar"); // "foo"

Sometimes, it may be important to run a side effect as soon as the Future has completed, in such a case you can use onComplete.

Future.of(() -> "foo")
      .onComplete(v -> System.out.println("Just finished: " + v))
      .onSuccess(v -> System.out.println("Success: " + v))
      .onFailure(ex -> System.err.println("Error: " + ex))
      .getOrElse("bar"); // "foo"

Which outputs:

Just finished: Success(foo)
Success: foo

Finally, note that you can chain as many onComplete and onSuccess as you wish, but if you want to run them in a specific order you may want to use andThen instead.

Future.of(() -> "foo")
      .andThen(v -> System.out.println("Finished #1: " + v))
      .andThen(v -> System.out.println("Finished #2: " + v))
      .andThen(v -> System.out.println("Finished #3: " + v))
      .onSuccess(v -> System.out.println("Success: " + v))
      .onFailure(ex -> System.err.println("Error: " + ex))
      .getOrElse("bar"); // "foo"

Which outputs:

Finished #1: Success(foo)
Finished #2: Success(foo)
Finished #3: Success(foo)
Success: foo

Canceling a Future

In order to cancel a Future, simply call cancel on it.

var future = Future.of(() -> "foo");
future.cancel();
future.isCancelled(); // true
future.getOrElse("bar"); // "bar"

Awaiting termination

To wait until completion without actually getting the value, just use await on the Future instance.

var future = Future.of(() -> "foo");
future.await(); // blocks the thread until the future has completed
future.isCompleted(); // true
future.getOrElse("bar"); // "foo"

Finding a Future within a sequence

If you have an Iterable of Future which may or may not be already completed and you want to find the first Future whose final value satisfies a predicate, you can use the find factory method.

var futures = Vector.of(
    Future.of(() -> "abcdef"),
    Future.of(() -> "abc"),
    Future.of(() -> "abcd"),
    Future.of(() -> "a"));

var option = Future.find(futures, 
    s -> s.length() >= 3 && s.length() < 5 && s.startsWith("a")); 
// Some(abc)

Finding the first completed Future within a sequence

If you have an Iterable of Future which may or may not be already completed and you want to find the first Future who has completed, you can use the firstCompletedOf method.

Imagine a function named firstHitWins(String) which calls an API with a Twitter handle as parameter, and the first hit to complete wins.

Random r = new Random();

// imagine some networking here
@SneakyThrows
String firstHitWins(String name) { 
    Thread.sleep(r.nextInt(2_000));
    return name; 
} 

var futures = Vector.of(
    Future.of(() -> firstHitWins("Alex")),
    Future.of(() -> firstHitWins("Jessica")),
    Future.of(() -> firstHitWins("Eva")));

var winner = Future.firstCompletedOf(futures);
winner.get(); // may be "Alex", "Jessica", or "Eva" 
              // depending on which Future completes first

The @SneakyThrows annotation from lombok here just saves us to write the function firstHitWins like this:

String firstHitWins(String name) throws InterruptedException {
    Thread.sleep(r.nextInt(2_000));
    return name;
} 

Running tasks

If you want to run some background computations of which you don't need any results, you can use run.

Future.run(() -> System.out.println("hello")).await();

// providing a custom Executor
Future.run(newSingleThreadExecutor(), () -> System.out.println("hello")).await();

Note that runRunnable is deprecated.

Accessing the underlying Executor

Future is based on Java's Executor and if you need to access it for any reason, you can do it by calling executor.

var future = Future.of(() -> "foo");
future.peek(System.out::println)
        .executor().execute(() -> System.out.println("bar"));

Note that there is also an executorService method which has been deprecated since Vavr 0.10.0 and more.

Providing a custom Executor

The of factory method has an overload specifically to let you give a custom Executor that the returned Future instance should use.

var future = Future.of(newSingleThreadExecutor(), () -> "foo");
future.peek(System.out::println)
        .executor().execute(() -> System.out.println("bar"));
    }

Mapping the Future

Like all the other values in Vavr, Future is a monadic container and thus, you can transform the value it is holding via map.

var str = Future.of(() -> "foo")
                .map(String::toUpperCase)
                .map(s -> "Hey " + s + "!")
                .getOrElse("Could not proceed");
System.out.println(str); // "Hey FOO!"

Note that using map won't force the Future to complete, the transformation you want to do on the final value will takes place when you will call get, getOrElse or await on the resulting Future.

var future1 = Future.of(() -> "foo")
                    .map(String::toUpperCase);
future1.isCompleted(); // false

var future2 = future1.map(s -> { throw new RuntimeException("boom"); });
future2.isCompleted(); // false
future2.await(); // Future(Failure(RuntimeException: boom))
future2.isFailure(); // true

future1.await(); // Future(Success(FOO))
future1.isSuccess(); // true

You can also nest calls to functions returning a Future with flatMap like you would do for an Option.

var str = Future.of(() -> "foo")
                .map(String::toUpperCase)
                .flatMap(s -> Future.of(() -> "Hey " + s + "!"))
                .getOrElse("Could not proceed");
System.out.println(str); // "Hey FOO!"

Note that Vavr provides also flatMapTry instead of flatMap when you need to use a CheckedFunction1 instead of a Function.

Folding futures

If you need to return a Future which contains the result of folding the given future values, then you can use fold.

Imagine a function named userScore which returns a user score, and we want to find the total score for all Future.

Random r = new Random();

@SneakyThrows
Integer userScore(String name) {
    Thread.sleep(r.nextInt(1_000));
    return r.nextInt(100);
}

var futures = Vector.of(
        Future.of(() -> userScore("Alex")),
        Future.of(() -> userScore("Jessica")),
        Future.of(() -> userScore("Eva")));

Future.fold(futures, 0, Integer::sum)
        .map(score -> "Total score: " + score)
        .get(); // "Total score 153"

Reducing Futures

If you need to return a Future which contains the reduced result of an Iterable of future values, you can use reduce.

var f1 = Future.of(() -> "My name");
var f2 = Future.of(() -> "is");
var f3 = Future.of(() -> "Alex");

Future.reduce(Vector.of(f1, f2, f3), 
        (a, b) -> a + " " + b).get(); // My name is Alex

Filtering Futures

You can filter a Future with a Predicate using filter, or with a CheckedPredicate using filterTry.

var future = Future.of(() -> "foo");

future.filter(s -> s.length() > 3).get(); 
// -> NoSuchElementException: Predicate does not hold for foo

future.filter(s -> s.length <= 3).get(); // "foo"

Zipping Futures

When using the Future API to consume data, it's really powerful to be able to consume multiple sources asynchronously but still process them at once with zip.

record Person(String firstName, String lastName) {}
record Address(String street, String city, String country) {}

// imagine that instead of creating manually entities we would
// fetch them through the network via a REST API for example
var name = Future.of(() -> new Person("John", "Doe"));
var address = Future.of(() -> new Address("13 rue des Clercs", "Metz", "FR"));

var info = name.zip(address).get(); // Tuple2<Person, Address>
System.out.println(info._1.firstName()); // "John"
System.out.println(info._2.city()); // "Metz"

If you need to apply a transformation to combine both elements of the Future to be zipped so that you can operate on the final result you can use zipWith.

Let's say that we want to combine both the Person and Address result to make a formated String, we can do it like this:

var name = Future.of(() -> new Person("John", "Doe"));
var address = Future.of(() -> new Address("13 rue des Clercs", "Metz", "FR"));

var str = name.zipWith(address,
        (person, address) -> String.format("%s %s lives at %s, %s, %s",
                person.firstName(), person.lastName(),
                address.street(), address.city(), address.country()))
        .get();
// -> "John Doe lives at 13 rue des Clercs, Metz, FR"

Recovering from errors

When a Future fails, you have many ways to handle the error. Vavr provides recover, recoverWith and fallBackTo to help you dealing with failures.

var future = Future.of(() -> 10/0)     // will boom, but handled by Future
                   .recover(ex -> 42); // Future<Integer>
future.isSuccess(); // true
future.get(); // 42

The recoverWith let you try to recover a failure by trying to evaluate another Future.

var future = Future.of(() -> 10/0)
                   .recoverWith(ex -> Future.of(() -> 42)); 
future.isSuccess(); // true
future.get(); // 42

Of course if the second Future called by recoverWith fails, the resulting Future will be a failure.

var future = Future.of(() -> 10/0)
                   .recoverWith(ex -> Future.of(() -> 42/0)); 
future.isSuccess(); // false
future.get(); // BOOM

And finally the fallBackTo method let you do something similar to recoverWith.

var future1 = Future.of(() -> 10/0);
var future2 = Future.of(() -> 42);

var test = future1.fallBackTo(f2);
test.await();
test.isSuccess(); // true
test.get(); // 42

And exactly like recoverWith if the Future you give to fallBackTo fails, the resulting Future will be a failure.

var future1 = Future.of(() -> 10/0);
var future2 = Future.of(() -> 20/0);

var test = future1.fallBackTo(f2);
test.await();
test.isSuccess(); // false

Try Interrop

You can create a Future from a Try using fromTry using the default executor or a custom one.

var future = Future.fromTry(Try.of(() -> "hello"));
var future = Future.fromTry(newSingleThreadExecutor(), Try.of(() -> "hello"));

Java Interrop

You can both transform a Vavr Future in a Java CompletableFuture (or Future) and the other way.

To do this use either fromJavaFuture, fromCompletableFuture, toJavaFuture and toCompletableFuture.

Using with collectors

Interrop with the Java Collectors is provided via collect()

Future.of(() -> "foo").collect(Collectors.toList()); // List(foo)

Generally the collect method is available on most of Vavr's types.


Match

Vavr offers an elegant way to do pattern matching in Java. Pattern matching is a really great feature to have in a programming language, and it helps the developer avoiding big if-else statements by reducing the amount of code and improving the readability.

To do this, Vavr provides:

  • Match, for initiate a pattern matching
  • Case, for explaining a case in the above pattern matching
  • Some predicates to help us write the needed Cases

To just show you in a glimpse what it looks like:

import static io.vavr.API.*;
import static io.vavr.Patterns.*;

String s = Match(42).of(
    Case($(13), "thirteen"),
    Case($(42), "fourty-two"),
    Case($(), "don't know this number")
); 
// "fourty-two"

This is like a better switch/case statement, it is type safe, avoid boilerplate, besides Case accepts lambdas.

Creating a Match

In order to create a Match instance you just need to use the API.Match static method.

var m = Match("foo"); // Match<String>

Adding cases

A Match is nothing without some cases, and to give it cases to iterate on you need to use the of factory methods.

A Case is made of two parts:

  • a Pattern, somewhat like a predicate
  • a return value, a Function, a Supplier, …
import static io.vavr.API.*;
import static io.vavr.Patterns.*;

String s = Match(42).of(
    Case($(13), "thirteen"),
    Case($(42), "fourty-two"),
    Case($(), "don't know this number")
);
// "fourty-two"

Patterns

Cases use patterns to test sample values. To make things simpler Vavr provide an API.$ function that takes a prototype and creates a Pattern out of it.

You can use $ like this:

  • $(), the wild card, this is like the default branch in a switch case
  • $(value), checking explictly for a specific value
  • $(predicate), checking for a predicate on the matched value

Vavr comes with some built-in patterns that you can use to avoid writing them yourself:

method what for
$TupleN(N…) TupleN
$Some() Option
$None() Option
$Left() Either
$Right() Either
$Success() Try
$Failure() Try
$Future() Future
$Invalid() Validation
$Valid() Validation
$Cons() List
$Nil() List

Let's see them all below, because this is really cool!

Tuple

You can deconstruct Tuple as part of cases in a match so that you can test each member for a specific value or a predicate.

For this, use Patterns.$TupleN where N is the type of Tuple.

The example below will return a different string is the second member of a Tuple2 is even or odd. You can see that we are checking the first member for a specific value of 1 and the second member with a predicate.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;

var s = Match(Tuple.of(1, 2)).of(
        Case($Tuple2($(1), $(e -> e % 2 == 0)), "second is even"),
        Case($Tuple2($(1), $(e -> e % 2 == 1)), "second is odd"),
        Case($(), "what")
);
// "second is even"

Option

You can deconstruct an Option as part of cases in a match so that you can test None and Some differently.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;

var s = Match(Option.of("foo")).of(
        Case($Some($("bar")), "bar"),
        Case($Some($(e -> e.length() == 3)), "string of size 3"),
        Case($None(), "empty"),
        Case($(), "what")
);
// "string of size 3"

s = Match(Option.of((String) null)).of(
        Case($Some($("bar")), "bar"),
        Case($Some($(e -> e.length() == 3)), "string of size 3"),
        Case($None(), "empty"),
        Case($(), "what")
);
// "empty" because Option.of(null) is None

Either

You can also deconstruct Either as part of cases in a match so that you can test Left and Right differently.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;

var s = Match(Either.right("foo")).of(
        Case($Right($("bar")), "bar"),
        Case($Right($("foo")), "foo"),
        Case($Left($()), "exception"),
        Case($(), "what")
);
// "foo"

Either<Throwable, Object> e = 
    Try.of(() -> { throw new RuntimeException("BOOM");}).toEither();

var s = Match(e).of(
        Case($Right($("bar")), "bar"),
        Case($Right($("foo")), "foo"),
        Case($Left($()), "exception"),
        Case($(), "what")
);
// "exception"

Try

You can deconstruct a Try as part of a cases in a match so that you can test Failure and Success differently.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;

var s = Match(Try.of(() -> "foo")).of(
        Case($Success($("bar")), "bar"),
        Case($Success($("foo")), "foo"),
        Case($Failure($()), "exception"),
        Case($(), "what")
);
// "foo"

s = Match(Try.failure(new RuntimeException("BOOM"))).of(
        Case($Success($("bar")), "bar"),
        Case($Success($("foo")), "foo"),
        Case($Failure($()), "exception"),
        Case($(), "what")
);
// "exception"

Validation

You can deconstruct a Validation as part of a cases in a match so that you can test for Valid and Invalid differently.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;

var s = Match(Validation.valid("foo")).of(
        Case($Valid($("foo")), i -> "ok"),
        Case($Invalid($()), e -> "error")
);
// "ok"

s = Match(Validation.invalid("foo")).of(
        Case($Valid($("foo")), i -> "ok"),
        Case($Invalid($()), e -> "error")
);
// "error"

List

You can deconstruct a List as part of a cases in a match so that you can test for Cons and Nil differently.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;

var s = Match(List.of(1, 2, 3)).of(
     Case($Nil(), "empty"),
     Case($Cons($(), $()), "not empty"));
// "not empty"   

s = Match(List.of()).of(
     Case($Nil(), "empty"),
     Case($Cons($(), $()), "not empty"));
// "empty"   

Predicates

As we've seen the API.$ function can take a predicate, and as such, Vavr comes with some built-in predicates.

method description
allOf(Predicate…) A combinator that checks if all of the given predicates are satisfied.
anyOf(Predicate…) A combinator that checks if at least one of the given predicates is satisfies.
exists(Predicate) A combinator that checks if one or more elements of an Iterable satisfy the predicate.
forAll(Predicate) A combinator that checks if all elements of an Iterable satisfy the predicate.
instanceOf(Class) Creates a Predicate that tests, if an object is instance of the specified type.
is(T value) Creates a Predicate that tests, if an object is equal to the specified value using Objects.equals() for comparison.
isIn(T… values) Creates a Predicate that tests, if an object is equal to at least one of the specified values using Objects.equals() for comparison.
isNull() Creates a Predicate that tests, if an object is null
isNotNull() Creates a Predicate that tests, if an object is not null
noneOf(Predicate…) A combinator that checks if none of the given predicates is satisfied.
not(Predicate) Negate a given predicate.

Let's see them all below.

allOf

Check that the value is both a String, and not null.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;
import static io.vavr.Predicates.*;

var s = Match("foo").of(
        Case($(allOf(instanceOf(String.class), isNotNull())), "non null String"),
        Case($(), "what"));
// "non null String"

anyOf

Check that the value is any String, but not null.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;
import static io.vavr.Predicates.*;

var s = Match("").of(
        Case($(anyOf(instanceOf(String.class), is($("")))), "it's a String"),
        Case($(), "what"));
// "it's a String"

instanceOf

Check that the value is a String.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;
import static io.vavr.Predicates.*;

var s = Match("foo").of(
        Case($(instanceOf(String.class)), "String"),
        Case($(), "what"));
// "String"

is

Check that the value is "foo".

import static io.vavr.API.*;
import static io.vavr.Patterns.*;
import static io.vavr.Predicates.*;

var s = Match("foo").of(
        Case($(is("foo")), "foo"),
        Case($(), "what"));
// "foo"

isIn

Check that the value is "foo", "bar" or "bazz".

import static io.vavr.API.*;
import static io.vavr.Patterns.*;
import static io.vavr.Predicates.*;

var s = Match("foo").of(
        Case($(isIn("foo", "bar", "bazz")), "in the list"),
        Case($(), "what"));
// "in the list"

isNull

Check that the value is null.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;
import static io.vavr.Predicates.*;

var s = Match(null).of(
        Case($(isNull()), "is null"),
        Case($(), "what"));
// "is null"

isNotNull

Check that the value is not null.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;
import static io.vavr.Predicates.*;

var s = Match(null).of(
        Case($(isNotNull()), "is not null"),
        Case($(), "what"));
// "what"

s = Match("Vavr").of(
        Case($(isNotNull()), "is not null"),
        Case($(), "what"));
// "is not null"

noneOf

Check that the value is nor 10 nor 20.

import static io.vavr.API.*;
import static io.vavr.Patterns.*;
import static io.vavr.Predicates.*;

var s = Match(15).of(
        Case($(noneOf(is(10), is(20))), "not 10 or 15"),
        Case($(), "what"));
// "not 10 or 15"

Validation

As stated by the Vavr documentation, the Validation type is an applicative function which facilitates accumulating errors. When trying to compose Monads, the combination process will short circuit at the first error. But Validation will continue processing, accumulating all errors.

In an application, when validating fields of a form, or a payload in a REST API, you may want to retrieve all the validation errors at the same time, instead of only one at a time.

The following example is taken from the Vavr documentation:

record Person(String name, int age) {
}

class PersonValidator {
    static final String VALID_NAME_CHARS = "[a-zA-Z ]";
    static final int MIN_AGE = 13;

    static Validation<Seq<String>, Person> validate(String name, int age) {
        return Validation.combine(validateName(name), validateAge(age))
                         .ap(Person::new);
    }

    static Validation<String, String> validateName(String name) {
        return CharSeq.of(name).replaceAll(VALID_NAME_CHARS, "")
                .transform(seq -> seq.isEmpty() ? valid(name)
                        : invalid("Name contains invalid characters: '" + 
                                  seq.distinct().sorted() + "'"));
    }

    static Validation<String, Integer> validateAge(int age) {
        return age < MIN_AGE
                ? invalid("Age must be at least " + MIN_AGE)
                : valid(age);
    }
}

Validation<Seq<String>, Person> valid = PersonValidator.validate("John Doe", 30);
// Valid(Person[name=John Doe, age=30])

Validation<Seq<String>, Person> invalid = 
        PersonValidator.validate("John? Doe!4", -1);
// Invalid(List(Name contains invalid characters: '!4?', 
//              Age must be greater than 13))

Creating a validation

To create a Validation you can either build a valid one with valid, an invalid one with invalid, convert an Either with fromEither or convert a Try with fromTry.

A Validation holds two values, an error or a valid value, like an Either.

var valid = Validation.valid("valid"); // Validation(valid)
var invalid = Validation.invalid("error"); // Invalid(error)

var v = Validation.fromEither(Either.Left("error"));
var v = Validation.fromEither(Either.right("valid"));

var v = Validation.fromTry(Try.of(() -> "valid"));
var v = Validation.fromTry(Try.failure(new Exception()));

Combining and applying validations

Vavr offers the possibility to combine up to 8 validations into one. Combining validations is done using the combine method.

Validation<String, String> validName(String name) {
    return name.matches("^[A-Z].*") ? 
        valid(name) : invalid("Name should start with an uppercase letter: " + name);
}

Validation<String, Integer> validAge(int age) {
    return age >= 21 ? valid(age) : invalid("Age should be at least 21: " + age);
}

// Creates a Builder which needs to be applied
Validation.combine(validName("Alex"), validAge(35));

Validation.combine(validName("Alex"), validAge(35))
    .ap((name, age) -> name + " is " + age + " yo");
// Valid(Alex is 35 yo)

Validation.combine(validName("jessica"), validAge(32))
    .ap((name, age) -> name + " is " + age + " yo");
// Invalid(List("Name should start with an uppercase letter: jessica"))

Validation.combine(validName("Eva"), validAge(4))
    .ap((name, age) -> name + " is " + age + " yo");
// Invalid(List("Age should be at least 21: 4"))

Note that using combine only creates a Builder instance which needs to be applied with ap. The ap method takes a FunctionN parameter, so that you can pass it a way to create a valid final object be it via a function or a constructor.

Retrieve the status of a Validation

In order to retrieve the status of a Validation, just use isValid or isInvalid.

var v = Validation.valid("foo");
v.isValid(); // true

var v = Validation.invalid("bar");
v.isInvalid(); // true

Additionnally, if you want to get the value or the error you can use get and getError.

var v = Validation.valid("foo");
v.get(); // "foo"

var v = Validation.invalid("bar");
v.getError(); // "bar"

Note that trying to call get on an invalid instance will throw. Same if calling getError on a valid instance.

Swapping a Validation

Like an Either you can swap a Validation using swap.

Validation.valid("foo").isValid(); // true
Validation.valid("foo").swap().isValid(); // false

Running a side effect

You can use the peek method which accepts a Consumer that you can use to run a side effect with the current valid value.

Validation.valid("foo")
           .peek(System.out::println); // will print "foo"

Note that if the Validation instance is invalid, peek will do nothing.

Folding a Validation

Using fold you can transform the Validation to a new value depending on it being valid or invalid.

var s = Validation.valid("foo")
                   .fold(error -> "default", 
                         String::toUpperCase);
// "FOO"

var s = Validation.<String, String>invalid("error")
                   .fold(error -> "default: " + error, 
                         String::toUpperCase);
// "default: error"

Mapping a Validation

Like the other Vavr values you can transform the content of a Validation be it the value using map or the error using mapError.

var v = Validation.valid("foo")
                   .map(String::toUpperCase);
// Valid("FOO")

var v = Validation.invalid("error")
                  .mapError(String::toUpperCase);
// Invalid("ERROR")

Functions

Java provides only two kind of functional interface that you can use:

  • Function: accepts only one parameter and returns a result
  • BiFunction: accepts two parameters and returns a result

Vavr goes up to 8 parameters:

  • Function0, Function1, Function2, …
  • Supports checked functions: CheckedFunction1, CheckedFunction2, …
  • Supports composition, lifting, currying and memoization

Creating functions

There are multiple ways you can create a Vavr Function.

By referencing an existing Java function:

Integer mySquare(int a) {
    return a * a;
}
Function1<Integer, Integer> square = Function1.of(this::mySquare);
square.apply(5); // 5 * 5 = 25

Or by providing a short notation :

Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
sum.apply(40, 2); // 42

Constants

You can create a function which always returns the given constant value, and this can be useful. In order to do this, use the constant method.

Function1<Integer, Integer> always42 = Function1.constant(42);
always42.apply(0);  // 42
always42.apply(-5); // 42
always42.apply(10); // 42

Composition

In mathematics, function composition is an operation that takes two functions f and g and produces a function h such that h(x) = g(f(x))

Functions can be composed:

  • f : X -> Y
  • g : Y -> Z
  • h : X -> Z = g(f(x))

Use compose or andThen for more natural (human) order.

Function1<String, String> upperCase = Function1.of(String::toUpperCase);
Function1<String, String> hi = s -> "Hi, " + s;
Function1<String, String> addMark = s -> s + " !";

Function1<String, String> welcome =
    addMark.compose(hi.compose(upperCase));
welcome.apply("tars"); // Hi, TARS !

Function1<String, String> welcome2 =
    upperCase.andThen(hi).andThen(addMark);
welcome2.apply("tars"); // Hi, TARS !

Lifting

Partial functions are functions that are valid for a specific subset of values but may yield errors for some input, for example dividing is valid for all values except when dividing by 0.

The lift function lifts a partial function into a total function

  • It can accept all input
  • Returns an Option instead of the value
  • Instead of throwing an exception it will return None
Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
Function2<Integer, Integer, Option<Integer>> safeDivide = Function2.lift(divide);

safeDivide.apply(15, 5); // Some(3)
safeDivide.apply(15, 0); // None

Note that Vavr also provides liftTry which returns a Try instead of an Option.

Partial application

In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.

Partial application allows you to create new function from an existing one by setting some arguments.

It is not to be confused with currying.

Function2<Integer, Integer, Integer> multiply = (a, b) -> a * b;
Function1<Integer, Integer> twoTimes = multiply.apply(2);

multiply.apply(4, 3); // 12
twoTimes.apply(9); // 18

Function3<Long, Long, Long, Long> computation = (a, b, c) -> a + (b * c);
Function2<Long, Long, Long> incMultiply = computation.apply(1L);

incMultiply.apply(2L, 3L); // 1 + (2 * 3) = 7

Currying

In mathematics and computer science, currying is the same as converting a function that takes n arguments into n functions taking a single argument each.

Function3<Long, Long, Long, Long> computation = (a, b, c) -> a + (b * c);
Function2<Long, Long, Long> incMultiply = computation.apply(1L);

incMultiply.apply(2L, 3L); // 1 + (2 * 3) = 7

Function1<Long, Function1<Long, Function1<Long, Long>>> curriedComputation =
    computation.curried();

curriedComputation.apply(1L).apply(2L).apply(3L); // 1 + (2 * 3) = 7

Memoization

In computing, memoization is an optimization technique used primarily to speed up computer programes by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

Memoization acts like some kind of caching, if you memoize a function, it will be only executed once for a specific input.

Function0<String> cachedUUID = Function0.of(UUID::randomUUID)
    .andThen(UUID::toString).memoized();

cachedUUID.apply(); // 88e93417-2adb-48f0-81f3-aa9bd41efad3
cachedUUID.apply(); // 88e93417-2adb-48f0-81f3-aa9bd41efad3

Function1<String, String> cachedUserUUID = 
    Function1.of((String user) -> user + ": " + UUID.randomUUID().toString())
    .memoized();
cachedUserUUID.apply("Tars"); // Tars: d01562a7-1a13-45c0-b567-3239f4d0abc1
cachedUserUUID.apply("Kipp"); // Kipp: 304b1e2d-4c76-4785-9acc-de04ecf87730   
cachedUserUUID.apply("Case"); // Case: 2e77448f-9a4d-4fc1-8b78-8805b035dd5b
cachedUserUUID.apply("Tars"); // Tars: d01562a7-1a13-45c0-b567-3239f4d0abc1

Reversing parameters

You can ask Vavr to reverse the parameters of your function with reversed

Function2<String, String, String> concat = (a, b) -> a + b;
concat.apply("abc", "def"); // abcdef
concat.reversed().apply("abc", "def"); // defabc

Getting the arity

If you need to get the arity of a Vavr function, juste use the arity method, like on Tuple

Function2<String, String, String> concat = (a, b) -> a + b;
concat.arity(); // 2

Using Tuples as parameters

If you need to change a function from taking N parameters to a TupleN instead you can use tupled().

Function2<String, String, String> concat = (a, b) -> a + b;
Function1<Tuple2<String, String>, String> concat2 = concat.tupled();
concat.apply("foo", "bar")
      .equals(concat2.apply(Tuple.of("foo", "bar"))); // true

Sneaky Throws

Vavr provides the unchecked method which transforms a CheckedFunctionN into a FunctionN.

Sneaky throwing means that the execution of the function is placed inside a try {} catch(Throwable) and rethrow an unchecked Exception in case something happens.

This is similar to Lombok's @SneakyThrows

Recovering

When using CheckedFunctionN you can use the recover method to give a fallback function that should compute a default value.

CheckedFunction1<String, String> yell = s -> s.toUpperCase() + "!";
var safeYell = yell.recover(e -> s -> "Don't yell at me: " + e.getMessage());
safeYell.apply("Hello"); // "HELLO!"
safeYell.apply(null); // "Don't yell at me: null"

In the example above the yell function is prone to NullPointerException, and we create another function named safeYell which will apply the yell function, but in case an exception is thrown, we will return Don't yell at me: and the exception message.


Collections

Vavr provides purely functional collections based on the Traversable class.

Performance characteristics of Vavr Collections

These information are extracted from the Vavr Javadoc.

Time complexity of sequential operations

head() tail() get(int) update(int, T) prepend(T) append(T)
Array const linear const const linear linear
CharSeq const linear const linear linear linear
Iterator const const — — — —
List const const linear linear const linear
Queue const const^a^ linear linear const const
PriorityQueue log log — — log log
Stream const const linear linear const^lazy^ const^lazy^
Vector const^eff^ const^eff^ const^eff^ const^eff^ const^eff^ const^eff^

Time complexity of Map/Set operations

contains/Key add/put remove min
HashMap const^eff^ const^eff^ const^eff^ linear
HashSet const^eff^ const^eff^ const^eff^ linear
LinkedHashMap const^eff^ linear linear linear
LinkedHashSet const^eff^ linear linear linear
Tree log log log log
TreeMap log log log log
TreeSet log log log log
  • const · constant time
  • const^a^ · amortized constant time, few operations may take longer
  • const^eff^ · effectively constant time, depending on assumptions like distribution of hash keys
  • const^lazy^ · lazy constant time, the operation is deferred
  • log · logarithmic time
  • linear · linear time

Vavr collections regarding Java collections

Vavr Java Description
Array Object[] Traversable wrapper for Object[]
CharSeq - A rich String wrapper
Iterator java.util.Iterator Compositional replacement for Java's Iterator
List - Immutable eager sequence of elements, using Nil and Cons
Queue java.util.ArrayDeque Immutable Queue storing elements allowing FIFO retrieval
PriorityQueue java.util.PriorityQueue Immutable Priority Queue
Stream java.util.Stream Immutable Stream as a lazy sequence of elements which may be infinitely long
Vector java.util.ArrayList The default Seq implementation that provides effectively constant time access to any element
HashMap java.util.HashMap Immutable HashMap based on a HAMT
HashSet java.util.HashSet Immutable HashSet
LinkedHashMap java.util.LinkedHashMap An immutable LinkedHashMap implementation that has predictable (insertion-order) iteration
LinkedHashSet java.util.LinkedHashSet An immutable HashSet implementation that has predictable (insertion-order) iteration
Tree - General Tree interface
TreeMap java.util.TreeMap Immutable SortedMap based on a Red/Black Tree
SortedSet java.util.SortedSet Immutable SortedSet based on a Red/Black Tree

Hierarchy


Sequences

In Vavr, sequential data structures provide support for :

  • basic operations like appending, inserting, or updating
  • filtering like removing with or without predicates, rejecting, and so on
  • selection like getting a specific element, finding one, or even slicing
  • transformation like cross product, combinations, permutations, sorting, ziping etc.
  • conversion and traversal
  • interop with Java mutable collections

Sequential data structures are either indexed (IndexedSeq) or linear (LinearSeq).

Indexed sequences are provided via Array, CharSeq and Vector implementations, while linear sequences are provided via Stack, List, Stream and Queue implementations.

Creating a sequence

Depending on the implementation you want you can always use empty(), or one of the one of the of(T), of(T...) and ofAll() factory methods.

Example with Vector. The same API is provided for the other Seq implementations.

Seq<Integer> s = Vector.of(1);
s = Vector.of(1, 2, 3)
s = Vector.empty();
var list = Arrays.asList(1, 2, 3); // java List
s = Vector.ofAll(list);
s = Vector.ofAll(list.stream()); // from a Stream

Appending to a sequence

In order to append elements to a Seq, you may use append, appendAll, insert, insertAll, prepend or prependAll.

Seq<Integer> s = Vector.of(1, 2);
s.append(3); // Vector(1, 2, 3)
s.appendAll(Vector.of(3, 4)); // Vector(1, 2, 3, 4)
s.insert(1, 10); // Vector(1, 10, 2)
s.insertAll(1, Vector.of(10, 20)); // Vector(1, 10, 20, 2)
s.prepend(0); // Vector(0, 1, 2)
s.prependAll(Vector.of(-1, 0)); // Vector(-1, 0, 1, 2)

Updating an element

In order to update an element at a specific index, use update.

Seq<Integer> s = Vector.of(1, 2);
s.update(1, 20); // Vector(1, 20)

Keep in mind that updating an element at a specific index perform differently depending on the Seq implementation you are using.

Removing and filtering elements

You can either remove objects based on equality, at a specific index or based on a predicate. To do so use either remove, removeAll, removeFirst, removeLast, reject, filter or retailAll.

Seq<Integer> s = Vector.of(1, 2, 3, 4, 1);
s.remove(1); // Vector(2, 3, 4, 1)
s.removeAll(1); // Vector(2, 3, 4)
s.removeAll(Vector.of(1, 2)); // Vector(3, 4)
s.removeAt(0); // Vector(2, 3, 4, 1)
s.removeFirst(e -> e % 2 == 1); // Vector(2, 3, 4, 1)
s.removeLast(e -> e % 2 == 1); // Vector(1, 2, 3, 4)
s.reject(e -> e % 2 == 1); // Vector(2, 4)
s.filter(e -> e % 2 == 0); // Vector(2, 4)
s.retainAll(Vector.of(1, 2)); // Vector(1, 2, 1)

Finding elements

You can use get or one of indexOf, indexOfSlice and indexWhere. Vavr provides also lastIndexOf versions of these methods. The indexOf methods return -1 when nothing can be found, and if you wish to have an Option instead use the indexOfOption variant.

Seq<Integer> s = Vector.of(1, 2, 3, 4, 1, 2);
s.get(0); // 1
s.indexOf(1); // 0
s.indexOf(5); // -1
s.indexOf(1, 1); // 4
s.lastIndexOf(1); // 4
s.indexOfOption(2); // Some(1)
s.indexOfOption(5); // None
s.lastIndexOfOption(5); // None
s.indexWhere(e -> e % 2 == 0); // 1
s.indexWhereOption(e -> e > 10); // None
s.lastIndexWhere(e -> e % 2 == 0); // 5
s.lastIndexWhereOption(e -> e < 0); // None
s.indexOfSlice(Vector.of(1, 2)); // 0
s.lastIndexOfSlice(Vector.of(1, 2)); // 4

Note that you can also use search to find indices of elements in the sequence.

var v = Vector.of(1, 2, 3, 4);
v.search(2); // 1

Slices and sub sequences

In order to get a slice or a sub sequence of a Seq you can use slice or subSequence. The only difference between both is that slice does not throw but instead return an empty Seq.

Seq<Integer> s = Vector.of(1, 2, 3, 4);
s.slice(0, 3); // Vector(1, 2, 3)
s.slice(0, 10); // Vector(1, 2, 3, 4)
s.slice(-100, 250); // Vector(1, 2, 3, 4)
s.subSequence(1); // Vector(2, 3, 4)
s.subSequence(3); // Vector(4)
s.subSequence(1, 2); // Vector(2, 3)
s.subSequence(50); // throws IndexOutOfBoundsException

You can check if a sequence contains a slice using containsSlice.

Seq<Integer> s = Vector.of(1, 2, 3, 4);
s.containsSlice(List.of(2, 3)); // true

Head and tail

You can retrieve the head (first element) and tail (the sequence without the first element) of a Seq with head and tail.

Seq<Integer> s = Vector.of(1, 2, 3, 4);
s.head(); // 1
s.headOption(); // Some(1)
s.tail(); // Vector(2, 3, 4)
s.tailOption(); // Some(Vector(2, 3, 4))

Seq<Integer> s = Vector.empty();
s.head(); // throws NoSuchElementException
s.headOption(); // None
s.tail(); // throws UnsupportedOperationException
s.tailOption(); // None

Note that there's also an init method which is conceptually the dual of tail meaning, returning all the elements except the last, and like head and tail it fails with an UnsupportedOperationException if the Seq is empty.

Dropping elements

You can either drop a fixed amount of element, from the start or the end of a sequence. You can also drop all elements until or while an element satisfies a predicate. To do so use one of the drop, dropRight, dropUntil or dropWhile methods.

Seq<Integer> s = Vector.of(1, 2, 3, 4);
s.drop(2); // Vector(3, 4)
s.dropRight(2); // Vector(1, 2)
s.dropUntil(e -> e > 3); // Vector(4)
s.dropWhile(e -> e < 3); // Vector(3, 4)

Note that implementations of IndexedSeq provides also dropRightUntil and dropRightWhile.

Inserting between elements

If you need to intersperse an element between all the elements of a Seq you can use intersperse.

var v = Vector.of("foo", "bar", "bazz");
v.intersperse("ok"); // Vector(foo, ok, bar, ok, bazz)
v.intersperse("::").collect(Collectors.joining()); // "foo::bar::bazz"

Padding

Padding a Seq with an element from at the beginning or the end is provided via leftPadTo and padTo.

var v = Vector.of(1, 2, 3);
v.padTo(5, 0); // Vector(1, 2, 3, 0, 0)
v.leftPadTo(5, 0); // Vector(0, 0, 1, 2, 3)
Vector.empty().padTo(3, 0); // Vector(0, 0, 0)

Taking elements

You can either take a fixed amount of element, from the start or the end of a sequence. You can also take all elements until or while an element satisfies a predicate. To do so use one of the take, takeRight, takeUntil or takeWhile methods.

Seq<Integer> s = Vector.of(1, 2, 3, 4);
s.take(2); // Vector(1, 2)
s.takeRight(2); // Vector(3, 4)
s.takeUntil(e -> e >= 3); // Vector(1, 2, 3)
s.takeWhile(e -> e < 3); // Vector(1, 2)

Note that implementations of IndexedSeq provides also takeRightUntil and takeRightWhile.

Checking if starting or ending

If you need to know if a sequence starts or ends with a specific Iterable you can use either startsWith or endsWith.

Seq<Integer> s = Vector.of(1, 2, 3, 4, 5);
s.startsWith(Vector.of(2, 3, 4)); // false
s.startsWith(Vector.of(1, 2, 3, 4)); // true
s.endsWith(Vector.of(2, 3, 4)); // false
s.endsWith(Vector.of(3, 4, 5)); // true

Note that startsWith is essentially the same as checking that indexOf returns 0, and endsWith for indexOf returning the size of the sequence minus the size of the iterable.

Folding and reducing sequences

Folding is essentially like reducing but using an initial value. The fold method takes both a zero value (meaning initial), and a BiFunction used to combine values and make a final new one.

var v = Vector.of(1, 2, 3); 
v.fold(0, (a, b) -> a + b); // 6
v.fold(0, Integer::sum); // identical: 6
v.fold(10, Integer::sum); // 16
v.reduce(Integer::sum); // 6

The fold and foldLeft do exactly the same. There's also foldRight which is like calling foldLeft on a reversed sequence.

var v = Vector.of("a", "b", "c");
v.fold("lol ", String::concat); // "lol abc"
v.foldRight(" lol", String::concat); // "abc lol"
v.foldRight(" lol", (acc, c) -> c + acc); // " lolcba"
v.reduce(String::concat); // abc 

Partitioning sequences

You can partition a Seq into a Tuple2 of Seqs where the first member of the Tuple contains the elements of the original Seq which satisfies a Predicate, and the second member, the rest of the elements.

var t = Vector.of("foo", "bar", "bazz").partition(s -> s.contains("a"));
t._1; // Vector(bar, bazz)
t._2; // Vector(foo)

Patching sequences

You can patch a Seq, meaning producing a new Seq where a slice of elements are replaced by another Iterable.

var v =Vector.of("a", "b", "c", "d", "e", "f");
v.patch(0, Vector.of("z", "y"), 2); // Vector(z, y, c, d, e, f)
v.patch(1, Vector.of("z", "y"), 4); // Vector(a, z, y, f)

Replacing an element

You can replace an element, or all elements matching equality in a Seq with either replace or replaceAll.

var v = Vector.of("a", "b", "a");
v.replace("a", "z"); // Vector(z, b, a)
v.replaceAll("a", "z"); // Vector(z, b, z)
v.replace("x", "z"); // Vector(a, b, a)

Note that replace will replace only the first match, if found.

Splitting sequences

If you need to split a sequence in two at a specific index or at the first element which satisfies a predicate, Vavr got you covered. All of splitAt(int), splitAt(Predicate) and splitAtInclusive(Predicate) returns a Tuple2<Seq, Seq>.

Seq<Integer> s = Vector.of(1, 2, 3, 4, 5);
s.splitAt(2); // (Vector(1, 2), Vector(3, 4, 5))

// (Vector(1, 2, 3), Vector(4, 5))
s.splitAt(e -> e > 2 && e % 2 == 0)

// (Vector(1, 2, 3, 4), Vector(5))
s.splitAt(e -> e > 2 && e % 2 == 0) 

Sorting sequences

Sorting of sequence is provided via sorted(), sorted(Comparator) with a custom Comparator or sortBy.

Seq<Integer> s = Vector.of(3, 2, 4, 1);
s.sorted(); // Vector(1, 2, 3, 4)
s.sorted(Comparator.reverseOrder()); // Vector(4, 3, 2, 1)

Seq<Person> x = Vector.of(
            new Person("Robert", "Blurb"),
            new Person("Jane", "Doe"), 
            new Person("Foo", "Bar"));
// Vector(Bar Foo, Blurb Robert, Doe Jane)
x.sortBy(p -> p.lastName); 

Shuffling sequences

Shuffling sequences is as easy as calling shuffle on a Seq instance.

Seq<Integer> s = Vector.of(1, 2, 3, 4);
s.shuffle(); // Vector(1, 3, 4, 2)
s.shuffle(); // Vector(2, 3, 4, 1)

Sliding over sequences

If you need to slide in a sequence, which means create a window of a specific size that you can iterate on, Vavr provides the sliding and slideBy methods.

Seq<Integer> s = Vector.of(1, 2, 3, 4);
s.sliding(2); // Iterator on [[1, 2], [2, 3], [3, 4]]

// Iterator on [[1], [2], [3], [4]]
s.slideBy(Function.identity()); 

Seq<Integer> s = Vector.of(1, 2, 3, 9, 4, 6);
// group odd and even siblings together
// Iterator on  [[1], [2], [3, 9], [4, 6]]
s.slideBy(e -> e % 2); 

Scanning sequences

If you need to compute a prefix scan of the elements of a sequence you can use scan, scanLeft and scanRight.

For example for producing a cumulative sum on the elements of a sequence you can do:

Seq<Integer> s = Vector.of(1, 2, 3);
s.scan(0, Integer::sum); // Vector(0, 1, 3, 6)
// 0
// 0 + 1 = 1
// 1 + 2 = 3
// 3 + 3 = 6
s.scanLeft(0, Integer::sum); // will do the same

s.scanRight(0, Integer::sum); // Vector(6, 5, 3, 0)
// reverse of:
// 0
// 0 + 3 = 3
// 3 + 2 = 5
// 5 + 1 = 6

Rotating sequences

Vavr provides a way to circularly rotate a sequence in both left and right direction with rotateLeft and rotateRight.

Seq<Integer> s = Vector.of(1, 2, 3, 4, 5);
s.rotateLeft(2); // Vector(3, 4, 5, 1, 2)
s.rotateRight(2); // Vector(4, 5, 1, 2, 3)

Reversing sequences

Reversing a sequence is a simple as calling the reverse() method.

Seq<Integer> s = Vector.of(1, 2, 3, 4);
s.reverse(); // Vector(4, 3, 2, 1)
s.reverseIterator(); // Iterator on Vector(4, 3, 2, 1)

Generating permutations

Getting all the possible permutations of a sequence is a simple as calling the permutations method.

Seq<Character> s = Vector.of('a', 'b', 'c');
s.permutations();
// Vector(
//     Vector(a, b, c), 
//     Vector(a, c, b), 
//     Vector(b, a, c), 
//     Vector(b, c, a), 
//     Vector(c, a, b), 
//     Vector(c, b, a)
// )

Generating combinations

Getting all the combinations of a sequence is as simple as calling the combinations() method.

Seq<Character> s = Vector.of('a', 'b', 'c');
s.combinations();
// Vector(
//     Vector(), 
//     Vector(a), 
//     Vector(b), 
//     Vector(c), 
//     Vector(a, b), 
//     Vector(a, c), 
//     Vector(b, c), 
//     Vector(a, b, c)
// )

Grouping elements

You can either group elements of a sequence using a chunck size with grouped(int) or using a custom Function using groupBy.

// Iterator on [List(Alex, Jessica), List(Eva)]
List.of("Alex", "Jessica", "Eva").grouped(2);

// Map(true => List(Alex, Eva)), false => List(Jessica))
List.of("Alex", "Jessica", "Eva").groupBy(s -> s.matches("^[AEIOU].*"));

Note that grouped returns a Seq of Seq whereas groupBy returns a Map.

Keeping distinct elements

If you need to get the distinct values of a sequence you can use the distinct or distinctBy methods.

Seq<Character> s = Vector.of('a', 'b', 'c', 'a', 'b');
s.distinct(); // Vector(a, b, c)

Seq<Person> x = Vector.of(
            new Person("Robert", "Blurb"),
            new Person("John", "Blurb"),
            new Person("Jane", "Doe"), 
            new Person("Foo", "Bar"));
// Vector(Blurb Robert, Doe Jane, Bar Foo)
x.distinctBy(p -> p.lastName); 

Generating cross products

Getting cross product of a sequence with itself or with another sequence is covered by the crossProduct method and its various overloading.

Seq<Character> s1 = Vector.of('a', 'b', 'c');
s1.crossProduct(); 
// Iterator on [(a, a), (a, b), (a, c)
//              (b, a), (b, b), (b, c)
//              (c, a), (c, b), (c, c)]

Seq<Integer> s2 = Vector.of(1, 2, 3);
s1.crossProduct(s2); 
// Iterator on [(a, 1), (a, 2), (a, 3)
//              (b, 1), (b, 2), (b, 3)
//              (c, 1), (c, 2), (c, 3)]

Zipping sequences

You can zip sequences in order to iterate on both of them at the same time. To do that simply use one of the zip, zipAll, zipWith methods.

Seq<Character> s1 = Vector.of('a', 'b', 'c');
Seq<Integer> s2 = Vector.of(1, 2, 3);
s1.zip(s2); // Vector((a, 1), (b, 2), (c, 3))

The zip method will stop zipping sequences when any of the two sequences are totally consumed. If the two sequences don't have the same size you can provide default value to be filled with using zipAll.

Seq<Character> s1 = Vector.of('a', 'b', 'c', 'd', 'e');
Seq<Integer> s2 = Vector.of(1, 2, 3);
s1.zipAll(s2, 
   'x', // if s1 is shorter than s2 then it will use 'x' to fill
   42   // if s2 is shorter than s1 then it will use 42 to fill
); 
// Vector((a, 1), (b, 2), (c, 3),
//        (d, 42), (e, 42))

If you need to apply a transformation to combine both elements of the sequences to be zipped so that you can iterate on the final result you can use zipWith.

Seq<Character> s1 = Vector.of('a', 'b', 'c');
Seq<Integer> s2 = Vector.of(1, 2, 3);
s1.zipWith(s2, (a, b) -> Character.toString(a) + b);
// Vector("a1", "b2", "c3")

Alternatively, if you need to iterate over a sequence but you also need the element index in the source sequence you can use zipWithIndex which will give you a sequence of Tuple2 where the first member is the actual element you are iterating on, and the second member the element's index.

Seq<Character> s = Vector.of('a', 'b', 'c');
s.zipWithIndex();
// Vector((a, 0), (b, 1), (c, 2))

Finally, you can also use zipWithIndex by providing it a BiFunction to compute final elements over iteration.

Seq<Character> s = Vector.of('a', 'b', 'c');
s.zipWithIndex((elem, index) -> Character.toString(elem) + index);
// Vector("a0", "b1", "c2")

Finding longest segments

Vavr provides the segmentLength which will compute the length of the longest segment whose elements all satisfy a given predicate.

The first parameter is a predicate, and the second parameter is the index from which the search begins.

var v = Vector.of(1, 2, 3, 4, 6, 8, 10, 11, 12);
v.segmentLength(i -> i % 2 == 0, 0); // 0
v.segmentLength(i -> i % 2 == 0, 1); // 1
v.segmentLength(i -> i % 2 == 0, 3); // 4

Summing elements

Considering a sequence of numbers, you can easily sum all the elements using either sum, reduce, fold or collect.

var v = Vector.of(1, 2, 3);
v.sum(); // 6
v.reduce(Integer::sum); // 6
v.fold(0, Integer::sum); // 6
v.collect(Collectors.summingInt(i -> i)); // 6

Product of elements

Considering a sequence of numbers, you can easily compute the product of all the elements using either product, reduce or fold.

var v = Vector.of(1, 2, 3, 4);
v.product(); // 24
v.reduce(Math::multiplyExact); // 24
v.fold(1, Math::multiplyExact); // 24

Note that 1 being the identity for the multiplication, computing the product of an empty Set will return 1.

Minimum and maximum

You can retrieve the minimum and maximum of a sequence, or any Traverseable by using min, minBy, max or maxBy.

var v = Vector.of(1, 0, 2, 3, 5, 4);
v.min(); // Some(0)
v.max(); // Some(5)

Use minBy and maxBy if you need a custom Comparator or Function to retrieve the thing you need to decide which is the min or max.

Note that both min and max returns an Option, for example for empty sequence the min or max will be None.

var v = Vector.of();
v.min(); // None

Averaging elements

You can compute the average value of a sequence, using the average method.

var v = Vector.of(1, 2, 3, 4);
v.average(); // Some(2.5) = Some((1+2+3+4)/4)

Note that average returns an Option.

Flattening sequences

If you have a Seq of Seq that you want to flatten you can use flatMap with the identity function.

var v = Vector.of(Vector.of(1, 2, 3), Vector.of(4, 5), Vector.of(6, 7, 8));
v.flatMap(Function.identity()); // Vector(1, 2, 3, 4, 5, 6, 7, 8)

var v = Vector.of(Vector.of(Vector.of(1, 2, 3)), Vector.of(Vector.of(4, 5)));
v.flatMap(Function.identity())
 .flatMap(Function.identity()); // Vector(1, 2, 3, 4, 5)

Making Strings

Considering a Seq of elements, you can make a String out of it by using either mkString or collect.

var v = Vector.of(1, 2, 3, 4);
v.mkString(); // "1234"
v.mkString("-"); // "1-2-3-4"
v.mkString("<", "-", ">"); // "<1-2-3-4>"

Alternatively, you can use mkCharSeq to create CharSeq.

var v = Vector.of(1, 2, 3, 4);
v.mkCharSeq(); // CharSeq(1, 2, 3, 4)
v.mkCharSeq("-"); // CharSeq(1, -, 2, -, 3, -, 4)
v.mkCharSeq("<", "-", ">"); // CharSeq(<, 1, -, 2, -, 3, -, 4, >)

Java Interop

In order to create Java structures from Vavr sequences you can use asJava to create an immutable sequence or asJavaMutable for a mutable one.

var v = Vector.of(1, 2, 3);
java.util.List<Integer> immutableView = v.asJava();
java.util.List<Integer> mutableView = v.asJavaMutable();

Note, the mutable view when modified not affect original Vavr collection as Vavr collections are immutable by design. Only the mutable view will be modified.

You can create a new Java collection from a Vavr collection using toJavaList. This is slower approach as each element is iterated and added to new Java class (linear time of creation).

var v = Vector.of(1, 2, 3);
java.util.List<Integer> jList = v.toJavaList(); // new Java collection

When needed, you can convert from Java API collection to Vavr collection as follows:

java.util.List<Integer> jList = Arrays.asList(1, 2, 3);
var v = Vector.ofAll(jList); // Vector(1, 2, 3)

Using with collectors

Interrop with the Java Collectors is provided via collect()

List.of("how", "are", "you").collect(Collectors.joining(" ")); // "how are you"

Generally the collect method is available on all of Vavr's collections.


Sets

In Vavr, the Set interface has multiple implementations:

  • BitSet1, BitSet2, BitSetN: an immutable BitSet implementation
  • HashSet: an immutable HashSet implementation
  • LinkedHashSet: an immutable HashSet implementation that has predictable (insertion-order) iteration
  • TreeSet: a sorted Set implementation

The Set data structures provide support for :

  • basic operations like adding, replacing, and removing values
  • filtering like removing with or without predicates
  • selection like getting a specific element, finding one, or even slicing
  • transformation like diffing, intersection, union, zipping
  • conversion and traversal
  • interop with Java mutable collections

Creating a Set

Depending on the implementation you want you can always use empty(), or one of the one of the of(T), of(T...) and ofAll() factory methods.

Example with HashSet. The same API is provided for the other Set implementations.

Set<Integer> s = HashSet.of(1);
s = HashSet.of(1, 2, 3)
s = HashSet.empty();
var list = Arrays.asList(1, 2, 3); // java List
s = HashSet.ofAll(list);
s = HashSet.ofAll(list.stream()); // from a Stream

Creating ranges

You can create a Set based on a range, be it from two integers, two longs, or two characters using the range factory method.

Set<Integer> s = HashSet.range(0, 0); // HashSet()
Set<Integer> s = HashSet.range(0, 5); // HashSet(0, 1, 2, 3, 4)
Set<Integer> s = HashSet.range(-3, 1); // HashSet(-3, -2, -1, 0)

Set<Character> s = HashSet.range('a', 'g'); // HashSet(a, b, c, d, e, f)
Set<Long> s = HashSet.range(0L, 4L); // HashSet(0L, 1L, 2L, 3L)

Note that there is also a rangeBy method which let you provide a step.

Set<Integer> s = HashSet.rangeBy(0, 0, 1); // HashSet()
Set<Integer> s = HashSet.rangeBy(0, 4, 2); // HashSet(0, 2)
Set<Integer> s = HashSet.rangeBy(-3, 1, 3); // HashSet(-3)

Set<Character> s = HashSet.rangeBy('a', 'g', 2); // HashSet(a, c, e)
Set<Long> s = HashSet.rangeBy(0L, 4L, 4L); // HashSet(0L)

Finally rangeClosed and rangeClosedBy which are essentially the same as range and rangedBy but inclusive regarding of the upper bound.

Set<Integer> s = HashSet.rangeClosed(0, 0); // HashSet(0)
Set<Integer> s = HashSet.rangeClosed(0, 5); // HashSet(0, 1, 2, 3, 4, 5)
Set<Integer> s = HashSet.rangeClosed(-3, 1); // HashSet(-3, -2, -1, 0, 1)

Set<Character> s = HashSet.rangeClosed('a', 'g'); // HashSet(a, b, c, d, e, f, g)
Set<Long> s = HashSet.rangeClosed(0L, 4L); // HashSet(0L, 1L, 2L, 3L, 4L)

Set<Integer> s = HashSet.rangeByClosed(0, 0, 1); // HashSet(0)
Set<Integer> s = HashSet.rangeByClosed(0, 4, 2); // HashSet(0, 2, 4)
Set<Integer> s = HashSet.rangeByClosed(-3, 1, 3); // HashSet(-3, 1)

Set<Character> s = HashSet.rangeByClosed('a', 'g', 2); // HashSet(a, c, e, g)
Set<Long> s = HashSet.rangeByClosed(0L, 4L, 4L); // HashSet(0L, 4L)

Appending to a Set

In order to append elements to a Set, you may use add, or addAll.

Set<Integer> s = HashSet.of(1, 2);
s.add(3); // HashSet(1, 2, 3)
s.addAll(Vector.of(3, 4)); // HashSet(1, 2, 3, 4)
s.addAll(HashSet.of(3, 4)); // HashSet(1, 2, 3, 4)
s.add(1);  // HashSet(1, 2, 3)
s.addAll(Vector.of(1, 1, 1, 2, 2, 3, 3, 4)); // HashSet(1, 2, 3, 4)

Note that a Set may not contain duplicates, thus adding an element which is already in the set won't have any effect.

Besides, every operation modifying a Set will return a new one, the original Set won't be modified, since collections in Vavr are immutable.

Updating an element

In order to update an element in a Set, use replace.

Set<Integer> s = HashSet.of(1, 2);
s.replace(1, 3); // HashSet(2, 3)
s.replace(1, 2); // HashSet(2)

Note that the API provide also a replaceAll method but it's the same as calling replace.

Removing and filtering elements

You can either remove objects based on equality or on a predicate. To do this you can use either remove, removeAll, reject, filter or even retainAll.

Set<Integer> s = HashSet.of(1, 2, 3, 4, 1);
s.remove(1); // HashSet(2, 3, 4)
s.removeAll(Vector.of(1, 2)); // HashSet(3, 4)
s.reject(e -> e % 2 == 1); // HashSet(2, 4)
s.filter(e -> e % 2 == 0); // HashSet(2, 4)
s.retainAll(List.of(1, 2)); // HashSet(1, 2)

Finding elements

A Set lets you check that it contains an element using contains or containsAll.

Set<Integer> s = HashSet.of(1, 2, 3, 4, 1, 2);
s.contains(0); // false
s.contains(1); // true
s.containsAll(Vector.of(0, 1, 2)); // false
s.containsAll(Vector.of(2, 3, 4)); // true

Head and tail

You can retrieve the head (first element) and tail (the sequence without the first element) of a Set . It makes more sense with a LinkedHashSet where the order of insertion is guaranteed.

Use either head, headOption, tail or tailOption.

Set<Integer> s = LinkedHashSet.of(1, 2, 3, 4, 1, 2);
s.head(); // 1
s.headOption(); // Some(1)
s.tail(); // LinkedHashSet(2, 3, 4)
s.tailOption(); // Some(LinkedHashSet(2, 3, 4))

Set<Integer> s = LinkedHashSet.empty();
s.head(); // throws NoSuchElementException
s.headOption(); // None
s.tail(); // throws UnsupportedOperationException
s.tailOption(); // None

Getting the size

When you need to get the size of a Set, use either length or size which are doing the same thing.

var s = HashSet.of(1, 2, 3);
s.length(); // 3
s.size(); // 3

Dropping elements

You can drop a fixed amount of element from a Set.

Be aware that for the HashSet implementation, both drop and dropRight do the same thing (dropRight delegates call to drop), but for LinkedHashSet implementation, drop and dropRight do what implied from the method names.

You can also drop all elements until or while an element satisfies a predicate with dropWhile.

Set<Integer> s = LinkedHashSet.of(1, 2, 3, 4, 1, 2);
s.drop(2); // LinkedHashSet(3, 4)
s.dropRight(2); // LinkedHashSet(1, 2)

Set<Integer> s = HashSet.of(1, 2, 3, 4, 1, 2);
s.drop(2); // HashSet(3, 4)
s.dropRight(2); // HashSet(3, 4)

s.dropUntil(e -> e > 3); // HashSet(4)
s.dropWhile(e -> e < 3); // HashSet(3, 4)

Note that for Set implementations, dropUntil will call dropWhile with the negated given predicate.

Note also that for all methods relying on any order, the iteration is guaranteed only using a LinkedHashSet or a TreeSet, otherwise the iteration order is not guaranteed at all.

Taking elements

You can either take a fixed amount of element, from the start or the end of a sequence. You can also take all elements until or while an element satisfies a predicate. To do so use one of the take, takeRight, takeUntil or takeWhile methods.

Set<Integer> s = LinkedHashSet.of(1, 2, 3, 4, 1, 2);
s.take(2); // LinkedHashSet(1, 2)
s.takeRight(2); // LinkedHashSet(3, 4)

Set<Integer> s = HashSet.of(1, 2, 3, 4);
s.take(2); // HashSet(1, 2)
s.takeRight(2); // HashSet(1, 2)

s.takeUntil(e -> e >= 3); // HashSet(1, 2)
s.takeWhile(e -> e < 3); // HashSet(1, 2)

Note that takeUntil and takeWhile will have the same effect when using negated predicates.

Note also that for all methods relying on any order, the iteration is guaranteed only using a LinkedHashSet or a TreeSet, otherwise the iteration order is not guaranteed at all.

Sorting sets

Sorting of Set is provided via the SortedSet interface whose only implementation is TreeSet, so you may use a TreeSet or if you already have Set that you want to sort, you can create a TreeSet from it by using the toSortedSet() method which has two overload, one taking a custom Comparator.

Set<Integer> s = HashSet.of(3, 2, 4, 1);
s.toSortedSet(); // TreeSet(1, 2, 3, 4)
s.toSortedSet(Comparator.reverseOrder()); // TreeSet(4, 3, 2, 1)

record Person(String firstName, String lastName) {}
Set<Person> p = HashSet.of(new Person("Rich", "Hickey"), 
                           new Person("John", "Doe"), 
                           new Person("John", "Rambo"));

s.toSortedSet((a, b) -> b.lastName().compareTo(a.lastName())); 
// TreeSet(Person("John Rambo"), Person("Rich Hickey"), Person("John Doe"))

Partitioning sets

You can partition a Set into a Tuple2 of Sets where the first member of the Tuple contains the elements of the original Set which satisfies a Predicate, and the second member, the rest of the elements.

var t = HashSet.of("foo", "bar", "bazz").partition(s -> s.contains("a"));
t._1(); // Set(bar, bazz)
t._2(); // Set(foo)

Sliding sets

If you need to slide in a Set, which means create a window of a specific size that you can iterate on, Vavr provides the sliding and slideBy methods.

Set<Integer> s = LinkedHashSet.of(1, 2, 3, 4);
s.sliding(2); 
// Iterator on [LinkedHashSet(1, 2), LinkedHashSet(2, 3), LinkedHashSet(3, 4)]

s.slideBy(Function.identity()); 
// Iterator on [LinkedHashSet(1), LinkedHashSet(2), LinkedHashSet(3), LinkedHashSet(4)]

Set<Integer> s = LinkedHashSet.of(1, 2, 3, 9, 4, 6);
// group odd and even siblings together
s.slideBy(e -> e % 2); 
// Iterator on  [LinkedHashSet(1), LinkedHashSet(2), LinkedHashSet(3, 9), LinkedHashSet(4, 6)]

Note that for all methods relying on any order, the iteration is guaranteed only using a LinkedHashSet or a TreeSet, otherwise the iteration order is not guaranteed at all.

Scanning sets

If you need to compute a prefix scan of the elements of a sequence you can use scan, scanLeft and scanRight.

For example for producing a cumulative sum on the elements of a sequence you can do:

Set<Integer> s = LinkedHashSet.of(1, 2, 3);
s.scan(0, Integer::sum); // LinkedHashSet(0, 1, 3, 6)
// 0
// 0 + 1 = 1
// 1 + 2 = 3
// 3 + 3 = 6
s.scanLeft(0, Integer::sum); // will do the same

s.scanRight(0, Integer::sum); // LinkedHashSet(6, 5, 3, 0)
// reverse of:
// 0
// 0 + 3 = 3
// 3 + 2 = 5
// 5 + 1 = 6

Note that for all methods relying on any order, the iteration is guaranteed only using a LinkedHashSet or a TreeSet, otherwise the iteration order is not guaranteed at all.

Grouping elements

You can either group elements of a Set using a chunck size with grouped(int) or using a custom Function using groupBy.

HashSet.of("Alex", "Jessica", "Eva").grouped(2);
// Iterator on [Set(Alex, Jessica), Set(Eva)]

HashSet.of("Alex", "Jessica", "Eva").groupBy(s -> s.matches("^[AEIOU].*"));
// Map(true => Set(Alex, Eva)), false => Set(Jessica))

Note that grouped returns a Seq of Seq whereas groupBy returns a Map.

Union of sets

If you need to combine two sets by keeping all their values and removing duplicates (because this is a Set), then use union.

Set<Character> s1 = HashSet.of('a', 'b', 'c');
Set<Character> s2 = HashSet.of('b', 'z');

s1.union(s2); // HashSet(a, b, c, z)

Intersection of sets

If you need to retain values that are present in two different sets, then use intersect.

Set<Character> s1 = HashSet.of('a', 'b', 'c');
Set<Character> s2 = HashSet.of('b', 'z');

s1.intersect(s2); // HashSet(b)

Difference of sets

If you need to retain values that are present in one set and not in the other, then use diff.

Set<Character> s1 = HashSet.of('a', 'b', 'c');
Set<Character> s2 = HashSet.of('b', 'z');

s1.diff(s2); // HashSet(a, c)
s2.diff(s1); // HashSet(z)

Keeping distinct elements

You don't need to do anything because this is one of the properties of Set to avoid duplicates.

Zipping sets

You can zip sequences in order to iterate on both of them at the same time. To do that simply use one of the zip, zipAll, zipWith methods.

Set<Character> s1 = LinkedHashSet.of('a', 'b', 'c');
Set<Integer> s2 = LinkedHashSet.of(1, 2, 3);
s1.zip(s2); // LinkedHashSet((a, 1), (b, 2), (c, 3))
// exact type is LinkedHashSet<Tuple2<Character, Integer>>

The zip method will stop zipping sequences when any of the two sequences is totally consumed. If the two sequences don't have the same size you can provide default value to be filled with using zipAll.

Set<Character> s1 = LinkedHashSet.of('a', 'b', 'c', 'd', 'e');
Set<Integer> s2 = LinkedHashSet.of(1, 2, 3);
s1.zipAll(s2, 
   'x', // if s1 is shorter than s2 then it will use 'x' to fill
   42   // if s2 is shorter than s1 then it will use 42 to fill
); 
// LinkedHashSet((a, 1), (b, 2), (c, 3),
//               (d, 42), (e, 42))

If you need to apply a transformation to combine both elements of the sets to be zipped so that you can iterate on the final result you can use zipWith.

Set<Character> s1 = LinkedHashSet.of('a', 'b', 'c');
Set<Integer> s2 = LinkedHashSet.of(1, 2, 3);
s1.zipWith(s2, (a, b) -> Character.toString(a) + b);
// LinkedHashSet("a1", "b2", "c3")

Alternatively if you need to iterate over a sequence but you also need the element index in the source sequence you can use zipWithIndex which will give you a sequence of Tuple2 where the first member is the actual element you are iterating on, and the second member the element's index.

Set<Character> s1 = LinkedHashSet.of('a', 'b', 'c');
s1.zipWithIndex();
// LinkedHashSet((a, 0), (b, 1), (c, 2))

Finally, you can also use zipWithIndex by providing it a BiFunction to compute final elements over iteration.

Set<Character> s = LinkedHashSet.of('a', 'b', 'c');
s1.zipWithIndex((elem, index) -> Character.toString(elem) + index);
// LinkedHashSet("a0", "b1", "c2")

Summing elements

Considering a sequence of numbers, you can easily sum all the elements using either sum, reduce, fold or collect.

var s = HashSet.of(1, 2, 3);
s.sum(); // 6
s.reduce(Integer::sum); // 6
s.fold(0, Integer::sum); // 6
s.collect(Collectors.summingInt(i -> i)); // 6

Product of elements

Considering a sequence of numbers, you can easily compute the product of all the elements using either product, reduce or fold.

var s = HashSet.of(1, 2, 3, 4);
s.product(); // 24
s.reduce(Math::multiplyExact); // 24
s.fold(1, Math::multiplyExact); // 24

Note that 1 being the identity for the multiplication, computing the product of an empty Set will return 1.

Minimum and maximum

You can retrieve the minimum and maximum of a sequence, or any Traverseable by using min, minBy, max or maxBy.

var s = HashSet.of(1, 0, 2, 3, 5, 4);
s.min(); // Some(0)
s.max(); // Some(5)

Use minBy and maxBy if you need a custom Comparator or Function to retrieve the thing you need to decide which is the min or max.

Note that both min and max return an Option.

Averaging elements

You can compute the average value of a sequence, using the average method.

var v = HashSet.of(1, 2, 3, 4);
s.average(); // Some(2.5) = Some((1+2+3+4)/4)

Note that average returns an Option.

Making Strings

Considering a Seq of elements, you can make a String out of it by using either mkString or collect.

var s = LinkedHashSet.of(1, 2, 3, 4);
s.mkString(); // "1234"
s.mkString("-"); // "1-2-3-4"
s.mkString("<", "-", ">"); // "<1-2-3-4>"

Alternatively, you can use mkCharSeq to create CharSeq.

var s = LinkedHashSet.of(1, 2, 3, 4);
s.mkCharSeq(); // CharSeq(1, 2, 3, 4)
s.mkCharSeq("-"); // CharSeq(1, -, 2, -, 3, -, 4)
s.mkCharSeq("<", "-", ">"); // CharSeq(<, 1, -, 2, -, 3, -, 4, >)

Interop with Java

You can create a Java Set from a Vavr one by using toJavaSet().

Using with collectors

Interop with the Java Collectors is provided via collect()

LinkedHashSet.of("how", "are", "you").collect(Collectors.joining(" ")); 
// "how are you"

Note that the order in this example depends on the fact that we're using a LinkedHashSet, order is not guaranted with a HashSet.

Generally the collect method is available on all of Vavr's collections.


Maps

In Vavr, the Map interface has multiple implementations:

  • HashMap: an immutable HashMap implementation
  • LinkedHashMap: an immutable HashMap implementation that has predictable (insertion-order) iteration
  • TreeMap: a sorted Map implementation

The Map data structures provide support for :

  • basic operations like adding, replacing, removing values
  • filtering like removing with or without predicates
  • selection like finding and extracting both keys and values
  • transformation like merging, zipping and unzipping
  • conversion and traversal
  • interop with Java mutable collections

Creating a Map

Depending on the implementation you want you can always use empty(), or one of the one of the of(T), of(T...) and ofAll() factory methods.

Example with HashMap. The same API is provided for the other Map implementations.

var m = HashMap.of(1, "foo"); // 1 => foo
m = HashMap.of(1, "foo", 2, "bar") // 1 => foo, 2 => bar
m = HashMap.empty();
m = HashMap.of(Tuple.of(1, "foo")) // 1 => foo

The ofAll methods has overloads for creating a Map from another Map or from a Stream and Functions.

You can also use the ofEntries factory method which has overloads for Iterable, varargs of Entry or Tuple2.

var m = HashMap.of(1, "foo"); // 1 => foo

m = HashMap.ofEntries(Tuple.of(1, "foo"), Tuple.of(2, "bar"));
// 1 => foo, 2 => bar

m = HashMap.ofEntries(new SimpleEntry(1, "foo"));
// 1 => foo

m = HashMap.ofEntries(Vector.of(Tuple.of(1, "foo"), Tuple.of(2, "bar"));
// 1 => foo, 2 => bar

Adding entries to a Map

In order to add entries to a Map, you may use put.

var m = HashMap.empty();
m.put(1, "foo"); // 1 => foo
m.put(Tuple.of(1, "foo")); // 1 => foo
m.put(1, "foo").put(1, "bar", (current, newValue) -> current + newValue); 
// 1 => foobar

Note that the overload of put(K, V, BiFunction) will apply the function with both the current value of the key if it exists and the new value, so that you can merge with the logic you need.

Alternatively, you can use computeIfAbsent to insert an entry if it's missing. It returns a Tuple2 whose first member is the value if the key was found, and the value inserted otherwise. The second member of the Tuple2 is the new Map instance (the same one if not modified).

var m = HashMap.of(1, "foo", 2, "bar"); // 1 => foo, 2 => bar
m.computeIfAbsent(3, v -> "hello" + v); 
// ("hello3", Map(1 => foo, 2 => bar, 3 => hello3))
m.computeIfAbsent(1, v -> "hello" + v); 
// ("foo", Map(1 => foo, 2 => bar))

Updating an entry

In order to update an entry you use either put, replace, or replaceValue .

var m = HashMap.of(1, "foo", 2, "bar", 3, "bar"); // 1 => foo, 2 => bar, 3 => bar
m.replaceValue(1, "bazz"); // 1 => bazz, 2 => bar, 3 => bar
m.replace(1, "foo", "bazz"); // 1 => bazz, 2 => bar, 3 => bar
m.replace(Tuple.of(1, "foo"), Tuple.of(3, "bazz")); // 2 => bar, 3 => bazz

For put usage, see Adding entries to a Map above.

Alternatively, you can use computeIfPrsent to update an entry if it's already present. It returns a Tuple2 whose first member is an Option holding the updated value if the key was found, None otherwise. The second member of the Tuple2 is the new Map instance (the same one if not modified).

var m = HashMap.of(1, "foo", 2, "bar"); // 1 => foo, 2 => bar
m.computeIfPresent(1, (k, v) -> v + ".updated"); 
// (Some(foo.updated), Map(1 => foo.updated, 2 => bar))
m.computeIfAbsent(3, (k, v) -> v + ".updated"); 
// (None, Map(1 => foo, 2 => bar))

Finally you can use replaceAll to update multiple values in a Map.

var m = HashMap.of(1, "foo", 2, "bar", 3, "lol"); // 1 => foo, 2 => bar
m.replaceAll((k, v) -> v + ".updated"); 
// 1 => foo.updated, 2 => bar.updated, 3 => lol.updated
m.replaceAll((k, v) -> k % 2 == 0 ? v + ".updated" : v); 
// 1 => foo, 2 => bar.updated, 3 => lol

Removing and filtering elements

You can either remove objects based on equality, at a specific index or based on a predicate.

var m = HashMap.of(1, "foo", 2, "bar"); // 1 => foo, 2 => bar
m.remove(1); // 2 => bar
m.removeAll(Vector.of(1, 2)); // empty HashMap

m.reject(e -> e._2.startsWith("f")); // 2 => bar
m.reject((k, v) -> v.startsWith("f")); // 2 => bar
m.filter(e -> e._2.startsWith("f")); // 1 => foo
m.filter((k, v) -> v.startsWith("f")); // 1 => foo

m.rejectKeys(k -> k == 1); // 2 => bar
m.filterKeys(k -> k == 1); // 1 => foo

m.rejectValues(k -> k.startsWith("f")); // 2 => bar
m.filterValues(k -> k.startsWith("f")); // 1 => foo

Note that there are also removeKeys and removeValues which are deprecated and just aliases of rejectKeys and rejectValues.

Alternatively, you can use retainAll.

var m = HashMap.of(1, "foo", 2, "bar", 3, "lol"); // 1 => foo, 2 => bar, 3 => lol

// 1 => foo, 2 => bar
m.retainAll(List.of(Tuple.of(1, "foo"), Tuple.of(2, "bar")));

Finding elements

You can use get or getOrElse.

var m = HashMap.of(1, "foo", 2, "bar"); // 1 => foo, 2 => bar
m.get(1); // Some("foo")
m.get(5); // None
m.getOrElse(5, "hey"); // Some("hey")

You can also check if a Map contains a key or a value.

var m = HashMap.of(1, "foo", 2, "bar"); // 1 => foo, 2 => bar
m.contains(Tuple.of(1, "foo")); // true
m.containsKey(1); // true
m.containsValue("foo"); // true

Head and tail

You can retrieve the head (first element) and tail (the sequence without the first element) of a Map with head and tail. It makes sense for TreeMap and LinkedHashMap where the order of iteration is guaranteed.

var m = LinkedHashMap.of(1, "foo", 2, "bar"); // 1 => foo, 2 => bar
m.head(); // Tuple2(1, "foo")
m.headOption(); // Some(Tuple2(1, "foo"))
m.tail(); // 2 => bar
m.tailOption(); // Some(2 => bar)

var m = LinkedHashMap.empty();
m.head(); // throws NoSuchElementException
s.headOption(); // None
s.tail(); // throws UnsupportedOperationException
s.tailOption(); // None

Getting the size

When you need to get the size of a Map, use either length or size which are doing the same thing.

var m = HashMap.of(1, "foo", 2, "bar");
m.length(); // 2
m.size(); // 2

Retrieving the keys

When in need of the keys defined in a Map, use keySet() which will return a Set of the keys in that Map instance.

var m = HashMap.of(1, "foo", 2, "bar");
m.keySet() // HashSet(1, 2)

Note that depending on the Map implementation you're using, then Set instance will be different. Calling keySet() on an HashMap will produce a HashSet, calling it on a LinkedHashMap will produce a LinkedHashSet, and finally calling keySet() on a TreeMap will produce a TreeSet so that you can iterate on the keys in the same order you would iterate on the Map entries.

Retrieving the values

If you need to iterate over the values in a Map, use values which will return a Stream of the values in that Map instance.

var m = HashMap.of(1, "foo", 2, "bar");
m.values() // Stream(foo, bar)

Values will be ordered in the Stream depending on your Map implementation.

Alternatively, you can create a Function1<K, Option<V>> which goal is to retrieve the value in the Map given a key, by using the lift method.

var m = HashMap.of(1, "foo", 2, "bar");
m.lift().apply(1); // Some(foo)
m.lift().apply(5); // None

Dropping elements

You can either drop a fixed amount of element, from the start or the end of a Map. You can also drop all elements until or while an element satisfies a predicate. To do so use one of the drop, dropRight, dropUntil or dropWhile methods.

var m = LinkedHashMap.of(1, "foo", 2, "bar"); // 1 => foo, 2 => bar
m.drop(1); // 2 => bar
m.dropRight(1); // 1 => foo
m.dropUntil(e -> e._1 >= 2); // 2 => bar
m.dropWhile(e -> e._1 < 2); // 2 => bar

It makes sense for TreeMap and LinkedHashMap where the order of iteration is guaranteed.

Taking elements

You can either take a fixed amount of element, from the start or the end of a Map. You can also take all elements until or while an element satisfies a predicate. To do so use one of the take, takeRight, takeUntil or takeWhile methods.

var m = LinkedHashMap.of(1, "foo", 2, "bar"); // 1 => foo, 2 => bar
m.take(1); // 1 => foo
m.takeRight(1); // 2 => bar
m.takeUntil(e -> e._1 >= 2); // 1 => foo
m.takeWhile(e -> e._1 < 2); // 1 => foo

It makes sense for TreeMap and LinkedHashMap where the order of iteration is guaranteed.

Sorting Map

Sorting of Map is provided via the TreeMap implementation, so you may use a TreeMap or if you already have Map that you want to sort, you can create a TreeMap from it by using the toSortedMap() method which has two overload, one taking a custom Comparator.

var m = HashMap.of(3, "bazz", 1, "foo", 2, "bar"); // 1 => foo, 2 => bar, 3 => bazz
m.toSortedMap(Integer::compare, t -> t._1, t -> t._2) // 1 => foo, 2 => bar, 3 => bazz
m.toSortedSet(Comparator.reverseOrder()); // 3 => bazz, 2 => bar, 1 => foo

Mapping Maps

Vavr let you use map on Map entries using map, on keys using mapKeys and on values using mapValues.

var m = HashMap.of(1, "foo", 2, "bar");
m.map((k, v) -> Tuple.of(k * 10, v.toUpperCase()));
// HashMap(10 => FOO, 20 => BAR)

m.map((k, v) -> Tuple.of(k > 1 ? k - 1 : k, v.toUpperCase()));
// HashMap(1 => BAR)
// in this example we overwrite value for key 1 with BAR

m.mapKeys(k -> k * 10); // HashMap(10 => foo, 20 => bar)
m.mapValues(String::toUpperCase); // HashMap(1 => FOO, 2 => BAR)

Alternatively, you can use bimap to map both keys and values.

var m = HashMap.of(1, "foo", 2, "bar");

// HashMap(10 => FOO, 20 => BAR)
m.bimap(k -> k * 10, String::toUpperCase);

Sliding over Maps

If you need to slide in a sequence, which means create a window of a specific size that you can iterate on, Vavr provides the sliding and slideBy methods.

var m = TreeMap.of(1, "foo", 2, "bar", 3, "bazz"); // 1 => foo, 2 => bar, 3 => bazz
m.sliding(2); // Iterator on [[1 => foo, 2 => bar], [2 => bar, 3 => bazz]]

m.slideBy(Function.identity()); 
// Iterator on [[1 => foo], [2 => bar], [3 => bazz]]

var m = TreeMap.of(1, "foo", 2, "bar", 4, "bazz"); // 1 => foo, 2 => bar, 4 => bazz
m.slideBy(e -> e._1 % 2); 
// group odd and even siblings together
// Iterator on  [[1 => foo], [2 => bar, 4 => bazz]]

Note that for all methods relying on any order, the iteration is guaranteed only using a LinkedHashMap or a TreeMap, otherwise the iteration order is not guaranteed at all.

Scanning entries

If you need to compute a prefix scan of the elements of a sequence you can use scan, scanLeft and scanRight.

For example for producing a cumulative sum on the elements of a sequence you can do:

var m = TreeMap.of(1, "foo", 2, "bar", 3, "bazz");
m.scan(Tuple.of(0, "hello"), 
    (e1, e2) -> Tuple.of(e1._1 + e2._1, e1._2 + " " + e2._2));
// 0 => hello, 1 => hello foo, 3 => hello foo bar, 6 => hello foo bar baz
//
// 0 
// 0 + 1 = 1
// 1 + 2 = 3
// 3 + 3 = 6
m.scanLeft(Tuple.of(0, "hello"), 
    (e1, e2) -> Tuple.of(e1._1 + e2._1, e1._2 + " " + e2._2)); 
// will do the same but resulting in a Vector of Tuple2
// [(0, hello), (1, hello foo), (3, hello foo bar), (6, hello foo bar bazz)]

m.scanRight(Tuple.of(0, "hello"), 
    (e1, e2) -> Tuple.of(e1._1 + e2._1, e1._2 + " " + e2._2)); 
// [(6, foo bar bazz hello), (5, bar bazz hello), (3, baz hello), (0, hello)]
// reverse of:
// 0
// 0 + 3 = 3
// 3 + 2 = 5
// 5 + 1 = 6

Grouping elements

You can either group elements of a Map using a chunck size with grouped(int) or using a custom Function using groupBy.

HashMap.of(1, "Alex", 2, "Jessica", 3, "Eva").grouped(2);
// Iterator on [Map(1 => Alex, 2 => Jessica), Map(3 => Eva)]

HashMap.of("Alex", "Jessica", "Eva").groupBy(s -> s._2.matches("^[AEIOU].*"));
// Map(true => Map(1 => Alex, 3 => Eva)), false => Map(2 => Jessica))

Note that grouped returns an Iterator of Map whereas groupBy returns a Map of Maps.

Merging two Maps

Vavr provides a way to merge two maps using merge.

var m1 = TreeMap.of(1, "foo", 2, "bar");
var m2 = TreeMap.of(1, "foo", 3, "bazz");

m1.merge(m2); // 1 => foo, 2 => bar, 3 => bazz

var m3 = TreeMap.of(1, "lol", 3, "bazz");
m1.merge(m3, (current, newValue) -> current + "/" + newValue);
// 1 => foo/lol, 2 => bar, 3 => bazz

Reversing entries

Reversing a sequence is a simple as calling the reversed() method.

var m = TreeMap.of(1, "foo", 2, "bar");
m.reversed(); // 2 => bar, 1 => foo

It makes sense only for TreeMap and LinkedHashMap since it's guaranteed that they have a consistent iteration order.

Keeping distinct entries

Calling distinct on a Map will have no effect since it holds only unique entries of key to value.

You can use distinctBy to remove duplicates based on a custom Comparator. Note that distinctBy uses iteration order and will keep the first copy of duplicated entries (if any).

var m = TreeMap.of(1, "foo", 2, "bar", 3, "foo");
m.distinctBy(Comparator.comparing(a -> a._2));  
// 1 => foo, 2 => bar

Folding and reducing Maps

Folding is essentially like reducing but using an initial value. The fold method takes both a zero value (meaning initial), and a BiFunction used to combine values and make a final new one.

var m = TreeMap.of(1, "foo", 2, "bar", 3, "foo");
m.fold(Tuple.of(0, ""), (a, b) -> Tuple.of(a._1 + b._1, a._2 + b._2)); 
// (6, foobarfoo)

m.fold(Tuple.of(10, "hello"), (a, b) -> Tuple.of(a._1 + b._1, a._2 + b._2)); 
// (16, hellofoobarfoo)

m.reduce((a, b) -> Tuple.of(a._1 + b._1, a._2 + b._2)); 
// (6, foobarfoo)

The fold and foldLeft do exactly the same. There's also foldRight like on other sequences.

Zipping entries

You can zip entries of two Maps by iterating on both of them at the same time. To do that simply use one of the zip, zipAll, zipWith methods.

var m1 = TreeMap.of(1, "foo", 2, "bar", 3, "bazz");
var m2 = TreeMap.of(10, "FOO", 20, "BAR", 30, "BAZZ");

m1.zip(m2); 
// [((1, foo), (10, FOO)), ((2, bar), (20, BAR)), ((3, bazz), (30, BAZZ))]

In the previous example, it will produce a sequence of type Seq<Tuple2<Tuple2<Integer, String>, Tuple2<Integer, String>>>.

A Tuple2 for each entry of each maps, stored in a Tuple2 holding them both.

The zip method will stop zipping maps when any of the two sequences is fully consumed. If the two sequences don't have the same size you can provide default value to be filled with using zipAll.

var m1 = TreeMap.of(1, "foo", 2, "bar", 3, "bazz", 4, "lol");
var m2 = TreeMap.of(10, "FOO", 20, "BAR", 30, "BAZZ");
m1.zipAll(m2, 
    Tuple.of(0, "xxx"), // if m1 is shorter than m2 then it will use (0, xxx) to fill
    Tuple.of(100, "XXX") // if m2 is shorter than m1 it will use (100, XXX) to fill 
); 
// [((1, foo), (10, FOO)), 
//  ((2, bar), (20, BAR)), 
//  ((3, bazz), (30, BAZZ)),
//  ((4, lol), (100, XXX))]

If you need to apply a transformation to combine both entries of the maps to be zipped so that you can iterate on the final result you can use zipWith.

var m1 = TreeMap.of(1, "foo", 2, "bar", 3, "bazz", 4, "lol");
var m2 = TreeMap.of(10, "FOO", 20, "BAR", 30, "BAZZ");

m1.zipWith(m2, (a, b) -> (a._1 + b._1) + "/" + (a._2 + b._2))
// [11/fooFOO, 22/barBAR, 33/bazzBAZZ]

Alternatively if you need to iterate over a Map but you also need the entry index in the source sequence you can use zipWithIndex which will give you a sequence of Tuple2 where the first member is the actual element you are iterating on, and the second member the element's index.

var m1 = TreeMap.of(1, "foo", 2, "bar");
m1.zipWithIndex();
// [((1, foo), 0), ((2, bar), 1))

Finally, you can also use zipWithIndex by providing it a BiFunction to compute final elements over iteration.

var m = TreeMap.of(1, "foo", 2, "bar");
m.zipWithIndex((e, index) -> e._1 + "/" + e._2 + "/" + index);
// [1/foo/0, 2/bar/1]

Partitioning Maps

You can partition a Map into a Tuple2 of Maps where the first member of the Tuple2 contains the entries of the original Map which satisfy a Predicate, and the second member is the rest of the entries.

var t = HashMap.of(1, "foo", 2, "bar", 3, "bazz").partition(e -> e._2.contains("a"));
t._1(); // Map(2 => bar, 3 => bazz)
t._2(); // Map(1 => foo)

Interop with Java

You can convert a Vavr Map to a Java Map using toJavaMap()

Using with collectors

Interop with the Java Collectors is provided via collect()

LinkedHashMap.of(1, "how", 2, "are", 3, "you").collect(Collectors.counting()); // 3

Generally the collect method is available on all of Vavr's collections.


Streams

Vavr provides a support for an immutable Stream as a lazy sequence of elements which may be infinitely long.

A Stream is composed of a head and a lazy evaluated tail Stream.

There are two implementations of the Stream interface:

  • Empty, an empty Stream
  • Cons, a Stream containing at least one element.

In Vavr, a Stream extends LinearSeq and thus supports :

  • basic operations like creating, generating,
  • filtering like removing with or without predicates, rejecting and so on
  • selection like getting a specific element, finding one or even slicing
  • transformation like cross product, combinations, permutations, sorting, ziping etc.
  • conversion and traversal
  • interop with Java streams

Creating a sequence

Stream supports multiple ways of creating streams, like empty(), or one of the one of the of(T), of(T...) and ofAll() factory methods.

Stream<Integer> s = Stream.of(1);
s = Stream.of(1, 2, 3)
s = Stream.empty();

var list = Arrays.asList(1, 2, 3); // java List
s = Stream.ofAll(list);
s = Stream.ofAll(list.stream()); // from a Stream

Appending to a Stream

In order to append elements to a Seq, you may use append, appendAll, insert, insertAll, prepend or prependAll.

Stream<Integer> s = Stream.of(1, 2);
s.append(3); // Stream(1, 2, 3)
s.appendAll(Vector.of(3, 4)); // Stream(1, 2, 3, 4)
s.insert(1, 10); // Stream(1, 10, 2)
s.insertAll(1, Vector.of(10, 20)); // Stream(1, 10, 20, 2)
s.prepend(0); // Stream(0, 1, 2)
s.prependAll(Vector.of(-1, 0)); // Stream(-1, 0, 1, 2)

Updating an element

In order to update an element at a specific index, use update.

Stream<Integer> s = Stream.of(1, 2);
s.update(1, 20); // Stream(1, 20)

Removing and filtering elements

You can either remove objects based on equality, at a specific index or based on a predicate.

Stream<Integer> s = Stream.of(1, 2, 3, 4, 1);
s.remove(1); // Stream(2, 3, 4, 1)
s.removeAll(1); // Stream(2, 3, 4)
s.removeAll(Vector.of(1, 2)); // Stream(3, 4)
s.removeAt(0); // Stream(2, 3, 4, 1)
s.removeFirst(e -> e % 2 == 1); // Stream(2, 3, 4, 1)
s.removeLast(e -> e % 2 == 1); // Stream(1, 2, 3, 4)
s.reject(e -> e % 2 == 1); // Stream(2, 4)
s.filter(e -> e % 2 == 0); // Stream(2, 4)

Finding elements

You can use get or one of indexOf, indexOfSlice and indexWhere. Vavr provides also lastIndexOf versions of these methods. The indexOf methods return -1 when nothing can be found, and if you wish to have an Option instead use the indexOfOption variant.

Stream<Integer> s = Stream.of(1, 2, 3, 4, 1, 2);
s.get(0); // 1
s.indexOf(1); // 0
s.indexOf(5); // -1
s.indexOf(1, 1); // 4  
// where indexOf takes 2 arguments: element to search and starting index
s.indexOf(2, 3) // 5
s.lastIndexOf(1); // 4
s.indexOfOption(2); // Some(1)
s.indexOfOption(5); // None
s.lastIndexOfOption(5); // None
s.indexWhere(e -> e % 2 == 0); // 1
s.indexWhereOption(e -> e > 10); // None
s.lastIndexWhere(e -> e % 2 == 0); // 5
s.lastIndexWhereOption(e -> e < 0); // None
s.indexOfSlice(Vector.of(1, 2)); // 0
s.lastIndexOfSlice(Vector.of(1, 2)); // 4

Slices and sub sequences

In order to get a slice or a sub sequence of a Seq you can use slice or subSequence. The only difference between both is that slice does not throw but instead return an empty Seq.

Stream<Integer> s = Stream.of(1, 2, 3, 4);
s.slice(0, 3); // Stream(1, 2, 3)
s.slice(0, 10); // Stream(1, 2, 3, 4)
s.slice(-100, 250); // Stream(1, 2, 3, 4)
s.subSequence(1); // Stream(2, 3, 4)
s.subSequence(3); // Stream(4)
s.subSequence(1, 2); // Stream(2, 3)
s.subSequence(50); // throws IndexOutOfBoundsException

Head and tail

You can retrieve the head (first element) and tail (the Stream without the first element) of a Seq with head and tail.

Stream<Integer> s = Stream.of(1, 2, 3, 4);
s.head(); // 1
s.headOption(); // Some(1)
s.tail(); // Stream(2, 3, 4)
s.tailOption(); // Some(Stream(2, 3, 4))

Stream<Integer> s = Stream.of();
s.head(); // throws NoSuchElementException
s.headOption(); // None
s.tail(); // throws UnsupportedOperationException
s.tailOption(); // None

Dropping elements

You can either drop a fixed amount of element, from the start or the end of a Stream. You can also drop all elements until or while an element satisfies a predicate. To do so use one of the drop, dropRight, dropUntil or dropWhile methods.

Stream<Integer> s = Stream.of(1, 2, 3, 4);
s.drop(2); // Stream(3, 4)
s.dropRight(2); // Stream(1, 2)
s.dropUntil(e -> e > 3); // Stream(4)
s.dropWhile(e -> e < 3); // Stream(3, 4)

Note that the API provides also dropRightUntil and dropRightWhile.

Taking elements

You can either take a fixed amount of element, from the start or the end of a Stream. You can also take all elements until or while an element satisfies a predicate. To do so use one of the take, takeRight, takeUntil or takeWhile methods.

Stream<Integer> s = Stream.of(1, 2, 3, 4);
s.take(2); // Stream(1, 2)
s.takeRight(2); // Stream(3, 4)
s.takeUntil(e -> e >= 3); // Stream(1, 2)
s.takeWhile(e -> e < 3); // Stream(1, 2)

Note that the API provides also takeRightUntil and takeRightWhile.

Checking if starting or ending

If you need to know if a stream starts or ends with a specific Iterable you can use either startsWith or endsWith.

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
s.startsWith(Vector.of(2, 3, 4)); // false
s.startsWith(Vector.of(1, 2, 3, 4)); // true
s.endsWith(Vector.of(2, 3, 4)); // false
s.endsWith(Vector.of(3, 4, 5)); // true

Note that startsWith is essentially the same as checking that indexOf returns 0, and endsWith for indexOf returning the size of the stream minus the size of the iterable.

Splitting streams

If you need to split a stream in two at a specific index or at the first element which satisfies a predicate, Vavr got you covered. All of splitAt(int), splitAt(Predicate) and splitAtInclusive(Predicate) returns a Tuple2<Seq, Seq>.

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
s.splitAt(2); // (Stream(1, 2), Stream(3, 4, 5))

s.splitAt(e -> e > 2 && e % 2 == 0);
// (Stream(1, 2, 3), Stream(4, 5))

s.splitAt(e -> e > 2 && e % 2 == 1);
// (Stream(1, 2, 3, 4), Stream(5))

Sorting streams

Sorting of streams is provided via sorted(), sorted(Comparator) with a custom Comparator or sortBy.

Stream<Integer> s = Stream.of(3, 2, 4, 1);
s.sorted(); // Stream(1, 2, 3, 4)
s.sorted(Comparator.reverseOrder()); // Stream(4, 3, 2, 1)

Stream<Person> x = Stream.of(
            new Person("Robert", "Blurb"),
            new Person("Jane", "Doe"), 
            new Person("Foo", "Bar"));
x.sortBy(p -> p.lastName); 
// Stream(Bar Foo, Blurb Robert, Doe Jane)

Sliding over streams

If you need to slide in a Stream, which means create a window of a specific size that you can iterate on, Vavr provides the sliding and slideBy methods.

Stream<Integer> s1 = Stream.of(1, 2, 3, 4);
s1.sliding(2); // Iterator on [Stream(1, 2), Stream(2, 3), Stream(3, 4)]

s1.slideBy(Function.identity()); 
// Iterator on [Stream(1), Stream(2), Stream(3), Stream(4)]

Stream<Integer> s2 = Stream.of(1, 2, 3, 9, 4, 6);
// group odd and even siblings together
s2.slideBy(e -> e % 2); 
// Iterator on [Stream(1), Stream(2), Stream(3, 9), Stream(4, 6)]

Scanning streams

If you need to compute a prefix scan of the elements of a Stream you can use scan, scanLeft and scanRight.

For example for producing a cumulative sum of the elements of a sequence you can do:

Stream<Integer> s = Stream.of(1, 2, 3);
s.scan(0, Integer::sum); // Stream(0, 1, 3, 6)
// 0
// 0 + 1 = 1
// 1 + 2 = 3
// 3 + 3 = 6
s.scanLeft(0, Integer::sum); // will do the same

s.scanRight(0, Integer::sum); // Stream(6, 5, 3, 0)
// reverse of:
// 0
// 0 + 3 = 3
// 3 + 2 = 5
// 5 + 1 = 6

Rotating streams

Vavr provides a way to circularly rotate a sequence in both left and right direction with rotateLeft and rotateRight.

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
s.rotateLeft(2); // Stream(3, 4, 5, 1, 2)
s.rotateRight(2); // Stream(4, 5, 1, 2, 3)

Reversing streams

Reversing a Stream is a simple as calling the reverse() method.

Stream<Integer> s = Stream.of(1, 2, 3, 4);
s.reverse(); // Stream(4, 3, 2, 1)
s.reverseIterator(); // Iterator on Stream(4, 3, 2, 1)

Generating permutations

Getting all the possible permutations of a Stream is a simple as calling the permutations method.

Stream<Character> s = Stream.of('a', 'b', 'c');
s.permutations();
// Stream(
//.    Stream(a, b, c), 
//.    Stream(a, c, b), 
//     Stream(b, a, c), 
//.    Stream(b, c, a), 
//.    Stream(c, a, b), 
//.    Stream(c, b, a)
// )

Generating combinations

Getting all the combinations of a Stream is as simple as calling the combinations() method.

Stream<Character> s = Stream.of('a', 'b', 'c');
s.combinations();
// Stream(
//     Stream(), 
//     Stream(a), 
//     Stream(b), 
//     Stream(c), 
//     Stream(a, b), 
//     Stream(a, c), 
//     Stream(b, c), 
//     Stream(a, b, c)
// )

Keeping distinct elements

If you need to get the distinct values of a Stream you can use the distinct or distinctBy methods.

Stream<Character> s = Stream.of('a', 'b', 'c', 'a', 'b');
s.distinct(); // Stream(a, b, c)

Stream<Person> x = Stream.of(
            new Person("Robert", "Blurb"),
            new Person("John", "Blurb"),
            new Person("Jane", "Doe"), 
            new Person("Foo", "Bar"));
x.distinctBy(p -> p.lastName()); 
// Stream(Blurb Robert, Doe Jane, Bar Foo)

Generating cross products

Getting cross products of a Stream with itself or with another Stream is covered by the crossProduct method and its various overloading.

Stream<Character> s1 = Stream.of('a', 'b', 'c');
s1.crossProduct(); 
// Iterator on [Stream(a, a), Stream(a, b), Stream(a, c)
//              Stream(b, a), Stream(b, b), Stream(b, c)
//              Stream(c, a), Stream(c, b), Stream(c, c)]

Stream<Integer> s2 = Stream.of(1, 2, 3);
s1.crossProduct(s2); 
// Iterator on [Stream(a, 1), Stream(a, 2), Stream(a, 3)
//              Stream(b, 1), Stream(b, 2), Stream(b, 3)
//              Stream(c, 1), Stream(c, 2), Stream(c, 3)]

Zipping streams

You can zip two Stream instances in order to iterate on both of them at the same time. To do that simply use one of the zip, zipAll or zipWith methods.

Stream<Character> s1 = Stream.of('a', 'b', 'c');
Stream<Integer> s2 = Stream.of(1, 2, 3);
s1.zip(s2); // Stream(Tuple2('a', 1), Tuple('b', 2), Tuple2('c', 3))

The zip method will stop zipping streams when any of the two streams are totally consumed. If the two streams don't have the same size you can provide default value to be filled with using zipAll.

Stream<Character> s1 = Stream.of('a', 'b', 'c', 'd', 'e');
Stream<Integer> s2 = Stream.of(1, 2, 3);
s1.zipAll(s2, 
   'x', // if s1 is shorter than s2 then it will use 'x' to fill
   42   // if s2 is shorter than s1 then it will use 42 to fill
); 
// Stream((a, 1), (b, 2), (c, 3),
//        (d, 42), (e, 42))

If you need to apply a transformation to combine both elements of the streams to be zipped so that you can iterate on the final result you can use zipWith.

Stream<Character> s1 = Stream.of('a', 'b', 'c');
Stream<Integer> s2 = Stream.of(1, 2, 3);
s1.zipWith(s2, (a, b) -> Character.toString(a) + b);
// Stream("a1", "b2", "c3")

Alternatively if you need to iterate over a Stream but you also need the element index in the source sequence you can use zipWithIndex which will give you a sequence of Tuple2 where the first member is the actual element you are iterating on, and the second member the element's index.

Stream<Character> s = Stream.of('a', 'b', 'c');
s.zipWithIndex();
// Stream((a, 0), (b, 1), (c, 2))

Finally, you can also use zipWithIndex by providing it a BiFunction to compute final elements over iteration.

Stream<Character> s = Stream.of('a', 'b', 'c');
s.zipWithIndex((elem, index) -> Character.toString(elem) + index);
// Stream("a0", "b1", "c2")

Vavr in action

Try: a real world example

Imagine you are writing a service which can be seen as a pipeline, like:

  1. data in
  2. transform (may have side effect)
  3. data out
  4. transform (may have side effect)
  5. data out
  6. repeat…

But what if during step 6 we need data which was computed during step 2?

We need to keep a context of the pipeline which will be passed during the execution of the pipeline.

Of course you don’t want to execute step 4 if step 2 failed.

Using Lombok, it’s really easy to create Java beans enriched with plenty of features which makes the code easy to both write and read, this is our context.

Example

Let’s take a look at some sort of registration pipeline where we can imagine the business is the following:

  1. Given an id
  2. Retrieve a user details: email, first name and password (not really secure ;-)
  3. Register an account on Twitter
  4. Authenticate on Twitter
  5. Tweet Hello, world
  6. Update the user details with the Twitter account id
  7. Log something in case of success
  8. Return the tweet URL
  9. In case of error anywhere log something also

Simple code

A simple code example for this could be like the following:

interface UserService {
    User byId(String userId);
    void updateTwitterAccount(String userId, String twitterId);
}

interface TwitterService {
    TwitterAccount register(String email, String firstName, String password);
    String authenticate(String email, String password);
    Tweet tweet(String authToken, String message);
}

@RequiredArgsConstructor
class TwitterRegistrationService {
    final UserService userService;
    final TwitterService twitterService;
    final BusinessLogger blog;
    
    /**
     * Register the given user on Twitter,
     * post a hello world and return this tweet URL.
     *
     * @param userId the user id
     * @return the tweet URL
     */
    public String register(String userId) {
        try {
            User user = userService.byId(userId);
            if (user == null) {
                blog.logErrorRegisteringTwitterAccount(userId);
                return null;
            }
            
            TwitterAccount account = twitterService.register(
                     user.email, user.firstName, user.password);
            if (account == null) {
                blog.logErrorRegisteringTwitterAccount(userId);
                return null;
            }
            
            String authToken = twitterService.authenticate(
                     user.email, user.password);
            if (authToken == null) {
                blog.logErrorRegisteringTwitterAccount(userId);
                return null;
            }
            
            Tweet tweet = twitterService.tweet(authToken, "Hello, world!");
            if (tweet == null) {
                blog.logErrorRegisteringTwitterAccount(userId);
                return null;
            }
            
            userService.updateTwitterAccount(userId, account.id);
            
            blog.logSuccessRegisteringTwitterAccount(userId);
            
            return tweet.url;
        } catch (Exception e) {
            blog.logErrorRegisteringTwitterAccount(userId, e);
            return null;
        }
    }
}

This code is easy to follow but has a lot of repetitions, first we could return Options so that the code don’t have to check for null.

But we'll rather look at how to use Try to solve this problem. Each time you see a check like if (foo == null) we'll use the Try monad to avoid it and chain the next computation.

Improved code

Let’s see how we can improve the code by just modifying the RegistrationService and creating the context object.

Context

The context needs to store anything which is useful for the pipeline to execute, here we make use of Lombok to avoid boilerplate and make things clearer:

@Data
@Accessors(chain = true)
class Context {
    String id, email, firstName, password;
    String accountId, token, url;
    
    public Context(User user) {
        this.id = user.id;
        this.email = user.email;
        this.firstName = user.firstName;
        this.password = user.password;
    }
}

Note the usage of @Accessors which will help to write less code later on.

The Lombok annotations will help writing cleaner and concise code. Of course you could also use MapStruct so that it can generate a Mapper from User to Context and avoid some manual code ;).

RegistrationService

@RequiredArgsConstructor
class TwitterRegistrationService {
    final UserService userService;
    final TwitterService twitterService;
    final BusinessLogger blog;
    
    /**
     * Register the given user on Twitter,
     * post a hello world and return this tweet URL.
     *
     * @param userId the user id
     * @return the tweet URL if any
     */
    public Option<String> register(String userId) {
        return userDetails(userId)
            .flatMap(this::registerTwitter)
            .flatMap(this::authenticate)
            .flatMap(this::tweet)
            .andThen(this::updateUserTwitterAccount)
            .andThen(c -> blog.logSuccessRegisteringTwitterAccount(userId))
            .onFailure(e -> blog.logErrorRegisteringTwitterAccount(userId, e))
            .map(Context::getUrl)
            .toOption();
    }
  
    // Create a registration context based on the userId
    Try<Context> userDetails(String userId) {
        return Try.of(() -> userService.byId(userId)).map(Context::new);
    }
    
    // register a twitter account for the user
    Try<Context> registerTwitterAccount(Context c) {
        return Try.of(() -> twitterService.register(c.email, c.firstName, c.password))
            .map(account -> c.setAccountId(account.id));
    }
    
    // authenticate on twitter as the newly created user
    Try<Context> authenticate(Context c) {
        return Try.of(() -> twitterService.authenticate(c.email, c.password))
            .map(c::setToken);
    }
    
    // tweet "Hello, world!" and retrieve the tweet URL
    Try<Context> tweet(Context c) {
        return Try.of(() -> twitterService.tweet(c.token, "Hello, world!"))
            .map(tweet -> c.setUrl(tweet.url));
    }
    
    void updateUserTwitterAccount(Context c) {
        return Try.run(() -> userService.updateTwitterAccount(c.id, c.accountId));
    }
}

Each if (foo == null) from the original code has been replaced by its own function taking a Context and returning a Try.

Each of these function will call another service be it the twitterService or the userService, finally the register function uses all theses construct and create a pipeline of execution by using flatMap to deal with the fact that the different functions return a Try and thus may fail.

Indeed, we want to short circuit and stop as soon as an error occurs, hopefully everything goes well and the code reaches and execute the map(Context::getUrl) and return the tweet URL.

Almost the same amount of code (around 50 lines), but now the pipeline is clear, you can clearly see what registering is about.

Besides, you avoid a big try catch and tell clearly to the consumer of your RegistrationService that register may fail and return None.

You could return a Try also but in this case any error is correctly logged (imagine going into ELK) so there’s no need for the caller to know exactly what went wrong, just that it went wrong.

This pattern

I came with this pattern which plays really well with Vavr & Lombok APIs when I started to use Vavr a lot, I don’t know if it has a name except (and I don’t care that much :-), it’s pure pragmatism, that’s what I would have done if I was to use Clojure at work, just passing a map from step to step, associng keys in it.

However in Java it’s more practical to define and use a custom object than using a Map<String, Object>, that’s where Lombok comes just as needed to reduce the boilerplate.

The Gilded Rose Kata

The Gilded Rose Kata is a well known refactoring kata originally created by Terry Hughes.

In this kata you're supposed to improve some legacy code which has a really high cyclomatic complexity, with non existing tests. My goal in this chapter is not to show you how to write such tests before refactoring, but instead assuming you already have some tests, how we can refactor it with Java and a touch of Vavr to make the code shine and easier to upgrade.

Requirements specifications

Hi and welcome to team Gilded Rose.

As you know, we are a small inn with a prime location in a prominent city ran by a friendly innkeeper named Allison.

We also buy and sell only the finest goods. Unfortunately, our goods are constantly degrading in quality as they approach their sell by date.

We have a system in place that updates our inventory for us. It was developed by a no-nonsense type named Leeroy, who has moved on to new adventures.

Your task is to add the new feature to our system so that we can begin selling a new category of items.

First an introduction to our system:

  • All items have a SellIn value which denotes the number of days we have to sell the item
  • All items have a Quality value which denotes how valuable the item is
  • At the end of each day our system lowers both values for every item

Pretty simple, right? Well this is where it gets interesting:

  • Once the sell by date has passed, Quality degrades twice as fast
  • The Quality of an item is never negative
  • “Aged Brie” actually increases in Quality the older it gets
  • The Quality of an item is never more than 50
  • “Sulfuras”, being a legendary item, never has to be sold or decreases in Quality
  • “Backstage passes”, like aged brie, increases in Quality as its SellIn value approaches; > Quality increases by 2 when there are 10 days or less and by 3 when there are 5 days or less but Quality drops to 0 after the concert.

This is the original code as can be found on the Emily Bache repository.

Item.java

public class Item {
    public String name;
    public int sellIn;
    public int quality;

    public Item(String name, int sellIn, int quality) {
        this.name = name;
        this.sellIn = sellIn;
        this.quality = quality;
    }

    @Override
    public String toString() {
        return this.name + ", " + this.sellIn + ", " + this.quality;
    }
}

GildedRose.java

class GildedRose {
    Item[] items;

    public GildedRose(Item[] items) {
        this.items = items;
    }

    public void updateQuality() {
        for (int i = 0; i < items.length; i++) {
            if (!items[i].name.equals("Aged Brie")
                    && !items[i].name.equals(
                        "Backstage passes to a TAFKAL80ETC concert")) {
                if (items[i].quality > 0) {
                    if (!items[i].name.equals("Sulfuras, Hand of Ragnaros")) {
                        items[i].quality = items[i].quality - 1;
                    }
                }
            } else {
                if (items[i].quality < 50) {
                    items[i].quality = items[i].quality + 1;

                    if (items[i].name.equals(
                        "Backstage passes to a TAFKAL80ETC concert")) {
                        if (items[i].sellIn < 11) {
                            if (items[i].quality < 50) {
                                items[i].quality = items[i].quality + 1;
                            }
                        }

                        if (items[i].sellIn < 6) {
                            if (items[i].quality < 50) {
                                items[i].quality = items[i].quality + 1;
                            }
                        }
                    }
                }
            }

            if (!items[i].name.equals("Sulfuras, Hand of Ragnaros")) {
                items[i].sellIn = items[i].sellIn - 1;
            }

            if (items[i].sellIn < 0) {
                if (!items[i].name.equals("Aged Brie")) {
                    if (!items[i].name.equals(
                        "Backstage passes to a TAFKAL80ETC concert")) {
                        if (items[i].quality > 0) {
                            if (!items[i].name.equals(
                                "Sulfuras, Hand of Ragnaros")) {
                                items[i].quality = items[i].quality - 1;
                            }
                        }
                    } else {
                        items[i].quality = items[i].quality - items[i].quality;
                    }
                } else {
                    if (items[i].quality < 50) {
                        items[i].quality = items[i].quality + 1;
                    }
                }
            }
        }
    }
}

First Refactoring

As you can directly see, this code is really complex and hard to follow.

  1. Replace the classic for loop with an ehanced one, because clearly the code is doing nothing with the index.
  2. Extract some constants for the various Strings used in the code
  3. Simplify some branches (if inside of ifs without any else)
class GildedRose {
    static String BACKSTAGE = "Backstage passes to a TAFKAL80ETC concert";
    static String SULFURAS = "Sulfuras, Hand of Ragnaros";
    static String AGED_BRIE = "Aged Brie";
    Item[] items;

    public GildedRose(Item[] items) {
        this.items = items;
    }

    public void updateQuality() {
        for (Item item : items) {
            if (!item.name.equals(AGED_BRIE)
                    && !item.name.equals(BACKSTAGE)) {
                if (item.quality > 0 && !item.name.equals(SULFURAS)) {
                    item.quality = item.quality - 1;
                }
            } else {
                if (item.quality < 50) {
                    item.quality = item.quality + 1;

                    if (item.name.equals(BACKSTAGE)) {
                        if (item.sellIn < 11 && item.quality < 50) {
                            item.quality = item.quality + 1;
                        }

                        if (item.sellIn < 6 && item.quality < 50) {
                            item.quality = item.quality + 1;
                        }
                    }
                }
            }

            if (!item.name.equals(SULFURAS)) {
                item.sellIn = item.sellIn - 1;
            }

            if (item.sellIn < 0) {
                if (!item.name.equals(AGED_BRIE)) {
                    if (!item.name.equals(BACKSTAGE)) {
                        if (item.quality > 0 && !item.name.equals(SULFURAS)) {
                            item.quality = item.quality - 1;
                        }
                    } else {
                        item.quality = 0;
                    }
                } else if (item.quality < 50) {
                    item.quality = item.quality + 1;
                }
            }
        }
    }
}

Read the spec, then create separated functions

Not much changed, the code is still pretty ugly. The code is clearly too tied, if we take a look at the specifications we can see that items can be processed differently based on their name, and each of them have a clear requirements.

Note that the sellIn will always be decreased except for Sulfuras.

void ageItem(Item item) {
    if (!item.name.equals(SULFURAS))
        item.sellIn -= 1;
}

Sulfuras never increase or decrease in quality, because it's a legend.

void updateSulfuras(Item item) {
    // do nothing
}

Regarding Aged Brie, the quality always increases (but not over 50 since this is the maximum), and twice as fast if the sellIn is negative,

void updateAgedBrie(Item item) {
    if (item.quality < 50) {
        if (item.sellIn < 0)
            item.quality = min(50, item.quality + 2);
        else
            item.quality = min(50, item.quality + 1);
    }
}

For Back Stage

  • if the sellIn is negative, quality must be reset
  • if the sellIn is below 5, quality increases thrice as fast
  • if the sellIn is below 10, quality increases twice as fast
  • otherwise it just increases
void updateBackStage(Item item) {
    if (item.sellIn < 0)
        item.quality = 0;
    else if (item.sellIn < 5)
        item.quality = min(50, item.quality + 3);
    else if (item.sellIn < 10)
        item.quality = min(50, item.quality + 2);
    else
        item.quality = min(50, item.quality + 1);
}

For any other item, we assume it always decreases (it cannot be negative though), but twice as fast if the sellIn is 0 or below.

void updateDefault(Item item) {
    if (item.quality > 0) {
        if (item.sellIn <= 0)
            item.quality = Math.max(0, item.quality - 2);
        else
            item.quality = Math.max(0, item.quality - 1);
    }
}

Now that we have these functions, we can replace the updateQuality method:

public void updateQuality() {
    for (Item item : items) {
        ageItem(item);

        if (item.name.equals(AGED_BRIE))
            updateAgedBrie(item);
        else if (item.name.equals(SULFURAS))
            updateSulfuras(item);
        else if (item.name.equals(BACKSTAGE))
            updateBackStage(item);
        else
            updateDefault(item);
    }
}

This is now just a glorified if. Adding a new Item type to handle would just mean that we need to add a new branch to the if and the specific method to handle it.

Enhance the Item class

Ok nice, but where is Vavr, how can it help reduce the code ? We will do it with both Vavr and Lombok to also reduce some boilerplate.

Now modify the Item class to add some logic for increasing, decreasing and reseting the quality.

@AllArgsConstructor
public class Item {
    public String name;
    public int sellIn, quality;

    public Item decreaseQuality(int quantity) {
        quality = max(0, quality - quantity);
        return this;
    }

    public Item increaseQuality(int quantity) {
        quality = min(50, quality + quantity);
        return this;
    }

    public Item resetQuality() {
        quality = 0;
        return this;
    }

    public Item decreaseSellIn() {
        sellIn -= 1;
        return this;
    }

    @Override
    public String toString() {
        return this.name + ", " + this.sellIn + ", " + this.quality;
    }
}

Now we can update the GildedRose class to remove some logic and manual calculations, and also improve some code with ternary operators.

The updateAgedBrie methods now becomes only one line, because the Item.increaseQuality handles the maximum value, so there's no need to check for it:

Item updateAgedBrie(Item item) {
    return item.increaseQuality(item.sellIn < 0 ? 2 : 1);
}

Same goes for the updateDefault method, because the Item.decreaseQuality handle the minimal value which is 0.

Item updateDefault(Item item) {
    return item.decreaseQuality(item.sellIn <= 0 ? 2 : 1);
}

Finally for the updateBackStage method it simplifies the reading by avoiding the manual computation and bounding checks.

Item updateBackStage(Item item) {
    if (item.sellIn < 0)
        return item.resetQuality();
    else if (item.sellIn < 5)
        return item.increaseQuality(3);
    else if (item.sellIn < 10)
        return item.increaseQuality(2);
    else
        return item.increaseQuality(1);
}

Oh and for updateSulfuras:

Item updateSulfuras(Item item) {
    return item;
}

And now, Vavr

Ok, still no Vavr!

I know, I know, here it comes. First we'll replace the array of items with a Seq, in the constructor.

Seq<Item> items;

public GildedRose(Item[] items) {
    this.items = Vector.of(items);
}

Then I would like to replace that if statement in updateQuality by a function dispatching using a HashMap, to do this create a new variable named updater.

Map<String, Function<Item, Item>> updater =
        HashMap.of(AGED_BRIE, this::updateAgedBrie,
                   SULFURAS, this::updateSulfuras,
                   BACKSTAGE, this::updateBackStage);

This map uses the Item name as key, and a reference to a function taking an Item and returning an Item as values.

And use it inside the updateQuality method

public void updateQuality() {
    for (Item item : items) {
        ageItem(item);

        updater.getOrElse(item.name, this::updateDefault)
                .apply(item);
    }
}

Notice that we used item.name to retrieve the function to apply on the item variable. Notice also that we used getOrElse so that for any item except Sulfuras, Aged Brie and BackStage we used the updateDefault function. The getOrElse method will then return a Function<Item, Item> that we need to call using apply(item).

We've completely removed an if with a Map lookup and a method chaining on apply. This is great because now if we need to handle another kind of item, we just need to declare a function to handle it, and add it to our HashMap.

We can now gain some lines of code using Function instead of declaring functions the traditional way. Besides we don't need a function for dealing with Sulfuras anymore since it just returns the given input, so we can safely replace it with Function.identity().

Function<Item, Item> updateAgedBrie = item ->
        item.increaseQuality(item.sellIn < 0 ? 2 : 1);
        
Function<Item, Item> updateDefault = item ->
        item.decreaseQuality(item.sellIn <= 0 ? 2 : 1);
        
Function<Item, Item> updateBackStage = item -> {
    if (item.sellIn < 0)
        return item.resetQuality();
    else if (item.sellIn < 5)
        return item.increaseQuality(3);
    else if (item.sellIn < 10)
        return item.increaseQuality(2);
    else
        return item.increaseQuality(1);
};

Map<String, Function<Item, Item>> updater =
        HashMap.of(SULFURAS, identity(),
                   AGED_BRIE, updateAgedBrie,
                   BACKSTAGE, this::updateBackStage);

That's already pretty cool, but we gained nothing regarding the updateBackStage method. However Vavr has us covered with Match.

Function<Item, Item> backStage = item ->
        Match(item).of(Case($(i -> i.sellIn < 0), item::resetQuality),
                Case($(i -> i.sellIn < 5), () -> item.increaseQuality(3)),
                Case($(i -> i.sellIn < 10), () -> item.increaseQuality(2)),
                Case($(), () -> item.increaseQuality(1)));

Map<String, Function<Item, Item>> updater =
        HashMap.of(SULFURAS, identity(),
                   AGED_BRIE, updateAgedBrie,
                   BACKSTAGE, updateBackStage);

This is pretty neat, the Match evals the differente Case and returns as soon as it finds a match.

Vavr offers function composition, so we can try to create a specific function for ageItem so that we can compose it with the one that we get back from the HashMap.

Function<Item, Item> ageItem = item ->
        item.name.equals(SULFURAS) ? item : item.decreaseSellIn();

Now we're ready to use it with Function.andThen, and the updateQuality has now became only one line long.

public void updateQuality() {
    items = items.map(it -> ageItem.andThen(updater.getOrElse(it.name, defaultItem)).apply(it));
}

The only last problem is that the Item class is still mutable, and we're going to make it immutable right away, because immutable objects are thread-safe as are Vavr collections and types, and you're ready to parallelize your program if needed. Immutability has a lot of advantages.

@With
@AllArgsConstructor
public class Item {
    public String name;
    public int sellIn, quality;

    public Item decreaseQuality(int quantity) {
        return withQuality(max(0, quality - quantity));
    }

    public Item increaseQuality(int quantity) {
        return withQuality(min(50, quality + quantity));
    }

    public Item resetQuality() {
        return withQuality(0);
    }

    public Item decreaseSellIn() {
        return withSellIn(sellIn - 1);
    }

    @Override
    public String toString() {
        return this.name + ", " + this.sellIn + ", " + this.quality;
    }
}

Using Lombok's nice @With utility, it's really easy to create a new Item instance when you need to modify only one field. If you need to modify more than that then you can use the @Builder annotation instead, or simply use the classic new keyword.

To conclude this Kata, let's see what code we have:

public class GildedRose {
    static final String AGED_BRIE = "Aged Brie";
    static final String SULFURAS = "Sulfuras, Hand of Ragnaros";
    static final String BACKSTAGE = "Backstage passes to a TAFKAL80ETC concert";
    Seq<Item> items;

    public GildedRose(Item[] items) {
        this.items = Vector.of(items);
    }

    Function<Item, Item> ageItem = item ->
        item.name.equals(SULFURAS) ? item : item.decreaseSellIn();
    Function<Item, Item> agedBrie = item ->
        item.increaseQuality(item.sellIn < 0 ? 2 : 1) : item;
    Function<Item, Item> defaultItem = item ->
        item.decreaseQuality(item.sellIn <= 0 ? 2 : 1) : item;

    Function<Item, Item> backStage = item ->
        Match(item).of(Case($(i -> i.sellIn < 0), item::resetQuality),
            Case($(i -> i.sellIn < 5), () -> item.increaseQuality(3)),
            Case($(i -> i.sellIn < 10), () -> item.increaseQuality(2)),
            Case($(), () -> item.increaseQuality(1)));

    Map<String, Function<Item, Item>> updater =
        HashMap.of(SULFURAS, identity(), BACKSTAGE, backStage, AGED_BRIE, agedBrie);

    public void updateQuality() {
        items = items.map(it -> ageItem.andThen(
            updater.getOrElse(it.name, defaultItem)).apply(it));
    }
}

Around 30 lines of maintainable code.

We've seen how to :

  1. avoid writing an if by using a HashMap to dispatch functions calls.
  2. use function composition to deal with both the quality update and the decreasing of sellIn at the sametime.
  3. use the very versatile Match API to functionally return values based on predicates.
  4. remove the use of Item[] with a Seq.
  5. replace a big for loop with a sequence iteration
  6. use Lombok to avoid some boilerplate code and make things easy while making the Item object immutable.

Git repository

The code from this previous example is available at agrison/gilded-rose-kata-vavr.

From HTTP to the Database, and back

Vavr comes with a lot of modules as we've already seen, but there is also native support for Spring Data so that you can speak to your database using Vavr.

Besides using the support for Jackson, you can read from the network using Vavr structures, and write to it also.

Initiating the project

Let's imagine that we want to build a really small REST API about movies. It will let the consumer get a list of movies and actors. Also, we want to be able to create actors, movies, and add them to a cast.

I want to keep the domain easy so that we can focus on what Vavr can bring to the implementation.

We're going to use Josh Long‘s second favorite place on the internet, start.spring.io.

Just go there and create a new project with the following dependencies:

  • Spring Web
  • Spring Data JPA
  • Spring Dev Tools
  • Lombok

Once generated, just go to your pom.xml file and add the vavr, vavr-jackson, h2 and commons-lang3 dependencies.

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.10.5</version>
</dependency>
<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr-jackson</artifactId>
    <version>0.10.5</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.10</version>
</dependency>

Configuring Vavr Jackson

Now let's edit DemoApplication.java and configure the Jackson module.

@Bean
public VavrModule vavrModule() {
    return new VavrModule();
}

That's it, you're covered.

About the application

We want to keep the model simple:

  • Movie
    • an ID
    • a title representing the movie name
    • a release date
    • a small synopsis
  • Actor
    • an ID
    • a first name and last name
    • a date of birth
  • Cast
    • an ID
    • an Actor
    • a Movie
    • the role of the actor in that movie

Let's code all these entities.

The entities

The Actor entity.

@With
@Entity
@ToString
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Actor {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long actorId;
    String firstName;
    String lastName;
    LocalDate dateOfBirth;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof Actor otherActor) {
            return Option.of(actorId)
                    .map(id -> id.equals(otherActor.actorId))
                    .getOrElse(false);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Option.of(actorId).map(Object::hashCode)
                .getOrElse(42);
    }
}

The Movie entity.

@With
@Entity
@ToString
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Movie {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long movieId;
    String title;
    LocalDate releaseDate;
    String synopsis;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof Movie otherMovie) {
            return Option.of(movieId)
                    .map(id -> id.equals(otherMovie.movieId))
                    .getOrElse(false);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Option.of(movieId).map(Object::hashCode)
                .getOrElse(43);
    }
}

And the Cast entity.

@Entity
@ToString
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Cast {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long castId;
    @ManyToOne
    @JoinColumn(name = "movieId")
    Movie movie;
    @ManyToOne
    @JoinColumn(name = "actorId")
    Actor actor;
    String role;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof Cast otherCast) {
            return Option.of(castId)
                    .map(id -> id.equals(otherCast.castId))
                    .getOrElse(false);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Option.of(castId).map(Object::hashCode)
                .getOrElse(44);
    }
}

We've been using Lombok but JPA entities need special care regarding equals and hashCode. Indeed, we want to check equality only on the id field, whereas Lombok would have generated full equality based on all fields, that we do not want.

For this, we used Option to avoid null checks by ourselves.

The Repository layer

Now that we have our models we need to create some Spring Data JPA repositories so that we can do some CRUD operations on it.

Our REST API is simple and regarding actors, we just want to read one or read them all.

@Repository
public interface ActorRepository extends PagingAndSortingRepository<Actor, Long> {
    Option<Actor> findByActorId(Long id);
    Seq<Actor> findAll();
}

Note the use of Vavr's Option and Seq when trying to read by ID or reading all the actors.

Regarding movies, we need the exact same features.

@Repository
public interface MovieRepository extends PagingAndSortingRepository<Movie, Long> {
    Option<Movie> findByActorId(Long movieId);
    Seq<Movie> findAll();
}

And to finish regarding casting, we need to find all the casts by actor and by movie.

@Repository
public interface CastRepository extends PagingAndSortingRepository<Cast, Long> {
    Seq<Cast> findAllByActor(Actor actor);
    Seq<Cast> findAllByMovie(Movie movie);
}

The insertion of a new movie, actor, and cast is already handled by the default Spring repository, so we don't need to do anything about that.

The Service layer

The CastService needs to be able to retrieve the casts for an actor and for a movie, by delegating calls to the CastRepository.

@Service
@RequiredArgsConstructor
public class CastService {
    private final ActorRepository actorRepository;
    private final MovieRepository movieRepository;
    private final CastRepository castRepository;

    public Seq<Cast> castForActor(Long actorId) {
        return castFor(actorId, actorRepository, castRepository::findAllByActor);
    }

    public Seq<Cast> castForMovie(Long movieId) {
        return castFor(movieId, movieRepository, castRepository::findAllByMovie);
    }

    private <T> Seq<Cast> castFor(Long entityId, CrudRepository<T, Long> repository,
        Function<T, Seq<Cast>> mapper) {
        return repository.findById(entityId)
                .map(mapper)
                .orElse(Vector.empty());
    }

    public Try<Cast> addActorToMovie(Movie movie, Long actorId, String role) {
        return Try.of(() -> actorRepository.findById(actorId)
                .map(a -> castRepository.save(new Cast(null, movie, a, role)))
                .orElseThrow(() -> new RuntimeException("Actor not found")));
    }
}

Both the ActorService and the MovieService will use their own ActorRepository and MovieRepository in addition to the CastRepository.

@Service
@RequiredArgsConstructor
public class ActorService {
    private final ActorRepository repository;
    private final CastService castService;

    public Seq<Actor> all() {
        return repository.findAll();
    }

    public Option<Actor> byId(Long id) {
        return repository.findByActorId(id);
    }

    public Seq<Cast> castsForActor(Long actorId) {
        return castService.castForActor(actorId);
    }

    public Try<Actor> create(Actor actor) {
        return Try.of(() -> repository.save(actor.withActorId(null)));
    }
}

The only special thing in this implementation is to use a Try to run the JPA entity saving.

@Service
@RequiredArgsConstructor
public class MovieService {
    private final MovieRepository repository;
    private final com.example.demo.service.CastService castService;

    public Seq<Movie> all() {
        return repository.findAll();
    }

    public Option<Movie> byId(Long id) {
        return repository.findByMovieId(id);
    }

    public Seq<Cast> castForMovie(Long id) {
        return castService.castForMovie(id);
    }

    public Try<Movie> create(Movie movie) {
        return Try.of(() -> repository.save(movie.withMovieId(null)));
    }

    public Try<Cast> addActorToMovieCast(Long movieId, Long actorId, String role) {
        return repository.findById(movieId)
                .map(movie -> castService.addActorToMovie(movie, actorId, role))
                .orElseThrow(() -> new RuntimeException("Movie not found"));
    }
}

The Controller layer

Validating inputs

When a user of our REST API will need to create an actor, a movie, or a cast they'll have to POST some JSON. This is why we'll be creating three new Java POJO.

public record NewActor(String firstName, String lastName, LocalDate dateOfBirth) {}

public record NewMovie(String title, String synopsis, LocalDate releaseDate) {}

public record NewCast(Long actorId, String role) {}

So pretty much the same as the Actor and Movie entities except that we don't care about an id field because we'll be creating entities and the user is not supposed to send some already existing ID.

We've seen in the Validation chapter that Vavr offers some facilities for validating inputs, and we're going to use them.

Let's create those for NewActor and NewMovie which are pretty similar.

public class ActorValidator {
    public static Validation<Seq<String>, Actor> validate(NewActor actor) {
        return Validation.combine(
                firstNameCannotBeBlank(actor.firstName()),
                lastNameCannotBeBlank(actor.lastName()),
                dateInThePast(actor.dateOfBirth()))
                .ap((firstName, lastName, date) -> new Actor(null, firstName, 
                                                             lastName, date));
    }

    static Validation<String, String> firstNameCannotBeBlank(String name) {
        return StringUtils.isBlank(name) ? invalid("First name cannot be blank") : 
                                           valid(name);
    }

    static Validation<String, String> lastNameCannotBeBlank(String name) {
        return StringUtils.isBlank(name) ? invalid("Last name cannot be blank") : 
                                           valid(name);
    }

    static Validation<String, LocalDate> dateInThePast(LocalDate date) {
        return !date.isBefore(LocalDate.now()) ? 
            invalid("Date of birth must be in the past") : valid(date);
    }
}

For a NewActor we validate that the first name and last name must be required and that the date of birth must be in the past, whereas for a NewMovie we validate that the title must be required, the synopsis must be maxed 100 characters long and the release date must be in the past or present.

public class MovieValidator {
    public static Validation<Seq<String>, Movie> validate(NewMovie movie) {
        return Validation.combine(
                titleCannotBeBlank(movie.title()),
                synopsisMaxLength(movie.synopsis()),
                dateInThePastOrPresent(movie.releaseDate()))
                .ap((title, synopsis, date) -> new Movie(null, title, 
                                                         date, synopsis));
    }

    static Validation<String, String> titleCannotBeBlank(String name) {
        return StringUtils.isBlank(name) ? invalid("Movie name cannot be blank") : 
                                           valid(name);
    }

    static Validation<String, String> synopsisMaxLength(String synopsis) {
        return StringUtils.length(synopsis) > 100 ? 
            invalid("Synopsis must be 100 chars max") : valid(synopsis);
    }

    static Validation<String, LocalDate> dateInThePastOrPresent(LocalDate date) {
        return (date.isAfter(LocalDate.now())) ? 
            invalid("Release date must be in the past") : valid(date);
    }
}

The REST API

Verb URL Description
GET /api/actors List all the actors
GET /api/actors/{id} Retrieve a specific actor
GET /api/actors/{id}/cast Retrieve the casts where this specific actor appears in
POST /api/actors Add a new actor
GET /api/movies List all the movies
GET /api/movies/{id} Retrieve a specific movie
GET /api/movies/{id}/cast Retrieve a specific movie cast
POST /api/movies Add a new movie

Let's start with the ActorController.

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/actors")
public class ActorController {
    private final ActorService actorService;

    @GetMapping
    public Seq<Actor> list() {
        return actorService.all();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Actor> read(@PathVariable Long id) {
        return ResponseEntity.of(actorService.byId(id).toJavaOptional());
    }

    @GetMapping("/{id}/cast")
    public Seq<Cast> casts(@PathVariable Long id) {
        return actorService.castsForActor(id);
    }

    @PostMapping
    public ResponseEntity<Actor> post(@RequestBody NewActor actor) {
        return Match(ActorValidator.validate(actor)).of(
                Case($Valid($()), actorService::create),
                Case($Invalid($()), x -> {
                    throw new ResponseStatusException(BAD_REQUEST, x.mkString());
                }))
                .map(e -> ResponseEntity.status(CREATED).body(e))
                .getOrElseThrow(() -> new ResponseStatusException(INTERNAL_SERVER_ERROR));
    }
}

And for the MovieController

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/movies")
public class MovieController {
   private final MovieService movieService;

   @GetMapping
   public Seq<Movie> list() {
       return movieService.all();
   }

   @GetMapping("/{id}")
   public ResponseEntity<Movie> read(@PathVariable Long id) {
       return ResponseEntity.of(movieService.byId(id).toJavaOptional());
   }

   @GetMapping("/{id}/cast")
   public Seq<Cast> casts(@PathVariable Long id) {
       return movieService.castForMovie(id);
   }

   @PostMapping
   public ResponseEntity<Movie> post(@RequestBody NewMovie movie) {
       return Match(MovieValidator.validate(movie)).of(
               Case($Valid($()), movieService::create),
               Case($Invalid($()), x -> {
                   throw new ResponseStatusException(BAD_REQUEST, x.mkString());
               }))
               .map(e -> ResponseEntity.status(CREATED).body(e))
               .getOrElseThrow(() -> new ResponseStatusException(INTERNAL_SERVER_ERROR));
   }
}

You will notice that most of the calls are just returning plainly what the service layer is returning from the database.

The ResponseEntity.of() takes a Java Optional so we can use Vavr's interop to create an Optional from an Option. The ResponseEntity.of() will return a 200 OK if the Option is defined, and a 404 Not Found in case it's empty.

In the post method we use Vavr's Pattern Matching functionality mostly for the sake of it, you could directly use the Validation API and map to get what we want.

The code calls our ActorValidator and MovieValidator which try to validate the input. We then match the result, and in case it was valid then we call actorService.create or movieService.create which will, in turn, return the new actor or movie. In case the validation fails we throw a Bad Request. In case anything goes wrong during the insertion on the database side, we throw an Internal Server Error.

The testing sample

Since we're using an embedded H2 database just to test our app works, you need to set it in the application.properties.

spring.h2.console.enabled=true
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.database=h2
spring.datasource.plaform=h2
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

Now simply create a data.sql file in the resources directory.

DROP TABLE IF EXISTS cast;
DROP TABLE IF EXISTS actor;
DROP TABLE IF EXISTS movie;

CREATE TABLE movie
(
    movie_id     INT AUTO_INCREMENT PRIMARY KEY,
    title        VARCHAR(250) NOT NULL,
    release_date DATE         NOT NULL,
    synopsis     VARCHAR(2000) DEFAULT NULL
);

CREATE TABLE actor
(
    actor_id      INT AUTO_INCREMENT PRIMARY KEY,
    first_name    VARCHAR(50) NOT NULL,
    last_name     VARCHAR(50) NOT NULL,
    date_of_birth DATE DEFAULT NULL
);

CREATE TABLE cast
(
    cast_id  INT AUTO_INCREMENT PRIMARY KEY,
    movie_id INT NOT NULL,
    actor_id INT NOT NULL,
    role     VARCHAR(100) DEFAULT NULL
);

INSERT INTO movie(title, release_date, synopsis)
values ('Interstellar', '2014-11-04',
        'A team of explorers travel through a wormhole in space in an attempt to ensure humanity''s survival');

INSERT INTO actor(first_name, last_name, date_of_birth)
values ('Matthew', 'McConaughey', '1969-11-04');
INSERT INTO actor(first_name, last_name, date_of_birth)
values ('Anne', 'Hathaway', '1982-11-12');
INSERT INTO actor(first_name, last_name, date_of_birth)
values ('Jessica', 'Chastain', '1977-03-24');

INSERT INTO cast(movie_id, actor_id, role)
VALUES (1, 1, 'Cooper');
INSERT INTO cast(movie_id, actor_id, role)
VALUES (1, 2, 'Brand');
INSERT INTO cast(movie_id, actor_id, role)
VALUES (1, 3, 'Murph');

commit;

Running the application

In your IDE just run the main application, and use your preferred HTTP client, I'll be using the one built-in with IntelliJ so that I don't leave it.

Fetching the list of movies:

GET http://localhost:8080/api/movies

HTTP/1.1 200 
Content-Type: application/json
Date: Mon, 30 Nov 2020 13:44:37 GMT

[
  {
    "movieId": 1,
    "title": "Interstellar",
    "releaseDate": "2014-11-04",
    "synopsis": "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival"
  }
]

Response code: 200; Time: 13ms; Content length: 177 bytes

Fetching the casting of Interstellar:

GET http://localhost:8080/api/movies/1/cast

HTTP/1.1 200 
Content-Type: application/json
Date: Mon, 30 Nov 2020 13:46:54 GMT

[
  {
    "castId": 1,
    "movie": {
      "movieId": 1,
      "title": "Interstellar",
      "releaseDate": "2014-11-04",
      "synopsis": "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival"
    },
    "actor": {
      "actorId": 1,
      "firstName": "Matthew",
      "lastName": "McConaughey",
      "dateOfBirth": "1969-11-04"
    },
    "role": "Cooper"
  },
  {
    "castId": 2,
    "movie": {
      "movieId": 1,
      "title": "Interstellar",
      "releaseDate": "2014-11-04",
      "synopsis": "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival"
    },
    "actor": {
      "actorId": 2,
      "firstName": "Anne",
      "lastName": "Hathaway",
      "dateOfBirth": "1982-11-12"
    },
    "role": "Brand"
  },
  {
    "castId": 3,
    "movie": {
      "movieId": 1,
      "title": "Interstellar",
      "releaseDate": "2014-11-04",
      "synopsis": "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival"
    },
    "actor": {
      "actorId": 3,
      "firstName": "Jessica",
      "lastName": "Chastain",
      "dateOfBirth": "1977-03-24"
    },
    "role": "Murph"
  }
]

Response code: 200; Time: 17ms; Content length: 917 bytes

Fetching all the actors:

GET http://localhost:8080/api/actors

HTTP/1.1 200 
Content-Type: application/json
Date: Mon, 30 Nov 2020 13:47:21 GMT

[
  {
    "actorId": 1,
    "firstName": "Matthew",
    "lastName": "McConaughey",
    "dateOfBirth": "1969-11-04"
  },
  {
    "actorId": 2,
    "firstName": "Anne",
    "lastName": "Hathaway",
    "dateOfBirth": "1982-11-12"
  },
  {
    "actorId": 3,
    "firstName": "Jessica",
    "lastName": "Chastain",
    "dateOfBirth": "1977-03-24"
  }
]

Response code: 200; Time: 11ms; Content length: 256 bytes

Fetching Matthew :

GET http://localhost:8080/api/actors/1

HTTP/1.1 200 
Content-Type: application/json
Date: Mon, 30 Nov 2020 13:47:58 GMT

{
  "actorId": 1,
  "firstName": "Matthew",
  "lastName": "McConaughey",
  "dateOfBirth": "1969-11-04"
}

Response code: 200; Time: 9ms; Content length: 87 bytes

Fetching Matthew‘s appearances:

GET http://localhost:8080/api/actors/1/cast

HTTP/1.1 200 
Content-Type: application/json
Date: Mon, 30 Nov 2020 13:48:56 GMT

[
  {
    "castId": 1,
    "movie": {
      "movieId": 1,
      "title": "Interstellar",
      "releaseDate": "2014-11-04",
      "synopsis": "A team of explorers travel through a wormhole in space in an attempt to ensure humanity's survival"
    },
    "actor": {
      "actorId": 1,
      "firstName": "Matthew",
      "lastName": "McConaughey",
      "dateOfBirth": "1969-11-04"
    },
    "role": "Cooper"
  }
]

Response code: 200; Time: 17ms; Content length: 310 bytes

Trying to add a new actor with invalid data:

POST http://localhost:8080/api/actors
{
  "firstName": "Denzel",
  "lastName": "Washington",
  "dateOfBirth": "2048-12-18"
}

HTTP/1.1 400 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 30 Nov 2020 13:52:43 GMT
Connection: close

{
  "timestamp": "2020-11-30T13:52:43.376+00:00",
  "status": 400,
  "error": "Bad Request",
  "trace": "org.springframework.web.server.ResponseStatusException: 400 BAD_REQUEST \"Date of birth must be in the past\"\r\n...",
  "message": "Date of birth must be in the past",
  "path": "/api/actors"
}

Response code: 400; Time: 38ms; Content length: 5463 bytes

Adding a new actor correctly:

POST http://localhost:8080/api/actors
{
  "firstName": "Denzel",
  "lastName": "Washington",
  "dateOfBirth": "1954-12-18"
}

HTTP/1.1 201 
Content-Type: application/json
Date: Mon, 30 Nov 2020 13:51:47 GMT

{
  "actorId": 4,
  "firstName": "Denzel",
  "lastName": "Washington",
  "dateOfBirth": "1954-12-18"
}

Response code: 201; Time: 31ms; Content length: 81 bytes

Recap

Throughout this web app we've been using Vavr for:

  • Validating inputs
  • Pattern matching to check for validation results
  • Read from the database using Seq and Option to avoid checking for nulls
  • Try for dealing with sensitive parts
  • Option for responding the correct status code through the use of Spring's ResponseEntity
  • Written Vavr structures as JSON on the network via the Vavr module for Jackson

So we made use of Vavr from reading on the network, to the database, and back to the client. Vavr can be used everywhere.

Of course the Vavr Validation part could have been replaced with standard Java validation API using annotations, it was a way for me to demonstrate how all these useful constructs work together and let you have nice code to work with.

If your code needs performance I would just avoid the Pattern Matching and use the Validation monad instead.

Git repository

The code from this previous example is available at agrison/vavr-sb-demo.

Analysing football data

You already know that I live in Metz, so I support the FC Metz.

The club's best season was 1997-1998, I was 12 years old, was going to the Stade Saint-Symphorien each match. What a year, I don't think the FC Metz will be able to win the Ligue 1 championship soon, so as a Metz citizen and FC Metz fan, the 97-98 season is one we cherish because we finished 1st. I hear you! You know I'm lying, we finished second after Lens due to the goal average, but still, it's 1st ex aequo, and it was a great team.

And like any FC Metz supporter, I have to remind you that FC Metz is the only french club who won at Camp Nou, in 1984.

Having explained a bit of history, I would like that we parse some kind of dataset all together, and find how we can extract some useful pieces of information using the Vavr Collections.

Player dataset

Imagine the following dataset players.csv which contains the list of players of the FC Metz during season 97-98.

Columns for this sample are:

  • first name
  • last name
  • date of birth
  • position.
Lionel,LETIZI,1973-05-28,GOAL
Pascal,PIERRE,1968-05-28,DEFENDER
Jeff,STRASSER,1974-10-05,DEFENDER
Geoffrey,TOYES,1973-05-18,DEFENDER
Stéphane,RONDELAERE,1971-01-16,DEFENDER
Sylvain,KASTENDEUCH,1963-08-31,DEFENDER
Philippe,GAILLOT,1965-02-28,DEFENDER
Rigobert,SONG,1976-07-01,DEFENDER
Danny,BOFFIN,1965-07-10,MIDFIELDER
Frédéric,MEYRIEU,1968-02-09,MIDFIELDER
Grégory,PROMENT,1978-12-10,MIDFIELDER
Jocelyn,BLANCHARD,1972-05-28,MIDFIELDER
Cyril,SERREDSZUM,1971-10-02,MIDFIELDER
Robert,PIRES,1973-10-29,FORWARD
Jonathan,JAGER,1978-05-23,FORWARD
Bruno,RODRIGUEZ,1972-11-25,FORWARD
Vladan,LUKIC,1970-02-16,FORWARD
Mihaili,TOTH,1974-12-27,FORWARD
Louis,SAHA,1978-08-08,FORWARD
Franck,HISTILLOLES,1973-01-02,FORWARD
Amara,TRAORÉ,1965-09-25,FORWARD

Game dataset

Now imagine a second dataset games.csv which contains the list of games played by the FC Metz during season 97-98.

1997-08-02 20:00,Lyon,away,1,0,Ligue 1,Journée 01,LUKIC@49
1997-08-08 20:00,Bordeaux,home,4,1,Ligue 1,Journée 02,TOYES@13:RODRIGUEZ@64:RODRIGUEZ@84:SAHA@90
1997-08-15 20:00,Châteauroux,away,2,1,Ligue 1,Journée 03,PIRES@8:RODRIGUEZ@67
1997-08-22 20:00,Paris SG,home,2,1,Ligue 1,Journée 04,PIRES@62:RODRIGUEZ@68
1997-08-29 20:00,AS Monaco,away,2,1,Ligue 1,Journée 05,LUKIC@21:PIRES@90
1997-09-05 20:00,Rennes,away,2,2,Ligue 1,Journée 06,RODRIGUEZ@19:GAILLOT@89
1997-09-12 20:00,Cannes,home,2,0,Ligue 1,Journée 07,GAILLOT@62:MEYRIEU@70
1997-09-21 20:00,Bastia,away,0,0,Ligue 1,Journée 08,
1997-09-26 20:00,Auxerre,home,3,0,Ligue 1,Journée 09,RODRIGUEZ@33:MEYRIEU@44:PIRES@82
1997-10-05 20:00,Strasbourg,away,0,2,Ligue 1,Journée 10,
1997-10-08 20:00,Le Havre,home,2,0,Ligue 1,Journée 11,PIRES@57:BLANCHARD@68
1997-10-16 20:00,Marseille,away,0,2,Ligue 1,Journée 12,
1997-10-25 20:00,Montpellier,home,0,1,Ligue 1,Journée 13,
1997-10-31 20:00,Lens,away,1,1,Ligue 1,Journée 14,PIRES@44
1997-11-08 20:00,Guingamp,home,2,1,Ligue 1,Journée 15,MEYRIEU@3:BOFFIN@66
1997-11-15 20:00,Nantes,away,1,1,Ligue 1,Journée 16,HISTILLOLES@21
1997-11-21 20:00,Toulouse,home,2,1,Ligue 1,Journée 17,LUKIC@25:PIRES@83
1997-11-30 20:00,Bordeaux,away,2,2,Ligue 1,Journée 18,PIRES@6:HISTILLOLES@67
1997-12-05 20:00,Châteauroux,home,2,0,Ligue 1,Journée 19,RODRIGUEZ@27:PIRES@40
1997-12-14 20:00,Paris SG,away,1,1,Ligue 1,Journée 20,PIRES@69
1997-12-18 20:00,AS Monaco,home,3,0,Ligue 1,Journée 21,GAILLOT@25:RODRIGUEZ@66:BOFFIN@73
1998-01-10 20:00,Rennes,home,1,0,Ligue 1,Journée 22,RODRIGUEZ@44
1998-01-20 20:00,Cannes,away,1,1,Ligue 1,Journée 23,PIRES@43
1998-01-24 20:00,Bastia,home,0,1,Ligue 1,Journée 24,
1998-02-04 20:00,Auxerre,away,0,0,Ligue 1,Journée 25,
1998-02-13 20:00,Strasbourg,home,1,0,Ligue 1,Journée 26,JAGER@57
1998-02-21 20:00,Le Havre,away,1,2,Ligue 1,Journée 27,LUKIC@66
1998-03-06 20:00,Marseille,home,3,2,Ligue 1,Journée 28,RODRIGUEZ@23:SONG@71:RODRIGUEZ@78
1998-03-13 20:00,Montpellier,away,1,0,Ligue 1,Journée 29,SERREDSZUM@82
1998-03-29 20:00,Lens,home,0,2,Ligue 1,Journée 30,
1998-04-08 20:00,Guingamp,away,1,0,Ligue 1,Journée 31,LUKIC@33
1998-04-17 20:00,Nantes,home,3,2,Ligue 1,Journée 32,LUKIC@27:RODRIGUEZ@52:MEYRIEU@74
1998-04-25 20:00,Toulouse,away,1,0,Ligue 1,Journée 33,MEYRIEU@57
1998-05-09 20:00,Lyon,home,1,0,Ligue 1,Journée 34,RODRIGUEZ@4
1998-01-17 20:00,Le Mans,away,1,1,coupeFrance,1/32 de finale,RODRIGUEZ@11
1998-02-08 20:00,Bastia,home,1,0,coupeFrance,1/16 de finale,RODRIGUEZ@90
1998-02-28 20:00,Bourg Peronnas,away,0,2,coupeFrance,1/8 de finale,
1998-01-05 20:00,Gueugnon,away,2,1,coupeLigue,1/16 de finale,PIRES@18:LUKIC@87
1998-01-31 20:00,Martigues,away,2,0,coupeLigue,1/8 de finale,PIRES@13:LUKIC@53
1998-02-16 20:00,Paris SG,away,0,1,coupeLigue,1/4 de finale,
1998-10-21 20:00,Karlsruhe,home,0,2,coupeUEFA,1/16 de finale aller,
1997-11-04 20:00,Karlsruhe,away,1,1,coupeUEFA,1/16 de finale retour,BOFFIN@10
1997-09-16 20:00,Mouscron,away,2,0,coupeUEFA,1/32 de finale aller,MEYRIEU@22:RODRIGUEZ@26
1997-09-30 20:00,Mouscron,home,4,1,coupeUEFA,1/32 de finale retour,RODRIGUEZ@5:RODRIGUEZ@25:KASTENDEUCH@39:GAILLOT@90

Columns for this sample are:

  • date & time
  • opponent
  • location
  • FC Metz score
  • opponent score
  • championship
  • event
  • the list of FC Metz goals in a specific format Player@Minute:Player@Minute:....

Java records

First let's create some records to hold all these data. Starting with Player.

record Player(String firstName, String lastName, LocalDate dob, String type) {
    String name() {
        return firstName + " " + lastName;
    }
}

A Goal is composed of a Player and a time (the minute it happened).

record Goal(Player player, int time) {}

Finally, a Game contains the different columns we saw above while looking at the dataset, plus some methods to know if the FC Metz won the game or not, and display an event.

record Game(LocalDateTime dateTime, String opponent, String location, 
            int ownScore, int opponentScore,
            String championship, String event, Seq<Goal> goals) {
    String display() {
        return String.format("%s - %s/%s: %s",
                dateTime, championship, event, (location.equals("home")
                 ? "Metz " + ownScore + " - " + opponentScore + " " + opponent 
                 : opponent + " " + opponentScore + " - " + ownScore + " Metz"));
    }

    int points() {
        return ownScore > opponentScore ? 3 : ownScore == opponentScore ? 1 : 0;
    }

    boolean draw() {
        return points() == 1;
    }

    boolean won() {
        return points() == 3;
    }

    boolean lost() {
        return points() == 0;
    }
}

Reading datasets

Now is the time to try loading the players in a Seq, of course we could use any CSV reader, but what fun would that be, besides there are no CSV edge cases in our dataset, so we can do it the old way. We'll use Try.withResources just for fun.

Seq<Player> players = Try.withResources(() -> new FileInputStream("players.csv"))
        .of(InputStream::readAllBytes)
        .map(b -> List.of(new String(b).split(lineSeparator())))
        .map(line -> line.map(Player::fromCsv))
        .getOrElseThrow(e -> new RuntimeException("boom", e));

The Try will close the FileInputStream for us, so that we need just to do our business which is:

  1. reading the content to a String
  2. make a List out of all the lines
  3. for each line make a player out of it
  4. get the built List or else throw.
  5. of course for a big file we would buffer and read line by line, but it's not the goal of this example

As you can see we need to modify the Player record which needs some logic to extract data from a line and make a Player of it.

record Player(String firstName, String lastName, LocalDate dob, String type) {
    static Player fromCsv(String line) {
        String[] parts = line.split(",");
        return new Player(parts[0], parts[1], LocalDate.parse(parts[2]), parts[3]);
    }

    // ...
}

We just split on commas and create a Player from the different separated values. Just to be sure it works:

System.out.println(players.take(3).mkString("\n"));
Player[firstName=Lionel, lastName=LETIZI, dob=1973-05-28, type=GOAL]
Player[firstName=Pascal, lastName=PIERRE, dob=1968-05-28, type=DEFENDER]
Player[firstName=Jeff, lastName=STRASSER, dob=1974-10-05, type=DEFENDER]

Good!

Now we need to load the list of games. Remember that for each game we have a list of potential goals referencing players, so we'll definitely need to be able to search a player by their last name.

Function<String, Player> findPlayer = s -> players.find(p -> s.equals(p.lastName))
        .getOrElseThrow(() -> new RuntimeException("No player: " + s));

Or even the following you want to use memoize, just because you can:

Function<String, Player> findPlayer = Function1.<String, Player>of(
        s -> players.find(p -> s.equals(p.lastName))
                .getOrElseThrow(() -> new RuntimeException("No player: " + s))).memoized();

Now let's back to our game parsing, we'll do exactly like with the loading of players, except that we want them sorted by date.

Seq<Game> games = Try.withResources(() -> new FileInputStream("games.csv"))
        .of(InputStream::readAllBytes)
        .map(b -> List.of(new String(b).split(lineSeparator())))
        .map(line -> line.map(g -> Game.fromCsv(g, findPlayer)))
        .map(g -> g.sortBy(Game::dateTime))
        .getOrElseThrow(e -> new RuntimeException("boom", e));

And don't forget to add the fromCsv method to our Game record.

record Game(LocalDateTime dateTime, String opponent, String location, 
            int ownScore, int opponentScore,
            String championship, String event, Seq<Goal> goals) {
    static Game fromCsv(String line, Function<String, Player> playerFinder) {
        String[] parts = line.split(",");
        return new Game(LocalDateTime.parse(parts[0], 
                        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")),
                parts[1], parts[2], parseInt(parts[3]),
                parseInt(parts[4]), parts[5], parts[6],
                parts.length == 8 ?
                        List.of(parts[7].split(":"))
                            .map(s -> s.split("@"))
                            .map(p -> new Goal(playerFinder.apply(p[0]), 
                                               parseInt(p[1])))
                        : List.empty());
    }
    
    // ...
}

Like for the players we split on commas and extract what is needed. The only tricky stuff is parsing the last column to a Seq of Goal. To do this we split on : and for each goal in order to retrieve the player and the minute we split on @.

Notice that we use our playerFinder function that we created earlier (named findPlayer) to retrieve a Player by last name. In case there are no goals, we return an empty sequence.

Let's just test that it works.

Playing with data

System.out.println(games.head());
Game[dateTime=1997-08-02T20:00, opponent=Lyon, location=away, ownScore=1, opponentScore=0, championship=Ligue 1, event=Journée 01, goals=List(Goal[player=Player[firstName=Vladan, lastName=LUKIC, dob=1970-02-16, type=FORWARD], time=49])]

Thank you to Vladan Lukić who started the season.

Now that we have our dataset loaded in a Seq we can traverse it and retrieve some useful informations.

First player to score for FC Metz

games.head().goals.head().player.name(); // Vladan LUKIC

Last player to score for FC Metz

games.last().goals.last().player.name(); // Bruno RODRIGUEZ

Total number of goals

games.flatMap(Game::goals).length(); // 61

The best striker

games.flatMap(Game::goals) // keep only the goals for each games
     // group goals by player
     .groupBy(Goal::player)
     // find the player with the maximum number of goals
     .maxBy(t -> t._2.size())
     // make a String out of the player name and number of goals 
     .map(t -> t._1.name() + ": " + t._2.size() + " goals"); 

// Bruno RODRIGUEZ: 18 goals

Top 5 strikers

games.flatMap(Game::goals) // keep only the goals for each games
     // group goals by player (making a Map)
     .groupBy(Goal::player)
     // make a sequence of entries instead of a Map
     .map(t -> Tuple.of(t._1, t._2.size()))
     // reverse sort by number of goals
     .sortBy(reverseOrder(), t -> t._2)
     // take the first five
     .take(5)
     // make a String out of the player name and number of goals
     .map(t -> t._1.name() + ": " + t._2 + " goals").mkString("\n\t");

// Bruno RODRIGUEZ: 18 goals
// Robert PIRES: 13 goals
// Vladan LUKIC: 8 goals
// Frédéric MEYRIEU: 6 goals
// Philippe GAILLOT: 4 goals

The largest victory

// sort games in reverse depending on FC Metz score
games.sortBy(reverseOrder(), Game::ownScore)
     // find the max based on the difference of goals between FC Metz and opponent
     .maxBy(g -> g.ownScore - g.opponentScore)
     // make a string to display
     .map(Game::display);

// 1997-08-08T20:00 - Ligue 1/Journée 02: Metz 4 - 1 Bordeaux

The largest defeat

// sort games in reverse depending on opponent score
games.sortBy(reverseOrder(), Game::opponentScore) 
     // find the max based on the difference of goals between opponent and FC Metz
     .maxBy(g -> g.opponentScore - g.ownScore)
     // make a string to display
     .map(Game::display);

// 1997-10-05T20:00 - Ligue 1/Journée 10: Strasbourg 2 - 0 Metz

The total number of points in Ligue 1

Seq<Game> ligue1 = games.filter(g -> "Ligue 1".equals(g.championship));

ligue1.map(Game::points).sum(); // 68

Find who we won, lose and draw against

ligue1.filter(Game::lost).flatMap(g -> HashSet.of(g.opponent)).mkString(", ");
// Lost against: Strasbourg, Marseille, Montpellier, Bastia, Le Havre, Lens

ligue1.filter(Game::draw).flatMap(g -> HashSet.of(g.opponent)).mkString(", ");
// Draw against: Rennes, Bastia, Lens, Nantes, Bordeaux, Paris SG, Cannes, Auxerre

ligue1.filter(Game::won).flatMap(g -> HashSet.of(g.opponent)).mkString(", ");
// Won against: Lyon, Bordeaux, Châteauroux, Paris SG, AS Monaco, Cannes, Auxerre, 
//     Le Havre, Guingamp, Toulouse, Châteauroux, AS Monaco, Rennes, Strasbourg, 
//     Marseille, Montpellier, Guingamp, Nantes, Toulouse, Lyon

Find the longest undefeated streak

Stream.range(0, ligue1.length()) // create a stream from 0 to the number of games
      // for each game make a tuple with the current position
      // and the longest sequence of games where FC Metz won at least 1pt
      .map(i -> Tuple.of(i, ligue1.segmentLength(g -> g.points() > 0, i)))
      // find the maximum continuous sequence length
      .maxBy(t -> t._2)
      // for this sequence length retrieve the original games sub sequence
      .map(t -> ligue1.subSequence(t._1, t._1 + t._2))
      // transform to display  
      .map(Game::display)
      // make a final string
      .mkString("\n"));

// 1997-10-31T20:00 - Ligue 1/Journée 14: Lens 1 - 1 Metz
// 1997-11-08T20:00 - Ligue 1/Journée 15: Metz 2 - 1 Guingamp
// 1997-11-15T20:00 - Ligue 1/Journée 16: Nantes 1 - 1 Metz
// 1997-11-21T20:00 - Ligue 1/Journée 17: Metz 2 - 1 Toulouse
// 1997-11-30T20:00 - Ligue 1/Journée 18: Bordeaux 2 - 2 Metz
// 1997-12-05T20:00 - Ligue 1/Journée 19: Metz 2 - 0 Châteauroux
// 1997-12-14T20:00 - Ligue 1/Journée 20: Paris SG 1 - 1 Metz
// 1997-12-18T20:00 - Ligue 1/Journée 21: Metz 3 - 0 AS Monaco
// 1997-01-10T20:00 - Ligue 1/Journée 22: Metz 1 - 0 Rennes
// 1998-01-20T20:00 - Ligue 1/Journée 23: Cannes 1 - 1 Metz

10 games in a row without losing is not that bad. But this is just for Ligue 1, copy paste the same code but replacing ligue1 with games.

Stream.range(0, games.length()) // create a stream from 0 to the number of games
      // for each game make a tuple with the current position
      // and the longest sequence of games where FC Metz won at least 1pt
      .map(i -> Tuple.of(i, games.segmentLength(g -> g.points() > 0, i)))
      // find the maximum continuous sequence length
      .maxBy(t -> t._2)
      // for this sequence length retrieve the original games sub sequence
      .map(t -> games.subSequence(t._1, t._1 + t._2))
      // transform to display  
      .map(Game::display)
      // make a final string
      .mkString("\n"));

// 1997-10-31T20:00 - Ligue 1/Journée 14: Lens 1 - 1 Metz
// 1997-11-04T20:00 - coupeUEFA/1/16 de finale retour: Karlsruhe 1 - 1 Metz
// 1997-11-08T20:00 - Ligue 1/Journée 15: Metz 2 - 1 Guingamp
// 1997-11-15T20:00 - Ligue 1/Journée 16: Nantes 1 - 1 Metz
// 1997-11-21T20:00 - Ligue 1/Journée 17: Metz 2 - 1 Toulouse
// 1997-11-30T20:00 - Ligue 1/Journée 18: Bordeaux 2 - 2 Metz
// 1997-12-05T20:00 - Ligue 1/Journée 19: Metz 2 - 0 Châteauroux
// 1997-12-14T20:00 - Ligue 1/Journée 20: Paris SG 1 - 1 Metz
// 1997-12-18T20:00 - Ligue 1/Journée 21: Metz 3 - 0 AS Monaco
// 1998-01-05T20:00 - coupeLigue/1/16 de finale: Gueugnon 1 - 2 Metz
// 1998-01-10T20:00 - Ligue 1/Journée 22: Metz 1 - 0 Rennes
// 1998-01-17T20:00 - coupeFrance/1/32 de finale: Le Mans 1 - 1 Metz
// 1998-01-20T20:00 - Ligue 1/Journée 23: Cannes 1 - 1 Metz

It was 13 in a row, counting all competitions.

Average goals per match

games.map(Game::ownScore).average(); // 1.386

Do we score more in the first or second half

games.flatMap(Game::goals)
     .groupBy(g -> g.time <= 45)
     .maxBy(t -> t._2.size())
     .map(t -> (t._1 ? "first" : "second") + " (" + t._2.size() + ")");

// Most goals in second (32) period

The youngest striker

games.flatMap(Game::goals)
     .sortBy(reverseOrder(), g -> g.player.dob)
     .map(Goal::player)
     .head();

// Player[firstName=Louis, lastName=SAHA, dob=1978-08-08, type=FORWARD]

Are we better at home or away

games.groupBy(Game::location)
     .mapValues(g -> g.map(Game::points).reduce(Integer::sum))
     .maxBy(t -> t._2);

// (home, 48)

We're better at home :)

Constructing the evolution of score & goals

Using the zip, zipWith and scan constructs, this turns out really easy.

Seq<Integer> points = ligue1.map(Game::points).scan(0, Integer::sum).tail();
Seq<Integer> goals = ligue1.map(g -> g.goals.length()).scan(0, Integer::sum).tail();

System.out.println("            Pts  Goals\n" +
        ligue1.map(Game::event)
              .zipWith(points, (game, pts) -> game + ":  " + pts)
              .zipWith(goals, (game, g) -> game + "  " + g)
              .mkString("\n"));

//              Pts  Goals
// Journée 01:    3      1
// Journée 02:    6      5
// Journée 03:    9      7
// Journée 04:   12      9
// Journée 05:   15     11
// Journée 06:   16     13
// Journée 07:   19     15
// Journée 08:   20     15
// Journée 09:   23     18
// Journée 10:   23     18
// Journée 11:   26     20
// Journée 12:   26     20
// Journée 13:   26     20
// Journée 14:   27     21
// Journée 15:   30     23
// Journée 16:   31     24
// Journée 17:   34     26
// Journée 18:   35     28
// Journée 19:   38     30
// Journée 20:   39     31
// Journée 21:   42     34
// Journée 22:   45     35
// Journée 23:   46     36
// Journée 24:   46     36
// Journée 25:   47     36
// Journée 26:   50     37
// Journée 27:   50     38
// Journée 28:   53     41
// Journée 29:   56     42
// Journée 30:   56     42
// Journée 31:   59     43
// Journée 32:   62     46
// Journée 33:   65     47
// Journée 34:   68     48

We've finished the season with 68 points and 48 goals scored.

Recap

We've seen that the Vavr Collections library offers plenty of possibilities to find, group, sort, map, scan, zip the data we need. It offers a convenient and powerful API that you can use to do what you need.

Git repository

The code from this previous example is available at agrison/vavr-football-data.

Advent Of Code

There are tons of code challenge on the web, but each year I love to participate in the Advent of Code.

Advent of Code is an Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like. People use them as a speed contest, interview prep, company training, university coursework, practice problems, or to challenge each other.

Each year since 2015, a new set of challenges to solve.

Let's see how Vavr makes it easy to solve the Day 1 of 2020.

You're provided a list of integers, and you need to find two entries that sum to 2020 in that list. When found you multiply those two numbers together and you have the response.

As an example, here is a list of numbers:

1721
979
366
299
675
1456

After searching a little you find that 1721 + 299 = 2020, and so the answer is 1721 * 299 = 514579.

Day 01: Naive implementation

Let's do a naive Java implementation using Java standard collections:

int solve(List<Integer> ints) {
    for (int i : ints) {
        for (int j : ints) {
            if (i + j == 2020) {
                return i * j;
            }
        }
    }

    return -1;
}

List<Integer> prices = Arrays.asList(1721, 979, 366, 299, 675, 1456);
int solution = solve(prices); // 514579

Great, that was pretty easy. Now the second parts asks you that you solve the same puzzle but by finding 3 entries that sum to 2020.

Ok le'ts just modify the solve method:

int solve(List<Integer> ints) {
    for (int i : ints) {
        for (int j : ints) {
            for (int k: ints) {
                if (i + j + k == 2020) {
                    return i * j * k;
                }
            }
        }
    }

    return -1;
}

List<Integer> prices = Arrays.asList(1721, 979, 366, 299, 675, 1456);
int solution = solve(prices); // 241861950

Awesome it works, but now the solve method isn't able to solve the first part of the puzzle.

What if next time I have to solve the same puzzle but for 4 entries that sum to 2020? We need to make the solve function more generic, and it's going to be a mess.

Day 01: With Vavr

Vavr actually spoils all the fun, because we've seen in the Sequences chapter that it already has a combinations() method.

// List(514579, 241861950)
List.of(2, 3).map(i ->
        prices.combinations(i)
                .filter(e -> e.reduce(Integer::sum) == 2020)
                .map(e -> e.reduce(Math::multiplyExact))
                .head());

That's it, using combinations we we'll be able to iterate over all the possible combinations of size N, then filter those were the sum of all the elements are 2020, and finally map to the multiplication of all those entries.

If you need to extract the function for next time.

int solve(List<Integer> ints, int size) {
    return ints.combinations(size)
            .filter(e -> e.reduce(Integer::sum) == 2020)
            .map(e -> e.reduce(Math::multiplyExact))
            .head();
}

List<Integer> prices = Arrays.asList(1721, 979, 366, 299, 675, 1456);

// List(514579, 241861950)
List.of(2, 3).map(size -> solve(prices, size));

Or even:

Function2<List<Integer>, Integer, Integer> solve = (ints, size) ->
        ints.combinations(size)
            .filter(e -> e.reduce(Integer::sum) == 2020)
            .map(e -> e.reduce(Math::multiplyExact))
            .head();

List<Integer> prices = Arrays.asList(1721, 979, 366, 299, 675, 1456);

// List(514579, 241861950)
List.of(2, 3).map(solve.apply(prices));               

I definitely encourage you to participate in the Advent of Code, it's great for your brain, for your skills and a wonderful way to challenge yourself and learn a language or a library.


Vavr Matchers

Now that you are using Vavr controls and collections throughout your code, it could be a good idea to simplify your testing around them.

If you are using Hamcrest for your matching facilities in your unit test you can use vavr-matchers, otherwise if you are using AssertJ you can use the assertj-vavr module.

I will explain the vavr-matchers library below. You can find it on Github at agrison/vavr-matchers.

Installation

The library is available on Maven Central, and you can install it directly with the following piece of XML in your pom.xml

<dependency>
  <groupId>me.grison</groupId>
  <artifactId>vavr-matchers</artifactId>
  <version>1.0</version>
</dependency>

Usage

import static me.grison.vavr.matchers.VavrMatchers.*;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;

class AllTests {
    @Test
    public void testTry() {
        Try<Integer> age = Try.of(() -> 30);

        // ensure the Try is a success and its value is less than 40
        assertThat(age, isSuccess(lessThan(40)));
    }
    
    @Test
    public void testTraversable() {
        List<Integer> ages = List.of(28, 35, 36, 40);

        // ensure not empty
        assertThat(ages, not(isEmpty()));

        // ensure length is 4
        assertThat(ages, hasLength(4));

        // ensure it contains 35
        assertThat(ages, contains(35));

        // ensure it contains at least a value less than 30
        assertThat(ages, contains(lessThan(30)));

        // ensure that all values are less than 50
        assertThat(ages, allMatch(lessThan(50)));
    }
}

See below for all available matchers.

Matchers

Option

Assertion Description
isDefined() Verifies that an Option is defined
isDefined(Matcher) Verifies that an Option is defined and its content matches a Matcher
isEmpty() Verifies that an Option is undefined
assertThat(Option.of("foo"), isDefined());
assertThat(Option.of(41), isDefined(lessThan(50)));
assertThat(Option.none(), not(isDefined()));

assertThat(Option.of("foo"), not(isEmpty()));
assertThat(Option.none(), isEmpty());

Try

Assertion Description
isSuccess() Verifies that a Try is a Success
isSuccess(Matcher) Verifies that a Try is a Success and its content matches a Matcher
isFailure() Verifies that a Try is a Failure
isFailure(Class<E extends Throwable>) Verifies that a Try is a Failure and its cause is a specific Throwable
assertThat(Try.success("foo"), isSuccess());
assertThat(Try.of(() -> 40), isSuccess(lessThan(50)));
assertThat(Try.failure(new IllegalStateException()), not(isSuccess()));

assertThat(Try.failure(new IllegalStateException()), isFailure());
assertThat(Try.success("foo"), not(isFailure()));
assertThat(Try.failure(new IllegalStateException()), isFailure(IllegalStateException.class));
assertThat(Try.failure(new IllegalStateException()), not(isFailure(NullPointerException.class));

Either

Assertion Description
isRight() Verifies that an Either is a Right
isRight(Matcher) Verifies that an Either is a Right and its content matches a Matcher
isLeft() Verifies that an Either is a Left
isLeft(Matcher) Verifies that an Either is a Left and its content matches a Matcher
assertThat(Either.right("foo"), isRight());
assertThat(Either.left("foo"), not(isRight()));
assertThat(Either.right(40), isRight(lessThan(50)));

assertThat(Either.left("foo"), isLeft());
assertThat(Either.right("foo"), not(isLeft()));
assertThat(Either.left(40), isLeft(lessThan(50)));

Traversable

Assertion Description
isEmpty() Verifies that a Traversable is empty
hasLength(int) Verifies that a Traversable has a specific length
hasLength(Matcher) Verifies that a Traversable has a length matching a Matcher
contains(T) Verifies that a Traversable contain a specific element
contains(Matcher) Verifies that a Traversable contain a specific element matching a Matcher
containsInAnyOrder(T…) Verifies that a Traversable contain the given elements
containsInAnyOrder(Traversable) Verifies that a Traversable contain the given elements
allMatch(Matcher) Verifies that a Traversable contain only elements matching a Matcher
isSorted() Verifies that a Traversable is sorted
isReverseSorted() Verifies that a Traversable is reverse sorted
startsWith(T…) Verifies that a Traversable starts with the given elements
startsWith(Traversable) Verifies that a Traversable starts with the given elements
endsWith(T…) Verifies that a Traversable ends with the given elements
endsWith(Traversable) Verifies that a Traversable ends with the given elements
isUnique() Verifies that a Traversable contains no duplicates
var list = List.of(1, 2, 3);
assertThat(List.of(), isEmpty());
assertThat(list, not(isEmpty()));

assertThat(list, hasLength(3));
assertThat(list, hasLength(lessThan(5)));

assertThat(list, contains(3));
assertThat(list, contains(lessThan(2)));

assertThat(list, containsInAnyOrder(1, 3));
assertThat(list, containsInAnyOrder(List.of(1, 3)));

assertThat(list, allMatch(lessThan(5)));

assertThat(list, isSorted());
assertThat(List.of(3, 2, 1), isReverseSorted());

assertThat(list, startsWith(1, 2));
assertThat(list, startsWith(List.of(1, 2)));

assertThat(list, endsWith(2, 3));
assertThat(list, endsWith(List.of(2, 3)));

assertThat(list, isUnique());
assertThat(List.of(1, 2, 3, 2), not(isUnique()));

Map

Assertion Description
containsKeys(T…) Verifies that a Map contains at least the given keys
containsKeys(Traversable) Verifies that a Map contains at least the given keys
containsValues(T…) Verifies that a Map contains at least the given values
containsValues(Traversable) Verifies that a Map contains at least the given values
contains(T key, U value) Verifies that a Map contains at least the given entry
var map = HashMap.of(1, 2, 3, 4); // 1 => 2, 3 => 4
assertThat(map, containsKeys(List.of(1, 3)));
assertThat(map, containsKeys(1, 3));
assertThat(map, not(containsKeys(1, 2)));

assertThat(map, containsValues(List.of(2, 4)));
assertThat(map, containsValues(2, 4));
assertThat(map, not(containsValues(2, 3)));

assertThat(map, contains(1, 2));
assertThat(map, contains(3, 4));
assertThat(map, not(contains(1, 3)));

Future

Assertion Description
isCancelled() Verifies that a Future is cancelled
isCompleted() Verifies that a Future is completed
isCompleted(Matcher) Verifies that a Future is completed and its content matches a Matcher
var f = Future.of(() -> {
    Thread.sleep(10_000);
    return 1;
});
assertThat(f, not(isCompleted()));
f.cancel();
assertThat(f, isCancelled());

f = Future.of(() -> 1);
f.get();
assertThat(f, not(isCancelled()));
assertThat(f, isCompleted());
assertThat(f, isCompleted(is(1)));

Lazy

Assertion Description
isEvaluated() Verifies that a Lazy has been evaluated
isEvaluated(Matcher) Verifies that a Lazy has been evaluated and its content matches a Matcher
var l = Lazy.of(() -> 1);
l.get();
assertThat(l, isEvaluated());
assertThat(l, isEvaluated(is(1)));

l = Lazy.of(() -> 1);
assertThat(l, not(isEvaluated()));

Tuple

Assertion Description
hasArity(int) Verifies that a Tuple has a specific arity
hasArity(Matcher) Verifies that a Tuple has a specific arity matching a Matcher
assertThat(Tuple.of(1), hasArity(1));
assertThat(Tuple.of(1, 2), hasArity(2));
assertThat(Tuple.of(1, 2), hasArity(is(2)));
assertThat(Tuple.of(1, 2, 3), hasArity(3));
assertThat(Tuple.of(1, 2), not(hasArity(5)));

Validation

Assertion Description
isValid() Verifies that a Validation is valid
isValid(Matcher) Verifies that a Validation is valid and its content matches a Matcher
isInvalid() Verifies that a Validation is invalid
isInvalid(Matcher) Verifies that a Validation is invalid and its content matches a Matcher
assertThat(Validation.valid(1), isValid());
assertThat(Validation.valid(1), isValid(is(1)));
assertThat(Validation.invalid(1), not(isValid()));

assertThat(Validation.valid(1), not(isInvalid()));
assertThat(Validation.invalid(1), isInvalid());
assertThat(Validation.invalid(1), isInvalid(is(1)));

Vavr and Kotlin

Vavr Kotlin is a set of Kotlin niceties including:

  • idiomatic factory methods
  • extension forms of sequences
  • conversions to and from Kotlin collections

Of course you can still use the whole Java API from Kotlin.

Usage

import io.vavr.kotlin.*

That's it.

The examples below are mostly taken from the Vavr Kotlin Wiki.

Tuple

You can create a Tuple using the tuple function.

val t1 = tuple("foo") // Tuple1<String>
val t2 = tuple("foo", 42) // Tuple2<String, Integer>
val t3 = tuple("foo", 42, true) // Tuple3<String, Integer, Boolean>

You can also interrop from Kotlin's built-in Pair type to Vavr's Tuple2 and the opposite.

val t = ("foo" to 42).tuple() // Tuple2<String>
val p = tuple("foo", 42).pair() // Pair<String, Integer>

Option

Kotlin has first-class nullables, this is why the Option constructor can be null-aware:

val none: None = option(null)
val someFoo: Some<String> = option("foo")

val none = none()
val someFoo = some("foo")

Vavr Kotlin also add extensions to the boolean type to that you can build options from it.

false.option("foo") // None
true.option("bar") // Some("bar")

Either

Vavr Kotlin provides left and right to create Either instances.

val l = left("Error") // Left(Error)
val r = right("foo") // Right(foo)

Either can be turned into a Validation using validation().

val valid = right("foo").validation() // Validation<String, String>
val invalid = left("nope").validation() // Validation<String, String>

Try

Vavr Kotlin provides success and failure to create Try instances.

val succes = success("foo")  // Success<String>
val failure = failure(RuntimeException())  // Failure

In order to execute a lambda like you would with Try.of in Java, you can do it like this:

val success = `try` { -> "foo" }
val fail = `try` { throw RuntimeException() }

List

You can create a Vavr's List by using the list function.

val l = list(1, 2, 3, 4)

Interop with Kotlin's MutableList is possible with toMutableList() while converting a Kotlin's Iterable to a Vavr's List is possible with toVavrList().

val mutable = list(1, 2, 3, 4).toMutableList() // Kotlin list
val vavr = listOf(1, 2, 3, 4).toVavrList() // Vavr list

Set

You can create a Vavr's Set by using either the hashSet, linkedHashSet, or the treeSet function.

val hashSet = hashSet(1, 2, 3, 4, 5)
val linkedHashSet = linkedHashSet("foo", "bar", "bazz")
val treeSet = treeSet("abc", "def", "ghi")

Interop with Kotlin's MutableSet is possible with toMutableSet() while converting a Kotlin's Set to a Vavr's List is possible with toVavrSet().

val mutable = hashSet(1, 2, 3, 4).toMutableSet() // Kotlin set
val vavr = setOf(1, 2, 3, 4).toVavrSet() // Vavr set

Map

You can create a Vavr's Map by using either the hashMap, linkedHashMap, or the treeMap function.

val hahsMap = hashMap(1 to "foo", 2 to "bar", 3 to "baz")
val linkedHashMap = linkedHashMap(1 to "foo", 2 to "bar", 3 to "baz")
val treeMap = treeMap(1 to "foo", 2 to "bar", 3 to "baz")

Interop with Kotlin's MutableMap is possible with toMutableMap() while converting a Kotlin's Map to a Vavr's List is possible with toVavrMap().

// Kotlin Map
val mutable = hashMap(1 to "foo", 2 to "bar", 3 to "bazz").toMutableMap() 
val vavr = mapOf(1 to "foo", 2 to "bar", 3 to "bazz").toVavrMap() // Vavr set

Vavr and Property Testing

From Wikipedia:

In computer science, a property testing algorithm for a decision problem is an algorithm whose query complexity to its input is much smaller than the instance size of the problem. Typically property testing algorithms are used to decide if some mathematical object (such as a graph or a boolean function) has a “global” property, or is “far” from having this property, using only a small number of “local” queries to the object.

In other words a property is a combination of an invariant (something which is supposed to be always true) with a generator of inputs. Each time a value is generated - the input -, the invariant is assumed to be a predicate to be checked on this input.

A property is said to be falsified as soon as a predicate cannot be satisfied, and the testing phase stops.

Usage

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

FizzBuzz

We'll try to write a test with 100% coverage of FizzBuzz.

First let's write a FizzBuzz implementation with Vavr a Stream zipping for the sake of it. Because I'm writing a book about Vavr.

Stream<String> fizzBuzzStreamer() {
    var fizz = Stream.of("", "", "Fizz").cycle();
    var buzz = Stream.of("", "", "", "", "Buzz").cycle();
    var nums = Stream.from(1);
    return fizz.zip(buzz).zip(nums)
            .map(step -> Option.of(step._1.apply(String::concat))
                    .filter(Predicate.not(String::isBlank))
                    .getOrElse(step._2.toString()));
}

We create 3 Streams:

  • one which will infinitely return "", then "", then Fizz
  • one which will infinitely return 4 empty strings, then Buzz
  • one which will inifintely generate integers, starting from 1

Then we zip the 3 Streams and iterate on 3 values at a time named step.

For each step we concatenate the String from the first and second stream, and we push it into an Option in order to filter its content. If the content of the Option is a blank String then it means it's not Fizz, nor Buzz, so it must be another number, otherwise it's one of Fizz, Buzz or FizzBuzz.

Seems legit!

But we can use the vavr-test library to property test this FizzBuzz generator.

Let's create a @Test.

@Test
public void propertyTestFizzBuzz() {
    int size = 10_000, tries = 1_000;

    // retrieve the nth FizzBuzz value
    Function1<Integer, String> fizzBuzzAt = n -> 
        fizzBuzzStreamer().get(n - 1);

    // generator of positive integers
    var positiveInts = Arbitrary.integer().filter(i -> i > 0);
    // generator of positive integers divisible by 3
    var multiplesOf3 = positiveInts.filter(i -> i % 3 == 0);
    // generator of positive integers divisible by 5
    var multiplesOf5 = positiveInts.filter(i -> i % 5 == 0);
    // generator of positive integers divisible by both 3 and 5
    var multiplesOf15 = positiveInts.filter(i -> i % 3 == 0 && i % 5 == 0);
    // generator of positive integers not divisible by 3 nor by 5
    var normalNumber = positiveInts.filter(i -> i % 3 == 1 && i % 5 == 1);

    // function checking that given an integer divisible by 3
    // the FizzBuzz for that value will start with Fizz 
    // it can be either Fizz, or FizzBuzz
    CheckedFunction1<Integer, Boolean> multipleOf3IsAFizz = n ->
        fizzBuzzAt.apply(n).startsWith("Fizz");

    // function checking that given an integer divisible by 5
    // the FizzBuzz for that value will end with Buzz
    // it can be either Buzz, or FizzBuzz
    CheckedFunction1<Integer, Boolean> multipleOf5IsABuzz = n -> 
        fizzBuzzAt.apply(n).endsWith("Buzz");

    // function checking that given an integer divisible by 3 and 5
    // the FizzBuzz for that value will be FizzBuzz
    CheckedFunction1<Integer, Boolean> multipleOf15IsAFizzBuzz = n -> 
        fizzBuzzAt.apply(n).equals("FizzBuzz");

    // function checking that given an integer divisible nor divisible by 3 and 5
    // the FizzBuzz for that value will be the String representation of that integer 
    CheckedFunction1<Integer, Boolean> normalNumberIsNotFizzNorBuzz = n ->
        fizzBuzzAt.apply(n).equals(n.toString());

    // now define the properties to be checked
    List.of(Tuple.of("Multiple of 3 starts with Fizz", multiplesOf3, multipleOf3IsAFizz),
        Tuple.of("Multiple of 5 ends with Buzz", multiplesOf5, multipleOf5IsABuzz),
        Tuple.of("Multiple of 15 is FizzBuzz", multiplesOf15, multipleOf15IsAFizzBuzz),
        Tuple.of("Other numbers are untouched", normalNumber, normalNumberIsNotFizzNorBuzz))
        .forEach(t -> Property.def(t._1)
                    .forAll(t._2).suchThat(t._3)
                    .check(size, tries)
                    .assertIsSatisfied());
}

When executed, this test will output:

Multiple of 3 starts with Fizz: OK, passed 1000 tests in 992 ms.
Multiple of 5 ends with Buzz: OK, passed 1000 tests in 977 ms.
Multiple of 15 is FizzBuzz: OK, passed 1000 tests in 995 ms.
Other numbers are untouched: OK, passed 1000 tests in 984 ms.

It works great, we can be pretty sure our function is valid. At least way more than if we created ourselves a thousand of tests manually.

API

Arbitrary

The Arbitrary interface represents an arbitrary object of type T. It provides various methods to generate arbitrary objects to be used for property testing.

Most notable uses are the following functions.

method description
integer() Generates integers
localDateTime() Generates LocalDateTime, by default using a DAY interval
localDateTime(ChronoUnit) Generates LocalDateTime, by using a custom interval
localDateTime(LocalDateTime, ChronoUnit) Generates LocalDateTime around a given median date, by using a custom interval an
string(Gen) Generates String based on a given Generator
of(T...), ofAll(Gen) Generates an Arbitrary from a fixed set of values
stream(Arbitrary) Generates Arbitrary streams based on a given Arbitrary
list(Arbitrary) Generates Arbitrary listbased on a given Arbitrary
intersperse(Arbitrary) Intersperses values from this arbitrary with those of another one
distinct(), distinctBy(Comparator) Makes an Arbitrary which produces unique values

Gen

Generators are the building blocks for providing arbitrary objects.

Fixed one

You can create constant generators, that is, returning constantly the same value by using of().

Choosing

The choose methods have a number of overloadings which are suitable to use to make Generators.

method description
choose(char, char) Chooses between a minimum character and a maximum
choose(char...) Chooses between a defined set of characters
choose(Class<T>) Chooses between the values of a given Enum
choose(double, double) Chooses between a minimum double and a maximum
choose(int, int) Chooses between a minimum int and a maximum
choose(Iterable<T>) Chooses between a defined Iterable of objects
choose(long, long) Chooses between a minimum long and a maximum
choose(T...) Chooses between a defined set of objects

Failing

You can create a failing generator using the fail method which can take either no arguments and thus throw a RuntimeException with the message failed or you can give it an argument to customize the exception message.

Rest of the API

The Gen interface has also methods for choosing generators according to their frequency, intersperse with another generator, or even randomly choosing between a given set of Generators.

Alexandre Grison - //grison.me - @algrison