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を使ったマクロのサンプルを紹介しました。マクロ自体、作るのがちょっと大変ですが、工夫次第で面白いことができることを実感していただけたら幸いです。