groovy-macro-methodsについて(後編)

はじめに

これはG*アドベントカレンダーの最終日、第25日目の記事です。

今回は後編ということで、前編でご紹介したgroovy-macro-methodsを使用したマクロのサンプルをいくつか作成しましたので、これらのサンプルをご紹介することで「groovy-macro-methodsを使うとこんなことができるんだ」と思っていただけると幸いです。

なお、本記事は半年くらい前のワークショップで発表する予定だったネタですが、時間の都合上発表できなかったので、今回もその時のネタを使い回すことにしました。

サンプルについて

今回サンプルとして、「常に使いたくなるようなマクロ」ではなく「四年に一度くらい使いたいと思うようなマクロ」を目標として、5つ、マクロを作ってみました。 マクロ自体の内容は割と微妙ですが、「マクロのサンプルとしては手頃な内容なのでは?」と思っています。

サンプルのコードの場所とビルド

今回のgroovy-macro-methodsの
サンプルは、githubでコードを公開しています。

$ git clone https://github.com/touchez-du-bois/akatsuki.git

でコードをクローンし、

$ cd akatsuki
$ ./gradlew build

で、サンプルのクラスのみのjarファイルbuild/libs/akatsuki-x.y.z.jarが作成され、

$ cd akatsuki
$ ./gradlew shadowJar

で、サンプルのクラスとgroovy-macro-methodsのクラスが入ったjarファイルbuild/libs/akatsuki-x.y.z-all.jarが作成されます。

ちなみに、"akatsuki"は、この娘です。

f:id:touchez_du_bois:20151220002348j:plain

動作確認環境

動作確認は次の環境で行いました。

  • OS X 10.9.5 (Mavericks)
  • Java SE 8u66
  • Groovy 2.4.5
  • groovy-macro-methods 0.3.0

サンプルの概要

サンプルとして作成したマクロですが、次のとおりです。

  • nullSafeマクロ
  • doWhileマクロ
  • newTraitマクロ
  • matchマクロ
  • doWithDataマクロ

以降、実行結果を踏まえマクロの説明をしていきたいと思います。

nullSafeマクロ

JavaにはなくてGroovyにある便利な機能と言えば、いくつかありますが、その一つにnull safe operator "?"があります。

def result = a.b.c.d.e.f()

といったコードで、aからeのいずれかがnullの場合、NullPointerExceptionがスローされます。この場合、

def result = a?.b?.c?.d?.e?.f()

とnull safe operatorを明示すれば、途中がnullであってもNullPointerExceptionはスローされず、resultはnullとなります。

それでも、四年に一度くらい「自動でnull safe operatorを付けてくれないかなぁ…」と思ったことはないでしょうか?

そこで、nullSafeマクロです。

nullSafeマクロでは、メソッド呼び出しやプロパティ参照の対象となるオブジェクトに遡ってnull safe operatorを付けるようにASTを変換します。さらに、メソッド呼び出しの場合、引数のメソッド呼び出しやプロパティ参照に対してもnull safe operatorを付けるようにASTを変換します。

以下はnullSafeマクロの使用例です。

class A {
    String hello() {
        "Hello!"
    }
}
class B {
    A a
}
class C {
    B b
}

C obj = new C()
assert nullSafe(obj.b.a.hello()) == null

上の例の場合、nullSafeマクロに渡されたメソッド呼び出し「obj.b.a.hello()」が次のようにnull safe operatorを付けたASTに変換され実行されます。

assert obj?.b?.a?.hello() == null

doWhileマクロ

JavaにあってGroovyにないもの、
と言えば、do〜while文ですが、四年に一度くらい「do〜while文使いたいなぁ」と思ったことはないでしょうか?

そこでdoWhileマクロです。

厳密には「do〜while文」ではなく「do〜while式」なのですが、条件式とClosureを受け取って
「do〜while文」っぽいことをします。

def x = 0
dowhile ({
    x++
}, x == 0)
assert x == 1

または

def x = 0
dowhile (x == 0) {
    x++
}
assert x == 1

のように使います。

上の例の場合、doWhileマクロに渡された条件とClosureを、次のようなコードのASTに変換し実行します。

{
    { x++ }.call()
    while (x == 0) {
        { x++ }.call()
    }
}.call()

newTraitマクロ

Groovyの2.3から追加された機能
と言えば、traitですが、traitのインスタンスを直接生成することはできず、

  1. クラス定義時にimplementsでtraitを指定
  2. オブジェクトにasキーワードまたはwithTraitsメソッドでtraitを指定

する必要があります。例えば、

trait FlyingAbility {
    String fly() {
        "I'm flying!"
    }
}

というtraitがあった場合、

class Bird implements FlyingAbility {
}
def b = new Bird()
assert b.fly() == "I'm flying!"

