Pattern matching in Java

Hello, everyone! I would like to announce that I will continue a series of Vavr guides in my blog. Today we explore pattern matching. This is what functional languages are famous for. Java and other C-like languages can “imitate” it to some degree (using multiple if or switch statements) but only to some degree. By default, Java can not conquer complex pattern matching conditions, but with Vavr it is much easier to implement.

As usual, we will start with a brief dive to theory and then explore Vavr pattern matching API in-depth. By the way, this topic is close connected with other Vavr features, like Option, Streams and Try.

Table of Contents

What is pattern matching?

Let dive a bit into theory and see what does computer science understand as pattern matching. From a technical point of view, this is a mechanism for checking a value against a pattern. We should differentiate it from pattern recognition: unlike latest, pattern matching assert exact matches.

As it was mentioned earlier, C-like languages can emulate pattern matching feature up to some degree. For instance, we can use switch statements to execute some logic based on match (for example, name):

String name = "Carolina";

switch(name){
	case "Aneta":
		System.out.println("Hello, Aneta!");
		break;
	case "Barbora":
		System.out.println("Hello, Barbora!");
		break;
	case "Carolina":
		System.out.println("Hello, Carolina!");
		break;
	case "Denisa":
		System.out.println("Hello, Denisa!");
		break;
	default:
		System.out.println("Hello, stranger!");
		break;
}

Another behaviour, is to set a value, based on input. Let take a look on the another code snippet:

enum DIAS{
	LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO
}

String weekday;
Dias dia = Dias.MIERCOLES;

switch(dia){
	case LUNES:
		weekday = "Monday";
		break;
	case MARTES:
		weekday = "Tuesday";
		break;
	case MIERCOLES:
		weekday = "Wednesday";
		break;
	case JUEVES:
		weekday = "Thursday";
		break;
	case VIERNES:
		weekday = "Friday";
		break;
	case SABADO:
		weekday = "Saturday";
		break;
	case DOMINGO:
		weekday = "Sunday";
		break;
}

System.out.println(weekday); // output: Wednesday

You can achieve same results with multiple if statements. However, both examples are very simple. But what if we need to write more complex conditions? Imagine, we have an app, that validates, if user first name of user is male or female. While it may seem discrimination for somebody, in Slavic languages we need to figure out gender of users in order to provide grammatically correct messages. This time let use if statements:

String name = "Barbora";
String gender;

if (name.equalsIgnoreCase("Adam") || name.equalsIgnoreCase("Boris")){
	gender = "male";
} else if (name.equalsIgnoreCase("Anna") || name.equalsIgnoreCase("Barbora")){
	gender = "female";
}

System.out.println(gender); // output: female

Well, our app is pretty illiterate: it knows only 4 names! Imagine, if we will add more names to this condition (just Czech ones, and there are other Slavic countires on the planet!)… Basically, what do we need to do is to have 2 lists of names: male ones and female ones and then assert, if input is in the specific list. Well, we can do it like this:

List<String> femaleNames = ...;
List<String> maleNames = ...;

String name = "Lukas";
String gender;

for (String n:femaleNames){
	if (name.equalsIgnoreCase(n)){
		gender = "female";
	}
}

for (String n:maleNames){
	if (name.equalsIgnoreCase(n)){
		gender = "male";
	}
}

System.out.println(gender); //output = "male"

As you can see, while we can immitate some pattern matching using “native” Java tools, they are very limited. Hopefully, you don’t need to learn Haskell (although, it is a good idea) if you want to have pattern matching in your app. You can use Vavr library. Now, let see how to do it.

An anatomy of Vavr pattern matching API

First, we will understand the structure of pattern matching API in Vavr library. It consists of three main elements: Match, Case and patterns. Let explore them step by step.

Match

Vavr provides a Match API that is close to Scala’s match. It is represented by the class io.vavr.API.Match<T>. It is a starting point of pattern matching, that accepts a value, that we will need to match. Let take a look on the code snippet below:

String dia = "Miercoles";
String weekday = Match(dia).of(
	Case($("Lunes"), "Monday"),
	Case($("Martes"), "Tuesday"),
	Case($("Miercoles"), "Wednesday"),
	Case($("Jueves"), "Thursday"),
	Case($("Viernes"), "Friday"),
	Case($("Sabado"), "Saturday"),
	Case($("Domingo"), "Sunday"));

Something like this! From a techincal point of view, Match class is an easy structure:

  1. Accepts input value as argument
  2. Produces new value as a result of condition matching as value or option.

We already explored Vavr Option previously. In the example with days we produce exact value as a result of pattern matching. But we can return Option too. Check this code:

String number = "Four";

Option<Integer> digit = Match(number).of(
	Case($("One"), 1),
	Case($("Two"), 2),
	Case($("Three"), 3));

if (digit.isDefined()){
	System.out.println("Result  is: "+digit.get());
} else {
	System.out.println("No result!");
}

So, we don’t have to always return exact value, as we can wrap the result of pattern matching as option. Let move to cases now.

Case

Cases are defined in API.Match.Case. As you could note from previous example, this class allows to match conditional patterns. Generally, it has following structure:

Case($(predicate), ...)

With Case we can write much more concise code. There are two parts:

  • First, dollar-signed (that reminds me JQuery syntax) part – is condition to match
  • Second part – produced value

As we will note later on, conditions can be simple and complex. Take a look on this example:

int integer = 3;

String string = Match(integer).of(
    Case($(1), "one"),
    Case($(2), "two"),
    Case($(), "?")
);

System.out.println(string);

This is a very easy assertion. We can different built in patterns to build conditions, as well combine multiple conditions at one predicate. Let modify our previous name with genders and names using isIn predicate from Vavr:

String name = "Carolina";

String gender = Match(name).of(
	Case($(isIn("Anna", "Beata", "Carolina", "Denisa")), "female"),
	Case($(isIn("Adam", "Boris", "Cyril", "David")), "male"));

System.out.println(gender);

Patterns

Let take a look on provided conditons in previous examples. You can note that they use several “models”. There are three types of patterns in Vavr:

  • $(exact value) = the most common pattern. We check, that the value matches the exact value provided.
  • $() = a wildcard. We use this pattern like default in switches. In the previous example, we used $() to handle cases, when input does not match any provided pattern.
  • $(predicate) = in this pattern we can apply a predicate function to the input and the resulting boolean value is used to make a decision.

We already saw usages of first two types: $(exact value) and $() wildcard. We also can use predicates – Vavr has various built-in predicates to make our developer’s life easier. Let explore them.

Built-in predicates

In the previous part, in the example with names and genders we used built-in isIn predicate. There are many handy predicates that are already provided by Vavr to us. You can find them in io.vavr.Predicates.*. Let see them.

is

One of the most common predicate is is predicate. We already see examples of using Vavr to pattern matching. is predicate tests, if an object is equal to the specified value using Objects.equals(Object, Object) to compare. Let check an example:

int integer = 2;

String string = Match(integer).of(
    Case($(is(1)), "one"),
    Case($(is(2)), "two"),
    Case($(), "nothing")
);

In the code snippet above we used is to match exact value of provided input argument. While with primitives and strings you can drop predicates and use just $(exact value) pattern, with objects you need to use is, that will assert equality of objectts, based on equals() method implementation.

isIn

We talked before, that isIn is a handy method to assert that value belongs to specific range of objects, likewise we did with names and genders. Here is an another usage of isIn predicate – check even/odd number in Vavr’s style:

int number = 6;
String result = Match(number).of(
      Case($(isIn(2, 4, 6, 8)), "Even"), 
      Case($(isIn(1, 3, 5, 7, 9)), "Odd"));

System.out.println(result); //Even

You could understand isIn predicate as contains() method of Collections. When you use this predicate with custom objects, please note that, as with is, it also uses equals logic to compare input and pattern.

isNull/isNotNull

Input can be null, so we can use Vavr for null checking too. There are two built-in predicates to do this:

  • isNull checks that provided input is null
  • isNotNull checks that provided input is not null

Let have a quick example:

Object object = null;

String result = Match(object).of(
	Case($(isNull()), "Input is null"),
	Case($(isNotNull()), "Input is not null"));

instanceOf

