はじめに
たまたま最近、業務でエラーかノーマルを保持するオブジェクトのレビューが上がってきた時に、「Eitherにすれば?」と言ったのだが、JavaにはOptional
はあるがEither
に相当するようなクラスがない。
作るとしたらこんな感じかな〜、というのをリリースされたLTS版Java17を使いながら書いてみる。(結局、レビュー指摘はコミットされなかったが・・・。)
Either
import java.util.function.Function;
import java.util.function.Consumer;
//Eitherインターフェイス
public sealed interface Either<L,R> permits Either.Left,Either.Right{
public boolean isLeft();
default boolean isRight(){
return !this.isLeft();
}
public <A> Either<L,A> flatMap(Function<R,Either<L,A>> f);
public default <A> Either<L,A> map(Function<R,A> f){
return this.flatMap((R r) -> Either.rightOf(f.apply(r)));
}
public void foreach(Consumer<R> action);
public static <L,R> Either<L,R> leftOf(L value){
return new Left<L,R>(value);
}
public static <L,R> Either<L,R> rightOf(R value){
return new Right<L,R>(value);
}
//Right型
public static final record Right<L,R>(R value) implements Either<L,R>{
public boolean isLeft(){
return false;
}
public <A> Either<L,A> flatMap(Function<R,Either<L,A>> f){
return f.apply(this.value);
}
public void foreach(Consumer<R> action){
action.accept(this.value);
}
}
//Left型
public static final record Left<L,R>(L value) implements Either<L,R>{
public boolean isLeft(){
return true;
}
public <A> Either<L,A> flatMap(Function<R,Either<L,A>> f){
return new Left<L,A>(this.value);
}
public void foreach(Consumer<R> action){
return;
}
}
}
sealed
インターフェイスとクラスにsealed/permits
をつけて継承できるものを制限できるようになった。ここではEither
インターフェイスの継承をEither.Right
と Either.Left
に限定している。
record
Scalaのcase class
っぽいやつ。equals
やhashCode
,toString
メソッドなどが生える。
record
はfinal
なフィールドで、イミュータブルになる。(セッターに該当するメソッドは生えない)
Main
Eitherを使う方。
import static java.lang.System.out;
public class Main{
public static void main(String... args){
//型推論する場合でも、結局は型を教えてあげないといけない。
final var e1 = Either.<String,Integer>rightOf(10);
//型宣言の場合
final Either<String,Integer> e2 = Either.leftOf("Error!");
//flatMap/map でscalaのforの代わり
final var e3 = e1.flatMap(n1 -> e2.map(n2 -> n1 + n2));
out.println(e3) //Left[value=Error!]
}
}
Scalaと異なり、Nothing型
がないので、Eitherオブジェクトの生成は、ちょっと冗長になってしまう。
また、for式
(do記法
)もないので、自力でflatMap/mapでコンテキスト保った処理を書く必要があるので、やや使いにくい。
Scala相当
Scala相当だとこんな形になるかと。
val e1:Either[String,Int] = Right(10)
val e2:Either[String,Int] = Left("Error!")
val e3 = for (n1 <- e1; n2 <- e2) yield (n1 + n2)
println(e3)
おまけでプレビュー版のswitch
パターンマッチ
プレビュー版機能なので、コンパイラとJVMの両方に--enable-preview
オプションをつける。
import static java.lang.System.out;
public class Main{
public static void main(String... args){
final var e1 = Either.<String,Integer>rightOf(10);
int ret = switch(e1) {
case Either.Right<String,Integer> right -> right.value();
case Either.Left<String,Integer> left -> 0;
}
out.println(ret) // 10
}
}
$ javac --enable-preview --release 17 Main.java Either.java
$ java --enable-preview Main
終わりに
Javaのinterface
のデフォルトメソッドを、Scalaのtrait
と同じ感覚でミックスイン的に使って良いのかは、未だ謎・・・。
昔どこかで(Java8が出た当初) 「interfaceのdefaultメソッドは、Listインターフェイスなど広く利用される標準ライブラリのインターフェイスにメソッド追加したいためだから、普通は使わない方が良い」的な文章を見た記憶が・・・。