Vavrの紹介

1概要

この記事では、http://javaslang.io[Vavr]とは何か、なぜ必要なのか、そしてプロジェクトでそれをどのように使用するのかを詳しく探ります。

Vavrは、不変のデータ型と機能制御構造を提供するJava 8用の** 機能的ライブラリーです。

1.1. Mavenの依存関係

Vavrを使用するには、依存関係を追加する必要があります。

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.0</version>
</dependency>

常に最新のバージョンを使用することをお勧めします。 このリンク をたどってそれを得ることができます。

2オプション

Optionの主な目的は、Java型システムを利用してコード内のnullチェックを排除することです。

Option はVavrのオブジェクトコンテナで、 オプション Java 8の[オプション]。

Javaのオブジェクト参照はすべて null 値を持つことができるので、通常は使用する前に if ステートメントでNULL値をチェックする必要があります。これらのチェックにより、コードは堅牢で安定したものになります。

@Test
public void givenValue__whenNullCheckNeeded__thenCorrect() {
    Object possibleNullObj = null;
    if (possibleNullObj == null) {
        possibleNullObj = "someDefaultValue";
    }
    assertNotNull(possibleNullObj);
}

チェックしないと、単純な__NPEが原因でアプリケーションがクラッシュする可能性があります。

@Test(expected = NullPointerException.class)
public void givenValue__whenNullCheckNeeded__thenCorrect2() {
    Object possibleNullObj = null;
    assertEquals("somevalue", possibleNullObj.toString());
}

ただし、特に if ステートメントが複数回ネストされることになる場合は、チェックによってコードが冗長になり、読みにくくなります。

Option は、 nulls を完全に排除し、考えられるシナリオごとに有効なオブジェクト参照に置き換えることで、この問題を解決します。

Option を指定すると、 null 値は None のインスタンスに評価され、null以外の値は Some のインスタンスに評価されます。

@Test
public void givenValue__whenCreatesOption__thenCorrect() {
    Option<Object> noneOption = Option.of(null);
    Option<Object> someOption = Option.of("val");

    assertEquals("None", noneOption.toString());
    assertEquals("Some(val)", someOption.toString());
}

したがって、オブジェクト値を直接使用する代わりに、上記のようにそれらを Option インスタンス内にラップすることをお勧めします。

toString を呼び出す前にチェックを行う必要はありませんでしたが、以前のように NullPointerException を処理する必要はありませんでした。 Optionの toString は、各呼び出しで意味のある値を返します。

このセクションの2番目のスニペットでは、 null チェックが必要でした。このチェックを使用する前に、変数にデフォルト値を割り当てます。 nullがあっても、 Option はこれを1行で処理できます。

@Test
public void givenNull__whenCreatesOption__thenCorrect() {
    String name = null;
    Option<String> nameOption = Option.of(name);

    assertEquals("baeldung", nameOption.getOrElse("baeldung"));
}

またはnull以外の値

@Test
public void givenNonNull__whenCreatesOption__thenCorrect() {
    String name = "baeldung";
    Option<String> nameOption = Option.of(name);

    assertEquals("baeldung", nameOption.getOrElse("notbaeldung"));
}

null チェックなしで、1行で値を取得したりデフォルトを返したりできることに注目してください。

3タプル

Javaにはタプルデータ構造体と直接同等のものはありません。タプルは、関数型プログラミング言語では一般的な概念です。タプルは不変で、タイプセーフな方法で異なるタイプの複数のオブジェクトを保持できます。

VavrはJava 8にタプルをもたらします。タプルは取り込む要素の数に応じて Tuple1、Tuple2 から Tuple8 のタイプです。

現在8つの要素の上限があります。 tuple. n のようなタプルの要素にアクセスします。ここで、 n__は配列内のインデックスの概念に似ています。

public void whenCreatesTuple__thenCorrect1() {
    Tuple2<String, Integer> java8 = Tuple.of("Java", 8);
    String element1 = java8.__1;
    int element2 = java8.__2();

    assertEquals("Java", element1);
    assertEquals(8, element2);
}

最初の要素が n == 1 で取得されていることに注意してください。そのため、タプルは配列のようにゼロベースを使用しません。タプルに格納される要素の型は、上および下に示すように、その型宣言で宣言する必要があります。

@Test
public void whenCreatesTuple__thenCorrect2() {
    Tuple3<String, Integer, Double> java8 = Tuple.of("Java", 8, 1.8);
    String element1 = java8.__1;
    int element2 = java8.__2();
    double element3 = java8.__3();

    assertEquals("Java", element1);
    assertEquals(8, element2);
    assertEquals(1.8, element3, 0.1);
}