あるいは

class Bird {
}
def b = new Bird() as FlyingAbility
assert b.fly() == "I'm flying!"

というように使用します。

ですが、四年に一度くらい「traitのインスタンスが簡単に欲しいなぁ」と思ったことはないでしょうか?

そこで、newTraitマクロです。厳密には、Objectクラスのインスタンスにtraitを組み込んだプロキシオブジェクトを生成していますので、traitのインスタンスを生成、というわけではないのですが。

別の例になりますが、

trait Sample {
    String name
    int age
    String hello() {
       "Hello, my name is ${name}, and my age is ${age}"
    }
}
def x = newTrait(Sample, name: "aaa", age: 10)
assert x.hello() == "Hello, my name is aaa, and my age is 10"

というコードは、コンパイル時に、

{
    def obj = new Object().withTraits(Sample)
    obj['name'] = 'aaa'
    obj['age'] = 10
    return obj
}.call()

と、Objectクラスのインスタンスにtraitを組み込んだプロキシオブジェクトを生成、かつ初期化パラメータがある場合は初期化する、というASTに変換され実行されます。

matchマクロ

Groovyにあるといいなと思う機能
と言えば、match式ですが、四年に一度くらい「match式が使いたいなぁ」と思ったことはないでしょうか?

そこで、matchマクロです。Sergei Egorovさんのスライドに例として載っていたのですが、コードはなかったので実装してみました。

例えば、階乗を求めるメソッドで、matchマクロを使って実装してみたのが次のコードです。

def fact(num) {
    return match(num) {
        when String then fact(num.toInteger())    // (1)
        when (0|1) then 1    // (2)
        when 2 then 2    // (3)
        orElse num * fact(num - 1)    // (4)
    }
}
assert fact("5") == 120

matchマクロの引数を使って、その後のクロージャ内で分岐をしています。(1)ではnumがStringクラスの場合、(2)ではnumが0か1の場合、(3)ではnumが2の場合、(4)ではそれ以外の場合、といった判断分岐をしています。

上記のコードは、コンパイル時に

def fact(num) {
    return { it ->
        if ( it instanceof java.lang.String) {
            return this.fact(num.toInteger())
        }
        if ( it == 0 || it == 1) {
            return 1
        }
        if ( it == 2) {
            return 2
        }
        return num * this.fact( num - 1)
    }.call( num )
}

と、if文とreturn文のASTに変換され実行されます。

doWithDataマクロ

Spockにある便利な機能
と言えば、データテーブルですが、四年に一度くらい「テストコードじゃないところでデータテーブルを使いたいなぁ」と思ったことはないでしょうか?

def "データテーブルの例"() {
    expect:
        a + b == c

    where:
        a | b || c
        1 | 2 || 3
        4 | 5 || 9
        7 | 8 || 15
}

そこで、doWithDataマクロです。普通のGroovyコードで、Spockのようなデータテーブルを使うことができます。

doWithData {
    dowith:
        assert a + b == c

    where:
        a | b || c
        1 | 2 || 3
        4 | 5 || 9
        7 | 8 || 15
}

whereラベルの1行目で変数を宣言し、2行目以降に変数に設定する値を定義します。whereラベルで宣言した値は、行毎にdowithラベルで記述したコード中に変数に設定され実行されます。

上記のコードは、

{
    {   def a = 1 ; def b = 2 ; def c = 3
        assert a + b == c  }
    {   def a = 4 ; def b = 5 ; def c = 9
        assert a + b == c  }
    {   def a = 7 ; def b = 8 ; def c = 15
        assert a + b == c  }
}.call()

と、現状ベタに展開するASTに変換され実行されます。

最後に

前回と今回の2回にわたりgroovy-macro-methodsと、groovy-macro-methodsを使ったマクロのサンプルを紹介しました。マクロ自体、作るのがちょっと大変ですが、工夫次第で面白いことができることを実感していただけたら幸いです。

groovy-macro-methodsについて(前編)

はじめに

これはG*アドベントカレンダーの第14日目の記事です。

今回は、groovy-macro-methodsをご紹介します。

なお、本記事は半年くらい前のワークショップで発表する予定だったネタですが、時間の都合上発表できなかったので、今回その時のネタを使い回すことにしました。

groovy-macro-methodsとは

簡単に言いますと、groovy-macro-methodsは、AST(Abstract Syntax Tree : 抽象構文木)を引数に受け取り変換したASTを返すメソッドを、マクロ(コンパイル時にASTを変換する機能)としてサポートするためのライブラリです。

詳細は、次のspeakerdeckを参照してください。

groovy-macro-methodsの例

例えば、

def nullObject = null

assert null == nullObject.hashCode()

といったコードがある場合、NullPointerExceptionで落ちます。これを、

