Anleitung zum Pattern Matching in Vavr

1. Überblick

In diesem Artikel konzentrieren wir uns auf Pattern Matching with Vavr. Wenn Sie nicht wissen, was Sie über Vavr wissen, lesen Sie zuerst die Übersicht über __Vavr

Pattern Matching ist eine Funktion, die in Java nicht nativ verfügbar ist.

Man könnte es sich als die erweiterte Form einer switch-case -Anweisung vorstellen.

Der Vorteil von Vavrs Pattern Matching besteht darin, dass wir keine Stapel von switch -Fällen oder if-then-else -Anweisungen schreiben müssen. Es reduziert daher die Menge an Code und repräsentiert bedingte Logik auf eine von Menschen lesbare Weise.

Wir können die Pattern-Matching-API verwenden, indem Sie den folgenden Import durchführen:

import static io.vavr.API.** ;

2. Wie Pattern Matching funktioniert

Wie wir im vorherigen Artikel gesehen haben, kann der Mustervergleich verwendet werden, um einen switch -Block zu ersetzen:

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

Oder mehrere if -Anweisungen:

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

Die Schnipsel, die wir bisher gesehen haben, sind ausführlich und daher fehleranfällig.

Beim Pattern Matching verwenden wir drei Hauptbausteine: die beiden statischen Methoden Match , Case und atomare Muster.

Atomic Patterns repräsentieren die Bedingung, die ausgewertet werden sollte, um einen booleschen Wert zurückzugeben:

  • $ () : Ein Platzhaltermuster, das dem Fall default in ähnelt

eine switch-Anweisung. Es behandelt ein Szenario, in dem keine Übereinstimmung gefunden wird $ (Wert) ** : Dies ist das Gleichheitsmuster, bei dem ein Wert einfach ist

ist gleich der Eingabe.

  • $ (Prädikat) : Dies ist das Bedingungsmuster, für das ein Prädikat gilt

Die Funktion wird auf die Eingabe angewendet und der resultierende Boolesche Wert wird verwendet, um eine Entscheidung zu treffen.

Die switch - und if -Ansätze könnten durch einen kürzeren und präziseren Code wie folgt ersetzt werden:

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

Wenn die Eingabe keine Übereinstimmung erhält, wird das Platzhaltermuster ausgewertet:

@Test
public void whenMatchesDefault__thenCorrect() {
    int input = 5;
    String output = Match(input).of(
      Case($(1), "one"),
      Case($(), "unknown"));

    assertEquals("unknown", output);
}

Wenn kein Platzhaltermuster vorhanden ist und die Eingabe nicht übereinstimmt, wird ein Übereinstimmungsfehler angezeigt:

@Test(expected = MatchError.class)
public void givenNoMatchAndNoDefault__whenThrows__thenCorrect() {
    int input = 5;
    Match(input).of(
      Case($(1), "one"),
      Case($(2), "two"));
}

In diesem Abschnitt haben wir die Grundlagen des Vavr-Pattern-Matching behandelt. In den folgenden Abschnitten werden verschiedene Ansätze zur Bewältigung verschiedener Fälle behandelt, die in unserem Code wahrscheinlich auftreten werden.

3. Übereinstimmung mit Option

Wie wir im vorherigen Abschnitt gesehen haben, stimmt das Platzhaltermuster $ () mit Standardfällen überein, in denen keine Übereinstimmung für die Eingabe gefunden wurde.

Eine andere Alternative zum Einfügen eines Platzhalter-Musters besteht jedoch darin, den Rückgabewert einer Übereinstimmungsoperation in einer Option -Instanz einzuhüllen:

@Test
public void whenMatchWorksWithOption__thenCorrect() {
    int i = 10;
    Option<String> s = Match(i)
      .option(Case($(0), "zero"));

    assertTrue(s.isEmpty());
    assertEquals("None", s.toString());
}

Um ein besseres Verständnis von Option in Vavr zu erlangen, können Sie sich auf den Einführungsartikel beziehen.

4. Mit eingebauten Prädikaten übereinstimmen

Vavr wird mit einigen eingebauten Prädikaten geliefert, die unseren Code für den Menschen lesbarer machen. Daher können unsere ersten Beispiele mit Prädikaten weiter verbessert werden:

