Java Vavr 1

Java の ライブラリの1つの Vavr の Either を使ってみます。

環境

Vavr とは

Vavr は 独自のコレクションと関数型の制御機能を含む Java のオープンソースのライブラリです。

公式サイト
GitHub

Either とは

Either とは 成功と失敗のどちらかの状態を表すデータ構造です。

Javaの標準の機能であるOptionalも成功と失敗のどちらかの状態を表すことができますが失敗のときの詳細な情報を表すことができません。Optionalの場合はOptioanl.emptyだけです。

一方Eitherは、Either<String, Integer>のように成功と失敗それぞれに特定の型を定義できます。

使用方法

pom.xmlに依存を追加します。

1
2
3
4
5
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.2</version>
</dependency>

コード例

例として、ある数字aを10倍して、ある数字bで割るプログラムを考えます。
(このプログラムに意味はありません。)

Optional を使った場合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Optional<Integer> optional(Supplier<Optional<Integer>> func1, Integer value) {
Function<Integer, Integer> func2 = x -> x * 10;
BiFunction<Integer, Integer, Optional<Integer>> func3 = (x, y) -> {
if (y == 0) {
return Optional.empty();
}
return Optional.of(x / y);
};

Optional<Integer> data1 = func1.get();
Optional<Integer> data2 = data1.map(func2);
Optional<Integer> data3 = data2.flatMap(x -> func3.apply(x, value));
return data3;
}

このメソッドの引数は2つです。

  1. ある数字aを取得するSupplier。aを取得できない可能性があり、戻り値はOptional<Integer>としています。
  2. ある数字b

このメソッドを実行したときに失敗するケースは2つです。

  1. bの値が0の場合、整数を0で割ることはできないので、func3の結果がOptional.empty()になります。
  2. aを取得できない場合。つまりfunc1Optional.empty()を返した場合です。

では成功する場合、失敗する場合のテストコードを書いてみます。

Optional を使った場合(1)

まずは成功する場合です。

1
2
3
4
5
6
7
@Test
public void optional_1() {
Optional<Integer> data3 = optional(() -> Optional.of(10), 2);

assertThat(data3.isPresent()).isTrue();
assertThat(data3.get()).isEqualTo(50);
}

10 * 10 / 2 = 50なので、結果はOptional[50]となります。

Optional を使った場合(2)

続いて、bの値が0で失敗する場合です。

1
2
3
4
5
6
@Test
public void optional_2() {
Optional<Integer> data3 = optional(() -> Optional.of(10), 0);

assertThat(data3.isPresent()).isFalse();
}

戻り値はOptional.emptyです。

Optional を使った場合(3)

続いて、aを取得できずに失敗する場合です。

1
2
3
4
5
6
@Test
public void optional_3() {
Optional<Integer> data3 = optional(() -> Optional.empty(), 2);

assertThat(data3.isPresent()).isFalse();
}

こちらも戻り値はOptional.emptyです。
このように、戻り値がOptionalの場合、失敗したことはわかるけど何故かまではわかりません。
もちろんログを出力していればわかりますが、メソッドの結果として扱うことはできないという意味です。

Either を使った場合

Eitherは、Either<L, R>のように失敗と成功の2つの型を指定します。
どちらを使ってもいいわけではなく、LはLeftで失敗の時のデータ型、RはRightで成功のときのデータ型です。
これは、正しい(right)と右(right)から、Rには成功の結果を、そうではないLに失敗の結果を持つようです。

以下はコードの例です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Either<String, Integer> either(Supplier<Either<String, Integer>> func1, Integer value) {
Function<Integer, Integer> func2 = x -> x * 10;
BiFunction<Integer, Integer, Either<String, Integer>> func3 = (x, y) -> {
if (y == 0) {
return Either.left("value divide by zero");
}
return Either.right(x / y);
};

Either<String, Integer> data1 = func1.get();
Either<String, Integer> data2 = data1.map(func2);
Either<String, Integer> data3 = data2.flatMap(x -> func3.apply(x, value));
return data3;
}

このメソッドの引数は2つです。

  1. ある数字aを取得するSupplier。aを取得できない可能性があり、戻り値はEither<String, Integer>としています。
  2. ある数字b

このメソッドを実行したときに失敗するケースは2つです。

  1. bの値が0の場合、整数を0で割ることはできないので、func3の結果がEither.left()になります。
  2. aを取得できない場合。つまりfunc1Either.left()を返した場合です。