タプルの場所は、ユニットとしてよりよく処理され、転送できる任意のタイプのオブジェクトの固定グループを格納することです。より明白なユースケースは、Javaの関数またはメソッドから複数のオブジェクトを返すことです。

4試してみる

Vavrでは、 Try は計算のためのコンテナーです ** これは例外を引き起こす可能性があります。

Option はnull許容オブジェクトをラップするので、 if チェックで nulls を明示的に処理する必要はありませんが、 try try-catch ブロックで明示的に例外処理を行う必要がないように計算をラップします。

例えば次のようなコードを見てください。

@Test(expected = ArithmeticException.class)
public void givenBadCode__whenThrowsException__thenCorrect() {
    int i = 1/0;
}

try-catch ブロックがないと、アプリケーションはクラッシュします。これを回避するには、ステートメントを try-catch ブロックで囲む必要があります。

Vavrでは、同じコードを Try インスタンスにラップして結果を得ることができます。

@Test
public void givenBadCode__whenTryHandles__thenCorrect() {
    Try<Integer> result = Try.of(() -> 1/0);

    assertTrue(result.isFailure());
}

計算が成功したかどうかは、コードの任意の時点で選択によって検査できます。

上記のスニペットでは、単に成功または失敗をチェックすることを選択しました。デフォルト値を返すように選択することもできます。

@Test
public void givenBadCode__whenTryHandles__thenCorrect2() {
    Try<Integer> computation = Try.of(() -> 1/0);
    int errorSentinel = result.getOrElse(-1);

    assertEquals(-1, errorSentinel);
}

あるいは、私たちが選んだ例外を明示的にスローすることもできます。

@Test(expected = ArithmeticException.class)
public void givenBadCode__whenTryHandles__thenCorrect3() {
    Try<Integer> result = Try.of(() -> 1/0);
    result.getOrElseThrow(ArithmeticException::new);
}

上記のすべての場合において、Vavrの Try のおかげで、計算後に何が起こるかを制御できます。

5機能インターフェース

Java 8の登場で、特にラムダと組み合わせると 機能インターフェース が組み込まれて使用しやすくなります。

ただし、Java 8は2つの基本機能しか提供していません。 1つのパラメータだけを取り、結果を生成します。

@Test
public void givenJava8Function__whenWorks__thenCorrect() {
    Function<Integer, Integer> square = (num) -> num **  num;
    int result = square.apply(2);

    assertEquals(4, result);
}

2番目の引数は2つのパラメータのみを取り、結果を生成します。

@Test
public void givenJava8BiFunction__whenWorks__thenCorrect() {
    BiFunction<Integer, Integer, Integer> sum =
      (num1, num2) -> num1 + num2;
    int result = sum.apply(5, 7);

    assertEquals(12, result);
}

反対に、Vavrは最大8つのパラメーターをサポートし、メモ化、合成、およびカリー化のためのメソッドでAPIを強化することによって、Javaの機能インターフェースの概念をさらに拡張します。

タプルと同じように、これらの機能的インタフェースはそれらが取るパラメータの数に従って命名されます: Function0 Function1 Function2 など。

@Test
public void givenVavrFunction__whenWorks__thenCorrect() {
    Function1<Integer, Integer> square = (num) -> num **  num;
    int result = square.apply(2);

    assertEquals(4, result);
}

この:

@Test
public void givenVavrBiFunction__whenWorks__thenCorrect() {
    Function2<Integer, Integer, Integer> sum =
      (num1, num2) -> num1 + num2;
    int result = sum.apply(5, 7);

    assertEquals(12, result);
}

パラメータがなくても出力が必要な場合は、Java 8では Consumer typeを使用する必要があります。Vavrでは Function0 が役立ちます。

@Test
public void whenCreatesFunction__thenCorrect0() {
    Function0<String> getClazzName = () -> this.getClass().getName();
    String clazzName = getClazzName.apply();

    assertEquals("com.baeldung.vavr.VavrTest", clazzName);
}

5つのパラメータを持つ関数については、それは Function5 を使うことの問題です。

@Test
public void whenCreatesFunction__thenCorrect5() {
    Function5<String, String, String, String, String, String> concat =
      (a, b, c, d, e) -> a + b + c + d + e;
    String finalString = concat.apply(
      "Hello ", "world", "! ", "Learn ", "Vavr");

    assertEquals("Hello world! Learn Vavr", finalString);
}