@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 bietet mehr Prädikate als dies. Zum Beispiel können wir unsere Bedingung dazu verwenden, die Klasse der Eingabe zu überprüfen:

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

Oder ob die Eingabe null ist oder nicht:

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

Anstatt Werte in equals style abzugleichen, können wir contains style verwenden. Auf diese Weise können wir mit dem Prädikat isIn prüfen, ob eine Eingabe in einer Liste von Werten vorhanden ist:

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

Mit Prädikaten können wir noch mehr tun, z. B. mehrere Prädikate als einen einzigen Übereinstimmungsfall kombinieren. Um nur übereinzustimmen, wenn die Eingabe alle angegebenen Prädikate übergibt, können AND -Prädikate mit dem Prädikat allOf verwendet werden.

Ein praktischer Fall wäre, wenn wir prüfen möchten, ob eine Nummer in einer Liste enthalten ist, wie im vorherigen Beispiel. Das Problem ist, dass die Liste auch Nullen enthält. Wir möchten also einen Filter anwenden, der neben dem Abweisen von Zahlen, die nicht in der Liste enthalten sind, auch Nullen zurückweist:

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

Wenn eine Eingabe mit einer bestimmten Gruppe übereinstimmt, können wir ODER die Prädikate mithilfe des Prädikats anyOf ODER abgleichen.

Angenommen, wir prüfen Kandidaten nach ihrem Geburtsjahr und wir möchten nur Kandidaten, die 1990, 1991 oder 1992 geboren wurden.

Wenn kein solcher Kandidat gefunden wird, können wir nur diejenigen akzeptieren, die 1986 geboren wurden, und dies möchten wir auch in unserem Kodex deutlich machen:

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

Schließlich können wir sicherstellen, dass keine angegebenen Prädikate mit der noneOf -Methode übereinstimmen.

Um dies zu demonstrieren, können wir die Bedingung im vorherigen Beispiel negieren, sodass wir Kandidaten erhalten, die nicht in den oben genannten Altersgruppen sind:

@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. Übereinstimmung mit benutzerdefinierten Prädikaten

Im vorigen Abschnitt haben wir die eingebauten Prädikate von Vavr untersucht. Aber Vavr hört dort nicht auf. Mit dem Wissen über Lambdas können wir unsere eigenen Prädikate erstellen und verwenden oder sogar nur Inline schreiben.

Mit diesem neuen Wissen können wir ein Prädikat im ersten Beispiel des vorherigen Abschnitts einbinden und wie folgt umschreiben:

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

Wir können auch eine funktionale Schnittstelle anstelle eines Prädikats anwenden, falls wir mehr Parameter benötigen. Das include-Beispiel kann wie folgt umgeschrieben werden, wenn auch etwas ausführlicher, aber es gibt uns mehr Macht über das, was unser Prädikat tut:

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

Im obigen Beispiel haben wir eine Java 8 BiFunction erstellt, die einfach die isIn -Beziehung zwischen den beiden Argumenten überprüft.

Sie hätten auch Vavrs FunctionN verwenden können. Wenn die eingebauten Prädikate Ihren Anforderungen nicht ganz entsprechen oder Sie die gesamte Auswertung steuern möchten, verwenden Sie benutzerdefinierte Prädikate.

6. Objektzerlegung

Objektzerlegung ist der Prozess, bei dem ein Java-Objekt in seine Bestandteile zerlegt wird. Betrachten Sie beispielsweise den Fall der Zusammenfassung der Biodaten eines Mitarbeiters neben den Beschäftigungsinformationen:

public class Employee {

    private String name;
    private String id;

   //standard constructor, getters and setters
}

Wir können den Datensatz eines Mitarbeiters in seine Komponententeile zerlegen: name und id . Dies ist in Java ganz offensichtlich:

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

Wir erstellen ein Mitarbeiterobjekt und prüfen zuerst, ob es null ist, bevor wir einen Filter anwenden, um sicherzustellen, dass wir den Datensatz eines Mitarbeiters mit dem Namen Carl erhalten. Dann fahren wir fort und holen seine id ab. Die Java-Methode funktioniert, ist aber ausführlich und fehleranfällig.

Was wir im obigen Beispiel im Wesentlichen tun, besteht darin, das, was wir wissen, mit dem abzustimmen, was kommt. Wir wissen, dass wir einen Mitarbeiter namens Carl wollen, also versuchen wir, diesen Namen mit dem eingehenden Objekt abzugleichen.

