1. Overview
In this article, we are going to focus on Pattern Matching with Javaslang. If you do not know what about Javaslang, please read the Javaslang’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 Javaslang’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 from Javaslang 2.0 onwards:
import static javaslang.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 Javaslang 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 Javaslang, you can refer to the introductory article.
4. Match With Inbuilt Predicates
Javaslang 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); }
Javaslang 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, apart from rejecting numbers which are not in the list, we want to reject nulls as well:
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 also XOR predicates using the noneOf predicates so that an input gets a match when all conditions evaluate to false.
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 Javaslang. But Javaslang 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 Javaslang’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_whenCorrect() { 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 Javaslang’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 javaslang-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_whenDecomposesJavaslangWay_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 Javaslang 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 wild card 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.javaslang.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_whenDecomposesJavaslangWay_whenCorrect2() { 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 Javaslang. Indeed we can now write simpler and more concise code without the verbose switch and if statements, thanks to Javaslang.
To get the full source code for this article, you can check out the the Github project.