def nullObject = null

assert null == safe(nullObject.hashCode())

という風にsafeマクロで「コンパイル時にASTを変換」して、NullPointerExceptionで落ちないようにしたいと思います。つまり、「nullObject.hashCode()」といったメソッド呼び出しのASTを引数に受け取り、

def nullObject = null

assert null == (nullObject != null ? nullObject.hashCode() : null)

といった三項演算子のASTに変換してコンパイル時に置き換える、というマクロを定義します。この場合のsafeマクロの定義は次のようになります。

package foo.bar;

import org.codehaus.groovy.ast.expr.*;
import static org.codehaus.groovy.ast.tools.GeneralUtils.*;
import ru.trylogic.groovy.macro.runtime.*;

public class TestMacroMethods {
    @Macro
    public static Expression safe(MacroContext macroContext,
                                  MethodCallExpression callExpression) {
        return ternaryX(notNullX(callExpression.getObjectExpression()),
                                callExpression, constX(null));
    }
}

引数に「メソッド呼び出しの結果」ではなく「メソッド呼び出しのAST」を受け取り、三項演算子のASTに変換した結果(のAST)を戻り値として返します。 notNullX()が「引数がnullかどうかを判定するAST」を生成するメソッド(三項演算子の「nullObject != null」の部分)、constX()が「引数を定数として表すAST」を生成するメソッド、ternaryX()が「三項演算子のAST」を生成するメソッド、となります。

groovy-macro-methodsを使用したマクロの作り方

groovy-macro-methodsを使用してマクロを作る際、次の点に気をつけてください。

  1. public staticメソッドを用意する。
  2. メソッドに@Macroを付ける。
  3. メソッドの第一引数はMacroContextとする。
  4. メソッドの第二引数以降は変換したいASTのクラスとする。
  5. 戻り値はExpressionかExpressionの派生クラスとする。
  6. メソッド定義とは別に、拡張モジュールの定義を行う。

public staticメソッドを用意する。

マクロは、public staticのメソッドとして定義します。public staticでないとマクロとして認識してくれません。

メソッドに@Macroを付ける。

このメソッドがマクロであると宣言します。

メソッドの第一引数はMacroContextとする。

メソッドの第一引数はMacroContext型の値です。MacroContext自体は、次のクラスのオブジェクトを持ちます。

  • CompilationUnit
  • SourceUnit
  • マクロ自体のMethodCallExpression

先の例のように、必ずしも使用しないといけないわけではありません。

メソッドの第二引数以降は変換したいASTのクラスとする。

メソッドの第二引数以降は変換したいASTのクラスを指定します。 ただし、指定できるASTのクラスは、Expression(式)かその派生クラスのみとなります。なので、Statement(文)やその派生クラスをマクロに渡して変換することはできません。 これをなんとかしたい場合、ClosureExpression(クロージャ式)を引数とするマクロを定義し、ClosureExpression内のBlockStatement内のStatementを変換、といったことならできます。

戻り値はExpressionかExpressionの派生クラスとする。

戻り値はExpression(式)かExpressionの派生クラスとなります。Statement(文)やその派生クラスを戻り値として返すことはできません。 これをなんとかしたい場合、Closureを定義してcallメソッドを呼び出す、つまりClosureExpression(クロージャ式)のMethodCallExpression(メソッド呼び出し)を戻り値として返すマクロを定義し、そのClosureExpressionの中でStatementやその派生クラスを定義する、といったことならできます。

メソッド定義とは別に、拡張モジュールの定義を行う。

メソッドを定義するだけではマクロとして機能せず、groovyの拡張モジュールの定義を行う必要があります。

ファイル「META-INF/services/org.codehaus.groovy.runtime.ExtensionModule」を作成し、extensionClassesにマクロの定義を持つクラスのFQCN(完全修飾クラス名)を指定します。例えば先のsafeマクロの拡張モジュールの定義は次のとおりです。

moduleName=macro-module
moduleVersion=1.0
extensionClasses=foo.bar.TestMacroMethods 

最終的には

マクロを使用した場合、最終的には、

def nullObject = null

assert null == safe(nullObject.hashCode())

というコードは、

def nullObject = null

assert null == 
    MacroStub.INSTANCE.macroMethod
        (nullObject != null ?
            nullObject.hashCode() : null)

というコードにコンパイル時に変換され実行されます。macroMethodメソッド自体は、

public enum MacroStub {
    INSTANCE;
    public <T> T macroMethod(T obj) {
        return obj;
    }
}

のように引数を返すだけのメソッド呼び出しとなっています。

マクロの使い所