メソッド参照からVavr関数を作成するために、任意の関数に対して静的ファクトリメソッド FunctionN.of を組み合わせることもできます。次のような sum メソッドがあるとします。

public int sum(int a, int b) {
    return a + b;
}

このようにしてそれから関数を作成できます。

@Test
public void whenCreatesFunctionFromMethodRef__thenCorrect() {
    Function2<Integer, Integer, Integer> sum = Function2.of(this::sum);
    int summed = sum.apply(5, 6);

    assertEquals(11, summed);
}

6. コレクション

Vavrチームは、関数型プログラミングの要件、すなわち永続性、不変性を満たす新しいコレクションAPIの設計に多大な努力を払ってきました。

  • Javaコレクションは変更可能で、特に同時実行性がある場合には、プログラムの失敗の大きな原因になります。 Collection インターフェースは、次のようなメソッドを提供します。

interface Collection<E> {
    void clear();
}

このメソッドはコレクション内のすべての要素を削除し(副作用を生み出し)、何も返しません。 ConcurrentHashMap などのクラスは、すでに作成されている問題に対処するために作成されました。

そのようなクラスは、限界利益をゼロにするだけでなく、抜け穴を埋めようとしているクラスのパフォーマンスも低下させます。

  • 不変性により、スレッドセーフになります。** :そもそも存在しないはずの問題に対処するために新しいクラスを作成する必要はありません。

Javaのコレクションに不変性を追加するための他の既存の戦術では、さらに多くの問題、つまり例外が発生します。

@Test(expected = UnsupportedOperationException.class)
public void whenImmutableCollectionThrows__thenCorrect() {
    java.util.List<String> wordList = Arrays.asList("abracadabra");
    java.util.List<String> list = Collections.unmodifiableList(wordList);
    list.add("boom");
}

上記の問題はすべて、Vavrコレクションには存在しません。

Vavrでリストを作成するには:

@Test
public void whenCreatesVavrList__thenCorrect() {
    List<Integer> intList = List.of(1, 2, 3);

    assertEquals(3, intList.length());
    assertEquals(new Integer(1), intList.get(0));
    assertEquals(new Integer(2), intList.get(1));
    assertEquals(new Integer(3), intList.get(2));
}

リスト上で計算を実行するためのAPIも用意されています。

@Test
public void whenSumsVavrList__thenCorrect() {
    int sum = List.of(1, 2, 3).sum().intValue();

    assertEquals(6, sum);
}

Vavrコレクションは、Java Collections Frameworkにあるほとんどの共通クラスを提供し、実際にはすべての機能が実装されています。

重要なのは、 不変性 void戻り型の削除 副作用生成API 、基礎となる要素を操作するための 豊富な 関数、Javaのコレクションに比べて非常に短く、堅牢かつ コンパクトなコード です。オペレーション。

Vavrコレクションの全容はこの記事の範囲を超えています。

7. 検証

Vavrは、関数型プログラミングの世界からJavaに「応用関数」の概念をもたらします。最も単純な用語では、** 応用機能 によって、結果を累積しながら一連のアクションを実行できます。

クラス vavr.control.Validation はエラーの蓄積を容易にします。通常、プログラムはエラーが発生するとすぐに終了します。

ただし、 Validation は、プログラムがエラーをバッチとして処理するためにエラーの処理と累積を続けます。

name age でユーザーを登録していて、最初にすべての入力を取り、 Person インスタンスを作成するかエラーのリストを返すかを決定したいとします。これが Person クラスです。

public class Person {
    private String name;
    private int age;

   //standard constructors, setters and getters, toString
}

次に、 PersonValidator というクラスを作成します。各フィールドは1つのメソッドによって検証され、別のメソッドを使用してすべての結果を1つの Validation インスタンスにまとめることができます。

class PersonValidator {
    String NAME__ERR = "Invalid characters in name: ";
    String AGE__ERR = "Age must be at least 0";

    public Validation<Seq<String>, Person> validatePerson(
      String name, int age) {
        return Validation.combine(
          validateName(name), validateAge(age)).ap(Person::new);
    }

    private Validation<String, String> validateName(String name) {
        String invalidChars = name.replaceAll("[a-zA-Z]", "");
        return invalidChars.isEmpty() ?
          Validation.valid(name)
            : Validation.invalid(NAME__ERR + invalidChars);
    }

    private Validation<String, Integer> validateAge(int age) {
        return age < 0 ? Validation.invalid(AGE__ERR)
          : Validation.valid(age);
    }
}

