CLOVER🍀

That was when it all began.

Groovyのバインディング変数とローカル変数

Groovyでは、スクリプト中の変数のスコープが宣言方法で大きく変わるようです。これも仕事でGroovyを使っていて、ハマったのでメモ。

例えば、以下のスクリプトについて。
binding_local.groovy

outside = "binding object"
def outsideLocal = "local object"

println(outside) // OK
println(outsideLocal) // OK

def func() {
    println(outside) // OK
    //println(outsideLocal) // groovy.lang.MissingPropertyException: No such property
}

func()

この例では、2つの変数outsideとoutsideLocalをそれぞれスクリプトトップレベル中の処理とメソッド内で参照しようとしていますが、変数outsideLocalをメソッド内で参照している方はコメントにもある通り例外を送出して処理に失敗します。

実際、実行するとこんな感じです。

$ groovy binding_local.groovy 
binding object
local object
binding object
Caught: groovy.lang.MissingPropertyException: No such property: outsideLocal for class: binding_local
groovy.lang.MissingPropertyException: No such property: outsideLocal for class: binding_local
	at binding_local.func(binding_local.groovy:9)
	at binding_local.run(binding_local.groovy:12)

2つの変数の違いは、宣言時にdefキーワードの有無しか違いがないのに…。

他の言語ではこういうことは普通にできていた気がするので、最初意味がわかりませんでした。宣言を明示したいなぁくらいの感覚でdefを使っていたので、ものは試しとdefを外してみたら動作するではありませんか。

これはいったい、どういうことでしょう?

ちょっと気になったので、このスクリプトをgroovycしてできたクラスファイルを、JDで逆コンパイルしてみました。結果は、こちら。

import groovy.lang.Binding;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.BytecodeInterface8;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;

public class binding_local extends Script
{
  public binding_local()
  {
    binding_local this;
    CallSite[] arrayOfCallSite = $getCallSiteArray();
  }

  public binding_local(Binding arg1)
  {
    Binding context;
    CallSite[] arrayOfCallSite = $getCallSiteArray();
    ScriptBytecodeAdapter.invokeMethodOnSuperN($get$$class$groovy$lang$Script(), this, "setBinding", new Object[] { context });
  }

  public static void main(String[] args)
  {
    CallSite[] arrayOfCallSite = $getCallSiteArray();
    arrayOfCallSite[0].call($get$$class$org$codehaus$groovy$runtime$InvokerHelper(), $get$$class$binding_local(), args);
  }

  public Object run()
  {
    CallSite[] arrayOfCallSite = $getCallSiteArray(); String str = "binding object"; ScriptBytecodeAdapter.setGroovyObjectProperty(str, $get$$class$binding_local(), this, "outside");
    Object outsideLocal = "local object";

    arrayOfCallSite[1].callCurrent(this, arrayOfCallSite[2].callGroovyObjectGetProperty(this));
    arrayOfCallSite[3].callCurrent(this, outsideLocal); if ((__$stMC) || (BytecodeInterface8.disabledStandardMetaClass()))
    {
      return arrayOfCallSite[4].callCurrent(this); } else return func(); return null;
  }

  public Object func()
  {
    CallSite[] arrayOfCallSite = $getCallSiteArray(); return arrayOfCallSite[5].callCurrent(this, arrayOfCallSite[6].callGroovyObjectGetProperty(this)); return null;
  }

  static
  {
    __$swapInit();
    Long localLong1 = (Long)DefaultTypeTransformation.box(0L);
    __timeStamp__239_neverHappen1332081958027 = localLong1.longValue();
    Long localLong2 = (Long)DefaultTypeTransformation.box(1332081958026L);
    __timeStamp = localLong2.longValue();
  }
}

注目すべきは

  public Object run()
  {
    CallSite[] arrayOfCallSite = $getCallSiteArray(); String str = "binding object"; ScriptBytecodeAdapter.setGroovyObjectProperty(str, $get$$class$binding_local(), this, "outside");
    Object outsideLocal = "local object";

    arrayOfCallSite[1].callCurrent(this, arrayOfCallSite[2].callGroovyObjectGetProperty(this));
    arrayOfCallSite[3].callCurrent(this, outsideLocal); if ((__$stMC) || (BytecodeInterface8.disabledStandardMetaClass()))
    {
      return arrayOfCallSite[4].callCurrent(this); } else return func(); return null;
  }

ここでしょうか。明らかに、変数outsideを特別扱いしていますね。outsideLocalの方は、普通にローカル変数として定義されています。

それにしても、スクリプト中のトップレベルの変数は普通にスクリプトから生成されるクラスのフィールドになると思っていたんですけど、違うんですね。

こちらの書籍にも書いていました。

プログラミングGROOVY

プログラミングGROOVY

今回の例だと、

  • 変数outsideをバインディング変数
  • 変数outsideLocalをローカル変数(そりゃそうだ)