普通のメソッド呼び出しで事足りるのであれば、マクロを使う必要はないと思います。普通のメソッド呼び出しでは実現できない場合にマクロ(= AST変換)が使えないか検討してみてください。例えば、DSL(Domain Specification Language)、糖衣構文(シンタックスシュガー)、等です。

マクロ開発の問題点

マクロを開発する上で問題となることと言えば、マクロがどういうASTを返しているか、想定したASTを返しているか、といったことがわかりづらい、ということでしょう。

これを解決するために、GroovyConsoleを使用します。GroovyConsoleで「Inspect Ast」を実行すると、「Semantic Analysis」以降であれば、マクロを呼び出した際のAST変換後のコードを見ることができます。

後編に続く

さて、前編はここまで、groovy-macro-methodsとは何か、groovy-macro-methodsを使ったマクロの作り方をご紹介しました。

後編は、groovy-macro-methodsを使った「あまり実用的ではないのですが、サンプルとしてはまぁ何とか...」というマクロをいくつか作ってみましたので、それをご紹介することで、groovy-macro-methodsでどんなことができるかご紹介したいと思います。

RoboVMはGroovyの夢を見るか

はじめに

これはG*アドベントカレンダーの最終日、第25日目の記事です。

巷では「AndroidはGroovyの夢を見るか」という話がありますが、もう一つの雄である、iOSでGroovyは何とかならんのか、というのが今回のネタです。

もう少し具体的に言うと、JavaiOS用アプリを開発するソフトウェアとしてRoboVMがありますが、Groovyで使えないか?を探ります。

結論から言うと、力及ばず、夢は夢のままでした... (¦3[___]
#私の環境だけかもしれませんが...

この記事では、次の環境で動作をさせています。

OS: OSX 10.9.5
Java: 1.8.0_25
Groovy: 2.4.0-rc-1
Gradle: 2.2.1
XCode: 6.1.1
RoboVM: 1.0.0-beta-01

ここからは、私が何をして、何ができて、何ができなかったのか、第一歩でコケた話をお楽しみください。

RoboVMのインストール

RoboVMのインストール自体は簡単です。RoboVMの「Download the latest RoboVM release」からtar+gzファイルをダウンロードし、適当なディレクトリに展開します。

tar zxvf robovm-1.0.0-beta-01.tar.gz

あとは、展開してできるディレクトリを環境変数ROBOVM_HOMEに、${ROBOVM_HOME}/binを環境変数PATHに、それぞれ設定します。

先人の知恵を参考にする

既に「RoboVMをGroovyで」という先人が何人かいらっしゃるようで、今回はここから一式クローンして試してみます。

git clone https://github.com/msgilligan/GradleGroovyRobot.git

名前からわかるように、Gradleのプロジェクトになっています。build.gradleにちょっと手を入れ、次のように変更しました。

apply plugin: 'groovy'

repositories {
    mavenCentral()
    flatDir { dirs 'lib' }
}

def robovmver = "1.0.0-beta-01"
project.ext.set("robovmver", robovmver)

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.4.0-rc-1'
    compile "org.robovm:robovm-rt:$robovmver"
    compile "org.robovm:robovm-objc:$robovmver"
    compile "org.robovm:robovm-cocoatouch:$robovmver"
}

project.ext.set("robovm_home", "/Users/rhapsody/Tools/RoboVM/robovm-$robovmver")

// main class
String mainClass = "IOSDemo"
def groovyJar = ""

task findGroovyJar << {
    groovyJar = project.configurations.compile.find { it.name.startsWith("groovy-all-") }
}

// compile bytecode to llvm and run in the ios simulator
task run (dependsOn: [build, findGroovyJar]){

    doFirst {
        println(">> Running RoboVM")
        String cmd = "$project.robovm_home/bin/robovm -verbose -arch x86 -os ios -cp $project.robovm_home/lib/robovm-objc.jar:$project.robovm_home/lib/robovm-cocoatouch.jar:$projectDir/build/classes/main/:$groovyJar -run $mainClass"
        def proc = cmd.execute()

        proc.in.eachLine {line -> println line}
        proc.err.eachLine {line -> System.err.println( 'ERROR: ' + line)}
        proc.waitFor()
    }
}

サンプルのコード

サンプルのコードですが、クローンしたプロジェクトに含まれていますが、ここに載っているJavaのコードをGroovyで書きなおしてみました。ポイントは、@CompileStaticを使うことです。

import org.robovm.apple.coregraphics.CGRect
import org.robovm.apple.foundation.NSAutoreleasePool
import org.robovm.apple.uikit.UIApplication
import org.robovm.apple.uikit.UIApplicationDelegateAdapter
import org.robovm.apple.uikit.UIApplicationLaunchOptions
import org.robovm.apple.uikit.UIButton
import org.robovm.apple.uikit.UIButtonType
import org.robovm.apple.uikit.UIColor
import org.robovm.apple.uikit.UIControl
import org.robovm.apple.uikit.UIControlState
import org.robovm.apple.uikit.UIEvent
import org.robovm.apple.uikit.UIScreen
import org.robovm.apple.uikit.UIWindow
import groovy.transform.CompileStatic

