Play2(Java) on Java8(体験版)

Play framework 2.x Java and 1.x Advent Calendar 2013 - Adventarの13日目かつ2回目のエントリです。
孤高の戦士の一人です。


Play2もScalaフレームワークとして一定の認知を得てきた感ありますが、Javaは無いことにされがちで寂しい思いをしています。
確かにScalaだと使える機能が使えなかったりとフレームワークとしての力を十全使えるわけではないのでどちらがより良いかと言われたらScalaなのですが、だからと言ってダメダメかと言われたらそうではないはず、きっと。
とは言え、Scalaの文化もあってか、PlayやPlayが提供するクラスのAPIには関数を引数にとるものがそこそこあって、一応Javaのラッパは提供されているのですが、まぁ、しんどいよね*1、という場面がちょいちょいあります。
ただし、Java8のラムダ式が使えたらこの辺ちょっと世界が変わって見えるんじゃない?と思ったので試してみたよ、というのがこのエントリです。

準備

javaは1.8.0-ea、Playのバージョンは2.2.1を使いました。ただし、このままだとPlayアプリのコンパイルがこけます。
sbtがJava8の新しい命令に対応してないのが原因らしいので、sbtのバージョンを0.13.1-RC2以降に引き上げます。
id:xuwei さん曰く

らしく、せいぜいエラーが出なくなるだけらしいですが、試してみる分にはとりあえずこれで動きます。

Promise

Play2では非同期処理を簡単にかけます。
ScalaのFutureをラップしているPromiseというクラスが提供されているのですが、このクラスにこんな感じで関数を渡すことで処理を非同期化出来ます。

import play.libs.F.*;

public class Hoge {
    
    public void hoge(final int a, final int b) {

        // なんかしら整数を返す計算
        Promise<Integer> integerPromise = Promise.promise(new Function0<Integer>(){
            @Override
            public Integer apply() throws Throwable {
                return a + b;
            }
        });

        // なんかしら計算結果を文字列に変換
        Promise<String> stringPromise = integerPromise.map(new Function<Integer, String>(){
            @Override
            public String apply(Integer integer) throws Throwable {
                return "result is " + integer;
            }
        });

        // 更にその結果を出力
        stringPromise.onRedeem(new Callback<String>() {
            @Override
            public void invoke(String string) throws Throwable {
                System.out.println(string);
            }
        });
    }
}

ゴテゴテですね。
これが、Java8だとラムダ式が使えるようになるのでこう書けるようになるわけです。

import play.libs.F.*;

public class Hoge {
    
    public void hoge(int a, int b) {

        Promise<Integer> integerPromise = Promise.promise(() -> a + b);

        Promise<String> stringPromise = integerPromise.map(integer -> "result is " + integer);

        stringPromise.onRedeem(System.out::println);
    }
}

スッキリ!
実質的finalのおかげで、final修飾子も必要無くなりました。

import play.libs.F.*;

public class Hoge {
    
    public void hoge(int a, int b) {

        Promise
            .promise(() -> a + b)
            .map(i-> "result is " + i)
            .onRedeem(System.out::println)
        ;
    }
}

メソッドチェーンで繋げたら更にスッキリしました。つらくない!

これが出来るようになると、Play2(Java)で非同期処理を扱う際のつらさが激減すると思います。Play2では非同期レスポンスを扱う場合、controllerのメソッドの戻り値をPromise型にする必要がありますが、ラムダ式が使えるとそれも上記と同様に、下のようにシンプルに書けます。

package controllers;

import play.libs.F.*;
import play.mvc.*;
import views.html.index;

public class Application extends Controller {

    public static Result index() throws Exception {
        return ok(index.render("Your new application is ready."));
    }

    public static Promise<? extends Result> async() throws Exception {

        return Promise.promise(() -> {

            System.out.println("何かの処理");

            return ok(index.render("Your new application is ready aync"));
        });
    }
}

WebSocket

