パターンマッチングライブラリを作ってみた
Java8 でラムダ式が導入されたことで関数定義がシンプルに書けるようになりました。
このラムダ式を使えば Scala のパターンマッチみたいな事もある程度できるよねってことで pattern-matching4j というライブラリを作りました。
ライブラリの作成においては以下の記事を大変参考にさせていただきました。
ありがとうございます。
http://www.akirakoyasu.net/2012/12/22/re-thinking-pattern-matching-in-java/
http://d.hatena.ne.jp/nowokay/20131212
使い方
早速どんな感じに使うのか見ていきましょう。
FizzBuzz
FizzBuzz を print するメソッドは以下のように書けます。
void fizzbuzz(int number) {
match(number, //
caseBoolean(n -> n % 15 == 0, n -> System.out.println("fizzbuzz")), //
caseBoolean(n -> n % 3 == 0, n -> System.out.println("fizz")), //
caseBoolean(n -> n % 5 == 0, n -> System.out.println("buzz")), //
caseDefault(n -> System.out.println(n)));
}
PatternMatchers というクラスを static import しています。
matchメソッドは第1引数に対象の値、第2引数以降は各ケースの表現(条件関数 + 処理関数)を可変長引数で渡すことができます。
最初にマッチしたケースだけ実行されます。
caseBooleanメソッドは第1引数にマッチング条件として boolean を返す関数(Predicate)を指定し、第2引数にマッチした時に実行される関数(Consumer)を指定します。
caseDefaultメソッドは必ずマッチするケースで、実行される関数(Consumer)のみ指定します。
値を返す場合は以下のように書けます。
String fizzbuzz_(int number) {
return match(number, //
caseBoolean_(n -> n % 15 == 0, n -> "fizzbuzz"), //
caseBoolean_(n -> n % 3 == 0, n -> "fizz"), //
caseBoolean_(n -> n % 5 == 0, n -> "buzz"), //
caseDefault_(n -> n.toString()));
}
値を返さない場合との違いは、ケースを表現するメソッドが値をreturnする用のメソッドに変わったことと、実行される関数が値を返す関数(Function)に変わっています。
様々なパターンマッチング
上記の FizzBuzz の例では caseBoolean, caseDefault というメソッドが登場しました。
これらの他にも様々なパターンマッチングができます。
定数パターン
指定した値と一致するかどうかがマッチング条件になります。
switch文みたいなものですが、どんな型でも使用できます。
int num = 1;
match(num, //
caseValue(0, o -> fail()), //
caseValue(1, o -> assertThat(o, is(num))), //
caseDefault(o -> fail()));
型パターン
指定したクラスのインスタンスかどうかがマッチング条件になります。
instanseof みたいなものですが、castを書かないで済みます。
Number integer = 1;
match(integer,//
caseType(Integer.class, i -> assertThat(i, is(1))), //
caseType(Double.class, s -> fail()), //
caseDefault(o -> fail()));
ワイルドカードパターン
switch文のdefaultと同じようなものです。
これまでの例で出てきた caseDefault ですね。
条件パターン
上記の FizzBuzz では boolean を返す Predicate で条件を表現しました.
他にも org.hamcrest.Matcher や正規表現、型パターンとの組み合わせも使用できます。
org.hamcrest.Matcher
int num = 20;
match(num, //
caseMatcher(lessThan(10), s -> fail()), //
caseMatcher(greaterThanOrEqualTo(10), s -> assertThat(s, is(num))), //
caseDefault(o -> fail()));
正規表現
String str = "_test";
match(str, //
caseRegex(Pattern.compile("^test"), s -> fail()), //
caseRegex(Pattern.compile("^_"), s -> assertThat(s, is(str))), //
caseDefault(o -> fail()));
型パターンとの組み合わせ
Number integer = 1;
match(integer,//
caseType(Integer.class, i -> i == 0, i -> assertThat(i, is(integer))), //
caseType(Double.class, i -> i > 0, i -> fail()), //
caseType(Integer.class, i -> i > 0, i -> assertThat(i, is(integer))), //
caseDefault(o -> fail()));
null パターン
null の場合や not null の場合のパターン。
他にも書き方はありますが、null 専用の case メソッドを用意しています。
String str = "test";
match(str, //
caseNull(() -> fail()), //
caseNotNull(s -> assertThat(s, is(str))));
Optional パターン
Optional にも専用のメソッドを用意しています。
Optional#ifPresent(Consumer) と違って値が存在しない場合の処理も同時に指定できます。
String str = "test";
Optional<String> opt = Optional.ofNullable(str);
match(opt, //
casePresent(s -> assertThat(s, is(str))), //
caseEmpty(() -> fail()));
2つの値をマッチ対象にする
2つの値に対して別々にパターンを指定することができます。
Scala の Tuple パターンに似ていますが、Tuple ではないですし、残念ながら値を3つ以上指定することはできません。。。
以下は2つの値のマッチングが両方とも定数パターンの場合です。
int num1 = 5;
int num2 = 9;
match(num1, num2, //
caseValue(5, 5, (i1, i2) -> fail()), //
caseValue(5, 9, (i1, i2) -> assertThat(i1, is(num1))), //
caseDefault((i1, i2) -> fail()));
メソッドチェインで様々なパターンを組み合わせる書き方もできます。
以下は FizzBuzz の例で、定数パターンと型パターンを組み合わせています。
(caseDefaultでは型解決できないためcaseAny(Integer.class)としています。)
void fizzbuzz(int number) {
match(number % 3, number % 5, //
caseValue(0, 0, (i1, i2) -> System.out.println("fizzbuzz")), //
caseValue(0).and(caseAny(Integer.class)).then((i1, i2) -> System.out.println("fizz")), //
caseAny(Integer.class).and(caseValue(0)).then((i1, i2) -> System.out.println("buzz")), //
caseDefault((i1, i2) -> System.out.println(number)));
}
インストール方法
pattern-matching4j は Maven Central Repository にアップロードしています。
Gradle を使用する場合、以下のように依存ライブラリ設定を書くことで使用できます。
repositories {
mavenCentral()
}
dependencies {
compile 'com.github.equus52:pattern-matching4j:0.1.1'
}
まとめ
Java8 ラムダ式を使ったパターンマッチングでした。
近年、Java はかなり進化して使いやすくなりました。
Java の安定感・硬さは好きなので、より良い Java になってほしいと思っています。