@CompileStatic
class IOSDemo extends UIApplicationDelegateAdapter {

    UIWindow window = null
    int clickCount = 0

    @Override
    boolean didFinishLaunching(UIApplication application,
            UIApplicationLaunchOptions launchOptions) {

        final UIButton button = UIButton.create(UIButtonType.RoundedRect)
        button.frame = new CGRect(115.0f, 121.0f, 91.0f, 37.0f)
        button.setTitle('Click me!', UIControlState.Normal)

/*
        button.addOnTouchUpInsideListener(new UIControl.OnTouchUpInsideListener() {
            @Override
            public void onTouchUpInside(UIControl control, UIEvent event) {
                button.setTitle('Click #' + (++clickCount), UIControlState.Normal)
            }
        })
*/

        window = new UIWindow(UIScreen.mainScreen.bounds)
        window.backgroundColor = UIColor.lightGray()
        window.addSubview(button)
        window.makeKeyAndVisible()

        true
    }

    static void main(String[] args) {
        NSAutoreleasePool pool = new NSAutoreleasePool()
        UIApplication.main(args, null, IOSDemo)
        pool.close()
    }
}

これで実行の準備が整いました。では実行してみましょう。

gradle run

初回はバイトコードをネイティブなコードに変換しているようで時間がかかり、また私の環境だけかもしれませんが、エラーでiOSシミュレータが起動しなかったりする場合があります。そのような場合は、再度実行すると、次のようにiOSシミュレータが起動し、アプリが実行されます。

f:id:touchez_du_bois:20141226021149p:plain

ここまではできるのですが、ここから先には進めませんでした...orz

サンプルのコード、その2

先程のサンプルコードですが、コメントアウトしている箇所があります。

        button.addOnTouchUpInsideListener(new UIControl.OnTouchUpInsideListener() {
            @Override
            public void onTouchUpInside(UIControl control, UIEvent event) {
                button.setTitle('Click #' + (++clickCount), UIControlState.Normal)
            }
        })

ボタンにリスナーを登録して、ボタンがクリックされた場合に、ボタンのタイトルを再設定することをしています。

では、このコメントアウトしている箇所を有効にして実行してみましょう。iOSシミュレータが起動しますが、先程のようにアプリは実行されず、次のようなメッセージが出力されてしまいます。
#ちなみに、クローンしたプロジェクトのサンプルコードも基本的に同じで、リスナーを登録するコードはありませんでした。

[WARN] android.System: A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks.

[WARN] android.System: java.lang.Throwable: Explicit termination method 'close' not called
    at dalvik.system.CloseGuard.open(CloseGuard.java)
    at java.io.RandomAccessFile.<init>(RandomAccessFile.java)
    at java.io.RandomAccessFile.<init>(RandomAccessFile.java)
    at java.util.zip.ZipFile.<init>(ZipFile.java)
    at java.util.zip.ZipFile.<init>(ZipFile.java)
    at java.lang.PathClassLoader.init(PathClassLoader.java)
    at java.lang.PathClassLoader.findResource(PathClassLoader.java)
    at java.lang.ClassLoader.getResource(ClassLoader.java)
    at java.lang.ClassLoader.getResourceAsStream(ClassLoader.java)
    at org.codehaus.groovy.reflection.GeneratedMetaMethod$DgmMethodRecord.loadDgmInfo(GeneratedMetaMethod.java)
    at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.registerMethods(MetaClassRegistryImpl.java)
    at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java)
    at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java)
    at groovy.lang.GroovySystem.<clinit>(GroovySystem.java)
    at org.codehaus.groovy.runtime.InvokerHelper.<clinit>(InvokerHelper.java)
    at groovy.lang.GroovyObjectSupport.<init>(GroovyObjectSupport.java)
    at groovy.lang.Reference.<init>(Reference.java)
    at IOSDemo.didFinishLaunching(IOSDemo.groovy)
    at org.robovm.apple.uikit.UIApplicationDelegate$ObjCProxy.$cb$application$didFinishLaunchingWithOptions$(Unknown Source)
    at org.robovm.apple.uikit.UIApplication.main(Native Method)
    at org.robovm.apple.uikit.UIApplication.main(UIApplication.java)
    at IOSDemo.main(IOSDemo.groovy)


[WARN] java.lang.Class: Class.forName() failed to load 'java.lang.ClassValue'. Use the -forcelinkclasses command line option or add <forceLinkClasses><pattern>java.lang.ClassValue</pattern></forceLinkClasses> to your robovm.xml file to link it in.

