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
- Option
- Either
- Try
- Lazy
- Future
- Match
- Validation
- Functions
- Collections
- Sequences
- Sets
- Maps
- Folding and reducing Maps
- Streams
- Vavr in action
- Vavr Matchers
- Vavr and Kotlin
- Vavr and Property Testing
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:
- try to reverse the string
- then lowercase it
- then capitalize it
- then concatenate some other string with it
- then get the final result
- but if any of this fails I want the
Could not proceed
string instead.
- but if any of this fails I want the
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 Left
s 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 theEither
isLeft
. - The
peekLeft
method won't run any side effect if theEither
isRight
.
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:
- try to reverse the string
- then lowercase it
- then capitalize it
- then concatenate some other string with it
- then get the final result
- but if any of this fails I want the
Could not proceed
string instead.
- but if any of this fails I want the
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 theTry
isFailure
. - The
onFailure
method won't run any side effect if theTry
isSuccess
.
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:
- try to reverse the string
- then lowercase it
- then capitalize it
- then concatenate some other string with it
- then get the final result
- but if any of this fails I want the
Could not proceed
string instead.
- but if any of this fails I want the
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:
Pending
, which means that the computation is still ongoing and can be cancelled or completedCompleted
, 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 theFuture
has completed,false
otherwiseisSuccess
:true
if theFuture
has completed successfully,false
otherwiseisFailure
:true
if theFuture
has completed with an error,false
otherwiseisCancelled
:true
if theFuture
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 matchingCase
, for explaining a case in the above pattern matching- Some predicates to help us write the needed
Case
s
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
, aSupplier
, …
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
Case
s 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 thedefault
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 resultBiFunction
: 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 Seq
s 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 implementationHashSet
: an immutable HashSet implementationLinkedHashSet
: an immutable HashSet implementation that has predictable (insertion-order) iterationTreeSet
: 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 Set
s 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 implementationLinkedHashMap
: an immutable HashMap implementation that has predictable (insertion-order) iterationTreeMap
: 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 Function
s.
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 emptyStream
Cons
, aStream
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:
- data in
- transform (may have side effect)
- data out
- transform (may have side effect)
- data out
- 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:
- Given an id
- Retrieve a user details: email, first name and password (not really secure ;-)
- Register an account on Twitter
- Authenticate on Twitter
- Tweet
Hello, world
- Update the user details with the Twitter account id
- Log something in case of success
- Return the tweet URL
- 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 Option
s 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.
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;
}
}
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.
- Replace the classic for loop with an ehanced one, because clearly the code is doing nothing with the index.
- Extract some constants for the various Strings used in the code
- 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 :
- avoid writing an
if
by using aHashMap
to dispatch functions calls. - use function composition to deal with both the quality update and the decreasing of
sellIn
at the sametime. - use the very versatile
Match
API to functionally return values based on predicates. - remove the use of
Item[]
with aSeq
. - replace a big for loop with a sequence iteration
- 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
andOption
to avoid checking fornull
s Try
for dealing with sensitive partsOption
for responding the correct status code through the use of Spring'sResponseEntity
- 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:
- reading the content to a String
- make a
List
out of all the lines - for each line make a player out of it
- get the built
List
or else throw. - 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""
, thenFizz
- 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.