Перейти к основному содержимому

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

· 11 мин. чтения

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 :

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

С предикатами мы можем делать больше, например объединять несколько предикатов в один случай совпадения. Чтобы сопоставляться только тогда, когда входные данные проходят все заданную группу предикатов, мы можем использовать И предикаты, используя предикат 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);
}

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

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

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

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

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

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

public class Employee {

private String name;
private String id;

//standard constructor, getters and setters
}

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

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

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

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

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

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

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

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

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

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

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

Это означает, что мы должны научить 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.foreach.vavr.DemoPatterns.*;

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

Например, java.time.LocalDate можно разложить на год, месяц и день месяца. Давайте добавим шаблон неприменения в 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 действует как выражение, то есть возвращает результат. Однако мы можем заставить его производить побочный эффект, используя вспомогательную функцию , запущенную в лямбда-выражении.

Он принимает ссылку на метод или лямбда-выражение и возвращает 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. Действительно, благодаря Vavr теперь мы можем писать более простой и лаконичный код без многословного переключателя и операторов if.

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