はじめに
「関数型言語を学ぶことは実務でどう役に立ったか」という記事を見て、RxJavaを使ってみたいと思いました。
さて、RxJavaのwikiのオペレータの説明の多くは、Groovyで書かれています(こことか)。RxGroovyを使えば、RxJavaのAction1<T>やFunc0<R>のようなSAMインターフェースを引数にとるメソッドで、SAMインターフェースの変わりにクロージャを使えるようになります。それにより、Java 6やJava 7でのSAMインターフェース周りの無駄に冗長な記述が、Groovyのクロージャに置き換わり非常に簡潔になります。
Observable.from("Taro", "Jiro", "Saburo").subscribe{
println "Hello " + it + "!"
}
さて、Groovyの知識も経験も全然無い私ですが、Groovyの言語機能として暗黙的なクロージャの強制型変換というものがあり、SAMインターフェースを引数にとるメソッドで、変わりにクロージャを引数に渡せることは以前から知っていました。
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Log.v(TAG, "buttonClicked");
}
});
button.setOnClickListener { Log.v(TAG, "buttonClicked") }
「あれ、じゃあ?RxGroovyっていらなくない?Groovyって、もともとSAMインターフェースの引数の変わりにクロージャを渡せるじゃん!?」って小一時間悩んでしまいました。
実は、RxGroovyの対応バージョンはGroovy 2.0以上、そして暗黙的なクロージャの強制型変換がGroovyに追加されたのはGroovy 2.2からのようです。RxGroovyを使うことで、Groovy 2.0系や2.1系でもSAMインターフェースの変わりにクロージャを引数に渡せるようになるのですね。
この投稿では、
- RxGroovyを使わずGroovy 2.2以上でクロージャを使って簡潔にRxJavaを使うこと
- RxGroovyがやっていること
について分かる範囲でまとめてみました。
RxGroovyを使わずにGroovy 2.2以上を使いクロージャで簡潔に記述
Groovy 2.2以前から、SAMインターフェースを実装するインスタンスを、asとクロージャを使って生成することができました。(Groovy way to implement interfacesのImplement interfaces with a closure参照)
Runnable runnable = { println "Hello" } as Runnable
Groovy 2.2から、暗黙的な強制型変換(Implicit closure coercion)が言語機能に加わりました。
それにより以下のようなことが可能になりました。
def sample(Runnable runnable){
runnable.run()
}
Runnable runnable = { println "Hello" }
runnable.run()
sample({ println "Hello" })
// sample { println "Hello" } も可
さて、RxJavaでこれを使う場合を見てみます。まずJava6やJava7の場合の記述は、こんな感じになると思います。
import rx.Observable;
import rx.functions.Action1;
/* 略 */
Observable.from("Taro", "Jiro", "Saburo").subscribe(new Action1<String>() {
@Override
public void call(String string) {
System.out.println("Hello " + string + "!");
}
});
/* 略 */
次はRxGroovyを使わないでかつ、Groovy 2.0 or 2.1を使う場合を考えます。この条件でもクロージャとasで、簡潔なコーディングが可能です。
@Grab(group='com.netflix.rxjava', module='rxjava-core', version='0.20.4')
import rx.Observable;
import rx.functions.Action1;
Observable.from("Taro", "Jiro", "Saburo").subscribe(
{ println "Hello ${it}!" } as Action1
)
簡潔にはなりましたがasが邪魔ですね。次はRxGroovyを使わないでかつ、Groovy 2.2以上使う場合を考えます。
@Grab(group='com.netflix.rxjava', module='rxjava-core', version='0.20.4')
import rx.Observable;
import rx.functions.Action1;
Observable.from("Taro", "Jiro", "Saburo").subscribe({ println "Hello ${it}!" })
// Observable.from("Taro", "Jiro", "Saburo").subscribe{ println "Hello ${it}!" } でも可
非常にスッキリしましたね。
RxGroovyを使わなくても、Groovy 2.2以上を使えば、暗黙的なクロージャの強制型変換により、クロージャを用いて簡潔にRxJavaを使うことができますね。
RxGroovyは拡張モジュールを使って、クロージャ用のメソッドを追加している
拡張モジュール
Groovyでは、以前からカテゴリやMOPという機能を使って既存のクラスにメソッドを追加することができます。Groovy 2.0で、拡張モジュール(Extension modules)という機能が追加され、これを使ってメソッドを追加することも可能になりました。
基本的な拡張モジュールを使ってメソッドを追加する方法では、次の
- 拡張したいメソッドを定義したクラス(ヘルパークラス)
- モジュールディスクリプタ
を用意します。
拡張したいメソッドを定義したクラス(ヘルパークラス)は、カテゴリと同じ形式で追加するメソッドを定義します。
モジュールディスクリプタは、モジュールのMETA-INF/services
ディレクトリの中にorg.codehaus.groovy.runtime.ExtensionModule
というファイル名で用意します。その中で、モジュール名、バージョン、staticメソッドの拡張を定義したクラス、インスタンスメソッドの拡張を定義したクラスを記述します。
次のページがとても分かりやすいです。
さて、Creating an extension moduleというページの、Advanced modulesという節で、上記の方法とは違う拡張メソッドの定義方法があることが説明されています。org.codehaus.groovy.runtime.m12n.ExtensionModuleというクラスを使う方法のようです。
RxGroovyは、ExtensionModuleを使って拡張モジュールによるメソッド追加をしている
RxGroovyはExtensionModuleを使ってメソッドを追加して、クロージャをメソッドの引数として扱えるようにしています。次の資料がとても分かりやすく、大変参考になりました。
ExtensionModuleを使った拡張モジュールによるメソッド追加も、モジュールディスクリプタを用意するようです。RxGroovyだと、こいつですね。さてこのモジュールディスクリプタは、
moduleFactory=rx.lang.groovy.RxGroovyPropertiesModuleFactory
となっています。なってはいるのですが、先ほどの資料以外、このモジュールディスクリプタの書き方に言及している資料が見つからず。なんで、moduleFactory
という名前で設定するのかとか。多分私の調べ方が悪いのでしょうが...もしよかったら、「ここに資料がある」など教えていただけると嬉しいです。
リファレンスでモジュールディスクリプタの書き方は見つからなかったのですが、moduleFactory
というプロパティ名に該当する文字列は、StandardPropertiesModuleFactoryというクラスのMODULE_FACTORY_KEYというクラス定数にも定義されているようです。また、META-INF/services/org.codehaus.groovy.runtime.ExtensionModule
という文字列、はStandardPropertiesModuleFactoryというクラスのMODULE_META_INF_FILEというクラス定数にも定義されているようです。
さて、RxGroovyのミソの一つはモジュールディスクリプタに名前が記載されているrx.lang.groovy.RxGroovyPropertiesModuleFactoryのようです。このRxGroovyPropertiesModuleFactoryクラスは、ExtensionModuleのサブクラスである、rx.lang.groovy.RxGroovyExtensionModuleを生成します。
ExtensionModuleのクラスリファレンスには次のように説明されていました。
An extension module is a class responsible for providing a list of meta methods to the Groovy compiler and runtime. Those methods are use to "dynamically extend" exisiting classes by adding methods to existing classes.
なるほど、RxGroovyExtensionModuleクラスでMetaMethodを提供するのですね。(ここ)
RxGroovyExtensionModuleでは、ObservableクラスとBlockingObservableクラスにメソッドを追加しているようです。(ここ)
Action1<T>やFunc0<R>やObservable.OnSubscribe<T>などは、Functionというインターフェースを継承したSAMインターフェースです。ObservableクラスとBlockingObservableクラスには、これらを引数にとるメソッドがいくつかあります。ObservableクラスとBlockingObservableクラスが、これらのメソッドでFunctionインターフェースの変わりにクロージャを引数に取った時(ここ)、引数のクロージャをGroovyActionWrapperやGroovyFunctionWrapperというクラスでラップして、クロージャを扱えるようにしているみたいです。(こことかこことか)
まとめ
- RxJavaを利用する際、Groovy 2.2以上ならば暗黙的クロージャの強制型変換によりSAMインターフェースの変わりにクロージャを使える
- RxGroovyでは、拡張モジュールによりGroovy 2.0や2.1でもSAMインターフェースの変わりにクロージャを用いて、RxJavaを使えるようにしている
コードの字面は同じでも、Groovy 2.2で暗黙的クロージャの強制型変換を使っているのと、RxGroovyの拡張モジュールによる効能では、内部で実行されていることは違うので、その点には注意が必要です。
「ここ、違うぞ!」などあったら、コメントいただけると嬉しいです。