Loading class 'java.util.logging.ConsoleHandler' failed
java.lang.ClassNotFoundException: java.util.logging.ConsoleHandler

java.lang.ExceptionInInitializerError
    at org.codehaus.groovy.runtime.InvokerHelper.<clinit>(InvokerHelper.java)
    at groovy.lang.GroovyObjectSupport.<init>(GroovyObjectSupport.java)
    at groovy.lang.Reference.<init>(Reference.java)
    at IOSDemo.didFinishLaunching(IOSDemo.groovy)
    at org.robovm.apple.uikit.UIApplicationDelegate$ObjCProxy.$cb$application$didFinishLaunchingWithOptions$(Unknown Source)
    at org.robovm.apple.uikit.UIApplication.main(Native Method)
    at org.robovm.apple.uikit.UIApplication.main(UIApplication.java)
    at IOSDemo.main(IOSDemo.groovy)
Caused by: java.lang.NullPointerException
    at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java)
    at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java)
    at groovy.lang.GroovySystem.<clinit>(GroovySystem.java)
    ... 8 more


BUILD SUCCESSFUL

Total time: 34.537 secs

リスナーが登録できなければ、先に進めません...

時間もないので、今回はここまでです...

最後に

今後、やる気があれば、できたバイトコードデコンパイルしたり、Groovyのソースを追っかけてみるつもりです。

Android用のコードが入っているので、同じようにRoboVM用のコードを追加してなんとかなるといいのですが。

GParsでリモート処理

はじめに

これはG*アドベントカレンダーの第21日目の記事です。

今回は、GPars 1.3.0から追加される(であろう)機能の一つ、リモート処理(remoting)をご紹介します。

この記事では、次の環境で動作をさせています。

OS: OSX 10.9.5
Java: 1.8.0_25
Groovy: 2.4.0-beta-4

GParsとは

GParsとは、JavaやGroovyで並行処理を行うためのフレームワークです。12月21日時点では1.2.1が安定版のバージョンで、1.3.0はスナップショット版となっています。

GPars自体はGroovyに同梱されており、2.3.9、2.4.0-beta-4とも安定版の1.2.1が同梱されています。

GParsのリモート処理

GParsが元々持っている

  • Actor
  • Agent
  • Dataflow

は同じJVM上で動作させることになりますが、GParsのリモート処理とはこれらを別JVM上で動作せることが可能となります。これにより、別JVM上のActorにメッセージを送ったり、応答を受け取ったりすることができるようになります。

同じホスト上の別JVM上だけでなく、別ホスト上のJVM上でも動作させることが可能となっています。

なお、メッセージのやりとりについては、ベースにNettyを使って実現しているようです。

GPars 1.3.0のjarのビルド

GPars 1.3.0はまだ開発中なので、スナップショット版のjarファイルをリポジトリから取得するか、自分でビルドする必要があります。

公式ページからたどると、スナップショット版のjarファイルをリポジトリから取得できる記載がありますが、リポジトリに上がっているjarファイルにはリモート処理用のclassファイルが含まれていないようでしたので、今回は自分でビルドすることにします。

GitHubからソースを落とし、

git clone https://github.com/GPars/GPars.git

ソースディレクトリ内のgradlewコマンドでビルドします。

cd GPars
./gradlew

私の環境ではtestの途中で先に進まなかったので、control+Cで中断しjarファイルだけコピーすることにしました。

cloneしたディレクトリの下に「build/libs/gpars-1.3-SNAPSHOT.jar」ができていますので、${GROOVY_HOME}/lib/gpars-1.2.1.jarを削除して、代わりに「build/libs/gpars-1.3-SNAPSHOT.jar」を${GROOVY_HOME}/libにコピーします。

あと、先程も述べたとおりNettyを使っていますので、Mavenリポジトリからnetty-all-4.0.24.Final.jarを取得し${GROOVY_HOME}/libにコピーします。

リモート処理のサンプルコード

cloneしたディレクトリの下に「src/test/groovy/groovyx/gpars/samples/remote/」というディレクトリがあり、このディレクトリ配下のソースがリモート処理のサンプルコードとなっています。

ここからは、actor/calculatorのサンプルコードを使って、通常のActorとリモート処理のActorがどのように違うのか、を見てみたいと思います。

calculatorのサンプルコードについて

calculatorのサンプルコードですが、次のような内容になっています。

  • 計算を依頼し結果を表示するActor(A)と、実際に計算をするActor(B)の、2つのActorを用意。
  • AからBへは、2つの数値をメッセージとして順に送信する。
  • Bは受け取った2つのメッセージを加算し、計算結果としてAに返す。
  • 計算結果を受け取ったAは、標準出力に計算結果を出力する。

