halvaでJavaをScalaっぽく使おう

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

JavaでScalaっぽい機能を使う

Javaも8になってLambdaやStreamなどかなり便利になりましたが、Scalaに比べるとまだまだ物足りない人もいるかと思います。
開発でScalaのを使いたいけど、Scalaを使って開発するのはちょっと気が引ける・・・JavaでScalaの機能使えたらいいのにー
というのを実現してくれるライブラリが、Halvaです。  

Halvaとは

Halvaは、Annotation Processingを使って、JavaでScalaの各種イディオムを使用できるようにするライブラリです。
弊社blogで紹介しているlombokも同じ仕組みですね。

下記のような、Scalaが持つ機能をJavaで使用することができます。  

  • Case Classes and Case Objects
  • Pattern Matching and Extraction
  • For Comprehensions
  • Implicits
  • Type Aliases
  • Tuples
  • Constructor Sugars
  • Type Containers
  • Custom Monadic Fors
  • Anys

環境

今回使用した動作環境は以下のとおりです。

  • OS : MacOS X 10.10.5
  • java : 1.8.0_51
  • IDE : IDEA 15.0.2

設定

今回はIDEAのGradleプロジェクトを使用してサンプルを作成しました。
build.gradleは下記のようになります。halvaへの依存を記述するだけです。  

group 'halva-test'
version '1.0-SNAPSHOT'

apply plugin: 'java'
apply plugin: 'idea'

apply plugin: 'application'

mainClassName = '<YourMainClass>'

repositories {
    mavenCentral()
}

dependencies {
    // https://mvnrepository.com/artifact/io.soabase.halva/halva-processor
    compile group: 'io.soabase.halva', name: 'halva-processor', version: '0.2.1'
    // https://mvnrepository.com/artifact/io.soabase.halva/halva-parent
    compile group: 'io.soabase.halva', name: 'halva-parent', version: '0.2.1', ext: 'pom'
    // https://mvnrepository.com/artifact/io.soabase.halva/halva
    compile group: 'io.soabase.halva', name: 'halva', version: '0.2.1'
}

あとはIDEA上でAnnotation Processingが使えるように、
PreferencesのEnable annotation processingにチェックをつけておきます。

halva

halvaのサンプル実装

では、halvaを試してみましょう。

Caseクラス

ScalaといえばCaseクラスです。
Caseクラスとは、newでインスタンス化しなくてもOKとかequalsやtoStringを自動で用意しておいてくれたりとか、
パターンマッチで使えたりとかするとっても便利なやつです。
(Scalaのcaseクラスについてはこのへん参照)  

halvaでは@CaseClassをインターフェイスに付与することで、Caseクラスを定義することができます。

import io.soabase.halva.caseclass.CaseClass;

@CaseClass
public interface Example {
    String firstName();
    String lastName();
    int age();
}

Exampleを定義してコンパイルしたら、classes以下にあるgeneratedディレクトリの中身を確認してみてください。
「ExampleCase」という名前で、アクセサやコンストラクタが定義された実装クラスが生成されています。

// Auto generated from x.Example by Soabase io.soabase.halva.caseclass.CaseClass annotation processor
@Generated("io.soabase.halva.caseclass.CaseClass")
public class ExampleCase implements Serializable, Example, Tuplable, ClassTuplable {
    private static final Class classTuplableClass = ExampleCaseAny(Any.any(), Any.any(), Any.any()).getClass();

    private final String firstName;

    private final String lastName;

    private final int age;

