Pulog

vavr を用いた scala ライクなパターンマッチの使い方

Java で条件分岐というと、 if 文だったり swith 文、三項演算子等を思い浮かべると思うのですが、分岐のパターンが多くなってくるとどうしても可読性が下がってしまうのは仕方ないとも思いつつ、もう少しスマートな書き方は存在しないだろうかと思っていました。

そんな中、 vavr というライブラリに含まれているパターンマッチがなかなか良さげだったので紹介します。

※蛇足として vavr 上下反転させて一部大文字に置き換えると JAVA と読めるらしい。 vAvrJAVA 、確かに……

使い方

Gradle であれば、 build.gradledependencies に以下のライブラリを追加するだけで準備完了です。

build.gradle
dependencies {
  // https://mvnrepository.com/artifact/io.vavr/vavr
+ implementation group: 'io.vavr', name: 'vavr', version: '0.10.4'
}

シンプルな実装例

商品が "A" なら 1,000円、"B"なら2,000円、"C", "D", "E"いずれかなら3,000円。
それ以外なら "そんな商品は無い" とエラーを出すような処理です。

ifswitch を用いるなら一度 price 変数を宣言をして初期化し、それぞれの条件分岐のブロック内で再代入をする必要がありますが、 vavr を用いたパターンマッチであれば一度の変数宣言で目的の価格をセットできています。

コードの量も減らしつつ、変数 pricefinal 修飾子もつけられ、 ifswitch を用いた条件分岐より読みやすいコードになりました。

import java.util.Scanner;

import static io.vavr.API.*;
import static io.vavr.Predicates.isIn;

public class Example1 {

  public static void main(String[] _args) {

    System.out.println("商品名を入力してください");
    try (final var scanner = new Scanner(System.in)) {
      final var product = scanner.next();
      final var price = Match(product).option(
        Case($("A"), 1_000),
        Case($("B"), 2_000),
        Case($(isIn("C", "D", "E")), 3_000)
      ).getOrElseThrow(IllegalArgumentException::new);

      System.out.println("商品: " + product + ", 価格: " + price);
    } catch (IllegalArgumentException _e) {
      System.out.println("そんな商品は無い");
    }

  }

}

上記の実装の通り、 Case 関数でそれぞれのパターンと返したい値をセットで宣言することで機能します。

また、パターンをセットする $ 関数には以下がセット出来ます。

  • $() ワイルドカード
    • 上記では使用していないですが、 switch 文で言う default みたいなものが存在します。
  • $(value)
    • Match の引数と照らし合わせて同じかを判断するための値。
  • $(predicate)
    • java.util.Predicate を渡してあげれば内部で判定が行われます。 isIn メソッドを内包している io.vavr.Predicates はあくまで java.util.Predicate の util クラスとなります。

getOrElseThrowException を投げていますが、そもそもパターンを全て網羅できるのであれば option() の箇所を of() に差し替えてあげることが出来ます。

また、 getOrElseThrow の箇所を toJavaOptional だったり toJavaStream などに置き換えて Java の基本的なオブジェクトに変換ももちろん可能です。

実行例
> Task :Example1.main()
商品名を入力してください
C
商品: C, 価格: 3000

BUILD SUCCESSFUL in 4s

戻り値が不要な void メソッドを叩きたいだけの場合

run 関数が用意されているので、その中に書いてあげればなし得られます。

import java.util.List;
import java.util.Map;

import static io.vavr.API.*;
import static io.vavr.Predicates.instanceOf;

public class Example2 {

  public static void main(String[] _args) {

    List<Object> list = List.of(2, "HOGE", true, Map.of());
    list.forEach(key ->
      Match(key).of(
        Case($(instanceOf(String.class)), o -> run(() -> System.out.println("渡された文字は: " + o))),
        Case($(instanceOf(Integer.class)), o -> run(() -> System.out.println("3でかけた数は: " + 3 * o))),
        Case($(instanceOf(Boolean.class)), o -> run(() -> System.out.println("反転: " + !o))),
        Case($(), o -> run(() -> System.out.println("unknown")))
      )
    );

  }

}

上記のサンプルでお気づきかと思うのですが、 io.vavr.Predicates.instanceOf() で型判定を噛ますと、 lambda で渡されるパラメーターは型キャストがされた状態で渡されるので、上記のような System.out.println() が可能になっています。

これは Java 16 で採用された JEP 394: Pattern Matching for instanceof みたいなパターンマッチを取り入れていると言えます。

つい先日 Java 11 に変わる時期 LTS (長期サポート)な Java 17 がリリースされましたが、まだ Java 11 を使う場面も多いと思うので、重宝しそうです。

実行例
> Task :Example2.main()
3でかけた数は: 6
渡された文字は: HOGE
反転: false
unknown

BUILD SUCCESSFUL in 493ms

vavr の Tuple を使用した応用例

Java の標準には無い Tuple を vavr は用意してくれているので、合わせ技でより複雑な条件分岐の実装をすることも出来ます。

Tuple (タプル)とは順序付けをした複数の組み合わからなるオブジェクトみたいなものです。

以下の例ではおにぎりの具材とドリンクのサイズでセット割引の価格を出している例です (もうちょっと良い例は無かったのだろうか)

import io.vavr.Tuple;

import java.util.Arrays;
import java.util.Scanner;
import java.util.function.Function;
import java.util.stream.Collectors;

import static io.vavr.API.*;
import static io.vavr.Patterns.$Tuple2;
import static io.vavr.Predicates.isIn;
import static io.vavr.patternMatching.Example3.DrinkSize.SizeL;
import static io.vavr.patternMatching.Example3.RiceBall.*;

public class Example3 {

  public enum RiceBall {
    TunaMayo, Beef, Salt, Salmon, Umeboshi
  }

  public enum DrinkSize {
    SizeS, SizeM, SizeL
  }

  private static final Function<Enum<? extends Enum<?>>[], Object> options = e -> Arrays.stream(e)
          .map(Enum::name)
          .collect(Collectors.joining("|"));

  public static void main(String[] _args) {

    try (final var scanner = new Scanner(System.in)) {
      System.out.println("おにぎりを選択してください[" + options.apply(RiceBall.values()) + "]");
      var riceBall = RiceBall.valueOf(scanner.next());

      System.out.println("ドリンクのサイズを選択してください[" + options.apply(DrinkSize.values()) + "]");
      var drinkSize = DrinkSize.valueOf(scanner.next());

      var setMinus = Match(Tuple.of(riceBall, drinkSize)).of(
              Case($Tuple2($(isIn(TunaMayo, Salt, Umeboshi)), $()), 20),
              Case($Tuple2($(Beef), $()), 50),
              Case($Tuple2($(), $(SizeL)), 35),
              Case($(), 30)
      );

      System.out.println("セット料金で" + setMinus + "円安くなりました。");
    } catch (Exception e) {
      e.printStackTrace();
    }

  }

}

上記の実装は ykato/vavrExample に格納しているので、参考になれば幸いです。

実行例
> Task :Example3.main()
おにぎりを選択してください[TunaMayo|Beef|Salt|Salmon|Umeboshi]
Umeboshi
ドリンクのサイズを選択してください[SizeS|SizeM|SizeL]
SizeL
セット料金で20円安くなりました。

BUILD SUCCESSFUL in 9s

参考

やっぱり公式ドキュメント見るのが一番良いと思います。

Vavr User Guide

それでわ。