Wir zerlegen dann seine Details, um eine vom Menschen lesbare Ausgabe zu erhalten. Die Null-Checks sind lediglich defensive Overheads, die wir nicht brauchen.

Mit der Pattern Matching API von Vavr können wir unnötige Überprüfungen vergessen und uns einfach auf das Wesentliche konzentrieren, was zu sehr kompaktem und lesbarem Code führt.

Um diese Bestimmung verwenden zu können, muss in Ihrem Projekt eine zusätzliche vavr-match -Abhängigkeit installiert sein. Sie erhalten es unter https://search.maven.org/classic/#search%7Cga%7C1%7Cvavr-match

Der obige Code kann dann wie folgt geschrieben werden:

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

Die Schlüsselkonstrukte im obigen Beispiel sind die atomaren Muster $ ("Carl") und $ () , das Wertemuster das Platzhaltermuster. Wir haben diese im Link ausführlich besprochen:/vavr[Einführungsartikel von Vavr].

Beide Muster rufen Werte vom übereinstimmenden Objekt ab und speichern sie in den Lambda-Parametern. Das Wertemuster $ ("Carl") kann nur übereinstimmen, wenn der abgerufene Wert mit dem darin enthaltenen Wert übereinstimmt, d. H.

Andererseits stimmt das Platzhaltermuster $ () mit einem beliebigen Wert ** an seiner Position überein und ruft den Wert in den id -Lambda-Parameter ab.

Damit diese Zerlegung funktioniert, müssen wir Zerlegungsmuster definieren, die formal als "unapply" -Muster bekannt sind.

Dies bedeutet, dass wir der Pattern-Matching-API beibringen müssen, wie unsere Objekte zerlegt werden, sodass sich für jedes zu zerlegende Objekt ein Eintrag ergibt:

@Patterns
class Demo {
    @Unapply
    static Tuple2<String, String> Employee(Employee Employee) {
        return Tuple.of(Employee.getName(), Employee.getId());
    }

   //other unapply patterns
}

Das Anmerkungsverarbeitungstool generiert eine Klasse namens DemoPatterns.java , die wir statisch importieren müssen, wohin wir diese Muster anwenden möchten:

import static com.baeldung.vavr.DemoPatterns.** ;

Wir können auch eingebaute Java-Objekte zerlegen.

Zum Beispiel kann java.time.LocalDate in Jahr, Monat und Tag des Monats zerlegt werden. Fügen wir Demo.java sein unapply -Muster hinzu:

@Unapply
static Tuple3<Integer, Integer, Integer> LocalDate(LocalDate date) {
    return Tuple.of(
      date.getYear(), date.getMonthValue(), date.getDayOfMonth());
}

Dann der Test:

@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. Nebenwirkungen in Pattern Matching

Standardmäßig verhält sich Match wie ein Ausdruck, dh es gibt ein Ergebnis zurück. Wir können es jedoch zwingen, einen Nebeneffekt zu erzeugen, indem Sie die Hilfsfunktion run innerhalb eines Lambda verwenden.

Es nimmt eine Methodenreferenz oder einen Lambda-Ausdruck und gibt Void. zurück.

  • Betrachten Sie ein Szenario ** , in dem etwas gedruckt werden soll, wenn eine Eingabe eine gerade Zahl für eine einzelne Ziffer ist und eine andere Sache, wenn die Eingabe eine ungerade Ziffer ist, und eine Ausnahme ausgelöst wird, wenn die Eingabe keine dieser Eingaben ist.

Der Drucker mit der geraden Anzahl:

public void displayEven() {
    System.out.println("Input is even");
}

Die ungerade Anzahl Drucker:

public void displayOdd() {
    System.out.println("Input is odd");
}

Und die Matchfunktion:

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

Was würde drucken:

Input is even

8. Fazit

In diesem Artikel haben wir die wichtigsten Teile der Pattern Matching API in Vavr untersucht. Tatsächlich können wir nun dank Vavr einfacheren und prägnanteren Code ohne die ausführlichen Schalter und if-Anweisungen schreiben.

Den vollständigen Quellcode für diesen Artikel finden Sie unter https://github.com/eugenp/tutorials/tree/master/vavr [the Github-Projekt