No Programming, No Life

プログラミング関連の話題や雑記

メタプログラミングGroovy入門

Groovy!(挨拶)

最近Groovyであまり遊べていないfumokmmです。G* AdventCalender2012の10日目ということで、久々に記事を書かせていただいております。
せっかくの機会なので、Groovyでメタプログラミングする際のとっかかり部分をまとめてみました。自分の理解が至らないところがあると思いますので、変なところがあったらツッコミよろしくお願いします。では早速スタートです。

Groovyでメタプログラミング

GroovyではあらゆるクラスにExpandoMetaClassと呼ばれる特別なクラス(メタクラス)が提供されていて、メソッドやプロパティを利用する際にこのメタクラスを経由して様々な力を得ることができます。
たとえば、メタクラスに実行時、動的にメソッドを定義してあげれば、あたかも初めから存在していたかのようにそのメソッドが利用できるようになるという寸法です。メソッドの定義にはクロージャを、プロパティの定義にはクロージャ以外の値を利用します。

強力なメタプログラミングが可能なRubyと比較した場合、良くも悪くもGroovyはクラスに縛られる*1ため(Rubyはクラス定義自体を何度もオープンして書き直すことができる)、メタクラスというフィルタを通して、メタプログラミングを実現しているようですね。

先ほど、あらゆるクラスにメタクラスが提供されていると書きました。もう少し具体的に書くと、すべての java.lang.Class に metaClass プロパティが提供されているということになります。このプロパティを使ってExpandoMetaClassを利用することができます。

1. クラスにインスタンスメソッドを追加する

まぁ、言葉でダラダラ書くよりもコードで示したほうが分かりやすいと思うで、早速いってみようと思います。
以下に示すコードは

Groovy Version: 2.0.5 JVM: 1.7.0_05 Vendor: Oracle Corporation OS: Mac OS X

にて動作確認をしました。

まずはクラスにインスタンスメソッドを追加する例です。クラスにインスタンスメソッドを追加する時は、クラスからメタクラスを取得してメソッドを定義する形でクロージャを定義します。

例えば、文字列にその文字列を二倍(2回繰り返す)して返すメソッド #twice を実装すると以下のような感じになります。

String.metaClass.twice = { ->
  delegate * 2
}

assert '大事な事なので2回言いました'.twice() == '大事な事なので2回言いました大事な事なので2回言いました'

たぶんこれが一番基本形になると思うので、もういっちょサンプルを。
今度は文字列の大文字と小文字を入れ替えるメソッド #swapCase を実装してみています。

String.metaClass.swapCase = { ->
  def sb = '' << ''
  delegate.each {
    sb << (Character.isUpperCase(it as char) ?
             Character.toLowerCase(it as char) :
             Character.toUpperCase(it as char) )
  }
  sb.toString()
}

assert 'abcDe'.swapCase() == 'ABCdE'

ref. http://groovy.codehaus.org/ExpandoMetaClass


まず、出だしが String.metaClass で始まっています。これでStringクラスのメタクラスが取得できました。そして、そのメタクラスにメソッド名を指定し、 = { /* クロージャ定義 */ } と続きます。

備考1:メソッド名の動的定義

ちなみに、このメソッド名は、GStringを使って動的に解決してもよいので、以下はすべて同じ意味になります。

String.metaClass.twice = { -> delegate * 2 }
String name = 'twice'
String.metaClass."${name}" = { -> delegate * 2 }
String name = 'twice'
String.metaClass[name] = { -> delegate * 2 }
備考2:delegate

delegate は クロージャ内で利用できる暗黙変数で、この文脈ではStringのインスタンスを指しています。この場合、クラス内で自分自身を参照する時は this を使うのと同じニュアンスで利用して問題ないと思います。

2. クラスにコンストラクタを追加する

メソッドの中でもコンストラクタは特別で、constructor という名前で追加することができます。

// コンストラクタがないクラス
class Book {
  String title
}
Book.metaClass.constructor = { String title ->
  new Book(title:title)
}

def b = new Book('本')
assert b.title == '本'

ref. http://groovy.codehaus.org/ExpandoMetaClass+-+Constructors


コンストラクタを追加する場合は無限ループに注意しましょう。たとえば、以下のようなコードは無限ループに陥ります。

class Book {
  String title
}
// java.lang.StackOverflowError 発生!
Book.metaClass.constructor = { new Book() }

コンストラクタの中で、コンストラクタ自身を呼び出し続けることになりますので、当然ですよね。
これを回避するには、Groovy外部で解決してもらうしかないそうです。たとえば、SpringのBeanUtilsを使うなど。

@Grab('org.springframework:spring-beans:latest.integration')
class Book {
  String title
}
Book.metaClass.constructor = {
  org.springframework.beans.BeanUtils.instantiateClass(Book)
}
def b = new Book()
assert b.title == null

3. クラスにスタティックメソッドを追加する

今度はスタティックメソッドです。基本的にはインスタンスメソッドと同じですが、違うのは、metaClassの後に .static が挟まる部分です。こうすることでスタティックメソッドとして定義することができます。(ちなみに、ここでは定義に = ではなく、 << (左シフト) を利用していますが、これに下の備考を参照下さい)

class Book {
  String title
}
Book.metaClass.static.create << { String title ->
  new Book(title: title)
}
def b = Book.create('本')
assert b.title == '本'

ref. http://groovy.codehaus.org/ExpandoMetaClass+-+Static+Methods

備考3:<< と = の違い

メタクラスにメソッド定義する際には、= と<<が利用できます。両者の違いは、<<は既存メソッドが既に存在する場合、例外がスローされ、=の場合、例外は発生せず、上書きする動作となります。これはあくまでクラスに定義されている既存のメソッドだけですので、動的に追加したメソッドに関しては何度 << しても例外は発生しません。

class Book {
  String title
  int price = 2000
  def getPrice() { price }
}

// 例外発生!
//Book.metaClass.toString << { -> title } // Object#toString
//Book.metaClass.getPrice << { -> 10000 } // Book#getPrice

// これならOK
Book.metaClass.toString = { -> title }
Book.metaClass.getPrice = { -> "\\${delegate.@price}" }

def b = new Book(title: '本')
assert b.toString() == '本'
assert b.price == '\\2000'

// ただし、動的に追加したメソッドに関しては << でも例外が発生しない
Book.metaClass.getBookTitle << {-> "本のタイトルは${title}です。" }
Book.metaClass.getBookTitle << {-> "本のタイトルは${title}です。" }
assert b.bookTitle == '本のタイトルは本です。'

4. クラスにプロパティを追加する

今度はプロパティです。そう、フィールドですね。このフィールドを定義するには大体2つの方法があります、

シンプルな方法

まずはシンプルな方法からですが、こちらはmetaClassにメソッドを定義する容量で、クロージャではない値を指定するだけです。簡単ですね。この例では、times がそれに当たります。

String.metaClass.times = 1
String.metaClass.loop = { delegate * times }

def str = 'str'
assert str.loop() == 'str'
str.times = 3
assert str.loop() == 'strstrstr'
println str.loop()

この方法で定義したプロパティは読み書きが可能となります。

getter, setterを利用してプロパティを定義する

もう一つは、getterとsetterを利用する方法です。Groovyではget〜、set〜というメソッドはプロパティになるんでしたね。getterのみ定義した場合は、読み取り専用、setterのみ定義した場合は書き込み専用となります。

{->
  int times = 1
  String.metaClass.setLoopTimes = { int t -> times = t }
  String.metaClass.getLoop = { delegate * times }
}()

def str = 'str'
assert str.loop == 'str'

str.loopTimes = 3
assert str.loop == 'strstrstr'

ここでは、loopTimes プロパティは書き込み専用、受け取った値をローカル変数(メタメソッド定義時のスコープのみ見える)timesに
格納しているloopプロパティは読み込み専用となり、先ほど格納したtimesの値分、繰り返した文字列にして返却しています。

5. インタフェースにメソッドを追加する

メタクラスを使えば、普通ならメソッドを追加できないはずのインタフェースにもメソッドを追加することができてしまいます。
ここでは、サイズを2倍のサイズを取得するメソッドをコレクションに追加しています。

Collection.metaClass.sizeDoubled = { ->
  delegate.size() * 2
}

assert [1, 2, 3].sizeDoubled() == 6
assert ([1, 2] as Set).sizeDoubled() == 4

コレクションに追加しているので、リスト(ArrayList)でもセット(Set)でも利用できるようになっています。これはトレイトっぽいですね。

6. 個別のインスタンスにメソッドを追加する

今までがすべてクラスについてメソッドやプロパティを追加していたのですが、Groovyではnewしたインスタンスにもそれぞれ個別にメソッドやプロパティを追加できます。各インスタンスもそれぞれがメタクラスを持っているため、そこに今までと同様の方法でクロージャを定義してあげれば完了です。

String.metaClass.getSym = { delegate.intern() }
'+'.sym.metaClass.getOp = { -> {a, b -> a + b} }
'*'.sym.metaClass.getOp = { -> {a, b -> a * b} }

assert (1..10).inject('+'.sym.op) == 55
assert (1..10).inject('*'.sym.op) == 3628800

ref. http://groovy.codehaus.org/Per-Instance+MetaClass


一行目のgetSymは常に同じ文字列を指すため、internするメソッドを定義しているのみです。ここで大事なのは、二行目、三行目のgetOpの部分です。'+' という文字列と '*' という文字列にそれぞれgetOpというメソッドを定義し、クロージャを返却するようにしています。ここで取得されるクロージャは#injectなどに渡すことができますので、assertしているように読みやすい記述が可能となりますね。

もう一つサンプルです。

class Hoge {}
def a = new Hoge()
def b = new Hoge()
a.metaClass.who = { 'A' }
b.metaClass.who = { 'B' }

assert [a, b]*.who() == ['A', 'B']

インスタンス毎に違った挙動をしているのがわかりますね。

7. 動的メソッドの探索

動的に追加されたメソッドは普通にリフレクションを利用しても取得できないので、以下のメタメソッド探索用のメソッドを利用して取得します。

  • hasMetaMethod
  • getMetaMethod
  • hasMetaProperty
  • getMetaProperty
//------getMetaMethod/hasMetaMethod
assert String.metaClass.hasMetaMethod('toString', null)

assert !String.metaClass.hasMetaMethod('hello', null) // まだ#helloはない
assert !String.metaClass.hasMetaMethod('hello', [String] as Class[]) // まだ#helloはない

String.metaClass.hello = { "Hello $delegate" }
String.metaClass.hello = { name -> "Hello $name! $delegate" }

assert String.metaClass.hasMetaMethod('hello', null) // もう#helloはある
assert String.metaClass.hasMetaMethod('hello'      ) // もう#helloはある
assert String.metaClass.hasMetaMethod('hello', [String] as Class[]) // もう#helloはある

def hello  = String.metaClass.getMetaMethod('hello', null)
def hello2 = String.metaClass.getMetaMethod('hello'      )
def hello3 = String.metaClass.getMetaMethod('hello', [String] as Class[])
assert hello.invoke('World!!') == 'Hello World!!'
assert hello2.invoke('World!!') == 'Hello World!!'
assert hello3.invoke('World!!', 'fumo') == 'Hello fumo! World!!'

//------getMetaProperty/hasMetaProperty
assert String.metaClass.hasMetaProperty('empty')

assert !String.metaClass.hasMetaProperty('test') // まだtestはない

String.metaClass.test = "テスト"

assert String.metaClass.hasMetaProperty('test') // もうtestはある

def test = String.metaClass.getMetaProperty('test')
assert test.getProperty('abc') == 'テスト'

