CLOVER🍀

That was when it all began.

Object#withとClosureと

先週購入したこんな本を読んでいて、Groovyであんまり見慣れないコードが書いてあるのを見つけました。

実践プログラミングDSL ドメイン特化言語の設計と実装のノウハウ (Programmer’s SELECTION)

実践プログラミングDSL ドメイン特化言語の設計と実装のノウハウ (Programmer’s SELECTION)

こういうやつです。

newOrder.to.buy(100.shares.of('IBM')) {
    limitPrice 300
    allOrNone  true
    valueAs    {qty, unitPrice -> qty * unitPrice - 500}
}

Closure内ではあるクラスのメソッド(limitPriceとかallOrNoneとか)を読んでいるのですが、レシーバーがないのにどうやってるんだっけ?とか、Closureが第2引数なのに括弧の外にあるよ?とか思って不思議に思っていました。一応自己解決したのですが、よくよく見るといつものGroovy本にも書いてありました。

プログラミングGROOVY

プログラミングGROOVY

これのトリックは、Groovy JDKのObject#withとメソッドの最後の引数にClosureが来た場合の表記法を利用してますね。

Groovy JDKで拡張された、Object#with(Closure)を使用すると、引数で受け取ったClosure内でメソッドなどをwithのレシーバーとなったオブジェクトのメソッドやプロパティを、まるで自分で定義してあるかのように呼び出しできるみたいですね。

以下、ちょっと例。

class Person {
    String name

    def say() { println "I'm ${name}!" }
    def decorate() { "***${name}***" }
}

new Person(name: "Kazuhira").with {
    say()
    println(decorate())
}

実行結果。

I'm Kazuhira!
***Kazuhira***

なるほど、インスタンスを指定していないのも関わらず、withの呼び出しに使用したオブジェクトが持つメソッドが解決できていますね。

これのトリックは、Closure#delegateプロパティのようで、以下のようなコードでも同様の動きをさせることができます。

def clos = {
    say()
    println(decorate())
}

clos.delegate = new Person(name: "Kazuhira")
clos()

Closure#ownerは知っていましたが、全然関係ない書籍でClosure#delegateを理解することになりました(笑)。

続いて、メソッドに引数にClosureを取る場合ですが、Closureがメソッドの最後の引数となっている場合は、呼び出し括弧の外に置けるようです。よくScalaでは

def using[A <: { def close(): Unit }, B](resource: A)(body: A => B): B = {
  …省略…
}

using(new Resource) { r =>
  …省略…
}

みたいなコードを見かけると思いますが、これはカリー化をサポートしてないとできないと思っていたのでGroovyでは書けないと思ってましたわ。

例えば、こんな感じ。

def using(resource, closure) {
    try {
        closure(resource)
    } finally {
        resource?.close()
    }
}

class R {
    def put(v) { println v }
    def close() { println "Closed" }
}

using(new R()) { r ->
    r.put("Hello World")
}

実行結果。

Hello World
Closed

こちらもGroovy本に載っていました。が、さらっと書かれていたので流しちゃったんだろうなぁ…。

最後の引数がClosureであればよいので、その前の引数は複数あってもOKです。

def withClosure(v1, v2, closure) {
    println "v1 = ${v1}"
    println "v2 = ${v2}"
    closure()
}

withClosure("Hello", "World") {
    println("in Closure!")
}

GroovyはScala、Clojureほど細かく勉強してませんし、軽く書けるJavaみたいな感じで使っているので、こういう細かいところは見落としてしまったんでしょうなぁ…。ちょっともったいなかったです。