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, 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 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.

    finalboolean 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:

    finalboolean 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.

    // рассчет суммыfinalint zero = 0; // нейтральный элементfinal BiFunction<Integer, Integer, Integer> combine
           = (x, y) -> x + y; // функция объединенияfinalint 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 the default

    list. Let's make a single-linked 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 infinite 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) // от 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 element of the structure can be used as an 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
    @Overridedefault Function1<T1, Function1<T2, R>> curried() {
       return t1 -> t2 -> apply(t1, t2);
    }
    


    In a set of Function there are many useful methods. 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.

    importstatic io.vavr.API.*;
    importstatic io.vavr.Predicates.*;
    publicclassPatternMatchingDemo{
        publicstaticvoidmain(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.

    Also popular now: