クロージャの再帰処理

最近始めたGroovy。LLちっくな文法やメソッドが多く盛り込まれていて,非常に面白い。RubyかPythonに走ろうと思っていたが,やはり長年Javaをやってきた僕にとっては,Groovyが手に馴染みやすいのかもしれない。

さて,Groovyの文法において,Javaと比べて最も特徴的なものが,やはりクロージャではないかと思う。JavaSE7からクロージャがJava言語にも入るとか入らないとか議論されているが,Groovyではそんなクロージャをいち早く体感することができる。クロージャにより,LLならではの書き方が可能になる。ついネストが深くなりがちなコーディングになるような気がするが,非常にスマートな記述を様々な処理で実現することが可能だ。

クロージャの説明は こことか ここに任せるとして,今日Groovyでコーディングしていて直面した問題,それは「クロージャを再帰でコールする」処理の書き方である。関数として記述してしまえば書けることはもちろんわかっていたのだが,無名関数と言えるクロージャでも再帰処理を書けるんじゃないか,と思って試行錯誤してみた。

最初に試したのはこれだ。

def func = { x -> ・・・ func.call(x) }

結果はあえなくNG。funcなんて知らねーぜ,というエラーメッセージ。確かに,定義の中で定義対象をそのまま記述しているので,未定義なものになってしまうことは良く考えれば当然。

では,先に定義しておけばいいんだろ,と考えて次に試したのはこれ。

def func = {} func = { x -> ・・・ func.call(x) }

これは見事に成功。ちゃんと再帰処理が行われるようになった。処理内容を持たないクロージャを定義して変数に代入しておき,その変数の内容を再定義してあげれば既に定義されたものを参照することになるため,自分自身を呼びだすことが解決されるようになる,という感じである。

しかし,何かスマートじゃなく,ちょっとトリッキーな記述な感じを受けてしまう。別に実現できる記述方法があるんじゃないかと思い,さらに試行錯誤を繰り返した。

クロージャは,それ自身がオブジェクトであり,callメソッドを持っている。つまり,以下のような記述を行っても,再帰処理を実現できた。

def func = { x -> ・・・ call(x) }

あっけないくらいにcall()メソッドで再帰処理を記述することが可能。

各クロージャはそれぞれClosureクラス(のサブクラス)が内部的に生成され,そのインスタンスがクロージャの処理を担当する。つまり,クロージャ内でthisを記述した場合,そのクロージャのインスタンス自身を参照することができるような気がしてしまう。しかし,

def func = { x -> ・・・ this.call(x) }

という記述を行ってみたが,結果はNG。thisはクロージャのインスタンスを参照しているのではなく,クロージャを持っているインスタンス(上記がTest.groovyファイルで書かれていれば,Testクラスのインスタンスが該当)を参照しているようである。よって,this.call()をしても,クロージャのcall()メソッドが呼び出されるわけではないことに注意しなければならない。

さらに,ネストされたクロージャ内で外側のクロージャを再帰で呼ぶ,ということを試してみた。

def func = {} func = { x -> ・・・ x.each { y -> ・・・ func.call(y) } }

外側のクロージャを予め空処理で定義しておく最初に紹介したテクニックを応用している。つまり,クロージャを何らかの変数に入れておければ,それに対してcall()メソッドを呼び出すことで再帰処理は実現できるということだ。

ちなみに,上記の動作はgroovy-1.1-BETA1で確認を行った。1.0系とかの古いバージョンでは,クロージャ内のthisの解釈が異なるなど,動作結果がかなり異なってくるので,注意が必要である。

このエントリーをはてなブックマークに追加

関連記事

Remapにファームウェアビルド機能を追加しました

Google I/O 2023でのウェブ関連のトピック

2022年を振り返って

現在のRemapと今後のRemapについて

QMK FirmwareとRP2040でAudio機能を使えるようにする方法