«1. Обзор

В этой статье мы сосредоточимся на сопоставлении шаблонов с помощью Vavr. Если вы не знаете, что такое Vavr, сначала прочитайте обзор Vavr.

Сопоставление с образцом — это функция, изначально недоступная в Java. Можно думать об этом как о расширенной форме оператора switch-case.

Преимущество сопоставления с образцом в Vavr заключается в том, что оно избавляет нас от написания стопок операторов switch или операторов if-then-else. Таким образом, он уменьшает объем кода и представляет условную логику в удобочитаемом виде.

Мы можем использовать API сопоставления с образцом, выполнив следующий импорт:

import static io.vavr.API.*;

2. Как работает сопоставление с образцом

Как мы видели в предыдущей статье, сопоставление с образцом можно использовать для замены блока переключателя:

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

Или несколько операторов if:

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

Фрагменты, которые мы видели до сих пор, многословны и поэтому подвержены ошибкам. При использовании сопоставления с образцом мы используем три основных строительных блока: два статических метода Match, Case и атомарные образцы.

Атомарные шаблоны представляют собой условие, которое должно быть оценено для возврата логического значения:

    $(): шаблон подстановочного знака, аналогичный случаю по умолчанию в операторе switch. Он обрабатывает сценарий, в котором совпадение не найдено $(значение): это шаблон равенства, где значение просто сравнивается с входными данными. $(предикат): это условный шаблон, в котором функция предиката применяется к входным данным, а полученное логическое значение используется для принятия решения.

Переключатель и подходы if можно заменить более коротким и лаконичным фрагментом кода, как показано ниже:

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

Если входные данные не совпадают, оценивается шаблон подстановочных знаков:

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

    assertEquals("unknown", output);
}

~ ~~ Если нет подстановочного шаблона и входные данные не совпадают, мы получим ошибку сопоставления:

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

В этом разделе мы рассмотрели основы сопоставления шаблонов Vavr, а следующие разделы будут посвящены различные подходы к решению различных случаев, с которыми мы, вероятно, столкнемся в нашем коде.

3. Сопоставление с опцией

Как мы видели в предыдущем разделе, подстановочный шаблон $() соответствует случаям по умолчанию, когда совпадение для ввода не найдено.

Однако другой альтернативой включению шаблона подстановочных знаков является перенос возвращаемого значения операции сопоставления в экземпляр 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());
}

Чтобы лучше понять Option в Vavr, вы можете обратиться к вводному статья.

4. Сопоставление со встроенными предикатами

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 предлагает больше предикатов, чем этот. Например, вместо этого мы можем сделать так, чтобы наше условие проверяло класс ввода:

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

Или независимо от того, является ли ввод нулевым или нет:

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

Вместо сопоставления значений в стиле equals мы можем использовать стиль contains. . Таким образом, мы можем проверить наличие входных данных в списке значений с помощью предиката isIn: ввод передает все заданные группы предикатов, мы можем использовать И предикаты, используя предикат allOf.

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

Практический случай, когда мы хотим проверить, содержится ли число в списке, как мы сделали в предыдущем примере. Проблема в том, что список также содержит нули. Итак, мы хотим применить фильтр, который, помимо отклонения чисел, которых нет в списке, также будет отклонять нули: предикат anyOf.

Предположим, мы отбираем кандидатов по году рождения, и нам нужны только кандидаты, родившиеся в 1990, 1991 или 1992 году.

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

Если такой кандидат не найден, мы можем принять только тех, кто родился в 1986 году, и мы хотим чтобы сделать это понятным и в нашем коде:

Наконец, мы можем убедиться, что никакие предоставленные предикаты не совпадают, используя метод noneOf.

«Чтобы продемонстрировать это, мы можем отменить условие в предыдущем примере, чтобы получить кандидатов, не принадлежащих к вышеуказанным возрастным группам:

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

5. Сопоставление с пользовательскими предикатами

В предыдущем разделе мы рассмотрели встроенные предикаты Vavr. Но Вавр не останавливается на достигнутом. Зная лямбда-выражения, мы можем создавать и использовать собственные предикаты или даже просто записывать их в строку.

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

Используя эти новые знания, мы можем встроить предикат в первый пример предыдущего раздела и переписать его следующим образом:

