Руководство по сопоставлению с образцом в Vavr

1. Обзор

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

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

Можно думать об этом как о расширенной форме оператора switch-case .

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

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

import static io.vavr.API.** ;

2. Как работает Pattern Matching

Как мы видели в предыдущей статье, сопоставление с образцом можно использовать для замены блока 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);
}

Или несколько 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 и атомарные шаблоны.

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

  • $ () : шаблон подстановочного знака, который похож на случай default в

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

равно по сравнению с входом.

  • $ (предикат) : это условный шаблон, в котором предикат

Функция применяется к входным данным, а полученное логическое значение используется для принятия решения.

Подходы 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);
}

Вавр предлагает больше предикатов, чем это. Например, мы можем заставить наше условие проверять класс ввода вместо:

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

Или вход 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);
}

Вместо сопоставления значений в стиле equals мы можем использовать стиль contains . Таким образом, мы можем проверить, существует ли вход в списке значений с предикатом 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);
}

С предикатами мы можем сделать больше, например, объединяя несколько предикатов в один случай совпадения. Чтобы соответствовать только тогда, когда входные данные проходят всю заданную группу предикатов, мы можем AND предикаты, используя предикат allOf .

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

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

Чтобы соответствовать, когда вход совпадает с какой-либо из данной группы, мы можем ИЛИ предикаты, используя предикат anyOf .

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

Если такого кандидата не найдено, мы можем принять только тех, кто родился в 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);
}

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

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

@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. Сопоставить с пользовательскими предикатами

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

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

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

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

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

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

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

6. Разложение объекта

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

public class Employee {

    private String name;
    private String id;

   //standard constructor, getters and setters
}

Мы можем разложить записи Сотрудника на составные части: name и id . Это довольно очевидно в 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);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

   //other unapply patterns
}

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

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

Мы также можем разложить встроенные объекты 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());
}

Тогда тест:

@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. Побочные эффекты при сопоставлении с образцом

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

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

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

Принтер четного числа:

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

Принтер с нечетным номером:

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

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

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

Который напечатал бы:

Input is even

8. Заключение

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

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