と呼ぶらしいです。また、変数outsideLocalも@Fieldアノテーションを付与することにより、フィールドに昇格することができるそうな。

outside = "binding object"
@groovy.transform.Field
def outsideLocal = "local object"

ちなみに、クロージャからはバインディング変数もローカル変数も、普通に参照可能です。

def clos = {
    println(outside)
    println(outsideLocal)
}

それにしても、なんでこんな仕様になってるんでしょ?

ScalaとGroovyのクロージャで、再帰を使った場合の違い

最近、業務でこそっとGroovyを使いだしているのですが、軽くクロージャ周りでハマったのでメモ。

別に階乗のプログラムが書きたかったわけではないのですが、サンプルとして。

def factorial = { n ->
    if (n > 0) {
        n * factorial(n - 1)
    } else {
        1
    }
}

println(factorial(args[0].toInteger()))

こんなプログラムを書いて、実行します。

$ groovy recur_closure.groovy 5
Caught: groovy.lang.MissingMethodException: No signature of method: recur_closure.factorial() is applicable for argument types: (java.lang.Integer) values: [4]
groovy.lang.MissingMethodException: No signature of method: recur_closure.factorial() is applicable for argument types: (java.lang.Integer) values: [4]
	at recur_closure$_run_closure1.doCall(recur_closure.groovy:3)
	at recur_closure.run(recur_closure.groovy:35)

factorialなんてメソッド、知らないよと言って死んでくれます。

というわけで、なんかクロージャーに制限がありそうなので、本家の以下のサンプルを見てみました。
*そもそも、やりたかったのはディレクトリの再帰処理でした
http://groovy.codehaus.org/Recipes+For+File

上記の「Recursively deleting files and directories.:the Groovy Way」を参考にすると、こうなります。

def factorial
// def factorial = {} // <- これでもいい
factorial = { n ->
    if (n > 0) {
        n * factorial(n - 1)
    } else {
        1
    }
}

println(factorial(args[0].toInteger()))

実行すると

$ groovy recur_closure.groovy 5
120

おお、動きました。ポイントは、先にdefキーワードで変数だけ宣言しておくことですね。Closureクラスのインスタンスで初期化するかどうかは、どちらでもいいみたいです。

でも、これってちょっとイマイチですよねぇ。もう少しなんとかならんのかと調べたところ、GroovyのClosureにはcallというメソッドがあるらしく、これを使えばいいらしいです。

def factorial = { n ->
    if (n > 0) {
        n * call(n - 1)
    } else {
        1
    }
}

println(factorial(args[0].toInteger()))

これで、3番目のコードと同じ動作をします。

ところで、Scalaの場合はどうなのかな?と思い、ちょっと試してみました。

val factorial: Int => Int = n => if (n > 0) n * factorial(n - 1) else 1

println(factorial(args(0).toInt))

では、実行。

$ scala recur_closure.scala 5
120

…動きました。これはちょっと予想外でした。defによるメソッド定義なら動くことはわかっていますが、変数定義(val)でもいけるとは。まあ、Scalaはメソッドとフィールド(特にval)の扱いがかなり近いみたいなので、同じように使えるってことかな?

ただ、さすがに再帰定義なので型推論に頼りきることはできません。上のプログラムを

val factorial = (n: Int) => if (n > 0) n * factorial(n - 1) else 1

println(factorial(args(0).toInt))

のように変更してしまうと

$ scala recur_closure.scala 5
/xxxxx/recur_closure.scala:2: error: recursive value factorial needs type
val factorial = (n: Int) => if (n > 0) n * factorial(n - 1) else 1
                                           ^
one error found

再帰定義だから型が必要だよ、と怒られてしまいます。

まあ、普段Scalaを使う時は、こういう風にローカル関数で書くことの方が多いですかね。けっこう気楽に関数定義できるので。

def factorial(n: Int): Int = {
  @scala.annotation.tailrec
  def factorialInner(acc: Int, current: Int): Int =
    current match {
      case 0 => acc
      case n => factorialInner(acc * n, n - 1)
    }

  factorialInner(1, n)
}

println(factorial(args(0).toInt))

GroovyとScalaの両者のこの差は、第1級のオブジェクトとして扱っているのがClosureクラスのインスタンスなのかFunctionNクラスのインスタンスかの差なのでしょうかね。Groovyはクロージャを1級市民として扱うことに重きをおいたのかな?

とはいっても、両者の考え方の差なのでこれに優劣つける気はないのですがね。事実、Groovyは使ってみるとスクリプト言語らしいパワフルさを持っていて、書いていてけっこう楽しかったですし。

…ところで、完全な余談ですがClosureをアルファベットで書こうとすると、どうしてもClojureと打ち間違えてしまいます。前は、GroovyよりもClojureの方がプログラムを書いてましたからね。