30
29

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 5 years have passed since last update.

JavaAdvent Calendar 2014

Day 19

Java8 ラムダ式を使ったパターンマッチングライブラリを作った

Posted at

パターンマッチングライブラリを作ってみた

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 するメソッドは以下のように書けます。

FizzBuzz(値を返さない場合)
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)のみ指定します。

値を返す場合は以下のように書けます。

FizzBuzz:値をreturnする場合
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

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 メソッドを用意しています。

nullパターン
String str = "test";
match(str, //
    caseNull(() -> fail()), //
    caseNotNull(s -> assertThat(s, is(str))));

Optional パターン

Optional にも専用のメソッドを用意しています。
Optional#ifPresent(Consumer) と違って値が存在しない場合の処理も同時に指定できます。

Optionalパターン
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つの値のマッチングが両方とも定数パターンの場合です。

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)としています。)

2つの値をマッチ対象にする_メソッドチェイン_FizzBuzz
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-matching4jMaven Central Repository にアップロードしています。
Gradle を使用する場合、以下のように依存ライブラリ設定を書くことで使用できます。

build.gradle
repositories {
  mavenCentral()
}
dependencies {
  compile 'com.github.equus52:pattern-matching4j:0.1.1'
}

まとめ

Java8 ラムダ式を使ったパターンマッチングでした。

近年、Java はかなり進化して使いやすくなりました。
Java の安定感・硬さは好きなので、より良い Java になってほしいと思っています。

30
29
4

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
30
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?