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つです。
- ある数字
a
を取得するSupplier。a
を取得できない可能性があり、戻り値はOptional<Integer>
としています。
- ある数字
b
。
このメソッドを実行したときに失敗するケースは2つです。
b
の値が0の場合、整数を0で割ることはできないので、func3
の結果がOptional.empty()
になります。
a
を取得できない場合。つまりfunc1
がOptional.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つです。
- ある数字
a
を取得するSupplier。a
を取得できない可能性があり、戻り値はEither<String, Integer>
としています。
- ある数字
b
。
このメソッドを実行したときに失敗するケースは2つです。
b
の値が0の場合、整数を0で割ることはできないので、func3
の結果がEither.left()
になります。
a
を取得できない場合。つまりfunc1
がEither.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
を使っていますが、これはOptional
のget
と同じように良くない方法です。
Eitherにもmap
やgetOrElse
などが用意されているので本来はそちらを使いましょう。
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"); } }
|