    protected ExampleCase(String firstName, String lastName, int age) {
        if ( firstName == null ) {
            firstName = "";
        }
        if ( lastName == null ) {
            lastName = "";
        }
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

//これ以降もいろいろ記述されている    

・・・・・

}

生成されたcaseクラスは下記のように使用できます。

Example myExample = ExampleCase("syuta", "nakamura", 36); //インスタンス生成
System.out.println(myExample); // ExampleCase("syuta", "nakamura", 36)

参考:Caseクラス

パターンマッチ

Caseクラスが使えるならパターンマッチもつけなければ意味がありません。
もちろんパターンマッチも使用可能です。
下のサンプルは文字列をマッチさせる例です。

import io.soabase.halva.any.Any;
import io.soabase.halva.any.AnyVal;
import static io.soabase.halva.matcher.Matcher.match;

・・・java 

String sampleString = "hello";                     

AnyVal<String> str = new AnyVal<String>() {};

String result = match(sampleString)
    .caseOf(str, () -> "string is " + str.val()) 
    .get();

System.out.println(result); //  string is hello

caseクラスに対してのパターンマッチは下記。

Example myExample = ExampleCase("syuta", "nakamura", 36);

AnyVal<String> firstName = new AnyVal<String>(){};
AnyVal<String> lastName = new AnyVal<String>(){};
AnyVal<Integer> age = new AnyVal<Integer>(){};

String result = match(myExample)
    .caseOf(str, () -> "It's " + str.val())
    .caseOf(ExampleCaseAny(firstName,lastName,age), 
    () -> "object is " + firstName.val() + "," + lastName.val() + "," + age.val()).get();

System.out.println(result); //  object is syuta,nakamura,36

halvaでのパターンマッチについては、いろいろ使い方があるので、ドキュメントを参照してみてください。

参考:パターンマッチ

Tuple

便利なTupleも使えます。

import io.soabase.halva.tuple.Tuple;
import io.soabase.halva.tuple.details.Tuple2;
import static io.soabase.halva.tuple.Tuple.Tu;

・・・

Tuple2<String, Integer> myTuple = Tu("one", 2);
Tuple2<Integer, String> swapTuple = myTuple.swap();

参考:Tuple

For Comprehensions

for内包表記ってやつですね。これもhalvaで使用することができます。
動作確認のために、AnimalとZooというcaseクラスを作成します。

import io.soabase.halva.caseclass.CaseClass;
import java.util.List;

@CaseClass interface Animal {
    String name();
}

@CaseClass interface Zoo {
    String name();
    List<Animal> animals();
}

定義したcaseクラスを使い、forCompを使用してオブジェクトの探索をしています。
サンプルではループを回しながら、animals.sizeが2でlionを持つオブジェクトをTupleにして返しています。  

import java.util.Arrays;
import java.util.List;
import static io.soabase.halva.comprehension.For.forComp;
import static ZooCase.ZooCase;

・・・

//サンプルデータ作成
Animal lion = AnimalCase("lion");
Animal kangaroo = AnimalCase("kangaroo");
Animal bear = AnimalCase("bear");
Zoo zoo1 = ZooCase("zoo-1", Arrays.asList(lion, kangaroo, bear));
Zoo zoo2 = ZooCase("zoo-2", Arrays.asList(kangaroo, bear));
Zoo zoo3 = ZooCase("zoo-3", Arrays.asList(lion, bear));

List<Zoo> zooList = Arrays.asList(zoo1, zoo2, zoo3);

AnyVal<Zoo> zoo = Any.any();
AnyVal<Animal> animal = Any.any();

//ループを回しながら、animals.sizeが2でlionを持つオブジェクトをTupleにして返す
List<Tuple> result = forComp(zoo, zooList)
            .filter(() -> zoo.val().animals().size() == 2)
            .forComp(animal, () -> zoo.val().animals())
            .filter(() -> animal.val().name().equals("lion"))
            .yield(() -> Tu(zoo.val().name(), animal.val().name()));

System.out.println(result); //[("zoo-3", "lion")]

参考:For Comprehensions

まとめ

今回はJavaでScalaのイディオムを使用するためのライブラリ、halvaの紹介をしました。 紹介した機能の他にもいろいろと使えそうな機能があるので、別途紹介します。

参考サイトなど