Groovyで既存クラスのasTypeをOverrideする
はじめに
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だった場合の定義は再帰的になっているが、うまく動作する。
宿題
ちなみに、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は奥が深い。
参考
お題:ポーカーをGroovyで解いてみた
問題
ポーカー
5枚のカードを示す文字列を入力とし、ポーカーの役を出力せよ。
ただし:一枚のカードは、スートを表す文字+ランクを表す文字列 で構成される。
スートを表す文字は、S, H, D, C のいずれか
ランクを表す文字列は、2, 3, ..., 9, 10, J, Q, K, A のいずれか
下表の役に対応すること。下表の役に該当しない場合は '--' を出力すること。
カードはジョーカーを含まない52枚から5枚が選ばれる。
不正な入力への対応は不要。対応すべき役と、その役だった場合に出力する文字列は以下のとおり:
フォーカード : 4K
フルハウス : FH
スリーカード : 3K
ツーペア : 2P
ワンペア : 1P
上記のいずれにも該当しない : --例えば、入力が「D3C3C10D10S3」ならフルハウスなので「FH」と出力する。
入力が「S8D10HJS10CJ」ならツーペアなので「2P」と出力する。
回答
Groovyで関数型を意識したFizzBuzzを書いてみた
はじめに
なるべく関数型を意識して書いてみました。
なんとか140文字に収めることができました。
a={n,s->['']*n+s};f={n->[0..n,a(3,'fizz')*n,a(5,'buzz')*n].transpose().collect{i,f,b->[f,b].any()?f+b:i}};f(100).drop(1).each{println(it)}
ノート
ごちゃごちゃしてて分かりにくいので、読みやすくなるように軽く解説。
aクロージャ
a={n,s->['']*n+s}
まずはこれですが、n
(数値を想定)とs
(文字列を想定)の2引数を受け取るクロージャをaという変数に格納しています。
[''] * n + s
の結果がこのクロージャの値になりますが、これは例えばn
が3
で、s
が'fizz'
だった場合、
['', '', '', 'fizz']
というリストになります。
fクロージャ
f={n->[0..n,a(3,'fizz')*n,a(5,'buzz')*n].transpose().collect{i,f,b->[f,b].any()?f+b:i}}
さて、次は長いですが、順番に見て行きましょう。
まずは、transpose
ですが、これは複数指定されたリストの縦を横を組み替えたリストにするメソッドです。たとえば、こんな感じです。
assert [['a', 'b', 'c'], [1, 2, 3]].transpose() == [['a', 1], ['b', 2], ['c', 3]]
注意点としては、長過ぎた要素は無視されることでしょうか。
assert [['a'],['b','b'],['c','c','c']].transpose() == [['a', 'b', 'c']]
さて、fクロージャに戻って、transpose()している部分を見てみましょう。
[0..n, a(3,'fizz') * n, a(5,'buzz') * n].transpose()
これは、
- 第1要素が0〜nまでの数列
- 第2要素が、先ほどのaクロージャで作った、['', '', '', 'fizz'] 配列を n 回繰り返した配列
- 第3要素が、も同じく 'buzz'配列の n 回繰り返した配列
となりまして、それをtranspose()するわけですから、結果としては、以下のような配列となります。
[[0, '', ''], [1, '', ''], [2, '', ''], [3, 'fizz', ''], [4, '', ''], [5, '', 'buzz'], [6, '', ''], ...]
出来上がる配列の数は一番数が少ない 0..n に合わせられるのがミソですね。
さて、お次はcollectしている部分です。collectに渡しているクロージャはこんな感じになっています。
{ i, f, b -> [f, b].any() ? f+b : i}
これは、引数を3つ取り(それぞれ、数、fizz文字列、buzz文字列を想定)、結果の文字列へと加工している部分です。
たとえば、このクロージャに配列の [15, 'fizz', 'buzz'] を引数として渡すと、'fizzbuzz' という文字列が返却されます。
def toFizzBuzz = { i, f, b -> [f, b].any() ? f+b : i} assert 'fizzbuzz' == toFizzBuzz([15, 'fizz', 'buzz'])Groovyでは引数として配列を渡すと展開されて適用されますので、第1要素がi、第2要素がf、第3要素がbといったようにバインドされるイメージです。
[f, b].any() ? となっていますので、 'fizz' もしくは 'buzz' 文字列がある場合のみ、文字列を結合した結果とし、そうでなければ、数値の i を返却します。
fクロージャの戻り値
ここまでで、fクロージャはfizzbuzzの配列を返却できています。これでほぼ完成なのですが、あとは出力処理を行うのみとなります。
fクロージャを利用して結果出力
f(100).drop(1).each{println(it)}
ここでは、fクロージャに引数として100を与え、0〜100までのfizzbuzzリストを取得しています。
ただし、fizzbuzzは1〜nのという条件ですので、drop(1)で、1つ要素を取り覗いています。
あとはおなじみ、eachで回して出力しているだけです。
おわりに
Groovyでも書こうと思えばそれなりに関数型っぽく書けました。
以上、誰かの何かの参考になれば幸いです。
javaとかScalaで全角を半角に変換する方法をGroovyでもやってみた
はじめに
javaとかScalaで全角を半角に変換する方法 - ブログなんだよもんをGroovyでもやってみた。
元記事でも触れられておりますが、確かにこういう処理は再発明はしたくない。
ソース
Groovyの場合、Grape(@Grabアノテーション)でMavenリポジトリなどからライブラリを取得できる。今回のICU4Jの取得方法は現場で使L Java ライブラリ - 倭マン日記を参考にした。
追記1 (2012-05-12)
Spockでも \(・ω・)/ 書いてみた。
Happy Coding!
Groovyのチートシート
当記事は、Groovyのチートシート | Think Twiceへ移管しました。