Pengenalan kepada Vavr

1. Gambaran keseluruhan

Dalam artikel ini, kita akan meneroka dengan tepat apa itu Vavr, mengapa kita memerlukannya dan bagaimana menggunakannya dalam projek kita.

Vavr adalah perpustakaan berfungsi untuk Java 8+ yang menyediakan jenis data yang tidak berubah dan struktur kawalan fungsional.

1.1. Ketergantungan Maven

Untuk menggunakan Vavr, anda perlu menambahkan kebergantungan:

 io.vavr vavr 0.9.0 

Sebaiknya selalu menggunakan versi terbaru. Anda boleh mendapatkannya dengan mengikuti pautan ini.

2. Pilihan

Matlamat utama Option adalah untuk menghilangkan cek kosong dalam kod kami dengan memanfaatkan sistem jenis Java.

Option adalah wadah objek di Vavr dengan tujuan akhir yang serupa seperti Pilihan di Java 8. Vavr's Option menerapkan Serializable, Iterable, dan mempunyai API yang lebih kaya .

Oleh kerana mana-mana rujukan objek di Java dapat memiliki nilai nol , kita biasanya harus memeriksa apakah ada pernyataan sebelum menggunakannya. Pemeriksaan ini menjadikan kodnya kukuh dan stabil:

@Test public void givenValue_whenNullCheckNeeded_thenCorrect() { Object possibleNullObj = null; if (possibleNullObj == null) { possibleNullObj = "someDefaultValue"; } assertNotNull(possibleNullObj); }

Tanpa pemeriksaan, aplikasi boleh rosak kerana NPE sederhana :

@Test(expected = NullPointerException.class) public void givenValue_whenNullCheckNeeded_thenCorrect2() { Object possibleNullObj = null; assertEquals("somevalue", possibleNullObj.toString()); }

Walau bagaimanapun, pemeriksaan menjadikan kod tersebut tidak dapat dibaca dan tidak begitu mudah dibaca , terutamanya apabila pernyataan jika akhirnya disarang berkali-kali.

Pilihan menyelesaikan masalah ini dengan menghilangkan sepenuhnya nol dan menggantinya dengan rujukan objek yang sah untuk setiap senario yang mungkin.

Dengan Opsyen , nilai nol akan dinilai menjadi kejadian Tidak Ada , sementara nilai bukan nol akan dinilai menjadi contoh Sebilangan :

@Test public void givenValue_whenCreatesOption_thenCorrect() { Option noneOption = Option.of(null); Option someOption = Option.of("val"); assertEquals("None", noneOption.toString()); assertEquals("Some(val)", someOption.toString()); }

Oleh itu, daripada menggunakan nilai objek secara langsung, disarankan untuk membungkusnya di dalam contoh Opsyen seperti yang ditunjukkan di atas.

Perhatikan, bahawa kita tidak perlu melakukan pemeriksaan sebelum memanggil keString namun kita tidak perlu berurusan dengan NullPointerException seperti yang telah kita lakukan sebelumnya. Pilihan ini toString mengembalikan kita nilai-nilai yang bermakna dalam setiap panggilan.

Pada coretan kedua bahagian ini, kami memerlukan pemeriksaan nol , di mana kami akan menetapkan nilai lalai kepada pemboleh ubah, sebelum mencuba menggunakannya. Pilihan boleh menangani perkara ini dalam satu baris, walaupun terdapat nol:

@Test public void givenNull_whenCreatesOption_thenCorrect() { String name = null; Option nameOption = Option.of(name); assertEquals("baeldung", nameOption.getOrElse("baeldung")); }

Atau tidak kosong:

@Test public void givenNonNull_whenCreatesOption_thenCorrect() { String name = "baeldung"; Option nameOption = Option.of(name); assertEquals("baeldung", nameOption.getOrElse("notbaeldung")); }

Perhatikan bagaimana, tanpa pemeriksaan nol , kita dapat memperoleh nilai atau mengembalikan lalai dalam satu baris.

3. Tuple

Tidak ada setara langsung dengan struktur data tuple di Java. Tuple adalah konsep umum dalam bahasa pengaturcaraan berfungsi. Tuples tidak berubah dan dapat menyimpan pelbagai objek dari pelbagai jenis dengan cara yang selamat.

Vavr membawa tuple ke Java 8. Tuples adalah jenis Tuple1, Tuple2 hingga Tuple8 bergantung pada jumlah elemen yang akan mereka ambil.

