Vavrのパターンマッチングガイド

1概要

この記事では、Vavrによるパターンマッチングに焦点を当てます。あなたがVavrについて何を知らないならば、まず Vavrの概要 を読んでください。

パターンマッチングは、Javaでは本来利用できない機能です。

  • それは、** switch-case__ステートメントの高度な形式と考えることができます。

Vavrのパターンマッチングの利点は、 switch-caseまたは 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);
}

これまでに見たスニペットは冗長であり、したがってエラーが発生しやすくなっています。

パターンマッチングを使用するときは、3つの主要な構成要素を使用します。2つの静的メソッド Match Case 、およびAtomicパターンです。

アトミックパターンは、ブール値を返すために評価されるべき条件を表します。

  • $() :の default の場合と同様のワイルドカードパターン

switchステートメント一致が見つからないシナリオを処理します。 $(value)** :これは、値が単純に等しい場合のequalsパターンです。

入力と等しい。

  • $(predicate) :これは述語が置かれる条件付きパターンです

functionが入力に適用され、結果のブール値が決定に使用されます。

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オプションと一致

前のセクションで見たように、ワイルドカードパターン $() は、入力に対して一致が見つからないデフォルトのケースに一致します。

ただし、ワイルドカードパターンを含める代わりのもう1つの方法は、 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);
}

複数の述語を1つの一致ケースとして結合するなど、述語でできることはもっとあります。入力が述語のすべてのグループを通過したときだけ一致するようにするには、 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 関係を単純にチェックするJava 8 BiFunction を作成しました。

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

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

従業員オブジェクトを作成してから、フィルタを適用する前に最初にnullであるかどうかを確認して、名前が Carl の従業員のレコードになっていることを確認します。そして先に進み、彼の id を取り出します。 Javaの方法は機能しますが、冗長でエラーが発生しやすいです。

上記の例で基本的に行っていることは、私たちが知っていることと入ってくるものとを一致させることです。

それから私たちは彼の詳細を分解して人間が読める形式の出力を得ます。 nullチェックは、単に必要としない防御的なオーバーヘッドです。

VavrのパターンマッチングAPIを使用すると、不要なチェックを忘れることができ、重要なものに焦点を合わせるだけで済み、非常にコンパクトで読みやすいコードになります。

この規定を使用するには、プロジェクトに追加の vavr-match 依存関係がインストールされている必要があります。あなたはhttps://search.maven.org/classic/#search%7Cga%7C1%7Cvavr-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[Vavr入門記事]で、これらについて詳しく説明しました。

どちらのパターンも、一致したオブジェクトから値を取得し、それらをラムダパラメータに格納します。値パターン $(“ Carl”) は、取り出された値がその中にあるもの、つまり carl と一致する場合にのみ一致できます。

一方、 ワイルドカードパターン $() は、その位置にある任意の値 と一致し、その値を id lambdaパラメータに取り出します。

この分解が機能するためには、分解パターンまたは正式には 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 を使用することで、副作用を発生させることができます。

メソッド参照またはラムダ式を取り、__Voidを返します。

  • 入力が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のPattern Matching APIの最も重要な部分について説明しました。確かに、Vavrのおかげで、冗長なスイッチやifステートメントなしで、よりシンプルでより簡潔なコードを書くことができます。

この記事の完全なソースコードを入手するには、https://github.com/eugenp/tutorials/tree/master/vavr[the Github project]をチェックしてください。