age の規則は、それが0より大きい整数でなければならないということであり、 name の規則は、それが特殊文字を含んではいけないということです。

@Test
public void whenValidationWorks__thenCorrect() {
    PersonValidator personValidator = new PersonValidator();

    Validation<List<String>, Person> valid =
      personValidator.validatePerson("John Doe", 30);

    Validation<List<String>, Person> invalid =
      personValidator.validatePerson("John? Doe!4", -1);

    assertEquals(
      "Valid(Person[name=John Doe, age=30])",
        valid.toString());

    assertEquals(
      "Invalid(List(Invalid characters in name: ?!4,
        Age must be at least 0))",
          invalid.toString());
}
  • 有効な値は Validation.Valid インスタンスに含まれ、検証エラーのリストは Validation.Invalid インスタンス** に含まれます。そのため、どの検証メソッドでも2つのうちの1つを返す必要があります。

Inside Validation.Valid Person のインスタンスですが、inside Validation.Invalid はエラーのリストです。

8怠け者

Lazy は遅延計算された値を表すコンテナです。

結果が要求されるまで計算は延期されます。さらに、評価された値はキャッシュまたは記憶され、計算を繰り返すことなく必要になるたびに何度も返されます。

@Test
public void givenFunction__whenEvaluatesWithLazy__thenCorrect() {
    Lazy<Double> lazy = Lazy.of(Math::random);
    assertFalse(lazy.isEvaluated());

    double val1 = lazy.get();
    assertTrue(lazy.isEvaluated());

    double val2 = lazy.get();
    assertEquals(val1, val2, 0.1);
}

上記の例では、評価している関数は Math.random です。

2行目で、値を確認して、関数がまだ実行されていないことを確認します。これは、戻り値にまだ関心が示されていないためです。

3行目のコードでは、 Lazy.get を呼び出して計算値に関心を示しています。この時点で、関数が実行され、 Lazy.evaluated がtrueを返します。

また、先に進み、値をもう一度取得しようとして、 Lazy の記憶ビットを確認します。我々が提供した機能が再び実行された場合、我々は間違いなく異なる乱数を受け取るでしょう。

ただし、 Lazy は最後のアサーションが確認したときに、再び最初に計算された値を遅延的に返します。

9パターンマッチング

パターンマッチングは、ほとんどすべての関数型プログラミング言語におけるネイティブの概念です。今のところJavaにはそのようなことはありません。

代わりに、受け取った入力に基づいて計算を実行したり値を返したりしたい場合は、実行する正しいコードを解決するために複数の 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つのケースをチェックしているだけで、突然コードが複数行にわたることがわかります。各チェックは3行のコードを占めます。 100件までチェックしなければならないとしたら、300行程度になるでしょう。

別の方法は 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);
}

良くありません。私たちはまだ1小切手あたり3行を平均しています。多くの混乱とバグの可能性。 break 句を忘れてもコンパイル時には問題になりませんが、後で検出しにくいバグが生じる可能性があります。

Vavrでは、 switch ブロック全体を Match メソッドで置き換えます。

それぞれの case または if ステートメントは、 Case メソッド呼び出しに置き換えられます。

最後に、 $() のようなアトミックパターンは、式または値を評価する条件を置き換えます。これを Case の2番目のパラメータとしても提供します。

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

コードが非常にコンパクトであることに注意してください。チェックごとに平均1行だけです。パターンマッチングAPIはこれよりもはるかに強力で、より複雑なことを実行できます。

たとえば、原子式を述語に置き換えることができます。

help フラグと version フラグのコンソールコマンドを解析しているとします。

Match(arg).of(
    Case($(isIn("-h", "--help")), o -> run(this::displayHelp)),
    Case($(isIn("-v", "--version")), o -> run(this::displayVersion)),
    Case($(), o -> run(() -> {
        throw new IllegalArgumentException(arg);
    }))
);

短縮版(-v)に慣れているユーザーもいれば、フルバージョン(–version)に慣れているユーザーもいます。優れた設計者は、これらすべてのケースを考慮する必要があります。

いくつかの if ステートメントを必要とせずに、複数の条件を処理しました。述語、複数の条件、パターンマッチングの副作用については、別の記事で詳しく学びます。

10結論

この記事では、Java 8用の一般的な関数型プログラミング・ライブラリーであるVavrを紹介しました。私たちは、コードを改善するために素早く適応できる主な機能に取り組みました。

この記事の完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/vavr[Githubプロジェクト]にあります。