2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Java17でEither書く

Posted at

はじめに

たまたま最近、業務でエラーかノーマルを保持するオブジェクトのレビューが上がってきた時に、「Eitherにすれば?」と言ったのだが、JavaにはOptionalはあるがEitherに相当するようなクラスがない。
作るとしたらこんな感じかな〜、というのをリリースされたLTS版Java17を使いながら書いてみる。(結局、レビュー指摘はコミットされなかったが・・・。)

Either

Either.java
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.RightEither.Leftに限定している。

record

Scalaのcase classっぽいやつ。equalshashCode,toStringメソッドなどが生える。
recordfinalなフィールドで、イミュータブルになる。(セッターに該当するメソッドは生えない)

Main

Eitherを使う方。

Main.java
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相当だとこんな形になるかと。

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オプションをつける。

Main
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インターフェイスなど広く利用される標準ライブラリのインターフェイスにメソッド追加したいためだから、普通は使わない方が良い」的な文章を見た記憶が・・・。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?