1. Overview

In this article, we’re going to focus on Pattern Matching with Vavr. If you do not know what about Vavr, please read the Vavr‘s Overview first.

Pattern matching is a feature that is not natively available in Java. One could think of it as the advanced form of a switch-case statement.

The advantage of Vavr’s pattern matching is that it saves us from writing stacks of switch cases or if-then-else statements. It, therefore, reduces the amount of code and represents conditional logic in a human-readable way.

We can use the pattern matching API by making the following import:

import static io.vavr.API.*;

2. How Pattern Matching Works

As we saw in the previous article, pattern matching can be used to replace a switch block:

@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);
}

Or multiple if statements:

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

    assertEquals("three", output);
}

The snippets we have seen so far are verbose and therefore error prone. When using pattern matching, we use three main building blocks: the two static methods Match, Case and atomic patterns.

Atomic patterns represent the condition that should be evaluated to return a boolean value:

  • $(): a wild-card pattern that is similar to the default case in a switch statement. It handles a scenario where no match is found
  • $(value): this is the equals pattern where a value is simply equals-compared to the input.
  • $(predicate): this is the conditional pattern where a predicate function is applied to the input and the resulting boolean is used to make a decision.

The switch and if approaches could be replaced by a shorter and more concise piece of code as below:

@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);
}

If the input does not get a match, the wild-card pattern gets evaluated:

@Test
public void whenMatchesDefault_thenCorrect() {
    int input = 5;
    String output = Match(input).of(
      Case($(1), "one"), 
      Case($(), "unknown"));

    assertEquals("unknown", output);
}

If there is no wild-card pattern and the input does not get matched, we will get a match error:

@Test(expected = MatchError.class)
public void givenNoMatchAndNoDefault_whenThrows_thenCorrect() {
    int input = 5;
    Match(input).of(
      Case($(1), "one"), 
      Case($(2), "two"));
}

In this section, we have covered the basics of Vavr pattern matching and the following sections will cover various approaches to tackling different cases we are likely to encounter in our code.

3. Match With Option

As we saw in the previous section, the wild-card pattern $() matches default cases where no match is found for the input.

However, another alternative to including a wild-card pattern is wrapping the return value of a match operation in an Option instance:

@Test
public void whenMatchWorksWithOption_thenCorrect() {
    int i = 10;
    Option<String> s = Match(i)
      .option(Case($(0), "zero"));

    assertTrue(s.isEmpty());
    assertEquals("None", s.toString());
}

To get a better understanding of Option in Vavr, you can refer to the introductory article.

4. Match With Inbuilt Predicates

Vavr ships with some inbuilt predicates that make our code more human-readable. Therefore, our initial examples can be improved further with predicates:

@Test
public void whenMatchWorksWithPredicate_thenCorrect() {
    int i = 3;
    String s = Match(i).of(
      Case($(is(1)), "one"), 
      Case($(is(2)), "two"), 
      Case($(is(3)), "three"),
      Case($(), "?"));

    assertEquals("three", s);
}

Vavr offers more predicates than this. For example, we can make our condition check the class of the input instead:

@Test
public void givenInput_whenMatchesClass_thenCorrect() {
    Object obj=5;
    String s = Match(obj).of(
      Case($(instanceOf(String.class)), "string matched"), 
      Case($(), "not string"));

    assertEquals("not string", s);
}

Or whether the input is null or not:

@Test
public void givenInput_whenMatchesNull_thenCorrect() {
    Object obj=5;
    String s = Match(obj).of(
      Case($(isNull()), "no value"), 
      Case($(isNotNull()), "value found"));

    assertEquals("value found", s);
}

Instead of matching values in equals style, we can use contains style. This way, we can check if an input exists in a list of values with the isIn predicate:

@Test
public void givenInput_whenContainsWorks_thenCorrect() {
    int i = 5;
    String s = Match(i).of(
      Case($(isIn(2, 4, 6, 8)), "Even Single Digit"), 
      Case($(isIn(1, 3, 5, 7, 9)), "Odd Single Digit"), 
      Case($(), "Out of range"));

    assertEquals("Odd Single Digit", s);
}

