Guide to Pattern Matching in Vavr

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";
    }
    if (input == 1) {
        output = "one";
    }
    if (input == 2) {
        output = "two";
    }
    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
.

Leave a Reply

Your email address will not be published.