mike-neckのブログ

Java or Groovy or Swift or Golang

Kotlin で groovy.lang.Closure のインスタンスを生成・取得する(G* Advent Calendar 2017 6日目)

G* Advent Calendar 2017 の 6日目は groovy.lang.Closureインスタンスを Kotlin で生成・取得する方法です。

f:id:mike_neck:20150917235151p:plain
Groovy!

3分くらいで読み終われます。


groovy.lang.Closureインスタンスを Kotlin で生成・取得する方法です。Kotlin コードの中で Closure を作って Groovy に渡すということを指しています。


生成方法

早速ですが生成方法です。

ある A というクラスがあって、 Closure の中で用いるプロパティを A から取得したい場合は次のような関数を書きます。

fun <A> closure(a: A, cl: A.() -> Unit): Closure<Unit> = object: Closure<Unit>(a) {
  fun doCall() = this@closure.cl()
}

ちょっと欲張りに A というクラスで宣言した Closure で、中で用いるプロパティを B から取得しつつ、 C を引数で受け取って、 D を返す Closure を生成したい場合は次のような関数を書きます。

fun <A, B, C, D> closure(declaredAt: A, receiver: B, cl: B.(C) -> D): Closure<D> = object: Closure<Unit>(this) {
  init {
    delegate = receiver
  }
  fun doCall(c: C): D = this@closure.cl(c)
}

試してみる

最後の欲張りな記述方法を用いた例を書いてみます。次のような Kotlin コードを書きます。

object ClosureCreation {
  @JvmStatic
  fun <A> stringToIntClosure(declaredAt: A, delegate: Supplier<String>): Closure<Int> =
      closure<A, Supplier<String>, String, Int>(declaredAt, delegate) { s -> "${get()} $s".length }
}

この stringToIntClosure 関数を呼ぶと java.util.function.Supplier を引数にとって、kotlin.Int(実質は int) が返ってくる groovy.lang.Closure が返ってきます。

そして、こちらが利用するコードになります。

class ClosureCreationTest {
  @Test
  void 'Kotlin で 作った closure を Groovy で扱う'() {
    def closure = ClosureCreation.stringToIntClosure(this, [get: { 'Groovy' }] as Supplier<String>)
    assert closure('Calling closure made by Kotlin.') == 'Groovy Calling closure made by Kotlin.'.size()
  }
}

こちらは試していただければテストが通ることがわかるかと思います。


仕組み

Groovy の Closurejava.util.concurrent.Callable インターフェース と Runnable インターフェースを実装した抽象クラスになっています。

f:id:mike_neck:20171202144126p:plain

そして、それぞれの実装は次のようになっています。

public void run() {
  call();
}
public V call() {
  final Object[] NOARGS = EMPTY_OBJECT_ARRAY;
  return call(NOARGS);
}
public V call(Object... args) {
  return (V) getMetaClass().invokeMethod(this, "doCall", args);
}

invokeMethod という名前から想像がつく通り、最終的には doCall メソッドを動的に取得して実行します。したがって、欲しいパラメーターの型と戻り値の型をつかった doCall メソッドを object 式で作れば、 Groovy でなくても Closureインスタンスを生成できるという事になります。

また、先程までの説明ではプロパティは〜〜から取得するという説明をしましたが、これを実現するのが Closuredelegate です。何かしらのメソッド、フィールド(プロパティ)を Closure 内で取得する時の取得元になります。これは Kotlin のレシーバーつき関数(Foo.(Bar) -> Baz)のようなものです。Gradleのドキュメントなどで Closure を受け取るメソッドについては、たいてい 〜〜 is passed as the closure's delegate のように書かれていたりします。

応用

先日ブログに書いた、 junit-starter-plugin においても、ここで書いた変換を行っています。

mike-neck.hatenadiary.com

gradle-plugin-publish-plugin の DSL にて PluginBundleExtension を介して gradle plugin の id(plugins ブロックで指定するやつ)と displayName を設定しますが、PluginBundleExtensions 内部の plugins ブロックの引数が groovy.lang.Closure であるため、Kotlin の関数・レシーバーつき関数ではここを記述できないため、ここに用いた変換用の関数を使っています。

おわり