Functional Java Programming With Vavr

unnamed

Functional Java Programming With Vavr

Many have heard of such functional languages as Haskell and Clojure. But there are such languages as, for example, Scala. It combines both the PLO and the functional approach.

What about good old java? Is it possible to write programs on it in a functional style and how much can it hurt? Yes, there is Java 8 and lambda with streams. This is a big step for the language, but it is still not enough. Is it possible to come up with something in this situation? It turns out yes.

To begin with, let’s try to determine what the writing of code in the functional style means. First, we must operate not with variables and manipulations with them, but with chains of some calculations. In essence, a sequence of functions. In addition, we must have special data structures. For example, standard java collections are not suitable. Soon it will be clear why.

Consider the functional structure in more detail. Any such structure must satisfy at least two conditions:

  • Immutable – the structure must be immutable. This means that we fix the state of the object at the creation stage and leave it as such until the end of its existence. An obvious example of a condition violation: standard ArrayList.
  • Persistent – the structure should be kept in memory for as long as possible. If we created an object, then instead of creating a new one with the same state, we should use ready-made. More formally, such structures, when modified, retain all their previous states. References to these states must remain fully operational.

Obviously, we need some kind of third-party solution. And there is such a solution: the Vavr library. Today it is the most popular is Java for working in a functional style. Next, I will describe the main features of the library. Many, but not all, examples and descriptions were taken from official documentation.

Basic Vavr library data structures

Tuple

One of the most basic and simple functional data structures are tuples. A tuple is an ordered set of fixed length. Unlike lists, a tuple can contain data of any type.


Tuple tuple = Tuple.of(1, "blablabla", .0, 42L); // (1, blablabla, 0.0, 42)

Getting the desired item comes from calling the field with the item number in the tuple.


((Tuple4) tuple)._1 <span class="hljs-comment">// 1</span>

Note: indexing tuples starts from 1! In addition, to obtain the desired element, we must convert our object to the desired type with the appropriate set of methods. In the example above, we used a tuple of 4 elements, which means the conversion should be of type Tuple4. In fact, no one bothers us initially to make the desired type.


Tuple4 tuple = Tuple.of(1, "blablabla", .0, 42L); // (1, blablabla, 0.0, 42)
System.out.println(tuple._1); // 1

Top 3 Vavr collections

The list

Creating a list with Vavr is easy. Even easier than without Vavr.


List.of(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>)

What can we do with such a list? Well, firstly, we can turn it into a standard java list.


final boolean containThree = List.of(1, 2, 3)
.asJava()
.stream()
.anyMatch(x -> x == 3);

But in reality there is no great need for this, since we can do, for example, like this:


final boolean containThree = List.of(1, 2, 3)
.find(x -> x == 1)
.isDefined();

In general, the standard list of the Vavr library has many useful methods. For example, there is a fairly powerful convolution function, which allows you to combine a list of values by some rule and a neutral element.


// calculate the amount
final int zero = 0; // neutral element
final BiFunction <Integer, Integer, Integer> combine
= (x, y) -> x + y; // join function
final int sum = List.of (1, 2, 3)
.fold (zero, combine); // call convolution

One important point should be noted here. We have functional data structures, which means that we cannot change their state. How is our list implemented? Arrays are definitely not suitable for us.

Linked List as the default list

Let’s make a singly-connected list with non-shared objects. It turns out like this:

Java 1

Code example


List list = List.of(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>);

Each element of the list has two main methods: getting the head element (head) and all the others (tail).

Code example


list.head(); // 1
list.tail(); // List(2, 3)

Now, if we want to change the first item in the list (from 1 to 0), then we need to create a new list by reusing ready-made parts.

Java 2

Code example


final List tailList = list.tail (); // get the tail of the list
tailList.prepend (0); // add an element to the top of the list

And that’s it! Since our objects in the sheet are unchanged, we get a thread-safe and reusable collection. Elements of our list can be applied anywhere in the application and it is completely safe!

The turn

Another extremely useful data structure is the queue. How to make a queue for building effective and reliable programs in a functional style? For example, we can take data structures already known to us: two lists and a tuple

Java 3

Code example


Queue<Integer> queue = Queue.of(1, 2, 3)
.enqueue(4)
.enqueue(5);

When the first one ends, we expand the second one and use it for reading.

Java 4

It is important to remember that the queue should be the same, like all other structures. But what is the use of the queue, which does not change? In fact, there is a trick. As an accepted value of the queue, we get a tuple of two elements. First: the desired element of the queue, the second: what happened to the queue without this element.


System.out.println(queue); // Queue(1, 2, 3, 4, 5)
Tuple2<Integer, Queue<Integer>> tuple2 = queue.dequeue();
System.out.println(tuple2._1); // 1
System.out.println(tuple2._2); // Queue(2, 3, 4, 5)

Streams

The next important data structure is a stream. A stream is a stream of performing some actions on a certain, often abstract, set of values.

Some may say that Java 8 already has full-fledged streams and we don’t need new ones at all. Is it so?

First, let’s make sure that the java stream is not a functional data structure. Check the structure for variability. To do this, create such a small stream:


IntStream standardStream = IntStream.range(<span class="hljs-number">1</span>, <span class="hljs-number">10</span>);

Let’s do a look at all the elements in the stream:


standardStream.forEach(System.out::print);

In response, we get the output to the console: 123456789. Let’s repeat the iteration operation:


standardStream.forEach(System.out::print);

