📜 ⬆️ ⬇️

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:
')

Obviously, we need some kind of third-party solution. And there is such a solution: the Vavr library. Today it is the most popular library in 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 element comes from calling the field with the element number in the tuple.

 ((Tuple4) tuple)._1 // 1 

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


List


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

 List.of(1, 2, 3) 

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 that allows you to combine a list of values ​​by some rule and a neutral element.

 //   final int zero = 0; //   final BiFunction<Integer, Integer, Integer> combine = (x, y) -> x + y; //   final int sum = List.of(1, 2, 3) .fold(zero, combine); //   

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 default

We make a concatenated list with non-shared objects. It turns out like this:

image

Code example
 List list = List.of(1, 2, 3); 


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.

image
Code example
 final List tailList = list.tail(); //    tailList.prepend(0); //      


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!

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.

image

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.

image

image

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 stream. A stream is a stream of performing some actions on a certain, often abstract, set of values.

Someone 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. Let's check the structure for variability. To do this, create such a small stream:
 IntStream standardStream = IntStream.range(1, 10); 

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 brute force 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, but the cons of the 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 an 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) //  1   2 .map(x -> x + " "); //  //   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.

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 :



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("one", "two", "tree").mkString(", ")); // one, two, tree 

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:

 //    Function2<String, String, String> combineName = (lastName, firstName) -> firstName + " " + lastName; //           Function1<String, String> makeGriffinName = combineName .curried() .apply("Griffin"); //      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 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 other 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 just hang the event on a potential error and do not push it beyond the limits of the calculations.

Pattern matching


Often there is a situation in which we need to check the value of a variable and model the behavior of the 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 case is a keyword and is already taken.

Conclusion


In my opinion, the library is very cool, but it should be used very carefully. It can perfectly manifest itself in event-driven development. However, excessive and thoughtless use of it in standard imperative programming based on the pool of threads 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.

Source: https://habr.com/ru/post/421839/


All Articles