従来のActorでのコード

従来のActorを使って実現すると、こんな感じのコードになります。

// LocalCalculator.groovy

package groovyx.gpars.samples.remote.actor.calculator

import groovyx.gpars.actor.Actors

def answerActor = Actors.actor {
    println "Calculator - Answer"

    react { a->            // (*a)
        react { b->        // (*b)
            reply a + b    // (*c)
        }
    }
}

def queryActor = Actors.actor {
    println "Calculator - Query"

    answerActor << 1    // (*d)
    answerActor << 2    // (*e)

    react { println it }      // (*f)
}

[answerActor, queryActor]*.join()

計算をするanswerActorと、計算を依頼するqueryActorの、2つのActorのオブジェクトを生成し、動作させます。

上記のコードでは、queryActorから1, 2という数値をメッセージとして送信し(*d, *e)、2つのメッセージ受け取った(*a, *b)answerActorではそれらのメッセージを数値として加算し、その結果を返します(*c)。結果を受け取ったqueryActorは、その結果を標準出力に出力します(*f)。

これら2つのActorは、同じJVM上で動作することになります。

従来のActorの実行結果

上記のコードを実行してみます。

groovy LocalCalculator.groovy

実行すると、次の値が標準出力に出力されます。

Calculator - Query
Calculator - Answer
3

リモート処理のActorのコード : 計算をする側

リモート処理のActorのコードですが、計算をする側と計算を依頼する側にコードがわかれます。

計算をする側はこんな感じのコードになります。

// RemoteCalculatorAnswer.groovy

package groovyx.gpars.samples.remote.actor.calculator

import groovyx.gpars.actor.Actors
import groovyx.gpars.actor.remote.RemoteActors

def HOST = "localhost"
def PORT = 9000

def remoteActors = RemoteActors.create()    // (*a)
remoteActors.startServer HOST, PORT           // (*b)

def answerActor = Actors.actor {
    println "Remote Calculator - Answer"

    remoteActors.publish delegate, "remote-calculator"    // (*c)

    react { a->
        react { b->
            reply a + b
        }
    }
}

answerActor.join()

remoteActors.stopServer()    // (*d)

まず、リモート処理用のコンテキストを作成します(*a)。次に、localhostの9000ポートでメッセージ受信を開始します(*b)。

実際にメッセージを受信し処理をするコードは従来のActorと基本的には同じですが、"remote-calculator"という名前でメッセージを受信できるようにパブリッシュ処理を別途行います(*c)。

処理の最後に、コンテキストを終了させます(*d)。

リモート処理のActorのコード : 計算を依頼する側

計算を依頼する側はこんな感じのコードになります。

// RemoteCalculatorQuery.groovy

package groovyx.gpars.samples.remote.actor.calculator

import groovyx.gpars.actor.Actors
import groovyx.gpars.actor.remote.RemoteActors

def HOST = "localhost"
def PORT = 9000

def remoteActors = RemoteActors.create()    // (*a)

def queryActor = Actors.actor {
    println "Remote Calculator - Query"

    def remoteCalculator = remoteActors.get HOST, PORT, "remote-calculator" get()    // (*b)

    remoteCalculator << 1    // (*c)
    remoteCalculator << 2    // (*d)

    react { println it }    // (*e)
}
queryActor.join()

まず、リモート処理用のコンテキストを作成します(*a)。次に、localhostの9000ポート、"remote-calculator"という名前のActorに対してメッセージを送信できるよう、Proxyオブジェクトを取得します(*b)。

取得したProxyオブジェクトに対しメッセージを送信することで、Proxyオブジェクトを介して別JVM上のActorにメッセージを送信することができます(*c, *d)。

最後に、別JVM上のActorからメッセージを受信し標準出力に出力します(*e)。

リモート処理のActorの実行結果

上記のコードを実行してみます。まず、計算する側のコードを実行します。

groovy RemoteCalculatorQuery.groovy

実行すると、次の値が標準出力に出力されます。

Remote Calculator - Answer

次に、計算を依頼する側のコードを先程とは別のターミナルで実行します。

groovy RemoteCalculatorQuery.groovy

実行すると、次の値が標準出力に出力されます。一見すると同じJVM上で動作しているようにも見えますが、別のJVM上で計算した結果を受け取って表示しています。

Remote Calculator - Query
3

なお、私の環境では、次の警告メッセージとスタックトレース標準エラー出力に出力されました。