There is more we can do with predicates, like combining multiple predicates as a single match case.To match only when the input passes all of a given group of predicates, we can AND predicates using the allOf predicate.

A practical case would be where we want to check if a number is contained in a list as we did with the previous example. The problem is that the list contains nulls as well. So, we want to apply a filter that, apart from rejecting numbers which are not in the list, will also reject nulls:

@Test
public void givenInput_whenMatchAllWorks_thenCorrect() {
    Integer i = null;
    String s = Match(i).of(
      Case($(allOf(isNotNull(),isIn(1,2,3,null))), "Number found"), 
      Case($(), "Not found"));

    assertEquals("Not found", s);
}

To match when an input matches any of a given group, we can OR the predicates using the anyOf predicate.

Assume we are screening candidates by their year of birth and we want only candidates who were born in 1990,1991 or 1992.

If no such candidate is found, then we can only accept those born in 1986 and we want to make this clear in our code too:

@Test
public void givenInput_whenMatchesAnyOfWorks_thenCorrect() {
    Integer year = 1990;
    String s = Match(year).of(
      Case($(anyOf(isIn(1990, 1991, 1992), is(1986))), "Age match"), 
      Case($(), "No age match"));
    assertEquals("Age match", s);
}

Finally, we can make sure that no provided predicates match using the noneOf method.

To demonstrate this, we can negate the condition in the previous example such that we get candidates who are not in the above age groups:

@Test
public void givenInput_whenMatchesNoneOfWorks_thenCorrect() {
    Integer year = 1990;
    String s = Match(year).of(
      Case($(noneOf(isIn(1990, 1991, 1992), is(1986))), "Age match"), 
      Case($(), "No age match"));

    assertEquals("No age match", s);
}

5. Match With Custom Predicates

In the previous section, we explored the inbuilt predicates of Vavr. But Vavr does not stop there. With the knowledge of lambdas, we can build and use our own predicates or even just write them inline.

With this new knowledge, we can inline a predicate in the first example of the previous section and rewrite it like this:

@Test
public void whenMatchWorksWithCustomPredicate_thenCorrect() {
    int i = 3;
    String s = Match(i).of(
      Case($(n -> n == 1), "one"), 
      Case($(n -> n == 2), "two"), 
      Case($(n -> n == 3), "three"), 
      Case($(), "?"));
    assertEquals("three", s);
}

We can also apply a functional interface in the place of a predicate in case we need more parameters. The contains example can be rewritten like this, albeit a little more verbose, but it gives us more power over what our predicate does:

@Test
public void givenInput_whenContainsWorks_thenCorrect2() {
    int i = 5;
    BiFunction<Integer, List<Integer>, Boolean> contains 
      = (t, u) -> u.contains(t);

    String s = Match(i).of(
      Case($(o -> contains
        .apply(i, Arrays.asList(2, 4, 6, 8))), "Even Single Digit"), 
      Case($(o -> contains
        .apply(i, Arrays.asList(1, 3, 5, 7, 9))), "Odd Single Digit"), 
      Case($(), "Out of range"));

    assertEquals("Odd Single Digit", s);
}

In the above example, we created a Java 8 BiFunction which simply checks the isIn relationship between the two arguments.

You could have used Vavr’s FunctionN for this as well. Therefore, if the inbuilt predicates do not quite match your requirements or you want to have control over the whole evaluation, then use custom predicates.

6. Object Decomposition

Object decomposition is the process of breaking a Java object into its component parts. For example, consider the case of abstracting an employee’s bio-data alongside employment information:

public class Employee {

    private String name;
    private String id;

    //standard constructor, getters and setters
}

We can decompose an Employee’s record into its component parts: name and id. This is quite obvious in Java:

@Test
public void givenObject_whenDecomposesJavaWay_thenCorrect() {
    Employee person = new Employee("Carl", "EMP01");

    String result = "not found";
    if (person != null && "Carl".equals(person.getName())) {
        String id = person.getId();
        result="Carl has employee id "+id;
    }

    assertEquals("Carl has employee id EMP01", result);
}

We create an employee object, then we first check if it is null before applying a filter to ensure we end up with the record of an employee whose name is Carl. We then go ahead and retrieve his id. The Java way works but it is verbose and error-prone.