Kini terdapat had lapan elemen atas. Kami mengakses elemen tuple seperti tuple ._n di mana n serupa dengan tanggapan indeks dalam tatasusunan:

public void whenCreatesTuple_thenCorrect1() { Tuple2 java8 = Tuple.of("Java", 8); String element1 = java8._1; int element2 = java8._2(); assertEquals("Java", element1); assertEquals(8, element2); }

Perhatikan bahawa elemen pertama diambil dengan n == 1 . Jadi tuple tidak menggunakan asas sifar seperti array. Jenis elemen yang akan disimpan dalam tuple mesti dinyatakan dalam deklarasi jenisnya seperti yang ditunjukkan di atas dan di bawah:

@Test public void whenCreatesTuple_thenCorrect2() { Tuple3 java8 = Tuple.of("Java", 8, 1.8); String element1 = java8._1; int element2 = java8._2(); double element3 = java8._3(); assertEquals("Java", element1); assertEquals(8, element2); assertEquals(1.8, element3, 0.1); }

Tempat tuple adalah untuk menyimpan sekumpulan objek tetap dari jenis apa pun yang lebih baik diproses sebagai unit dan dapat dilalui. Kes penggunaan yang lebih jelas adalah mengembalikan lebih dari satu objek dari fungsi atau kaedah di Java.

4. Cuba

Di Vavr, Try adalah wadah untuk pengiraan yang boleh menyebabkan pengecualian.

Oleh kerana Option membungkus objek nullable sehingga kita tidak perlu mengurus null secara eksplisit dengan jika cek, Cuba bungkus pengiraan sehingga kita tidak perlu secara eksplisit mengurus pengecualian dengan blok catch-catch .

Ambil kod berikut sebagai contoh:

@Test(expected = ArithmeticException.class) public void givenBadCode_whenThrowsException_thenCorrect() { int i = 1 / 0; }

Tanpa blok cubaan , aplikasi akan hancur. Untuk mengelakkan ini, anda perlu memasukkan pernyataan tersebut dalam blok cubaan menangkap . Dengan Vavr, kita dapat membungkus kod yang sama dalam contoh Cuba dan mendapatkan hasilnya:

@Test public void givenBadCode_whenTryHandles_thenCorrect() { Try result = Try.of(() -> 1 / 0); assertTrue(result.isFailure()); }

Sama ada pengiraan berjaya atau tidak, maka dapat diperiksa dengan pilihan pada bila-bila masa dalam kod tersebut.

Dalam coretan di atas, kami telah memilih untuk memeriksa kejayaan atau kegagalan. Kami juga boleh memilih untuk mengembalikan nilai lalai:

@Test public void givenBadCode_whenTryHandles_thenCorrect2() { Try computation = Try.of(() -> 1 / 0); int errorSentinel = result.getOrElse(-1); assertEquals(-1, errorSentinel); }

Atau secara eksplisit membuang pengecualian pilihan kami:

@Test(expected = ArithmeticException.class) public void givenBadCode_whenTryHandles_thenCorrect3() { Try result = Try.of(() -> 1 / 0); result.getOrElseThrow(ArithmeticException::new); }

Dalam semua kes di atas, kami mempunyai kawalan terhadap apa yang berlaku selepas pengiraan, terima kasih kepada Vavr's Try .

5. Antara Muka Berfungsi

Dengan kedatangan Java 8, antara muka fungsional dibina dan lebih mudah digunakan, terutamanya apabila digabungkan dengan lambdas.

Namun, Java 8 hanya menyediakan dua fungsi asas. Satu hanya mengambil satu parameter dan menghasilkan hasilnya:

@Test public void givenJava8Function_whenWorks_thenCorrect() { Function square = (num) -> num * num; int result = square.apply(2); assertEquals(4, result); }

Yang kedua hanya mengambil dua parameter dan menghasilkan hasilnya:

@Test public void givenJava8BiFunction_whenWorks_thenCorrect() { BiFunction sum = (num1, num2) -> num1 + num2; int result = sum.apply(5, 7); assertEquals(12, result); }

On the flip side, Vavr extends the idea of functional interfaces in Java further by supporting up to a maximum of eight parameters and spicing up the API with methods for memoization, composition, and currying.

Just like tuples, these functional interfaces are named according to the number of parameters they take: Function0, Function1, Function2 etc. With Vavr, we would have written the above two functions like this:

@Test public void givenVavrFunction_whenWorks_thenCorrect() { Function1 square = (num) -> num * num; int result = square.apply(2); assertEquals(4, result); }