Play2ではWebSocketも簡単に扱えます。Play2でWebSocketを利用する場合は以下のようにします。

package controllers;

import play.libs.F.*;
import play.mvc.*;

public class Application extends Controller {

    public static WebSocket<String> webSocket() {

        // 抽象クラスであるWebSocketのonReadyを実装して返す
        return new WebSocket<String>() {

      // クライアントが接続してきた時に呼ばれる
            @Override
            public void onReady(final In<String> in, final Out<String> out) {
                
                // こんにちは!こんにちは!
                out.write("hello");
                
                // クライアントから何か入力されてきた時の処理を登録する
                in.onMessage(new Callback<String>() {
                    @Override
                    public void invoke(String message) throws Throwable {
                        out.write("message coming");
                        out.write(message);
                    }
                });
            }
        };
    }
}

簡単だけど面倒くさそう。
Java8が使えるようになった未来なら、これをもうちょっとスッキリ書けます。

    static <T> WebSocket<T> ws(Callback2<WebSocket.In<T>, WebSocket.Out<T>> f) {
        return new WebSocket<T>() {
            @Override
            public void onReady(In<T> in, Out<T> out) {
                try {
                    f.invoke(in, out);
                } catch (Throwable throwable) {
                    throw new RuntimeException(throwable);
                }
            }
        };
    }

ちょっとしたヘルパーメソッドを定義します。InとOutを引数に受け取って何かする関数fを引数にもらい、WebSocketオブジェクトのインスタンスを返します。受け取った関数をonReadyで実行するように定義しているだけですね。
で、このヘルパーを使ってさっきのメソッドを、Java8で実装すると以下のようになります。

package controllers;

import play.libs.F.*;
import play.mvc.*;

public class Application extends Controller {

    public static WebSocket<String> webSocket() {
        return ws((in, out) -> {

            out.write("hello");

            in.onMessage( message-> {
                out.write("message is coming");
                out.write(message);
            });
        });
    }
}

だいぶ見通しが良くなった気がしますね。*2

OptionとかEither

Play2にはScalaではおなじみのOptionやEitherを模倣したクラスがJava用に提供されています。これらのクラスもScalaよろしく関数を引数にとるメソッドがあるので、これまで同様一層簡素に書けるようになります。

Option<String> opt = Option.Some("1");
opt.map(i -> String.valueOf(i)).forEach(s -> System.out.println(s));
        
Either<Exception, String> e = Either.Right("1");
e.right.map(i -> String.valueOf(i)).forEach(System.out::println);

Java7まではmapメソッドに無名クラスを放り込む必要があったのでとてもじゃないけど普段使いしたいとは思わない感じでしたが、上記のように書けるなら常用する気も起きそうです。
ただし、Optionに関してはJava8からOptionalクラスが標準に追加されていて、そっちの方が高機能なのでそっち使ったほうがよさそうな気がします。

まとめ

Java8が使えるようになった未来、Play2(Java)アプリはこんな感じで書けるようになるんじゃないかな、というエントリでした。
Scalaがベースになっているお陰で処理を引数にとるAPIがちょこちょこあって、それが、JavaからPlayを使う時のつらみの一因になっている気がします。少なくとも僕はPromiseをmapで繋げたりするとウヒィッってなります。
しかし、Java8ならその手のつらさは大幅に解消されそうです。
Play(Java)、案外アリだと思いますがどうですかね?

ところで

Q. ほとんどScalaっぽいしいっそScala使えばいいのでは?
A. Scala使えない現場や書けない人だっているんですよ!


Q. Play2使える現場、多分Scala使えるように交渉するのそんなに難しくないのでは?
A. ウッ。


Q. ラムダ式サクサク使いこなせる人、Scala書けるようになるのそんなに難易度高くないのでは?
A. グワーッ


明日はmomma kazukoさんです。

*1:無名クラス

*2:WebSocketクラスが抽象クラスじゃなければヘルパーも要らなくてもっとスッキリ書けるはずなのだけど…。