Oops, this error occurred:


java.lang.IllegalStateException: stream has already been operated upon or closed

The fact is that standard streams are just some kind of abstraction over an iterator. Although the stream seems to be extremely independent and powerful, the cons of iterators have not gone away.

For example, the definition of a stream does not say anything about limiting the number of elements. Unfortunately, in the iterator it is, and therefore it is in the standard stream.

Fortunately, the Vavr library solves these problems. Make sure of this:


Stream stream = Stream.range(1, 10);
stream.forEach(System.out::print);
stream.forEach(System.out::print);

In response, we get 123456789123456789. What the first operation means does not “spoil” our stream.

Now let’s try to create an endless stream:

Stream infiniteStream = Stream.from (1);
System.out.println (infiniteStream); // Stream (1,?)

Note: when printing an object, we get not the infinite structure, but the first element and the question mark. The fact is that each subsequent element in the stream is generated on the fly. This approach is called lazy initialization. It is he who allows you to work safely with such structures.

If you have never worked with infinite data structures, then most likely you are thinking: why do we need it at all? But they can be extremely convenient. We write a stream that returns an arbitrary number of odd numbers, converts them to a string and adds a space:


Stream oddNumbers = Stream
.from (1, 2) // from 1 with step 2
.map (x -> x + ""); // formatting
// usage example
oddNumbers.take (5)
.forEach (System.out :: print); // 1 3 5 7 9
oddNumbers.take (10)
.forEach (System.out :: print); // 1 3 5 7 9 11 13 15 17 19

Just like that.

The general structure of collections

After we discussed the basic structures, it’s time to look at the general architecture of the functional collections of Vavr:

Java 5

Each structure element can be used as iterable:


StringBuilder builder = new StringBuilder ();
for (String word: List.of ("one", "two", "tree")) {
if (builder.length ()> 0) {
builder.append (",");
}
builder.append (word);
}
System.out.println (builder.toString ()); // one, two, tree

But it is worth thinking twice and seeing the dock before using for. The library allows you to make familiar things easier.


System.out.println(List.of(<span class="hljs-string">"one"</span>, <span class="hljs-string">"two"</span>, <span class="hljs-string">"tree"</span>).mkString(<span class="hljs-string">", "</span>)); <span class="hljs-comment">// one, two, tree</span>

Work with functions

The library has a number of functions (8 pieces) and useful methods for working with them. They are common functional interfaces with many interesting methods. The name of the functions depends on the number of arguments taken (from 0 to 8). For example, Function0 takes no arguments, Function1 takes one argument, Function2 takes two, and so on.


Function2<String, String, String> combineName =
(lastName, firstName) -> firstName + " " + lastName;
System.out.println(combineName.apply("Griffin", "Peter")); // Peter Griffin

In the functions of the library Vavr, we can do a lot of cool things. In terms of functionality, they go far ahead of the standard Function, BiFunction, etc. For example, currying. Currying is the construction of functions in parts. Let’s look at an example:


// Create a basic function
Function2 <String, String, String> combineName =
(lastName, firstName) -> firstName + "" + lastName;
// Based on the base we build a new function with one passed element
Function1 <String, String> makeGriffinName = combineName
.curried ()
.apply ("Griffin");
// We work as with full function
System.out.println (makeGriffinName.apply ("Peter")); // Peter Griffin
System.out.println (makeGriffinName.apply ("Lois")); // Lois Griffin

As you can see, quite concisely. The curried method is extremely simple, but it can be of great benefit.

Implementation of the curried method


@Override
default Function1<T1, Function1<T2, R>> curried() {
return t1 -> t2 -> apply(t1, t2);
}

There are many more useful methods in the Function set. For example, you can cache the return value of a function:


Function0<Double> hashCache =
Function0.of(Math::random).memoized();

double randomValue1 = hashCache.apply();
double randomValue2 = hashCache.apply();

System.out.println(randomValue1 == randomValue2); // true

Combat Exceptions

As we said earlier, the programming process must be safe. To do this, avoid various extraneous effects. Exceptions are their explicit generators.

You can use the Try class to safely handle exceptions in a functional style. Actually, this is a typical monad. It is not necessary to delve into theory for use. Just look at a simple example:


Try.of(() -> 4 / 0)
.onFailure(System.out::println)
.onSuccess(System.out::println);

As you can see from the example, everything is quite simple. We simply hang the event on a potential error and do not push it beyond the limits of calculations.

Pattern matching

Often there is a situation in which we need to check the value of a variable and model the behavior of a program depending on the result. Just in such situations comes to the aid of a wonderful search mechanism for a pattern. You no longer need to write a bunch of if else, just set up all the logic in one place.


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

public class PatternMatchingDemo {
public static void main(String[] args) {
String s = Match(1993).of(
Case($(42), () -> "one"),
Case($(anyOf(isIn(1990, 1991, 1992), is(1993))), "two"),
Case($(), "?")
);
System.out.println(s); // two
}
}

Note, Case is written with a capital letter because the case is a keyword and is already taken.

Conclusion

In my opinion, the library is very cool, but it is worth using it extremely carefully. It can perfectly manifest itself in event-driven development. However, excessive and thoughtless use of it in standard imperative programming based on the thread pool can bring a lot of headaches. In addition, our projects often use Spring and Hibernate, which are not always ready for such use. Before importing the library into your project, you need a clear understanding of how and why it will be used. What I will tell in one of my next articles.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.