and this:

@Test public void givenVavrBiFunction_whenWorks_thenCorrect() { Function2 sum = (num1, num2) -> num1 + num2; int result = sum.apply(5, 7); assertEquals(12, result); }

When there is no parameter but we still need an output, in Java 8 we would need to use a Consumer type, in Vavr Function0 is there to help:

@Test public void whenCreatesFunction_thenCorrect0() { Function0 getClazzName = () -> this.getClass().getName(); String clazzName = getClazzName.apply(); assertEquals("com.baeldung.vavr.VavrTest", clazzName); }

How about a five parameter function, it's just a matter of using Function5:

@Test public void whenCreatesFunction_thenCorrect5() { Function5 concat = (a, b, c, d, e) -> a + b + c + d + e; String finalString = concat.apply( "Hello ", "world", "! ", "Learn ", "Vavr"); assertEquals("Hello world! Learn Vavr", finalString); }

We can also combine the static factory method FunctionN.of for any of the functions to create a Vavr function from a method reference. Like if we have the following sum method:

public int sum(int a, int b) { return a + b; }

We can create a function out of it like this:

@Test public void whenCreatesFunctionFromMethodRef_thenCorrect() { Function2 sum = Function2.of(this::sum); int summed = sum.apply(5, 6); assertEquals(11, summed); }

6. Collections

The Vavr team has put a lot of effort in designing a new collections API that meets the requirements of functional programming i.e. persistence, immutability.

Java collections are mutable, making them a great source of program failure, especially in the presence of concurrency. The Collection interface provides methods such as this:

interface Collection { void clear(); }

This method removes all elements in a collection(producing a side-effect) and returns nothing. Classes such as ConcurrentHashMap were created to deal with the already created problems.

Such a class does not only add zero marginal benefits but also degrades the performance of the class whose loopholes it is trying to fill.

With immutability, we get thread-safety for free: no need to write new classes to deal with a problem that should not be there in the first place.

Other existing tactics to add immutability to collections in Java still create more problems, namely, exceptions:

@Test(expected = UnsupportedOperationException.class) public void whenImmutableCollectionThrows_thenCorrect() { java.util.List wordList = Arrays.asList("abracadabra"); java.util.List list = Collections.unmodifiableList(wordList); list.add("boom"); }

All the above problems are non-existent in Vavr collections.

To create a list in Vavr:

@Test public void whenCreatesVavrList_thenCorrect() { List intList = List.of(1, 2, 3); assertEquals(3, intList.length()); assertEquals(new Integer(1), intList.get(0)); assertEquals(new Integer(2), intList.get(1)); assertEquals(new Integer(3), intList.get(2)); }

APIs are also available to perform computations on the list in place:

@Test public void whenSumsVavrList_thenCorrect() { int sum = List.of(1, 2, 3).sum().intValue(); assertEquals(6, sum); }

Vavr collections offer most of the common classes found in the Java Collections Framework and actually, all features are implemented.

The takeaway is immutability, removal of void return types and side-effect producing APIs, a richer set of functions to operate on the underlying elements, very short, robust and compact code compared to Java's collection operations.

A full coverage of Vavr collections is beyond the scope of this article.

7. Validation

Vavr brings the concept of Applicative Functor to Java from the functional programming world. In the simplest of terms, an Applicative Functor enables us to perform a sequence of actions while accumulating the results.

The class vavr.control.Validation facilitates the accumulation of errors. Remember that, usually, a program terminates as soon as an error is encountered.

However, Validation continues processing and accumulating the errors for the program to act on them as a batch.

Consider that we are registering users by name and age and we want to take all input first and decide whether to create a Person instance or return a list of errors. Here is our Person class:

public class Person { private String name; private int age; // standard constructors, setters and getters, toString }

Next, we create a class called PersonValidator. Each field will be validated by one method and another method can be used to combine all the results into one Validation instance:

class PersonValidator { String NAME_ERR = "Invalid characters in name: "; String AGE_ERR = "Age must be at least 0"; public Validation
    
