1. 概要

この記事では、 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 は、Java8のOptionalのような同様の最終目標を持つVavrのオブジェクトコンテナです。 VavrのOptionは、シリアライズ可能、反復可能、を実装し、より豊富なAPIを備えています。

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を処理する必要はありませんでした。 オプションのtoStringは、各呼び出しで意味のある値を返します。

このセクションの2番目のスニペットでは、 null チェックが必要でした。このチェックでは、変数を使用する前に、デフォルト値を変数に割り当てます。 Option は、nullがある場合でも、これを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はタプルをJava8にもたらします。 タプルのタイプは、使用する要素の数に応じて、 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許容オブジェクトをラップするため、nullsifチェックで明示的に処理する必要がないため、Trytry-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の登場により、機能インターフェイスが組み込まれ、特にラムダと組み合わせると使いやすくなりました。

ただし、Java8は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などのパラメーターの数に応じて名前が付けられます。 Vavrを使用すると、上記の2つの関数を次のように記述できます。

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

パラメータがないが出力が必要な場合、Java8ではSupplier タイプを使用する必要があり、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);
}

また、任意の関数の静的ファクトリメソッド FunctionN.of を組み合わせて、メソッド参照からVavr関数を作成することもできます。 次の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コレクションフレームワークに見られるほとんどの一般的なクラスを提供し、実際にはすべての機能が実装されています。

要点は、不変性ボイドリターンタイプの削除副作用生成API 、基礎となる要素を操作するためのより豊富な関数のセットです[ 、Javaの収集操作と比較して、非常に短く、堅牢で、コンパクトコード

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

7. 検証

Vavrは、関数型プログラミングの世界からJavaに ApplicativeFunctorの概念をもたらします。 簡単に言うと、 Applicative Functorを使用すると、結果を蓄積しながら一連のアクションを実行できます

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

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

nameageでユーザーを登録していて、最初にすべての入力を取得して、 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つを返す必要があります。

Validation.Validの内部はPersonのインスタンスであり、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を返します。

また、値を get に再試行して、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行になり、良くありません。

もう1つの方法は、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);
}

これ以上はありません。 チェックごとに平均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. 結論

この記事では、Java8用の人気のある関数型プログラミングライブラリであるVavrを紹介しました。 コードを改善するためにすばやく適応できる主要な機能に取り組みました。

この記事の完全なソースコードは、Githubプロジェクトで入手できます。