vavr を用いた scala ライクなパターンマッチの使い方
Java で条件分岐というと、 if
文だったり swith
文、三項演算子等を思い浮かべると思うのですが、分岐のパターンが多くなってくるとどうしても可読性が下がってしまうのは仕方ないとも思いつつ、もう少しスマートな書き方は存在しないだろうかと思っていました。
そんな中、 vavr というライブラリに含まれているパターンマッチがなかなか良さげだったので紹介します。
※蛇足として vavr
上下反転させて一部大文字に置き換えると JAVA
と読めるらしい。 vAvr
と JAVA
、確かに……
使い方
Gradle であれば、 build.gradle
の dependencies
に以下のライブラリを追加するだけで準備完了です。
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円。
それ以外なら "そんな商品は無い" とエラーを出すような処理です。
if
や switch
を用いるなら一度 price
変数を宣言をして初期化し、それぞれの条件分岐のブロック内で再代入をする必要がありますが、 vavr を用いたパターンマッチであれば一度の変数宣言で目的の価格をセットできています。
コードの量も減らしつつ、変数 price
に final
修飾子もつけられ、 if
や switch
を用いた条件分岐より読みやすいコードになりました。
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 クラスとなります。
-
getOrElseThrow
で Exception
を投げていますが、そもそもパターンを全て網羅できるのであれば 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
参考
やっぱり公式ドキュメント見るのが一番良いと思います。
それでわ。