12 22, 2014 1:00:11 午前 io.netty.channel.DefaultChannelPipeline$TailContext exceptionCaught
警告: An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
java.io.IOException: Connection reset by peer
        at sun.nio.ch.FileDispatcherImpl.read0(Native Method)
        at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39)
        at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
        at sun.nio.ch.IOUtil.read(IOUtil.java:192)
        at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:379)
        at io.netty.buffer.UnpooledUnsafeDirectByteBuf.setBytes(UnpooledUnsafeDirectByteBuf.java:446)
        at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:881)
        at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:225)
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:119)
        at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:511)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:468)
        at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:382)
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:354)
        at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:116)
        at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:137)
        at java.lang.Thread.run(Thread.java:745)

最後に

GParsのリモート処理を使うことで、他サーバのActorと割とお手軽に連携する可能になります。また、今回は紹介できませんでしたが、AgentもDataflowについても、Actorと基本的に同じように使うことができると思います。

他サーバと処理を連携させるコードを書く場合などに使ってみるのはいかがでしょうか。

早く1.3.0がリリースされることを希望します。

GroovyでElasticSearchしてみた

はじめに

これはG*アドベントカレンダーの第24日目の記事です。

今回は、GroovyでElasticSearchサーバにアクセスするための、クライアントプログラムを書くお話です。

ElasticSearchとは

ElasticSearchは、オープンソースの検索エンジンです。詳細はこちらを参照してください。

ElasticSearchサーバへのアクセス

ElasticSearch自体、RESTfulな構造になっていますので、curlなどのツールを使ってアクセスすることができます。

また、Javaを始めとするプログラミング言語からアクセスするためのAPIも提供されています。APIが提供されている言語は次のとおりです。

今回はGroovy用のAPIを使って、ElasticSearchにドキュメントを登録するところまでをやってみます。時間の都合上、今回はこれが限界です...orz

Groovy用APIの詳細は、こちらのガイドを参照してください。

今回、Groovyはバージョン2.2.1を使用します。

実行に必要なライブラリの指定

実行に必要なライブラリは、@Grabアノテーションで指定します。

@Grab(group = 'org.elasticsearch', module = 'elasticsearch-lang-groovy', version = '1.5.0')

moduleの指定がガイドと違いますが、こちらの方が新しかったもので...

クライアント用ノードの設定

次にクライアント用ノードの設定を行います。Groovy用のノードビルダークラスが有りますので、このクラスのインスタンスを取得し、各種設定を行います。

GNodeBuilder nodeBuilder = nodeBuilder();
nodeBuilder.settings.put("node.client", true)
nodeBuilder.settings.put("cluster.name", "jggug")

なお、ガイドでは、クロージャで設定ができるようなことが書いてありましたが、今回試したバージョンでは上手く行かなかったので、クロージャを使用しないで設定を行っています。

クライアントのインスタンスの生成

次にクライアントのインスタンスを生成します。

GNode node = nodeBuilder.node()
GClient client = node.client

ドキュメントの登録

ドキュメントの登録は、indexメソッドに、インデックス名、タイプ名、ID、登録したいソースを指定します。

def indexR = client.index {
    index "test"
    type "type1"
    id "1"
    source {
        test = "value"
        complex {
            value1 = "value1"
            value2 = "value2"
        }
    }
}

ガイドのサンプルはこのとおりなのですが、これだと"complex"という空のオブジェクトが登録されるだけです...orz いろいろ調べてはみたのですが、上手く行かず...orz

終了処理

最後にクライアントの終了処理です。

node.stop().close()

全コード

これまでのコードを合わせると次のとおりです。

@Grab(group = 'org.elasticsearch', module = 'elasticsearch-lang-groovy', version = '1.5.0')

import org.elasticsearch.groovy.client.GClient
import org.elasticsearch.groovy.node.GNode
import org.elasticsearch.groovy.node.GNodeBuilder
import static org.elasticsearch.groovy.node.GNodeBuilder.*

GNodeBuilder nodeBuilder = nodeBuilder();
nodeBuilder.settings.put("node.client", true)
nodeBuilder.settings.put("cluster.name", "jggug")GNode node = nodeBuilder.node()
GClient client = node.client

def indexR = client.index {
    index "index_test"
    type "type1"
    id "1"
    source {
        test = "value"
        complex {
            value1 = "value1"
            value2 = "value2"
        }
    }
}

println "Indexed $indexR.response.id into $indexR.response.index/$indexR.response.type"

node.stop().close()

で?

使用したバージョンが悪いのか、ガイドのサンプルが悪いのか不明ですが、結局うまく登録できませんでした...orz

時間があれば、再チャレンジといきたいところです。

GrailsでElasticSearch

GroovyではなくGrailsでElasticSearchしたい場合ですが、現状次の2つのプラグインがあるようです。

どちらかのプラグイン(おそらく後者)を次のG*マガジンの「Plugin探訪」で紹介をしたいと思います。

次は

中途半端な記事になってしまいましたが、明日25日も私が担当します。 ネタは今から探します...orz