では成功する場合、失敗する場合のテストコードを書いてみます。

Either を使った場合(1)

まずは成功する場合です。

1
2
3
4
5
6
7
@Test
public void either_1() {
Either<String, Integer> data3 = either(() -> Either.right(10), 2);

assertThat(data3.isRight()).isTrue();
assertThat(data3.get()).isEqualTo(50);
}

10 * 10 / 2 = 50なので、結果はEither.right[50]となります。
EitherのRightの値を取得するためにgetを使っていますが、これはOptionalgetと同じように良くない方法です。
EitherにもmapgetOrElseなどが用意されているので本来はそちらを使いましょう。

Either を使った場合(2)

続いて、bの値が0で失敗する場合です。

1
2
3
4
5
6
7
@Test
public void either_2() {
Either<String, Integer> data3 = either(() -> Either.right(10), 0);

assertThat(data3.isLeft()).isTrue();
assertThat(data3.getLeft()).isEqualTo("value divide by zero");
}

戻り値はEither.left["value divide by zero"]です。
失敗の内容はgetLeftで取得することができます。

Either を使った場合(3)

続いて、aを取得できずに失敗する場合です。

1
2
3
4
5
6
7
@Test
public void either_3() {
Either<String, Integer> data3 = either(() -> Either.left("not found"), 2);

assertThat(data3.isLeft()).isTrue();
assertThat(data3.getLeft()).isEqualTo("not found");
}

戻り値はEither.left["not found"]です。

このようになぜ失敗したかの理由をLeftに保持できます。
この記事ではLeftにString型を使用していますが、独自の方やExceptionを指定することもできます。

今回は以上です。

コード

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.kujilabo</groupId>
<artifactId>vavr-001</artifactId>
<version>1.0.0-SNAPSHOT</version>

<properties>
<java.version>11</java.version>
</properties>

<dependencies>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.2</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.11.1</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>

</project>

EitherTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.kujilabo;

import static org.assertj.core.api.Assertions.assertThat;

import io.vavr.control.Either;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import org.junit.Test;

public class EitherTest {

Optional<Integer> optional(Supplier<Optional<Integer>> func1, Integer value) {
Function<Integer, Integer> func2 = x -> x * 10;
BiFunction<Integer, Integer, Optional<Integer>> func3 = (x, y) -> {
if (y == 0) {
return Optional.empty();
}
return Optional.of(x / y);
};

Optional<Integer> data1 = func1.get();
Optional<Integer> data2 = data1.map(func2);
Optional<Integer> data3 = data2.flatMap(x -> func3.apply(x, value));
return data3;
}

@Test
public void optional_1() {
Optional<Integer> data3 = optional(() -> Optional.of(10), 2);

assertThat(data3.isPresent()).isTrue();
assertThat(data3.get()).isEqualTo(50);
}

@Test
public void optional_2() {
Optional<Integer> data3 = optional(() -> Optional.of(10), 0);

assertThat(data3.isPresent()).isFalse();
}

@Test
public void optional_3() {
Optional<Integer> data3 = optional(() -> Optional.empty(), 2);

assertThat(data3.isPresent()).isFalse();
}

Either<String, Integer> either(Supplier<Either<String, Integer>> func1, Integer value) {
Function<Integer, Integer> func2 = x -> x * 10;
BiFunction<Integer, Integer, Either<String, Integer>> func3 = (x, y) -> {
if (y == 0) {
return Either.left("value divide by zero");
}
return Either.right(x / y);
};

Either<String, Integer> data1 = func1.get();
Either<String, Integer> data2 = data1.map(func2);
Either<String, Integer> data3 = data2.flatMap(x -> func3.apply(x, value));
return data3;
}


@Test
public void either_1() {
Either<String, Integer> data3 = either(() -> Either.right(10), 2);

assertThat(data3.isRight()).isTrue();
assertThat(data3.get()).isEqualTo(50);
}

@Test
public void either_2() {
Either<String, Integer> data3 = either(() -> Either.right(10), 0);

assertThat(data3.isLeft()).isTrue();
assertThat(data3.getLeft()).isEqualTo("value divide by zero");
}

@Test
public void either_3() {
Either<String, Integer> data3 = either(() -> Either.left("not found"), 2);

assertThat(data3.isLeft()).isTrue();
assertThat(data3.getLeft()).isEqualTo("not found");
}
}