Groovy MOP

Groovy にはメタプログラミングための API が用意されている。*1
関数の自動メモ化 の中で当てはめると Spring AOP のようなことを言語内で行える。

class MemoizeInterceptor implements Interceptor {
  def cache = [:]
  def hasResult
  @Override Object beforeInvoke(Object object, String methodName, Object[] arguments) {
    this.hasResult = cache.containsKey([methodName, *arguments])
  }
  @Override Object afterInvoke(Object object, String methodName, Object[] arguments, Object result) {
    def key = [methodName, *arguments]
    if (!cache.containsKey(key)) cache[key] = result
    cache[key]
  }
  @Override boolean doInvoke() {
    !this.hasResult
  }
}

def memoize(obj, Closure blk) {
  def proxy = ProxyMetaClass.getInstance(obj.class)
  proxy.interceptor = new MemoizeInterceptor()
  proxy.use(obj, blk)
}

def fib = { n -> println n; n <= 1 ? n : call(n - 1) + call(n - 2) }
memoize(fib) {
  assert fib(40) == 102334155
}

再帰呼び出しも上書きできてしまう。

Groovy 1.8 になって動作が変わっている部分

Groovy v1.8の新機能をサクっと紹介するよ - No Programming, No Life を見て新機能に目がいって気づかなかったがパフォーマンスの向上の部分だ。

"this"でのメソッド呼び出し時の引数がぴったり一致する場合、メソッドはダイレクトに呼び出されるようになります。なので、これは実行時のメソッド動的呼出しの際には行われません。


Groovy イン・アクションの GroovyInterceptable 例がエラーになった。

import org.codehaus.groovy.runtime.StringBufferWriter
import org.codehaus.groovy.runtime.InvokerHelper

class Traceable implements GroovyInterceptable {
  Writer writer = new PrintWriter(System.out)
  private int indent = 0
  Object invokeMethod(String name, Object args){
    writer.write("\n" + ' '*indent + "before method '$name'")
    writer.flush()
    indent++
    def metaClass = InvokerHelper.getMetaClass(this)
    def result = metaClass.invokeMethod(this, name, args)
    indent--
    writer.write("\n" + ' '*indent + "after method '$name'")
    writer.flush()
    return result
  }
}
class Whatever extends Traceable {
  int outer(){
    return inner()
  }
  int inner(){
    return 1
  }
}

def log = new StringBuffer()
def traceMe = new Whatever(writer: new StringBufferWriter(log))
assert 1 == traceMe.outer()
println log
assert log.toString() == """
before method 'outer'
 before method 'inner'
 after method 'inner'
after method 'outer'"""

inner の呼び出しが invokeMethod を経由しないで直接呼び出されるようになったので trace できていない。
disableopt オプションで最適化を off にしたら動作した。
もう一つの Interceptor の例は訳注に 1.1 の時点でエラーになったと書いてあるが 1.8 でも動作した*2

MOP2

Groovy イン・アクションの時点で MOP の設計の見直しがあると書いてあるが変わっていない気がする。
Groovy to infinity and beyond - GR8Conf Europe 2010 - Guillaume Lafor…
去年の資料だが MOP の部分は抽象的に書かれているので Groovy 2.0 以降になるのかな。
検索してもあまり使用例が見つからないのだけど現在の MOP でもかなりのパワーを秘めていると思う。

2011-05-08 追記

ブロックに引数で渡してみた。

def memoize(obj, Closure blk) {
  def proxy = ProxyMetaClass.getInstance(obj.class)
  proxy.interceptor = new MemoizeInterceptor()
  proxy.use(obj) { blk(obj) }
}

memoize({ n -> println n; n <= 1 ? n : call(n - 1) + call(n - 2) }) {
  assert it(40) == 102334155
}

*1:Groovy イン・アクション 7.6。 Google ブックスで公開されている

*2:スペースの数は調節した