Java has instanceOf operator. It is used to test whether the object is an instance of the specified type (class, subclass or interface). In a general form it looks like:

[Object] instanceof [Type]

Vavr also offers built-in instanceOf predicate. It tests, if an object is instance of the specified type. Suppose we have a classes Elephant and Panda that both are child classes of Animal. Take a look on the code snippet below:

Elephant john = new Elephant("John");
Panda xiao = new Panda("Xiao");

String who = Match(xiao).of(
	Case($(instanceOf(Elephant.class)), "This is an elephant, no doubts!"),
	Case($(instanceOf(Animal.class)), "It can be an elephant or not. But I am sure that is an animal!"));

System.out.pritnln(who);

You can find Javadoc for Predicate API here. Up to now, we used a single predicate for pattern matching. However, we can combine multiple conditions. Let explore how to do it.

Combine multiple predicates

There are three main ways to combine predicates in a one condition – these methods are also part of Predicate package:

  • anyOf = combinator that checks if at least one of the given predicates is satisfied.
  • allOf = combinator that checks if all of the given predicates are satisfied.
  • noneOf = combinator that checks if none of the given predicates is satisfied.

Let take an example to look how to combine multiple Vavr predicates:

String word = "Tres";

String language = Match(word).of(
	Case($(anyOf(isIn("Lunes", "Martes","Miercoles"), isIn("Uno", "Dos", "Tres"))), "Spanish"),
	Case($(anyOf(isIn("Monday", "Tuesday", "Wednesday"), isIn("One", "Two", "Third"))), "English"));

System.out.println("Language is :"+language);

In the code snippet below we combine two conditions: word is either is in list of weekdays or in list of numbers. In case of match, Vavr tells us in which language exists the provided word. In same way we can build conditions to satisfy that all or none predicates are matched. Here is an another sample:

Elephant elephant = new Elephant("John");
Elephant david = new Elephant("David");

Option<String> result = Match(elephant).of(
	Case($(allOf(instanceOf(Elephant.class), is(david))), "This elephant is David"));

if (result.isDefined()){
	System.out.println(result.get());
}

We match that all conditions are satisfied. If you need to assert more specific conditions, consider to write your own predicates.

Create custom predicates

In case built-in predicates do not offer required functionality, you can implement your own predicates using normal Java lambdas. Let take a look on a simple example:

int number = 4;

String result = Match(number).of(
	Case($(n->n%2==0), "even"),
	Case($(n->n%2!=0), "odd"));

System.out.println(result);

In the code snippet above we use lambda predicates to assert the input to be even or odd. Another example can be FizzBuzz task. In case you don’t know this classic programming problem: “Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”. (reference).

In this example we can utilize several from aforesaid tools. Take a look on my implementation:

int number = 45;

String message = Match(number).of(
		Case($(allOf((n->n%3==0), n(n->n%5==0))), "FizzBuzz"),
		Case($(n->n%5==0), "Buzz"),
		Case($(n->n%3==0), "Fizz"),
		Case($(), "Nothing"));

System.out.println(message);

This code uses allOf predicate to assert both conditions to print FizzBuzz. Then there are two Java lambdas to validate single patterns and finally it has wildcard $() pattern. Gregor Trefs has another FizzBuzz implementation with Vavr pattern matching, that I invite you to check.

Conclusion

During this post we explored how to write pattern matching in Java. While the language offers us several ways to immitate this functional programming concept, it is better to use Vavr library. We started from the structure of Match API and its elements. Next we observed built-in predicates offered out of the box, like is, isIn, isNull etc. We also viewed how to use Java lambdas to build custom predicates and combine several conditions using allOf,anyOf and noneOf. We will continue talk about Vavr and functional Java in future. Meanwhile, don’t hesitate to drop questions in comments below or send me message via social channels.

References

  • Emre Savcı Java Functional Programming & Pattern Matching With VAVR (2019) Medium, read here
  • Gregor Trefs Six Ways to Functional FizzBuzz with Vavr (2017) SitePoint, read here
  • Nikita Pavlenko How to use Javaslang Pattern Matching? (2017), read here

Leave a Reply

Your email address will not be published. Required fields are marked *