1. 概要

この記事では、Vavrを使用したパターンマッチングに焦点を当てます。 Vavrについてわからない場合は、最初に Vavrの概要をお読みください。

パターンマッチングは、Javaではネイティブに利用できない機能です。 これは、switch-caseステートメントの高度な形式と考えることができます。

Vavrのパターンマッチングの利点は、switchケースまたはif-then-elseステートメントのスタックを作成する手間が省けることです。 したがって、はコードの量を減らし、人間が読める方法で条件付きロジックを表します。

次のインポートを行うことで、パターンマッチングAPIを使用できます。

import static io.vavr.API.*;

2. パターンマッチングのしくみ

前の記事で見たように、パターンマッチングを使用して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);
}

これまでに見たスニペットは冗長であるため、エラーが発生しやすくなっています。 パターンマッチングを使用する場合、2つの静的メソッド Match Case 、およびアトミックパターンの3つの主要な構成要素を使用します。

アトミックパターンは、ブール値を返すために評価する必要がある条件を表します。

  • $():switchステートメントのdefaultの場合に似たワイルドカードパターン。 一致するものが見つからないシナリオを処理します
  • $(value):これは、入力と比較して、値が単純に等しい等しいパターンです。
  • $(predicate):これは、述語関数が入力に適用され、結果のブール値が決定に使用される条件付きパターンです。

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

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述語を使用して値のリストに入力が存在するかどうかを確認できます。

@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述語を使用できます。 述語。

実際のケースは、前の例で行ったように、番号がリストに含まれているかどうかを確認する場合です。 問題は、リストにnullも含まれていることです。 したがって、リストにない数値を拒否する以外に、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);
}

より多くのパラメーターが必要な場合は、述語の代わりに機能インターフェースを適用することもできます。 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);
}

上記の例では、2つの引数間のisIn関係をチェックするだけのJava8BiFunctionを作成しました。

これにもVavrのFunctionNを使用できます。 したがって、組み込みの述語が要件に完全に一致しない場合、または評価全体を制御したい場合は、カスタム述語を使用してください。

6. オブジェクトの分解

オブジェクト分解は、Javaオブジェクトをそのコンポーネント部分に分割するプロセスです。 たとえば、従業員のバイオデータを雇用情報と一緒に抽象化する場合を考えてみましょう。

public class Employee {

    private String name;
    private String id;

    //standard constructor, getters and setters
}

従業員のレコードをその構成要素である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のパターンマッチング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”)は、取得された値がその内部にあるものと一致する場合にのみ一致します。 カール

一方、ワイルドカードパターン$()は、その位置で任意の値と一致し、その値をidラムダパラメーターに取得します。

この分解を機能させるには、分解パターン、または正式にはunapplyパターンと呼ばれるものを定義する必要があります。

これは、パターンマッチングAPIにオブジェクトを分解する方法を教える必要があることを意味し、分解されるオブジェクトごとに1つのエントリが生成されます。

@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 を使用することにより、強制的に副作用を発生させることができます。

メソッド参照またはラムダ式を受け取り、 空所。

入力が1桁の偶数の場合に何かを出力し、入力が1桁の奇数の場合に別のものを出力し、入力がこれらのいずれでもない場合に例外をスローするシナリオを考えてみます。

偶数プリンター:

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のパターンマッチングAPIの最も重要な部分について説明しました。 実際、Vavrのおかげで、冗長なスイッチやifステートメントを使用せずに、より単純で簡潔なコードを記述できるようになりました。

この記事の完全なソースコードを入手するには、Githubプロジェクトをチェックしてください。