最近、業務でこそっと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の方がプログラムを書いてましたからね。