読者です 読者をやめる 読者になる 読者になる

No Programming, No Life

新しいNPNLです。http://d.hatena.ne.jp/fumokmm/ から引っ越してきました。

Groovyで既存クラスのasTypeをOverrideする

Groovy 小ネタ

はじめに

Groovyでは as演算子 を利用するためには #asType(Class) メソッドを実装すればよい。既存クラスには既に #asTypeメソッド が実装されているため、通常あまりいじることはない(と思われる)のだが、とある事情でいじる必要が出てきた。その際、ちとハマッたのでメモ。

文字列を自作のクラスに as する

ちと例が悪いのですが、こないだのポーカーのやつで利用したCardクラスを考える。

enum Suit {
  S('♤♠'), H('♡♥'), D('♢♦'), C('♧♣'),
  def mark
  Suit(mark){ this.mark = mark }
}

enum Rank {
  R2, R3, R4, R5, R6, R7, R8, R9, R10, RJ, RQ, RK, RA
  def rank(){ this.name().substring(1) }
}

class Card {
  Suit s
  Rank r
  String toString() {
    switch(s) {
      case [Suit.H, Suit.D] : return "${s.mark[0]}${r.rank()}"
      case [Suit.S, Suit.C] : return "${s.mark[1]}${r.rank()}"
    }
  }
}

enumのスート(Suit)はトランプの柄を表しており、同じくenumのRankはトランプのランク(A〜K)を表している。*1
そして、SuitとRankをコンポジションとして持つクラス、カード(Card)を定義した。

ここで、以下のような文字列から Card のインスタンスに型変換することを考える。
  • 'SA' -> スペードのエース
  • 'C5' -> クローバーの5
こんな感じにしたい
Card c1 = 'SA' as Card // -> スペードのエース
Card c2 = 'C5' as Card // -> クローバーの5

そこで、文字列クラス(String)の as を定義する。冒頭でも書いたが、Groovyではas演算子の定義にはasTypeを定義すればよい。ただしStringのasは既に定義されているため、既存の実装は残しつつ新たなルールを追加する形で実装する必要がある。

StringのasTypeを再定義
String.metaClass.define {
  oldAsType = String.metaClass.getMetaMethod("asType", [Class] as Class[])
  asType = { Class c ->
    if (c == Card)
      new Card(s:delegate[0] as Suit, r:'R'+delegate[1..-1] as Rank)
    else
      oldAsType.invoke(delegate, c)
  }
}

oldAsTypeに定義前のasTypeメソッドの保持しておき、ClassがCardの場合とそれ意外の場合で処理を切り分けるようにしている。

  • Cardの場合は文字列はenumにasできることを利用して、SuitとRankそれぞれに分解して型変換している
  • Cardでなかった場合は保持しておいた定義前のasTypeをinvokeして変換した結果を返却

Cardだった場合の定義は再帰的になっているが、うまく動作する。

今回使用したソースコード

動作確認: Groovy Version: 2.0.0 JVM: 1.7.0_05 Vendor: Oracle Corporation OS: Mac OS X

宿題

ちなみに、oldAsTypeで定義前のメソッドを保持しておく際に、以下のようにMethodClosureの形だとうまく動かなかった。

String.metaClass.define {
  oldAsType = String.metaClass.&asType // <- MethodClosure
  asType = { Class c ->
    if (c == Card)
      new Card(s:delegate[0] as Suit, r:'R'+delegate[1..-1] as Rank)
    else
      oldAsType(c) // <- MethodClosure
  }
}
Caught: groovy.lang.MissingMethodException: No signature of method: overrideAsType_err.oldAsType() is applicable for argument types: (java.lang.Class) values: [class Suit]
Possible solutions: asType(java.lang.Class)
groovy.lang.MissingMethodException: No signature of method: overrideAsType_err.oldAsType() is applicable for argument types: (java.lang.Class) values: [class Suit]
Possible solutions: asType(java.lang.Class)
	at overrideAsType_err$_run_closure1_closure2.doCall(overrideAsType_err.groovy:29)
	at overrideAsType_err$_run_closure1_closure2.doCall(overrideAsType_err.groovy:27)
	at overrideAsType_err.run(overrideAsType_err.groovy:33)

oldAsType()は java.lang.Class を受け取るんだよ! [class Suit] じゃダメなんだからね!ってことらしい…、SuitもClassのはずなんだが…。
とりあえずgetMetaMethodを使っておけばうまくいくので、いずれこの問題も調査しよう。

おわりに

実はこの完成までに定義が再帰してしまったりして、色々調べてここまで辿り着きました。EMCは奥が深い。

*1:enumは命名規約上、数値から始められないので、Rankは先頭に'R'を付けている。#toStringの際に'R'は取り除くようにしている。