Мы также можем применить функциональный интерфейс вместо предиката, если нам понадобится больше параметров. Пример contains можно переписать так, хотя и немного более подробно, но это дает нам больше возможностей над тем, что делает наш предикат:

В приведенном выше примере мы создали BiFunction Java 8, которая просто проверяет отношение isIn. между двумя аргументами.

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

Вы могли бы использовать для этого и функцию Vavr’s FunctionN. Поэтому, если встроенные предикаты не совсем соответствуют вашим требованиям или вы хотите контролировать всю оценку, используйте пользовательские предикаты.

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

6. Декомпозиция объекта

Декомпозиция объекта — это процесс разбиения объекта Java на составные части. Например, рассмотрим случай абстрагирования биоданных сотрудника вместе с информацией о занятости:

Мы можем разложить запись о сотруднике на составные части: имя и идентификатор. В Java это совершенно очевидно:

Мы создаем объект сотрудника, затем сначала проверяем, является ли он нулевым, прежде чем применять фильтр, чтобы гарантировать, что в итоге мы получим запись о сотруднике по имени Карл. Затем мы идем дальше и извлекаем его идентификатор. Способ Java работает, но он многословен и подвержен ошибкам.

public class Employee {

    private String name;
    private String id;

    //standard constructor, getters and setters
}

То, что мы в основном делаем в приведенном выше примере, сопоставляет то, что мы знаем, с тем, что приходит. Мы знаем, что нам нужен сотрудник по имени Карл, поэтому мы пытаемся сопоставить это имя с входящим объектом.

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

Затем мы разбираем его детали, чтобы получить удобочитаемый вывод. Нулевые проверки — это просто защитные накладные расходы, которые нам не нужны.

Благодаря Vavr Pattern Matching API мы можем забыть о ненужных проверках и просто сосредоточиться на том, что важно, что приводит к очень компактному и читабельному коду.

Чтобы использовать это положение, в вашем проекте должна быть установлена ​​дополнительная зависимость vavr-match. Вы можете получить его, перейдя по этой ссылке.

Приведенный выше код может быть записан следующим образом:

Ключевыми конструкциями в приведенном выше примере являются атомарные шаблоны $(\»Carl\») и $(), шаблон значения и шаблон шаблона соответственно. . Мы подробно обсуждали это во вводной статье Vavr.

Оба шаблона извлекают значения из соответствующего объекта и сохраняют их в параметрах лямбда. Шаблон значения $(\»Carl\») может совпадать только тогда, когда полученное значение соответствует тому, что находится внутри него, т. е. carl.

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

С другой стороны, подстановочный шаблон $() соответствует любому значению в его позиции и извлекает значение в лямбда-параметр id.

Чтобы эта декомпозиция работала, нам нужно определить шаблоны декомпозиции или то, что формально называется неприменимыми шаблонами.

Это означает, что мы должны научить API сопоставления шаблонов декомпозировать наши объекты, в результате чего для каждого объекта будет одна запись:

Инструмент обработки аннотаций сгенерирует класс с именем DemoPatterns.java, который мы должны статически импортировать туда, где мы хотим применить эти шаблоны:

Мы также можем декомпозировать встроенные объекты Java.

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

    // other unapply patterns
}

Например, java.time.LocalDate можно разложить на год, месяц и день месяца. Давайте добавим шаблон unapply в Demo.java:

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

Затем тест:

7. Побочные эффекты при сопоставлении шаблонов

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

По умолчанию Match действует как выражение, то есть возвращает результат. Однако мы можем заставить его производить побочный эффект, используя вспомогательную функцию, запущенную в лямбда-выражении.

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

Он принимает ссылку на метод или лямбда-выражение и возвращает Void.

«Рассмотрим сценарий, в котором мы хотим что-то напечатать, когда ввод представляет собой одноразрядное четное целое число, и другое дело, когда ввод представляет собой одноразрядное нечетное число, и генерировать исключение, когда ввод не является ни одним из них.

Принтер четных чисел:

Принтер нечетных чисел:

И функция сопоставления:

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

Что напечатает:

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

8. Заключение ~ ~~ В этой статье мы рассмотрели наиболее важные части API сопоставления с образцом в Vavr. Действительно, благодаря Vavr теперь мы можем писать более простой и лаконичный код без многословного переключателя и операторов if.

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

Чтобы получить полный исходный код для этой статьи, вы можете проверить проект Github.

Input is even

«

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.