What we are basically doing in the above example is matching what we know with what is coming in. We know we want an employee called Carl, so we try to match this name to the incoming object.

We then break down his details to get a human-readable output. The null checks are simply defensive overheads we don’t need.

With Vavr’s Pattern Matching API, we can forget about unnecessary checks and simply focus on what is important, resulting in very compact and readable code.

To use this provision, we must have an additional vavr-match dependency installed in your project. You can get it by following this link.

The above code can then be written as below:

@Test
public void givenObject_whenDecomposesVavrWay_thenCorrect() {
    Employee person = new Employee("Carl", "EMP01");

    String result = Match(person).of(
      Case(Employee($("Carl"), $()),
        (name, id) -> "Carl has employee id "+id),
      Case($(),
        () -> "not found"));
         
    assertEquals("Carl has employee id EMP01", result);
}

The key constructs in the above example are the atomic patterns $(“Carl”) and $(), the value pattern the wild card pattern respectively. We discussed these in detail in the Vavr introductory article.

Both patterns retrieve values from the matched object and store them into the lambda parameters. The value pattern $(“Carl”) can only match when the retrieved value matches what is inside it i.e. carl.

On the other hand, the wildcard pattern $() matches any value at its position and retrieves the value into the id lambda parameter.

For this decomposition to work, we need to define decomposition patterns or what is formally known as unapply patterns.

This means that we must teach the pattern matching API how to decompose our objects, resulting in one entry for each object to be decomposed:

@Patterns
class Demo {
    @Unapply
    static Tuple2<String, String> Employee(Employee Employee) {
        return Tuple.of(Employee.getName(), Employee.getId());
    }

    // other unapply patterns
}

The annotation processing tool will generate a class called DemoPatterns.java which we have to statically import to wherever we want to apply these patterns:

import static com.baeldung.vavr.DemoPatterns.*;

We can also decompose inbuilt Java objects.

For instance, java.time.LocalDate can be decomposed into a year, month and day of the month. Let us add its unapply pattern to Demo.java:

@Unapply
static Tuple3<Integer, Integer, Integer> LocalDate(LocalDate date) {
    return Tuple.of(
      date.getYear(), date.getMonthValue(), date.getDayOfMonth());
}

Then the test:

@Test
public void givenObject_whenDecomposesVavrWay_thenCorrect2() {
    LocalDate date = LocalDate.of(2017, 2, 13);

    String result = Match(date).of(
      Case(LocalDate($(2016), $(3), $(13)), 
        () -> "2016-02-13"),
      Case(LocalDate($(2016), $(), $()),
        (y, m, d) -> "month " + m + " in 2016"),
      Case(LocalDate($(), $(), $()),  
        (y, m, d) -> "month " + m + " in " + y),
      Case($(), 
        () -> "(catch all)")
    );

    assertEquals("month 2 in 2017",result);
}

7. Side Effects in Pattern Matching

By default, Match acts like an expression, meaning it returns a result. However, we can force it to produce a side-effect by using the helper function run within a lambda.

It takes a method reference or a lambda expression and returns Void.

Consider a scenario where we want to print something when an input is a single digit even integer and another thing when the input is a single digit odd number and throw an exception when the input is none of these.

The even number printer:

public void displayEven() {
    System.out.println("Input is even");
}

The odd number printer:

public void displayOdd() {
    System.out.println("Input is odd");
}

And the match function:

@Test
public void whenMatchCreatesSideEffects_thenCorrect() {
    int i = 4;
    Match(i).of(
      Case($(isIn(2, 4, 6, 8)), o -> run(this::displayEven)), 
      Case($(isIn(1, 3, 5, 7, 9)), o -> run(this::displayOdd)), 
      Case($(), o -> run(() -> {
          throw new IllegalArgumentException(String.valueOf(i));
      })));
}

Which would print:

Input is even

8. Conclusion

In this article, we have explored the most important parts of the Pattern Matching API in Vavr. Indeed we can now write simpler and more concise code without the verbose switch and if statements, thanks to Vavr.

To get the full source code for this article, you can check out the Github project.

Course – LS (cat=Java)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.