ref. http://groovy.codehaus.org/ExpandoMetaClass+-+Runtime+Discovery


取得したメタメソッドは#invokeで、メタプロパティは#getProperyで呼び出すことができます。

8. メタクラスDSL

上記までで色々とやってきましたが、なんとGroovyにはこれらを簡単に定義するためのDSLが用意されています。使わない手はないですね。
メタクラスDSLを利用するには、metaClass()メソッドを利用します。このメソッドは、クロージャを引数に1つだけとります。
なので、例えばBookクラスに対して利用する場合は以下のようになります。

class Book {
  String title
}
Book.metaClass {
  price = 2000
  showTitle = { -> println title }
  'static' {
    create << { String title ->
      new Book(title: title)
    }
  }
}

def b = Book.create('本')
assert b.title == '本'
assert b.price == 2000

b.showTitle() // => '本'

このDSLは、複数のプロパティやメソッドを一気に定義したい場合に特に有用です。それぞれの定義について、以下に解説します。

メタクラスDSLでインスタンスメソッドを定義する

インスタンスメソッドを書く場合は、変数に代入するように書きます。

Book.metaClass {
  showTitle = { -> println title }
}

これで、

def b = new Book()
b.showTitle() 

のような感じで使えます。

メタクラスDSLでスタティックメソッドを定義する

スタティックメソッドを書く場合は、'static' という文字列に対してクロージャを渡すように書きます。

Book.metaClass {
  'static' {
    create << { String title ->
      new Book(title: title)
    }
}

def b = Book.create('本')
assert b.title == '本'
メタクラスDSLでプロパティを定義する

プロパティは普通に変数っぽく定義するだけでよいです。

Book.metaClass {
   price = 2000
}
def b = Book.create('本')
assert b.price == 2000
もちろんインスタンスにも使えます
class Hoge {}
def a = new Hoge()
def b = new Hoge()

a.metaClass {
  who = { 'A' }
}
b.metaClass {
  who = { 'B' }
}

assert [a, b]*.who() == ['A', 'B']

ref. http://groovy.codehaus.org/ExpandoMetaClass+Domain-Specific+Language


慣れて来たらDSLを利用するのが便利だと思います。是非使ってみて下さい。

応用例

せっかくなので、何か作ってみようか…ということで、動的なプロパティ読み込みクラスを定義するサンプルを書いてみました。
長くなってしまったのでソースはgistをご参照下さい。

解説

これは、クラス名を使って同じ名前のプロパティファイルを読み取り、キー一覧を動的メソッド(get + キー名)としてクラスのコンストラクト時に追加してしまおうというサンプルです。

/** Fumo.properties読み込みクラス */
class Fumo extends StringProps {}
def fumo = new Fumo()
assert fumo.name == 'fumokmm'
assert fumo.occupation == 'System Engineer'

/** CalcOperator.properties読み込みクラス */
class CalcOperator extends ClosureProps {}
def op = new CalcOperator()
assert op.plus(1, 2) == 3
assert (1..5).inject(op.multiply) == 120

Fumo.propertiesは

name=fumokmm
occupation=System Engineer

CalcOperator.propertiesは

plus={ a, b -> a + b }
minus={ a, b -> a - b }
multiply={ a, b -> a * b }
div={ a, b -> a / b }

のような定義となっており、それぞれ、文字列として評価、クロージャとして評価するようにしています。

おわりに

今回ご紹介した部分はGroovyのメタプログラミングのほんのさわりでしかありません。しかし、ひとまず、今回ご紹介した例だけでも知っていると、既存のクラスにちょっとした機能追加を行ったり修正を加えたりすることが容易になると思います。もしご興味を持っていただけましたら、ぜひ色々と遊んでみて下さい。
ここまで長々とお読みいただき、ありがとうございました。

さて、お次は、@さんです!よろしくお願いします。

*1:最終的にはコンパイルされた.classは実行時に変更できない、といったニュアンス。