Guide to Pattern Matching in Vavr – Vavr中的模式匹配指南

最后修改: 2017年 1月 15日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

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.

在这篇文章中,我们将重点介绍Vavr的模式匹配。如果你不了解Vavr,请先阅读Vavr的概述

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.

模式匹配是Java中没有的功能。我们可以把它看作是高级形式的switch-case语句。

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.

Vavr的模式匹配的优势在于,它使我们不用写一堆switch case或if-then-else语句。因此,它减少了代码量,并以一种人类可读的方式表示条件逻辑。

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

我们可以通过下面的导入来使用模式匹配API。

import static io.vavr.API.*;

2. How Pattern Matching Works

2.模式匹配是如何工作的

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

正如我们在上一篇文章中看到的,模式匹配可以用来替换一个switch块。

@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:

或多个if语句。

@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.

到目前为止,我们所看到的片段是冗长的,因此容易出错。当使用模式匹配时,我们使用三个主要构件:两个静态方法MatchCase和原子模式。

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:

switchif方法可以被下面这段更短更简洁的代码所取代。

@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.

在这一节中,我们已经介绍了Vavr模式匹配的基础知识,下面几节将介绍处理我们的代码中可能遇到的不同情况的各种方法。

3. Match With Option

3.与选项匹配

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:

然而,包括通配符模式的另一种选择是将匹配操作的返回值包装在一个Option实例中。

@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.

为了更好地了解Vavr中的Option,你可以参考介绍性文章。

4. Match With Inbuilt Predicates

4.与内置谓语的匹配

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

Vavr带有一些内置的谓词,使我们的代码更容易被人阅读。因此,我们最初的例子可以通过谓词进一步改进。

@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:

Vavr提供了比这更多的谓词。例如,我们可以让我们的条件改成检查输入的类别。

@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:

或者说输入是否为null

@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:

我们可以使用contains风格来代替equals风格来匹配值。这样,我们可以用isIn谓词检查一个输入是否存在于一个值的列表中。

@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.

我们还可以用谓词做更多的事情,比如将多个谓词合并为一个单一的匹配案例。要想只在输入通过一组给定的谓词时进行匹配,我们可以使用allOf谓词来and谓词。

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.

为了在输入与给定组的任何一个相匹配时进行匹配,我们可以使用anyOf谓词来OR谓词。

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

假设我们根据候选人的出生年份进行筛选,我们只想要在1990、1991或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:

如果没有找到这样的候选人,那么我们只能接受1986年出生的人,我们希望在代码中也明确这一点。

@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.

最后,我们可以使用noneOf方法确保没有提供的谓词匹配。

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

5.与自定义谓词匹配

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.

在上一节中,我们探讨了Vavr的内置谓词。但Vavr并没有止步于此。有了lambdas的知识,我们可以建立和使用我们自己的谓词,甚至可以直接把它们写入内联。

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.

在上面的例子中,我们创建了一个Java 8 BiFunction,它只是检查两个参数之间的isIn关系。

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.

你也可以用Vavr的FunctionN来做这个。因此,如果内置的谓词不太符合你的要求,或者你想控制整个评估过程,那么就使用自定义谓词。

6. Object Decomposition

6.对象分解

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:

对象分解是将一个Java对象分解成其组成部分的过程。例如,考虑将雇员的生物数据与就业信息一起抽象出来的情况。

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:

我们可以将一个雇员的记录分解成其组成部分。nameid。这在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.

我们创建一个雇员对象,然后首先检查它是否为空,然后再应用一个过滤器,以确保我们最终得到一个名字为Carl的雇员记录。然后我们继续检索他的id。Java的方法是可行的,但是它很啰嗦,而且容易出错。

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.

在上面的例子中,我们所做的基本上是将我们知道的东西与进来的东西进行匹配。我们知道我们想要一个叫Carl的雇员,所以我们尝试将这个名字与传入的对象相匹配。

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.

有了Vavr的模式匹配API,我们可以忘记不必要的检查,只需关注重要的内容,从而得到非常紧凑和可读的代码。

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

为了使用这一规定,我们必须在你的项目中安装一个额外的vavr-match依赖项。你可以按照这个链接来获得它。

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.

上述例子中的关键结构是原子模式$(“Carl”)$(),分别是值模式和通配符模式。我们在Vavr介绍性文章中详细讨论了这些内容。

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.

这两种模式都从匹配的对象中获取数值,并将它们存储到lambda参数中。值模式$(“Carl”)只能在检索到的值与它里面的内容(即carl)相匹配时才能匹配。

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

另一方面,通配符模式$()匹配其位置上的任何值,并将该值检索到id lambda参数。

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

为了使这种分解发挥作用,我们需要定义分解模式或正式称为unapply模式。

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:

这意味着我们必须教模式匹配API如何分解我们的对象,从而使每个要分解的对象有一个条目。

@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:

注释处理工具将生成一个名为DemoPatterns.java的类,我们必须将其静态地导入到我们想要应用这些模式的地方。

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

We can also decompose inbuilt Java objects.

我们也可以对内置的Java对象进行分解。

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:

例如,java.time.LocalDate可以被分解为年、月、日。让我们把它的unapply模式添加到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

7.模式匹配中的副作用

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.

默认情况下,Match的行为像一个表达式,意味着它返回一个结果。然而,我们可以通过在lambda中使用辅助函数run来强制它产生一个副作用。

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

它接受一个方法引用或一个lambda表达式并返回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

8.结论

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.

在这篇文章中,我们已经探索了Vavr中模式匹配API的最重要部分。事实上,我们现在可以编写更简单、更简洁的代码,而不需要冗长的switch和if语句,这要感谢Vavr。

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

要获得本文的完整源代码,你可以查看Github项目