1. 개요

이 기사에서는 Vavr을 사용한 패턴 매칭에 중점을 둘 것입니다. Vavr에 대해 잘 모르는 경우 먼저 Vavr 의 개요 를 읽어보십시오 .

패턴 일치는 Java에서 기본적으로 사용할 수 없는 기능입니다. switch-case 문의 고급 형식 으로 생각할 수 있습니다.

Vavr의 패턴 일치의 장점은 스위치 케이스 또는 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";
    }
    else if (input == 1) {
        output = "one";
    }
    else if (input == 2) {
        output = "two";
    }
    else if (input == 3) {
        output = "three";
    } else {
        output = "unknown";
    }

    assertEquals("three", output);
}

지금까지 본 스니펫은 장황하므로 오류가 발생하기 쉽습니다. 패턴 일치를 사용할 때 세 가지 기본 빌딩 블록을 사용합니다. 두 가지 정적 메서드 Match , Case 및 원자 패턴입니다.

원자 패턴은 부울 값을 반환하기 위해 평가되어야 하는 조건을 나타냅니다.

  • $() : switch 문의 기본 케이스 와 유사한 와일드 카드 패턴입니다일치하는 항목이 없는 시나리오를 처리합니다.
  • $(value) : 이것은 값이 입력과 단순히 같음 비교되는 equals 패턴입니다.
  • $(predicate) : 입력에 술어 함수가 적용되고 결과 부울이 결정을 내리는 데 사용되는 조건부 패턴입니다.

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

입력이 일치하지 않으면 와일드카드 패턴이 평가됩니다.

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

Vavr의 Option 을 더 잘 이해하려면 소개 기사를 참조하십시오.

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

또는 입력이 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 술어 를 사용하여 값 List에 입력이 있는지 확인할 수 있습니다 .

@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 술어 를 사용하여 술어를 AND 할 수 있습니다 .

실제 사례는 이전 예에서와 같이 List에 숫자가 포함되어 있는지 확인하려는 경우입니다. 문제는 List에 null도 포함되어 있다는 것입니다. 따라서 List에 없는 숫자를 거부하는 것 외에도 null도 거부하는 필터를 적용하려고 합니다.

@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 술어를 사용하여 술어를 OR할 수 있습니다.

출생 연도로 후보자를 선별하고 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의 내장 술어를 탐색했습니다. 그러나 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);
}

위의 예에서 우리는 단순히 두 인수 간의 isIn 관계 확인 하는 Java 8 BiFunction 을 만들었습니다.

이를 위해 Vavr의 FunctionN 을 사용할 수도 있습니다. 따라서 내장된 술어가 요구 사항과 완전히 일치하지 않거나 전체 평가를 제어하려는 경우 사용자 정의 술어를 사용하십시오.

6. 객체 분해

개체 분해는 Java 개체를 구성 요소 부분으로 나누는 프로세스입니다. 예를 들어 고용 정보와 함께 직원의 바이오 데이터를 추상화하는 경우를 생각해 보십시오.

public class Employee {

    private String name;
    private String id;

    //standard constructor, getters and setters
}

Employee의 레코드를 구성 요소인 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);
}

직원 개체를 만든 다음 필터를 적용하기 전에 먼저 null인지 확인하여 이름이 Carl 인 직원의 레코드로 끝나는지 확인합니다 . 그런 다음 그의 ID 를 검색 합니다. Java 방식은 작동하지만 장황하고 오류가 발생하기 쉽습니다.

위의 예에서 우리가 기본적으로 하는 것은 우리가 알고 있는 것과 들어오는 것을 일치시키는 것입니다. 우리는 Carl 이라는 직원을 원한다는 것을 알고 있으므로 이 이름을 들어오는 객체와 일치시키려고 합니다.

그런 다음 사람이 읽을 수 있는 출력을 얻기 위해 그의 세부 정보를 분석합니다. null 검사는 단순히 우리가 필요로 하지 않는 방어적인 오버헤드입니다.

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.baeldung.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를 반환합니다.

입력이 한 자리 짝수일 때 무언가를 인쇄하고 입력이 한 자리 홀수일 때 다른 것을 인쇄하고 입력이 이들 중 어느 것도 아닐 때 예외를 throw하는 시나리오를 고려하십시오 .

짝수 프린터:

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. 결론

이 기사에서는 Vavr의 Pattern Matching API에서 가장 중요한 부분을 살펴보았습니다. 실제로 Vavr 덕분에 장황한 스위치와 if 문 없이 더 간단하고 간결한 코드를 작성할 수 있습니다.

이 기사의 전체 소스 코드를 얻으려면 Github 프로젝트 를 확인하세요 .

Generic footer banner