      validatePerson( String name, int age) { return Validation.combine( validateName(name), validateAge(age)).ap(Person::new); } private Validation validateName(String name) { String invalidChars = name.replaceAll("[a-zA-Z ]", ""); return invalidChars.isEmpty() ? Validation.valid(name) : Validation.invalid(NAME_ERR + invalidChars); } private Validation validateAge(int age) { return age < 0 ? Validation.invalid(AGE_ERR) : Validation.valid(age); } }
    

The rule for age is that it should be an integer greater than 0 and the rule for name is that it should contain no special characters:

@Test public void whenValidationWorks_thenCorrect() { PersonValidator personValidator = new PersonValidator(); Validation
    
      valid = personValidator.validatePerson("John Doe", 30); Validation
     
       invalid = personValidator.validatePerson("John? Doe!4", -1); assertEquals( "Valid(Person [name=John Doe, age=30])", valid.toString()); assertEquals( "Invalid(List(Invalid characters in name: ?!4, Age must be at least 0))", invalid.toString()); }
     
    

A valid value is contained in a Validation.Valid instance, a list of validation errors is contained in a Validation.Invalid instance. So any validation method must return one of the two.

Inside Validation.Valid is an instance of Person while inside Validation.Invalid is a list of errors.

8. Lazy

Lazy is a container which represents a value computed lazily i.e. computation is deferred until the result is required. Furthermore, the evaluated value is cached or memoized and returned again and again each time it is needed without repeating the computation:

@Test public void givenFunction_whenEvaluatesWithLazy_thenCorrect() { Lazy lazy = Lazy.of(Math::random); assertFalse(lazy.isEvaluated()); double val1 = lazy.get(); assertTrue(lazy.isEvaluated()); double val2 = lazy.get(); assertEquals(val1, val2, 0.1); }

In the above example, the function we are evaluating is Math.random. Notice that, in the second line, we check the value and realize that the function has not yet been executed. This is because we still haven't shown interest in the return value.

In the third line of code, we show interest in the computation value by calling Lazy.get. At this point, the function executes and Lazy.evaluated returns true.

We also go ahead and confirm the memoization bit of Lazy by attempting to get the value again. If the function we provided was executed again, we would definitely receive a different random number.

However, Lazy again lazily returns the initially computed value as the final assertion confirms.

9. Pattern Matching

Pattern matching is a native concept in almost all functional programming languages. There is no such thing in Java for now.

Instead, whenever we want to perform a computation or return a value based on the input we receive, we use multiple if statements to resolve the right code to execute:

@Test public void whenIfWorksAsMatcher_thenCorrect() { int input = 3; String output; if (input == 0) { output = "zero"; } if (input == 1) { output = "one"; } if (input == 2) { output = "two"; } if (input == 3) { output = "three"; } else { output = "unknown"; } assertEquals("three", output); }

We can suddenly see the code spanning multiple lines while just checking three cases. Each check is taking up three lines of code. What if we had to check up to a hundred cases, those would be about 300 lines, not nice!

Another alternative is using a switch statement:

@Test public void whenSwitchWorksAsMatcher_thenCorrect() { int input = 2; String output; switch (input) { case 0: output = "zero"; break; case 1: output = "one"; break; case 2: output = "two"; break; case 3: output = "three"; break; default: output = "unknown"; break; } assertEquals("two", output); }

Not any better. We are still averaging 3 lines per check. A lot of confusion and potential for bugs. Forgetting a break clause is not an issue at compile time but can result in hard-to-detect bugs later on.

In Vavr, we replace the entire switch block with a Match method. Each case or if statement is replaced by a Case method invocation.

Finally, atomic patterns like $() replace the condition which then evaluates an expression or value. We also provide this as the second parameter to Case:

@Test public void whenMatchworks_thenCorrect() { int input = 2; String output = Match(input).of( Case($(1), "one"), Case($(2), "two"), Case($(3), "three"), Case($(), "?")); assertEquals("two", output); }

Notice how compact the code is, averaging only one line per check. The pattern matching API is way more powerful than this and can do more complex stuff.

For example, we can replace the atomic expressions with a predicate. Imagine we are parsing a console command for help and version flags:

Match(arg).of( Case($(isIn("-h", "--help")), o -> run(this::displayHelp)), Case($(isIn("-v", "--version")), o -> run(this::displayVersion)), Case($(), o -> run(() -> { throw new IllegalArgumentException(arg); })) );

Some users may be more familiar with the shorthand version (-v) while others, with the full version (–version). A good designer must consider all these cases.

Without the need for several if statements, we have taken care of multiple conditions. We will learn more about predicates, multiple conditions, and side-effects in pattern matching in a separate article.

10. Conclusion

In this article, we have introduced Vavr, the popular functional programming library for Java 8. We have tackled the major features that we can quickly adapt to improve our code.

The full source code for this article is available in the Github project.