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 .