Functional programming in Java – introduction to Vavr.io
Vavr (formerly called Javaslang) is a functional library for Java 8+ that provides persistent data types and functional control structures. It enables writing Java code in a more functional way. It is inspired by Scala.
In this article, I would like to help you become familiar with Vavr collections and functions. I will use the 0.9.2 version which is the newest at the moment of writing it. However, the next major version is coming (1.0.0) and it will improve most aspects based on the user’s experience.
Getting started
To start using Vavr you need to add Gradle or Maven dependency to your project.
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.9.2</version>
</dependency>
Collections
Java’s standard collection are mutable what can lead to a lot of troubles (not only with thread safety). Java provides immutable collections like unmodifiableList but they still have methods like add, remove etc. which can be used by the programmer and it can explode at runtime.
Vavr implemented collection from scratch. They do not implement Java collection interfaces from java.util so they have different (better) API. They are based on java.lang.Iterable.
Vavr’s collection are immutable – cannot be modified after their creation. They are also persistent which means that they preserve the previous version of itself when being modified. All modifications of Vavr’s collections are not prohibited but simply return new version of themselves. It does not mean that they copy all stored values. All new versions share an immutable memory between instances. It makes them thread safe.
Java’s stream increased a number of operation for collections, but it is boilerplate (you always need to call stream() method at the beginning and collect() at the end). Vavr’s collection API has much more operations than Java’s stream (without boilerplate). I encourage you to take a look at documentation (https://static.javadoc.io/io.vavr/vavr/0.9.2/io/vavr/collection/package-summary.html).
import io.vavr.collection.List;
...
@Test
void persistenceAndImmutability() {
List<Integer> list1 = List.of(1, 2, 3);
//list1 cannot be changed after creation
List<Integer> list2 = list1.prepend(0).append(4);
//prepend and append operations return a "copy" of list1
assertThat(list1).containsExactly(1, 2, 3);
assertThat(list2).containsExactly(0, 1, 2, 3, 4);
}
Functions
Java.util.function provides us a set of functional interfaces that allow us to use lambda. All interfaces have hard to remember names like Supplier, Consumer, DoubleBinaryOperator, IntUnaryOperator, ToLongBiFunction, etc. Vavr comes with FunctionN interfaces, where N is the number of method’s parameters.
@Test
void methodWith3Parameters() {
Function3<Integer, Integer, Integer, Integer> sum
= (a, b, c) -> a + b + c;
Integer result = sum.apply(1, 2, 3);
assertThat(result).isEqualTo(6);
}
Java has one big limitation. You are not able to pass a method that throws checked exception as a parameter or lambda. You always have to catch the exception inside a lambda body, which decreases readability.
//some method which cannot be changed
private int getFileSize() throws FileNotFoundException {
return 10;
}
private void collectStats(Supplier<Integer> statsSupplier) {
int stats = statsSupplier.get();
...
}
@Test
void lambdaCheckedExceptionInJava() {
//The compiler complaints about Unhandled exception
//logStats(this::getFileSize);
Supplier<Integer> getFileSize = () -> {
try {
return getFileSize();
} catch (FileNotFoundException e) {
return 0;
}
};
collectStats(getFileSize);
With Vavr it is possible since there are set of CheckedFunctionN interfaces that throw Throwable.
@Test
void lambdaCheckedExceptionInVavr() {
CheckedFunction0<Integer> getFileSize = this::getFileSize;
collectStats(getFileSize);
}
private void collectStats(CheckedFunction0<Integer> statsSupplier) {
try {
int stats = statsSupplier.apply();
} catch (Throwable throwable) {
...
}
}
Functions – composition
The first function transformation I would like to describe is composition. In mathematics, function composition is the application of one function to the result of another to produce a third function. You can define a composed function using andThen or compose method:
Function1<Integer, Integer> plusTwo = a -> a + 2;
Function1<Integer, Integer> multiplyByTwo = a -> a * 2;
Function1<Integer, Integer> minusTwo = a -> a - 2;
@Test
void andThenComposition() {
Function1<Integer, Integer> add1MultiplyBy2AndSubtract2
= plusTwo.andThen(multiplyByTwo).andThen(minusTwo);
assertThat(add1MultiplyBy2AndSubtract2.apply(3).intValue())
.isEqualTo(8);
}
@Test
void composeComposition() {
Function1<Integer, Integer> add1MultiplyBy2AndSubtract2
= minusTwo.compose(multiplyByTwo).compose(plusTwo);
assertThat(add1MultiplyBy2AndSubtract2.apply(3).intValue())
.isEqualTo(8);
}
Functions – lifting
In mathematics, there is a term of partial function. The partial function works properly only for some input values. For another it is undefined. If the function is called with disallowed input value, it will typically throw an exception. You can lift a partial function into a total function that returns an Option result.
@Test
void lifting() {
Function2<Integer, Integer, Integer> divide = (x, y) -> x / y;
//ArithmeticException will be thrown
//Integer result = divide.apply(5, 0);
Function2<Integer, Integer, Option<Integer>> safeDivide
= Function2.lift(divide);
Option<Integer> result = safeDivide.apply(10, 5);
Option<Integer> noResult = safeDivide.apply(10, 0);
assertTrue(result.isDefined());
assertTrue(noResult.isEmpty());
}
Functions – partial application
It is possible to reduce the number of function parameters by using partial application. You can create a new function from an existing one by fixing some values. You can fix one or more parameters.
@Test
void partialApplication() {
Function4<Integer, Integer, Integer, Integer, Integer> sum
= (a, b, c, d) -> a + b + c + d;
Function2<Integer, Integer, Integer> sum12 = sum.apply(1,2);
assertThat(sum.apply(1, 1, 1, 1)).isEqualTo(4);
assertThat(sum12.apply(3, 4)).isEqualTo(10);
}
Functions – currying
Currying may look similar to partial application. It can also reduce the number of function parameters. However, it is based on a different concept. Currying produces a function with 1 parameter which results is another function with 1 parameter etc. The depth of it depends on several parameters of a curried function. It is hard to understand without an example:
Function3<Integer, Integer, Integer, Integer> sum
= (a, b, c) -> a + b + c;
Function1<Integer, Function1<Integer,
Function1<Integer, Integer>>> curriedSum = sum.curried();
assertThat(sum.apply(1, 2, 3)).isEqualTo(6);
assertThat(curriedSum.apply(1).apply(2).apply(3))
.isEqualTo(6);
}
Functions – memoization
Memoization is storing the result of function calls and returning the cached result when the same inputs occur again. It should be used only with pure functions. You can describe a function as pure when it does not have side effects and the result is only determined by its input value (pure function called with parameter x will always return the same value).
@Test
void memoization() {
Function0<Double> getRandom = Math::random;
Function0<Double> getCachedRandom = getRandom.memoized();
assertThat(getRandom.apply())
.isNotEqualTo(getRandom.apply());
assertThat(getCachedRandom.apply())
.isEqualTo(getCachedRandom.apply());
}
Appendix
Vavr provides also a lot of useful basic types like Tuples, monadic containers (Option, Try, Lazy, Either, Future and Validation) and pattern matching. They are beyond the scope of this article but they are crucial to write code in a functional manner.