倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

GDK のコレクションがこんなに便利なわけがない。 (Collection 編8) 型変換

今回は GDK が Collection インターフェースに追加するクラスのうち、型変換をするものを見ていきます。 今回でやっと Collection 最後かな。 ただ、まだ List をやらんといかんのだが。 型変換は便利だけどあんまり面白くないんでサラッといっちゃいましょう。

List asList()
Collection asImmutable()
Collection asSynchronized()

Object asType(Class clazz)

List toList()
Set toSet()

as と to で似たのがありますが、as は自分自身を返すこともあるけど、to は既に指定された型のインスタンスであってもコピーを返す、という違いがあるようです。 これは Groovy 一般に言えるんかな?

メソッド 返り値 since 説明
asList()
asImmutable()
asSynchronized()
List
Collection
Collection
1.0
1.5.0
1.0
List に変換して返す。 既に List なら自分自身を返す
不変(変更できない)コレクションを返す
同期かされたコレクションを返す
asType(Class) Object 1.0 指定された型に変換する
toList()
toSet()
List
Set
1.0
1.8.0
List に変換して返す。 List に対して呼び出すとコピーを返す
Set に変換して返す。 Set に対して呼び出すとコピーを返す

ではそれぞれのメソッドのサンプルコードを。

asXxxx() メソッド

まずは as で始まるメソッド。 asList() は呼び出し元のコレクションを List オブジェクトに変換しますが、後で見る toList() と違って、既に呼び出し元が List なら自分自身を返します。 Groovy 一般について as は自分自身を返すのかな?と思いきや、asImmutable() とかはそうでもないみたいw asImmutable(), asSynchronized() は java.util.Collections クラスの unmodifiableList(), unmodifiableSet() を呼び出した場合と同じようです(unmodifiableCollectin() と言った方がいいのか)。

def langs = ['Java', 'Groovy', 'Scala', 'Clojure']
def langSet = langs as Set

// asList()
assert langs.asList() instanceof ArrayList
assert langs.asList() == langs
assert langs.asList().is(langs)    // 元のオブジェクト

// asImmutable()
assert langs.asImmutable().class.name == 'java.util.Collections$UnmodifiableRandomAccessList'
assert langs.asImmutable() == Collections.unmodifiableList(langs)    // List に対して
assert langSet.asImmutable() == Collections.unmodifiableSet(langSet)    // Set に対して

// asSynchronized()
assert langs.asSynchronized().class.name == 'java.util.Collections$SynchronizedRandomAccessList'
assert langs.asSynchronized() == Collections.synchronizedList(langs)    // List に対して
assert langSet.asSynchronized() == Collections.synchronizedSet(langSet)    // Set に対して

asType() メソッド

asType() メソッドは as キーワードを使って型変換するときに呼び出されます。 as で始まるので変換の必要がなければ自分自身が返されます。 知らないクラスが指定された場合は Collection をとるコンストラクタによってインスタンス生成される模様。

toXxxx() メソッド

最後は to で始まるメソッド。 toList(), toSet() メソッド。 toList() は asList() と異なり、呼び出し元が List オブジェクトでもそのコピーが返されます。

// toList()
assert langs.toList() instanceof ArrayList
assert langs.toList() == langs
assert !langs.toList().is(langs)    // 元と別のオブジェクト

// toSet()
assert langs.toSet() instanceof HashSet
assert langs.toSet() == langSet
assert !langs.toSet().is(langSet)    // 元と別のオブジェクト

さぁ、これで Collection に追加されたは終了。 でも、まだ List に追加されたメソッドが結構・・・

プログラミングGROOVY

プログラミングGROOVY

GDK のコレクションがこんなに便利なわけがない。 (Collection 編7) 多重コレクション

今回は GDK が Collection インターフェースに追加している多重コレクション(多次元コレクション)に関連するメソッドを見ていきます。 今回扱うのは以下のメソッド:

Collection flatten()
Collection flatten(Closure flattenUsing)

Collection split(Closure closure)

Map groupBy(Object closures)
Map groupBy(Closure closure)
Map groupBy(List closures)

List combinations()

Iterator eachPermutation(Closure closure)

まぁ、前回までも多重コレクションに関するメソッドは出てきてたんですが、名前的に他の分類にしてました。 それらも含めて簡単にまとめると

  • 多重コレクションに対して呼び出すメソッド
    • collectNested()
    • flatten()
    • combinations()
  • 多重コレクションを返す
    • split()
    • combinations()
    • eachPermutation()*1
  • (多重)マップを返す
    • collectEntries()
    • countBy()
    • groupBy()
  • コレクションを返すクロージャを引数にとる(メソッドの返り値は多重コレクションとは限らない)
    • collectMany()

みたいな感じ。 どっちにしろあんまりスッキリした分類にならないなぁ。 まぁそれはともかく、今回見ていくメソッドをもう少し詳しく見てみると:

メソッド名 返り値 since 説明
flatten()
flatten(Closure)
Collection 1.6.0 多重コレクションを平坦化する
split(Closure) Collection 1.6.0 boolean 値を返すクロージャをとり、その真偽値でグループ分け
groupBy(Object)
groupBy(Closure)
groupBy(List)
Map 1.6.0 引数のクロージャの返す値でグループ化
combinations() List 1.5.0 多重コレクションに対して呼び出し、各コレクションの要素から1つずつ選んだ組合せを返す
eachPermutation(Closure) Iterator 1.7.0 全ての並べ替えに対してクロージャを適用する

なんか短文での説明がしにくいのでサンプル・コードを見ていきましょうかね。

flatten() メソッド

flatten() メソッドは、多重コレクションを平坦化します。 つまり、コレクションの要素としてまたコレクションがある場合、その要素をトップレベルのコレクションの要素にします。

assert [[1, 3, 5], [2, 4, 6]].flatten() == [1, 3, 5, 2, 4, 6]
assert [[1, 3, 5], [2, 4, 6]].flatten{ it**2 } == [1, 9, 25, 4, 16, 36]

引数としてクロージャを渡した場合、各要素に対してそのクロージャを実行し、その結果値をコレクションの要素にします。

split() メソッド

split() メソッドは、boolean 値を返すクロージャを引数にとり、そのクロージャを適用した結果の真偽値でグループ分けします。 数学的に言うと、クロージャが表す命題に対して真理集合とその補集合*2にグループ分けするって感じですね。

def langs = ["Java", "Groovy", "Scala", "Clojure", "Jython", "JRuby", "Go", "Smalltalk", "C++", "C#"]

assert langs.split{ it.startsWith 'J' } == [
    ["Java", "Jython", "JRuby"],    // 'J' で始まる
    ["Groovy", "Scala", "Clojure", "Go", "Smalltalk", "C++", "C#"]    // 'J' で始まらない
]

groupBy() メソッド

groupBy() は引数のクロージャを各要素に適用し、その値によって元の要素を分類したマップを返します。 マップのキーはクロージャを適用した結果のオブジェクトになります。

def langs = ["Java", "Groovy", "Scala", "Clojure", "Jython", "JRuby", "Go", "Smalltalk", "C++", "C#"]

assert langs.groupBy{ it[0] } == [
    J:['Java', 'Jython', 'JRuby'],
    G:['Groovy', 'Go'],
    S:['Scala', 'Smalltalk'],
    C:['Clojure', 'C++', 'C#']
]

2つ以上のクロージャを渡すとマップの値がさらにマップの多重マップが生成されます。 複数のクロージャは、配列(可変長引数)またはリストとして指定します:

def langs = ["Java", "Groovy", "Scala", "Clojure", "Jython", "JRuby", "Go", "Smalltalk", "C++", "C#"]
def langsJVM = langs[0..5]

// groupBy(Closure[])
assert langs.groupBy({ it[0] }, { it in langsJVM }) == [
    J:[(true):['Java', 'Jython', 'JRuby']],
    G:[(true):['Groovy'], (false):['Go']],
    S:[(true):['Scala'], (false):['Smalltalk']],
    C:[(true):['Clojure'], (false):['C++', 'C#']]
]

// groupBy(List<Closure>)
assert langs.groupBy([{it in langsJVM}, { it[0] }]) == [
    (true):[
        J:['Java', 'Jython', 'JRuby'],
        G:['Groovy'],
        S:['Scala'],
        C:['Clojure']
    ],
    (false):[
        G:['Go'],
        S:['Smalltalk'],
        C:['C++', 'C#']
    ]
]

クロージャの順番を逆にすると、もちろん別のマップが生成されます。

combinations() メソッド

combinations() は多重コレクションに対して呼び出せ、ネスとされた各コレクションから要素を1つずつ選んでコレクションを作り、それらのリストを返します

assert [['a', 'b'], ['c', 'd']].combinations() == [
    ['a', 'c'],    // ['a', 'b'] から 'a'、['c', 'd'] から 'c' を選んで作ったリスト
    ['b', 'c'],
    ['a', 'd'],
    ['b', 'd']
]

assert ['abc', 'def'].combinations().collect{ it.sum() } == [
        // String は各文字のコレクション(Iterable) として扱われる
    'ad', 'bd', 'cd',
    'ae', 'be', 'ce',
    'af', 'bf', 'cf',
]

具体的に書き下す気がするのはこの辺りまでかなw 要素がコレクションでない場合は、要素が1つのコレクションと見做されます:

assert ['a', ['b', 'c']].combinations() == [
    ['a', 'b'], ['a', 'c']
]

eachPermutation() メソッド

eachPermutation() は要素を有り得る全ての並べ方に並べ替えたものに対して引数のクロージャを適用します。 よく考えると多重コレクションは関係ないけど・・・ 使い方はこんな感じ:

def perm = [] as Set
[1, 2, 3].eachPermutation{ perm << it }
assert perm == [
    [1, 2, 3], [2, 3, 1], [3, 1, 2],
    [3, 2, 1], [2, 1, 3], [1, 3, 2]
].toSet()

元のコレクションが n 個の要素を持ってたら、n! 個の並べ替えに対してクロージャが適用されます。

多重コレクションや多重マップが出てくると、だんだん書き下して動作を把握するのが大変になってきますが、それでも小さい要素数のときに具体的に書き下すのは理解への第一歩って感じですね。

次回は Collection 編最後の型変換に関するメソッド。

プログラミングGROOVY

プログラミングGROOVY

*1:正確には多重コレクションではないけど、コレクションの Iterator なんで似たようなもん。 そもそも返り値を使わずにクロージャを渡して使うんだけど。

*2:全体集合を元のコレクションとする。

GDK のコレクションがこんなに便利なわけがない。 (Collection 編6) Reduce 処理

今回は GDK が Collection インターフェースに追加しているメソッドの内、Reduce 処理を行うものを見ていきます。 Reduce 処理とは各要素もしくはそれから得た値などをある意味で「足し上げる」メソッドです。 今回扱うメソッドはは以下のもの:

Number count(Object value)
Number count(Closure closure)
Map countBy(Closure closure)

Object sum()
Object sum(Object initialValue)
Object sum(Closure closure)
Object sum(Object initialValue, Closure closure)

Object max()
Object max(Closure closure)
Object max(Comparator comparator)
Object min()
Object min(Closure closure)
Object min(Comparator comparator)

Object inject(Closure closure)
Object inject(Object initialValue, Closure closure)

Reduce 処理のメソッドは(おそらく)すべて inject() メソッドで書くことができます。 以下のサンプルでいくつかそういったコードを試してます。

メソッド 返り値 since 説明
count(Object)
count(Closure)
Number 1.0
1.8.0
条件にあう要素の数え上げ
countBy(Closure) Map 1.8.0 クロージャの処理結果でグループ化して各グループの要素数を数え上げ
sum()
sum(Object)
sum(Closure)
sum(Object, Closure)
Object 1.0
1.5.0
1.0
1.5.0
各要素の足し上げ
max()
max(Closure)
max(Comparator)
Object 1.0 最大値を取得
min()
min(Closure)
min(Comparator)
Object 1.0 最小値を取得
inject(Closure)
inject(Object, Closure)
Object 1.8.7
1.0
汎用的な Reduce 処理

では各メソッドのサンプルを。

count(), countBy() メソッド

まずは要素の数を数え上げる count(), countBy() メソッド。 count() は引数のオブジェクトと等価な要素、もしくは引数のクロージャが true を返す元の要素の数を数えて返します。 countBy() は各要素をクロージャで変換し、それを元に要素をグループ化してそのグループの要素数を返します。 次回やる予定の groupBy() で値のリストをそのサイズに置き換えたような処理です。

def langsDup = ['Java', 'Groovy', 'Groovy', 'Scala', 'Java', 'Clojure']

// count(), countBy()
assert langsDup.count('Java') == 2
assert langsDup.count{ it.contains 'a' } == 3
assert langsDup.countBy{ it.size() } == [4:2, 6:2, 5:1, 7:1]

// countBy() はこんな感じの処理かな? もちろん実装は違うだろうけど
assert langsDup.countBy{ it.size() } == 
        langsDup.groupBy{ it.size() }.collectEntries{ key, value -> [key, value.size()] }

countBy() は最初「いるか?こんなの?」と思ったけど(笑)、他のメソッドで書こうとするとちょびっと面倒かもね。 上記の最後のサンプルも、Map#withDefault() とか使うともっとエレガントに書けそう。

sum() メソッド

sum() メソッドはその名の通り、全ての要素を足し上げるメソッドです。 足し上げ処理に使われるメソッドは plus() メソッド、すなわち「+」演算子です。 数値の場合はそのままですが、文字列のように「+」演算子が定義されているオブジェクトのコレクションに対しても問題なく実行できます。 クロージャをとるメソッドは、クロージャによって+演算子が定義されているオブジェクト(数値など)に変換してから足し上げを実行します:

assert (0..10).sum() == 55

def langs = ['Java', 'Groovy', 'Scala', 'Clojure']
assert langs.sum() == 'JavaGroovyScalaClojure'    // String の和
assert langs.sum{ it.size() } == 4 + 6 + 5 + 7    // 文字列長の和
assert langs.sum(8){ it.size() } == 8 + 4 + 6 + 5 + 7    // 初期値指定

// sum(Closure) の書き換え
assert langs.sum{ it.size() } == langs.collect{ it.size() }.sum()

// count(Object) の書き換え
assert langs.count('Java') == langs.sum{ it == 'Java' ? 1 : 0 }

max(), min() メソッド

max(), min() メソッドはコレクション中の最大値、最小値となる要素を返すメソッド。 引数がない場合は要素のクラスが持っている自然順序で比較が行われます。 自然順序以外の比較を行いたい場合はクロージャや Comparator を引数にとるメソッドを使えます:

def langs = ['Java', 'Groovy', 'Scala', 'Clojure']

// 自然順序(String の場合は辞書順序)で比較
assert langs.max() == 'Scala'
assert langs.min() == 'Clojure'

// 文字サイズで比較(クロージャ使用)
assert langs.max{ it.size() } == 'Clojure'
assert langs.min{ it.size() } == 'Java'

// 最後の文字から見る辞書順序で比較(Comparator 使用)
assert langs.max{ s0, s1 -> s0[-1] <=> s1[-1] ?: s0[-2] <=> s1[-2] } == 'Groovy'
assert langs.min{ s0, s1 -> s0[-1] <=> s1[-1] ?: s0[-2] <=> s1[-2] } == 'Scala'

そう言えば、比較を行うときは「<=>」演算子ってのがあったんですね。 最後のサンプルでは、最後の文字の辞書順序(同じならその前の文字の辞書順序)を使って比較しています。

inject() メソッド

最後は Reduce 処理の汎用型、inject() メソッド。 これを使うと大抵の Reduce 処理は書けます。 クロージャの引数は、第1引数が初期値もしくはそれまで「足し上げられた結果」、第2引数が次の要素です。 結果として、前の足し上げ結果に次の要素を足し上げたものを返します。 初期値が指定されていない場合は第1引数が初期値として渡されます:

def langs = ['Java', 'Groovy', 'Scala', 'Clojure']

// sum() の再実装
assert langs.inject(''){ sum, s -> sum + s } == 'JavaGroovyScalaClojure'

// 要素の長さの積
assert langs.inject(1){ prod, s -> prod * s.size() } == 4 * 6 * 5 * 7

// 初期値を指定しない場合は第1引数が初期値
assert langs.inject{ sum, s -> sum + s } == 'JavaGroovyScalaClojure'
assert langs.inject{ prod, s -> prod * s.size() } == 'Java' * 6 * 5 * 7
        // 初期値は文字列 'Java' なので、整数をかけると文字列の繰り返しが返されるヨ

// min() の再実装
assert langs.inject{ s0, s1 -> s0.size() <=> s1.size() ? s0 : s1 } == 'Java'

要素を変換して「足し上げ」を行う場合は、初期値を指定しないと変な結果になる場合があるので注意。

次回は多重コレクションに関するメソッドを扱う予定。

プログラミングGROOVY

プログラミングGROOVY

GDK のコレクションがこんなに便利なわけがない。 (Collection 編5) その他の集合生成

今回は GDK が Collection クラスに追加している、いろいろなコレクションを生成するメソッドを見ていきます。 「コレクションを生成する」と言っても、(static) ファクトリ・メソッドを集めて見ていくというわけではなく、メソッドの返り値がコレクションなんだけど、ちょっと他の分類に入れにくいなぁというものをまとめて見ていく感じです。 今回扱う unique() や sort() が「コレクションを生成する」というなら、今までに扱った collect() や findAll()、なんなら plus() や leftShift() だって「コレクションを生成する」と言えなくもないですからねぇ。 ということで、今回の分類はあんまり気にしないで下さいな。 これを踏まえて、今回見ていくメソッドは以下のもの:

List multiply(Number factor)

Collection unique()
Collection unique(Closure closure)
Collection unique(Comparator comparator)
Collection unique(boolean mutate)
Collection unique(boolean mutate, Closure closure)
Collection unique(boolean mutate, Comparator comparator)

List sort()
List sort(Closure closure)
List sort(Comparator comparator)
List sort(boolean mutate)
List sort(boolean mutate, Closure closure)
List sort(boolean mutate, Comparator comparator)
  • multiply() は Groovy では「*演算子として使えます
  • unique() と sort() は使い方が似てますな

各メソッドを表にする以下の通り:

メソッド 返り値 since 説明
multiply(Number) List 1.0 元のコレクションの要素を引数の回数だけ繰り返したコレクションを返す
unique()
unique(Closure)
unique(Comparator)
unique(boolean)
unique(boolean, Closure)
unique(boolean Comparator)
Collection 1.0
1.0
1.0
1.8.1
1.8.1
1.8.1
重複した要素を省いたコレクションを返す
sort()
sort(Closure)
sort(Comparator)
sort(boolean)
sort(boolean, Closure)
sort(boolean, Comparator)
List 1.0
1.0
1.0
1.8.1
1.8.1
1.8.1
要素を指定した順序に並べ替えたコレクションを返す

では、各メソッドのサンプルを見ていきましょう。

multiply() メソッド

multiply() メソッドは元の要素を指定回数だけ繰り返したコレクションを生成します。 Groovy では「*」演算子を使って multiply() メソッドを呼び出せます:

assert ['Groovy'] * 3 == ['Groovy', 'Groovy', 'Groovy']
assert ['Java', 'Groovy'] * 3 == ['Java', 'Groovy', 'Java', 'Groovy', 'Java', 'Groovy']

要素が1つの場合は java.util.Collections#fill() と同じような処理ですかね。

unique() メソッド

unique() は重複した要素の2つ目以降を削除したコレクションを返します。 boolean 値の引数は元のコレクション自体を変更するかどうか(true なら変更する、デフォルトは true)、クロージャの引数はその処理結果の equals() で2つの要素を同じと見做すかどうかを評価する、Comparator の引数は2つの要素の比較アルゴリズムをそれぞれ指定します。 デフォルトで(boolean 値を指定しないと)元のコレクションが変更されてしまうので注意。 Java の標準 API や Groovy 1.0 あたりの互換性から仕方ないかと思いますが、関数型言語が流行ってる昨今では少々逆風な気も。 まぁ、変更されるかどうかを指定できるだけでもヨシとする?

def langsDup0 = ['Java', 'Groovy', 'Groovy', 'Scala', 'Java', 'Clojure']

// unique()
assert langsDup0.unique() == ['Java', 'Groovy', 'Scala', 'Clojure']
assert langsDup0 == ['Java', 'Groovy', 'Scala', 'Clojure']    // デフォルトでは元も変更


def langsDup1 = ['Java', 'Groovy', 'Groovy', 'Scala', 'Java', 'Clojure']

// unique(boolean)
assert langsDup1.unique(false) == ['Java', 'Groovy', 'Scala', 'Clojure']
assert langsDup1 == ['Java', 'Groovy', 'Groovy', 'Scala', 'Java', 'Clojure']    // 変更されない


def langsDup2 = ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython', 'JRuby', 'Smalltalk', 'COBOL']

// unique(boolean, Closure)
assert langsDup2.unique(false){ it[0] } == ['Java', 'Groovy', 'Scala', 'Clojure']
    // 頭文字で同じかどうかを評価
assert langsDup2.unique(false){ it.size() } == ['Java', 'Groovy', 'Scala', 'Clojure', 'Smalltalk']
    // 文字列長で同じかどうかを評価

// unique(boolean, Comparator)
assert langsDup2.unique(false){ s0, s1 -> s0.size() - s1.size() } == 
        ['Java', 'Groovy', 'Scala', 'Clojure', 'Smalltalk']

個人的にはずっと false を指定して使いそう。 なんか、カテゴリか拡張モジュールでも作って一貫してイミュータブルな振る舞いをするようにしたいもんだ。

sort() メソッド

sort() は各要素を並べ替えるメソッド。 引数は unique() と似通っております。 boolean 値は元のコレクションを変更するかどうか(true なら変更する、デフォルトは true)、クロージャと Comparator の引数は順序を決定するアルゴリズムを指定します。 クロージャの返り値は Comparable でないといけないでしょうね。 使い方はこんな感じ:

def langs0 = ['Java', 'Groovy', 'Scala', 'Clojure']

// sort()
assert langs0.sort() == ['Clojure', 'Groovy', 'Java', 'Scala']
assert langs0 == ['Clojure', 'Groovy', 'Java', 'Scala']    // デフォルトで元が変更される


def langs1 = ['Java', 'Groovy', 'Scala', 'Clojure']

// sort(boolean)
assert langs1.sort(false) == ['Clojure', 'Groovy', 'Java', 'Scala']
assert langs1 == ['Java', 'Groovy', 'Scala', 'Clojure']    // 元が変更されない

// sort(boolean, Closure)
assert langs1.sort(false){ it.size() } == ['Java', 'Scala', 'Groovy', 'Clojure']
    // 文字列長で並べ替え

// sort(boolean, Comparator)
assert langs1.sort(false){ s0, s1 -> s0.size() - s1.size() } ==
        ['Java', 'Scala', 'Groovy', 'Clojure']
    // これも文字列長で並べ替え

まぁ、元のコレクションを変更するかどうかに気を付ければなんてことないですね。

次回はコレクションの各要素を走査してなんらかの値を計算する Reduce 処理を行うメソッドを見ていく予定。

プログラミングGROOVY

プログラミングGROOVY

GDK のコレクションがこんなに便利なわけがない。 (Collection 編4) 要素の蒐集 collect

今回は GDK が Collection クラスに追加しているメソッドの内、要素の蒐集を行うメソッドを見ていきます。 メソッド名は「collect」で始まるもの。 要素の蒐集とは、引数としてとったクロージャを各要素に適用し、その結果返される要素を集めて新たなコレクションとして返す処理です。 前回見た findResults() と同じようなメソッドですが、null 値を落としたりはしません。 今回見ていくメソッドは以下のもの:

List collect(Closure transform)
Collection collect(Collection collector, Closure transform)
List collect()

List collectMany(Closure projection)
Collection collectMany(Collection collector, Closure projection)

List collectNested(Closure transform)
Collection collectNested(Collection collector, Closure transform)
// List collectAll(Closure transform)
// Collection collectAll(Collection collector, Closure transform)

Map collectEntries(Closure transform)
Map collectEntries()
Map collectEntries(Map collector, Closure transform)
Map collectEntries(Map collector)

List getAt(String property)
  • Collection に追加されているメソッドなのに返り値が List とかになってるのは・・・よくわからん
  • collectAll() は collectNested() に名前が変更され、非推奨になってます
  • getAt(String) は collect で始まってませんが、似たような処理を行うのでここで扱います

各メソッドをもう少し詳しく見ていくとこんな感じ:

メソッド 返り値 since 説明
collect(Closure)
collect(Collection, Closure)
collect()
List
Collection
List
1.0
1.0
1.8.5
各要素にクロージャを適用し、処理結果を蒐集する
collectMany(Closure)
collectMany(Collection, Closure)
List
Collection
1.8.1
1.8.5
クロージャの処理結果がコレクションなら展開する
collectNested(Closure)
collectNested(Collection, Closure)
collectAll(Closure)
collectAll(Collection, Closure)
List
Collection
List
Collection
1.8.1
1.8.1
-
-
多重コレクションならその要素にクロージャの処理を適用する
collectEntries(Closure)
collectEntries(Map, Closure)
collectEntries()
collectEntries(Map)
1.7.9
1.7.9
1.8.5
1.8.5
Map 各要素からエントリを生成し、マップを構築する
getAt(String) List 1.0 各要素のプロパティを蒐集する

では、各メソッドの簡単なサンプルをそれぞれ見ていきましょう。

collect() メソッド

まずは collect() メソッド。 関数オブジェクトとかラムダ式的なものが使える言語では大抵実装されているメソッドですね。

def langs = ['Java', 'Groovy', 'Scala', 'Clojure']

assert langs.collect{ it.size() } == [4, 6, 5, 7]

assert 'Java'.collect() == ['J', 'a', 'v', 'a']

assert langs.collect([1, 2]){ it.size() } == [1, 2, 4, 6, 5, 7]
assert langs.collect(new HashSet()){ it.size() } == [4, 6, 5, 7].toSet()

引数をとらない collect() は例によって Closure.IDENTITY (コードで書くと { it } かな)が指定されているのと同じ処理がされます。 このメソッドは Collection ではなコンテナ・オブジェクト(要素を収めているオブジェクト、配列とか Iterator とかを含む)をコレクションに変換するときに便利。 今のサンプルでは Groovy が String を文字(1文字の String)の Iterable として扱えるのを利用して、各文字のコレクションに変換しています。

引数にコレクションをとる collect() は結果のコレクションにデフォルト値を設定したり、利用するコレクションの型を指定したりするのに使えます。

collectMany() メソッド

次は collectMany() メソッド。 collectMany() は引数のクロージャとしてコレクションを返すものを渡された場合に、それらのコレクションを展開して、結果のコレクションを平坦化します。

def langs = ['Java', 'Groovy', 'Scala', 'Clojure']

assert ['Java', 'Groovy'].collectMany{ it.collect() } == ['J', 'a', 'v', 'a', 'G', 'r', 'o', 'o', 'v', 'y']
    // it が String なら { it.collect() } は String の各文字を要素とするリストを返す

assert langs.collectMany{ it.collect() } == langs.collect{ it.collect() }.flatten()

flatten() はまだ扱ってませんが、多重コレクションを平坦化するメソッドです。 ただし、今のように collectMany() と collect().flatten() の結果が等しくなるのはクロージャが返すコレクションが多重コレクションになっていない場合だけです:

assert (0..3).collectMany{ [it, [it**2, it**3]] } == [0, [0, 0], 1, [1, 1], 2, [4, 8], 3, [9, 27]]
assert (0..3).collect{ [it, [it**2, it**3]] }.flatten() == [0, 0, 0, 1, 1, 1, 2, 4, 8, 3, 9, 27]

こういう場合に collectMany() がどの程度必要かはよく分かりませんがね。

collectNested() メソッド

collectNested() メソッドは、多重コレクションに対して呼ばれたときに、要素のコレクションのさらに要素に対してクロージャの処理を行います。 まぁ、再帰的にクロージャの処理を行うと言えばいいでしょうか。

def llangs = [[['Java', 'Groovy'], 'Scala'], ['Clojure', 'Fantom']]

assert llangs.collectNested{ it.size() } == [[[4, 6], 5], [7, 6]]

返されるコレクションは元のコレクションと同じネスト構造をしています。

collectEntries() メソッド

collectEntries() メソッドは、2要素の List*1 を返すクロージャをとり、それをキーと値にするマップを構築します。

assert langs.collectEntries{ [it[0], it] } == [J:'Java', G:'Groovy', S:'Scala', C:'Clojure']

def langArray = [['J', 'Java'], ['G', 'Groovy'], ['S', 'Scala'], ['C', 'Clojure']]
assert langArray.collectEntries() == [J:'Java', G:'Groovy', S:'Scala', C:'Clojure']

assert langs.collectEntries(F:'Fantom'){[it[0],it]} == [F:'Fantom', J:'Java', G:'Groovy', S:'Scala', C:'Clojure']
assert langs.collectEntries(J:'JavaScript'){ [it[0], it] } == [J:'Java', G:'Groovy', S:'Scala', C:'Clojure']

引数をとらない collectEntries() はやはり Closure.IDENTITY を渡したのと同じように処理されます。 したがって、リスト(要素が2つ)のコレクションに対して呼び出すとマップに変換してくれるという風に使うといいようです。 引数としてマップをとるものは、マップの初期値や使用するマップの型を指定するのに使います。 もしキーとして同じものがあれば上書きされます。

getAt() メソッド

最後は「collect」から始まらないメソッドですが、似たような処理をするメソッドである getAt() の使い方を見ていきます。 Groovy では getAt() メソッドは「[...]」という風に書いて実行できることに注意。 リストでは getAt(int) はその位置の要素を返しますが、ここでは int ではなく String をとり、各要素のプロパティを蒐集します:

String.metaClass.getHead = { delegate[0] }    // String にプロパティ head を定義

def langs = ['Java', 'Groovy', 'Scala', 'Clojure']

assert langs['head'] == ['J', 'G', 'S', 'C']

String クラスに適当なプロパティが無かったので head というプロパティ(getHead() メソッド)を追加しています。 もともとプロパティがあるならこんなことしなくても使えます。 ちなみにこれは「*.」演算子を使って以下のように書いても同じです:

assert langs*.head == ['J', 'G', 'S', 'C']

まぁ、この辺りの使い分けはお好みで。

単なる collect() メソッドは関数型言語とかでよくあるメソッドですが、他のメソッドは同様のメソッドがあっても言語によって名前が違うことがよくありそう。 また、メソッドによって引数にとるコレクションのネストの仕方がちょっと決まってたりするので使うときは結局ドキュメント見ながら、とかになっちゃいそう。

次回はコレクションを生成するようなメソッドを扱う予定。

プログラミングGROOVY

プログラミングGROOVY

*1:他にも大丈夫な型がありそうだけど別に List でいいんじゃね?

GDK のコレクションがこんなに便利なわけがない。 (Collection 編3) 要素の検索 find

今回は GDK が Collection クラスに追加しているメソッドの内、要素を検索するメソッドを見ていきます。 メソッド名でいうと「find」で始まるメソッドです。 これらは GDK が Object クラスに追加しているメソッドを見ていたシリーズ「あんたに GDK の何が分かるっていうの!?」の4回目「コンテナ・メソッド 後編」で扱ったメソッドとほとんど同じです、というかそこでも List オブジェクトでサンプルを書いていたので実質的に同じです。 ということで、サラッと流しましょう。 あと、findAll() と似たようなフィルタ処理を行う grep() もここで扱います。

今回扱うメソッドは以下のもの:

Object find(Closure closure)
Object find()

Collection findAll(Closure closure)
Collection findAll()

Object findResult(Closure closure)
Object findResult(Object defaultResult, Closure closure)

Collection findResults(Closure filteringTransform)

Collection grep(Object filter)
Collection grep()

引数がないメソッドは、引数をとる同名のメソッドに Closure.INDENTIY を渡したのと同じ処理をします。 findResults() メソッドは Object クラスの場合に出て来ませんでしたが、処理内容は名前から想像つくかと。 もう少し各メソッドを詳しく見るとこんな感じ:

メソッド 返り値 since 説明
find(Closure)
find()
Object 1.0
1.8.1
クロージャの処理結果が true の要素を1つ返す
findAll(Closure)
findAll()
Collection 1.5.6
1.8.1
クロージャの処理結果が true の要素を全て返す
findResult(Closure)
findResult(Object, Closure)
Object 1.7.5 クロージャの処理結果が null でないものを1つ返す
findResults(Closure) Collection 1.8.1 クロージャの処理結果が null でないもの全てを返す
grep(Object)
grep()
Collection 2.0 引数オブジェクトの isCase() メソッドによって true を返す要素を全て返す

では、各メソッドをサンプルとともに見ていきましょう。

fiind() メソッド

find() メソッドは boolean 値を返すクロージャを引数にとり、各要素をクロージャに処理させて true を返した最初の要素を返します。 List のように順序づけされたコレクションはその順序で最初に true を返した要素が返されます:

def langs = ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython', 'JRuby', 'Fantom']
assert langs.find{ it.contains 'o' } == 'Groovy'

assert (0..10).find() == 1

2つ目の場合、引数なしなので Closure.IDENTITY が渡されたものとして処理されますが、0は false と評価される(asBoolean() が false を返す)ので、次の1が find() の返り値となってます。

findAll() メソッド

findAll() は find() と似てますが、クロージャに処理された結果 true を返す全ての要素をコレクションにして返します。 クロージャによる要素のフィルタですね。

def langs = ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython', 'JRuby', 'Fantom']
assert langs.findAll{ it.startsWith 'J' } == ['Java', 'Jython', 'JRuby']

assert (0..10).findAll() == (1..10)

関数型言語では filter という名前の方がよく使われているかと。

findResult() メソッド

findResult() メソッドは、Object を返すクロージャを引数にとり、find() と違って変換結果が null でないならその要素を返します。 返り値が false と評価される(asBoolean() が false を返す)かどうかで返り値を判別しているわけではないので注意。

def langs = ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython', 'JRuby', 'Fantom']

assert langs.findResult{ it.size() > 6 ? it.toLowerCase() : null } == 'clojure'
assert langs.findResult('javascript'){ it.size() > 8 ? it.toLowerCase() : null } == 'javascript'

引数を2つとる findResult() は、全ての要素が null と評価された場合に(おそらく元が空集合の場合も)第1引数の値を返します。

findResults() メソッド

findResults() メソッドは、findResult() と似てますが、null でない全ての要素をコレクションとして返します。 それ以外は findResult() と同じ:

def langs = ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython', 'JRuby', 'Fantom']
assert langs.findResults{ it.size() > 5 ? it.toLowerCase() : null } == ['groovy', 'clojure', 'jython', 'fantom']

なんか、findResult() のときもそうだけど、findAll() と collect() を使って

langs.findAll{ it.size() > 5 }.collect{ it.toLowerCase() }

と書いた方が分かりやすそう。 findResult() の場合はデフォルト値を設定できる有難味はあるけど、findResults() っていらなくね? このサンプルがぎこちないだけかなぁ。

grep() メソッド

最後は「find」で始まらないけど、findAll() と同じようにフィルタの役目をする grep() メソッド。 grep() は引数としてクロージャを渡せば findAll() と同じように働きますが、クロージャ以外でも isCase() メソッドが定義されていればフィルタとして使えます。

def langs = ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython', 'JRuby', 'Fantom']
assert langs.grep(~/J.*/) == ['Java', 'Jython', 'JRuby']

assert (0..10).grep() == (1..10)

1つ目では正規表現をフィルタとして使ってます。 2つ目の引数がないものは Closure.IDENTITY が渡されたとみなすので、findAll() の場合と同じ。

なんか、Object クラスのときとほとんど同じ記事になりましたが、まぁそんなこともあるサ。 次回はメソッド名が「collect」で始まる要素の蒐集メソッド(予定)。

プログラミングGROOVY

プログラミングGROOVY

GDK のコレクションがこんなに便利なわけがない。 (Collection 編2) 要素の追加・削除

今回は GDK が Collection クラスに追加している要素の追加・削除に関連するメソッド。 要素の追加・削除は Java の標準 API にもあれこれありますが、GDK が追加しているのは Groovy の演算子に対応するもの(plus(), leftShift())、可変長引数(配列 Object[] を引数にとる)、クロージャを引数にとるもの、などです:

// 要素の追加
Collection plus(Object right)
Collection plus(Collection right)
Collection plus(Iterable right)

Collection leftShift(Object value)

boolean addAll(Object[] items)

// 要素の削除
boolean removeAll(Object[] items)
boolean removeAll(Closure condition)

boolean retainAll(Object[] items)
boolean retainAll(Closure condition)

各メソッドは Groovy でよくあるもの、もしくは Java 標準 API で同名のメソッドがあるものなので、使い方は難しくないと思いますが、それぞれのメソッドが元のコレクションを不変に保つのか要素を変更するのかは注意が必要かと。 返り値が Collection ではないもの(addAll(), removeAll(), retainAll())は元のコレクションを変更するのはいいかと思いますが*1、plus() は元のコレクションを変更しませんが、leftShift() は元のコレクションを変更します。 まぁ、以下のサンプルでそれぞれ試してるので、参考に。

要素の追加


まずは要素を追加するメソッド。 plus() は「+」演算子、leftShift() は「<<」演算子として使うのが普通かと。

メソッド 返り値 since 説明
plus(Object)
plus(Collection)
plus(Iterable)
Collection 1.5.0
1.8.7
1.5.0
要素の追加(+演算子
leftShift(Object) Collection 1.0 要素の追加(<< 演算子
addAll(Object[]) boolean 1.7.2 引数の要素を全て追加

Groovy では引数の最後が配列なら、可変長引数として使えるので、addAll(Object[]) は要素を列挙して追加するのに使えます。 ではサンプルコード:

def langs = ['Java']

// plus(Object), +
def langs1 = langs + 'Groovy'
assert langs == ['Java']    // 元のコレクションは変更されない
assert langs1 == ['Java', 'Groovy']

// plus(Collection), +
def langs2 = langs1 + ['Scala', 'Clojure']
assert langs1 == ['Java', 'Groovy']    // 元のコレクションは変更されない
assert langs2 == ['Java', 'Groovy', 'Scala', 'Clojure']

// leftShift(), <<
def langs3 = langs2 << 'Jython'
assert langs2 == ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython']    // 元のコレクションも変更される
assert langs3 == ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython']

langs3 << ['JRuby', 'Fantom']
assert langs3 == ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython', ['JRuby', 'Fantom']]
    // コレクションを << で追加すると、1つのオブジェクトとして処理される

def langs4 = ['Java', 'Groovy'] << 'Scala' << 'Clojure'    // << 演算子を連結できる
assert langs4 == ['Java', 'Groovy', 'Scala', 'Clojure']

// addAll()
assert langs == ['Java']
langs.addAll('Groovy', 'Scala', 'Clojure')    // 可変長引数
assert langs == ['Java', 'Groovy', 'Scala', 'Clojure']
  • + 演算子は元のコレクションを変更しません
  • << 演算子は連結して使えます。 他の多くのクラスでこのように使用できるようにするため(StringBuilder や Writer なども含め)、<< 演算子は元のオブジェクトを返すようになっていると思われます
  • << 演算子はあくまで add() と同じで、addAll() の機能はありません。 つまり、コレクションを << 演算子で追加しても、1つのオブジェクトとして追加されます
  • addAll(Object[]) は可変長引数として扱えます

要素の削除


次は要素を削除するメソッド。 Java の標準 API に同名のメソッドがあるのであまり使い方は問題ないかと。 これらはどちらも元のコレクションを変更します。

メソッド 返り値 since 説明
removeAll(Object[])
removeAll(Closure)
boolean 1.7.2 引数の要素もしくは引数で指定される要素を全て削除
retainAll(Object[])
retainAll(Closure)
boolean 1.7.2 引数の要素もしくは引数で指定される要素以外を全て削除

addAll() と同じく、配列を引数にとるものは可変長引数として扱えるので、要素を列挙して使用できます。 クロージャを引数にとるものは、boolean 値を返すクロージャをとり、removeAll() は true を返す要素を削除し、retainAll() は true を返す要素を残します。 まずは removeAll() から

def langs = ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython', 'JRuby', 'Fantom']

// removeAll(Object[])
langs.removeAll('Scala', 'Clojure', 'Fantom')
assert langs == ['Java', 'Groovy', 'Jython', 'JRuby']

// removeAll(Closure)
langs.removeAll{ it.startsWith 'J' }
assert langs == ['Groovy']

特に問題ないかと。 retainAll() は removeAll() と削除するか残すかが逆なだけですね。

langs = ['Java', 'Groovy', 'Scala', 'Clojure', 'Jython', 'JRuby', 'Fantom']

langs.retainAll('Java', 'Groovy', 'Scala', 'Clojure')
assert langs == ['Java', 'Groovy', 'Scala', 'Clojure']

langs.retainAll{ it.startsWith 'J' }
assert langs == ['Java']

Java 標準 API に同名のメソッドがあれば元のコレクションを変更するかどうかはそれに合わせてあるハズなので、その辺りから元のコレクションが変更されるかどうかを知っておくのがいいかと。

次回は要素を検索するメソッド(findXxxx() と言う名前のメソッド)の予定。

プログラミングGROOVY

プログラミングGROOVY

*1:Java 標準 API の同名のメソッドが元のコレクションの要素を変更するので、GDK が追加するメソッドも同じ挙動にしているんでしょう。

GDK のコレクションがこんなに便利なわけがない。 (Collection 編1) 集合演算、文字列関連

さて、今回から GDK が Collection に追加しているメソッドを見ていきます。 Object クラスに追加されているメソッドも、オーバライドされているものはまた扱います。

目次

分類はあくまで目安っす。 今回あつかうメソッドは集合演算と文字列関連のメソッド:

// 集合演算
boolean asBoolean()
boolean isCase(Object switchValue)
boolean containsAll(Object[] items)
Collection intersect(Collection right)
boolean disjoint(Collection right)

// 文字列関連
String join(String separator)
String toListString()
String toListString(int maxSize)

集合演算

まずは集合演算に関するメソッド:

メソッド 返り値 since 説明
asBoolean() boolean 1.7.0 空集合なら falseを、そうでないなら true を返す
isCase(Object) boolean 1.0 引数の要素を含んでいるかどうか
containsAll(Object[]) boolean 1.7.2 引数の配列の全要素を含んでいるかどうか
intersect(Collection) Collection 1.5.6 引数の集合との共通部分を返す
disjoint(Collection) boolean 1.0 引数の集合と共通部分がないかどうか

asBoolean() メソッド
asBoolean() メソッドは Object クラスでもでてきましたが、コレションでは空集合なら false、要素を持てば true を返します:

assert [0].asBoolean() == true
assert [].asBoolean() == false

isCase() メソッド
isCase() も Object クラスに定義されていたメソッドですが、コレクションの場合は引数のオブジェクトがこのコレクションの要素として含まれているなら true を、そうでないなら false を返します。 このメソッドは in キーワードや switch 文の case 節で使います:

def list1 = (0..10)    // [0, 1, 2, 3, ... , 10]

assert list1.isCase(0)
assert 0 in list1

containsAll() メソッド
containsAll(Object[]) メソッドは通常の Collection のメソッドに同名のものがありますが、集合ではなく配列を引数としてとれるようにしたメソッドです。 使い方は同じですが。

def list1 = (0..10)

def array = [2, 4] as Object[]
assert list1.containsAll(array)

assert list1.containsAll(1, 3, 7)

1つ目は無理して配列にしてますが、コレクションでできるならコレクションで。 Groovy では最後の引数がが配列なら可変長引数としてあつかえるので、2つ目ではオブジェクトをそのまま渡してます。 まぁ、このためにあるメソッドなんでしょうね。

intersect() メソッド
intersect() メソッドは引数のコレクションとの共通部分を返すメソッドです。

def list1 = (0..10)
def list2 = (0..20).findAll{ it % 2 == 0 }

assert list1.intersect(list2) == [0, 2, 4, 6, 8, 10]

もともとの Java 標準 API にもあってよさそうなメソッドですが・・・

disjoint() メソッド
disjoint() は引数のコレクションと共通部分がないかどうかを返します。 ない場合は true です。

def list1 = (0..10)
def list2 = (0..20).findAll{ it % 2 == 0 }

assert list1.disjoint(30..40)
assert !list1.disjoint(list2)

文字列関連

次は文字列関連のメソッド。

メソッド 返り値 since 説明
toListString()
toListString(int)
String 1.0
1.73
リスト形式の文字列を返す
join(String) String 1.0 要素の toString() を引数の文字列で連結して返す
def langs = ['Java', 'Groovy', 'Scala', 'Clojure']

// toListString()
assert langs.toListString() == '[Java, Groovy, Scala, Clojure]'
assert langs.toListString(15) == '[Java, Groovy, Scala, ...]'

// join()
assert langs.join('-') == 'Java-Groovy-Scala-Clojure'

int 値をとる toListString() メソッドは括弧[]で囲まれた文字列がおおよそ指定した文字数になる箇所で切って、残りを ... にします。 キッチリ指定した文字数にはならないもよう。 これらはデバッグとかで多用しそう。

次回は要素の追加とかかな?

プログラミングGROOVY

プログラミングGROOVY

  • 作者: 関谷和愛,上原潤二,須江信洋,中野靖治
  • 出版社/メーカー: 技術評論社
  • 発売日: 2011/07/06
  • メディア: 単行本(ソフトカバー)
  • 購入: 6人 クリック: 392回
  • この商品を含むブログ (155件) を見る

Groovy ノミックス (Class 編)

今回は Collection に突入しようかと思ってたんですが、前回メタクラスの流れにのって、GDK が Class クラスに追加しているメソッドを見ていきます。 今回見ていくメソッドは以下の通り:

boolean isCase(Object switchValue)

// インスタンス生成
Object newInstance()
Object newInstance(Object[] args)

// メタクラス
MetaClass getMetaClass()
void setMetaClass(MetaClass metaClass)
MetaClass metaClass(Closure closure)

// ミックイン
void mixin(Class categoryClass)
void mixin(List categoryClasses)
void mixin(Class[] categoryClass)
  • isCase() とメタクラス関連のメソッドは Object クラスのときに同じようなのが出てきました。 違いは後ほど。
  • newInstance() メソッドはインスタンスを生成するメソッドで、引数なしものは既に Class クラスに定義されています。 引数をとるものは java.lang.reflect.Constructor に似たものがありますね。
  • mixin は Object クラスでみた use() と同じく、カテゴリクラスの機能を混ぜ込むメソッドです。 Groovy ノミックスイン機能ですw

もう少し各メソッドを詳しく見てみると

メソッド 返り値 since 説明
isCase(Object) boolean 1.0 フィルタ
newInstance()
newInstance(Object[])
Object 1.0 インスタンス生成
getMetaClass()
setMetaClass(MetaClass)
metaClass(Closure)
MetaClass
void
MetaClass
1.5.0
1.6.0
1.6.0
メタクラスの取得・設定
mixin(Class)
mixin(List)
mixin(Class[])
void 1.6.0 カテゴリのミックスイン

それでは、各メソッドの使い方を簡単に見ていきましょう。 前回から引き続き、以下のような Person クラスを使います:

class Person{
    String name
    int age
}

あと、Groovy ではクラス・リテラルを参照する場合に「.class」をつける必要はありません。 「Person」とすると「Person.class」で返される Class オブジェクトが得られます。

isCase() メソッド

isCase() メソッドはフィルターとして使うメソッドで、Class クラスの場合、引数がこの Class クラスのインスタンスならば通すフィルターになります:

def waman = new Person(name:'倭マン', age:100)
assert Person.isCase(waman)
assert waman in Person

他にも switch-case 文で「case Person:」のように書くこともできます。 機能は同じで、switch 文に渡されたオブジェクトがこのクラスのインスタンスなら、その分岐を実行します。

newInstance() メソッド

newInstance() メソッドはそのクラスのインスタンスを生成するメソッドです。 引数なしのものはデフォルト・コンストラクタを、引数があるものはその引数にあったコンストラクタを呼び出してインスタンスを生成します:

def empty = Person.newInstance()
assert empty.name == null
assert empty.age == 0

def waman1 = Person.newInstance(name:'倭マン1', age:101)
assert waman1.name == '倭マン1'
assert waman1.age == 101

ドキュメントのいつのバージョンからか書いてませんが、Java 標準 API にも Class#newInstance() は定義されてますがね。

メタクラス関連のメソッド

メタクラス関連のメソッド

  • getMetaClass()
  • setMetaClass(MetaClass)
  • metaClass(Closure)

は前回に Object クラスで見たメソッドと同じです。 ただし、Class クラスに対してメタクラスの設定を行った場合、そのインスタンス全てに変更が施されます(ただし既にインスタンスが生成されている場合は影響を受けない)。 使い方は

Person.metaClass{
    introduce = { "My name is $delegate.name, $delegate.age years old." }
    greet = { Person someone -> "こんにちは、${someone.name}さん。" }
    greet = { String name, int age -> greeting(new Person(name:name, age:age)) }
}

def waman2 = new Person(name:'倭マン2', age:102)
assert waman2.introduce() == 'My name is 倭マン2, 102 years old.'

def waman3 = new Person(name:'倭マン3', age:103)
assert waman3.introduce() == 'My name is 倭マン3, 103 years old.'

使い方自体は Object クラスと同じです。

mixin

mixin (ミックスイン?ミクシン?)は、前回 use() メソッドの使い方で見たように、カテゴリクラスを用いて機能を追加するメソッドです。 前回の use() メソッドはクロージャーによってその機能追加のスコープを指定していましたが(w)、mixin はこれのグローバル・バージョン、すなわち全てのインスタンスに対してカテゴリクラスの機能を追加します。 こちらも mixin 前にインスタンス生成されている場合は機能が追加されません。 ではサンプル:

@Category(Person)
class PersonCategory{

    def greet(Person p){
        "こんにちは${p.name}さん。"
        // this でメソッドを呼び出されるオブジェクトを参照することも可
    }
}

Person.mixin PersonCategory    // mixin

def waman4 = new Person(name:'倭マン4', age:104)
def waman5 = new Person(name:'倭マン5', age:105)

assert waman4.greet(waman5) == 'こんにちは倭マン5さん。'

カテゴリクラスは前回の use() メソッドの箇所で説明したとおり @Category アノテーションを使用しなくても作成できますが、デフォルト・コンストラクタによってインスタンスが生成できないといけないようです。 つまり、前回見たように Files クラスのようなインスタンス生成を不可能にしてあるユーティリティ・クラスは使えません。

今回の Class クラスは前回の Object クラスのメタプログラミング用メソッドからの惰性でやってまいました。 次回こそは Collection を。

プログラミングGROOVY

プログラミングGROOVY

あんたに GDK の何が分かるっていうの!? (Object 編6) メタプログラミング用メソッド

今回は GDK が Object クラスに追加しているメタプログラミング用のメソッド。 Groovy ではメタオブジェクト・プロトコルという方式を使ってオブジェクトの挙動を動的に変更できるので、メタプログラミングを行いたい場合はメタクラスの設定をあれこれ行うことが多いと思います。 通常、メタクラスを設定する場合はそのメタクラスを持つオブジェクト全てについて挙動を変更したいので Class クラスからメタクラスを取得して設定を行いますが、今回扱う Object クラスに対してメタクラスの変更を行うと、その変更はそのオブジェクトだけにしか適用されないので注意。 あと、スコープを設定してオブジェクトの挙動を変更するカテゴリについても見ていきます。 今回扱うメソッドは以下のもの:

// メタクラス
MetaClass getMetaClass()
MetaClass metaClass(Closure closure)
void setMetaClass(MetaClass metaClass)

// メタプロパティ
MetaProperty hasProperty(String name)
List getMetaPropertyValues()

// (メタ)メソッド
List respondsTo(String name, Object[] argTypes)
List respondsTo(String name)

// カテゴリ
Object use(Class categoryClass, Closure closure)
Object use(List categoryClassList, Closure closure)
Object use(Object[] array)

もう少し各メソッドを見てみるとこんな感じ:

メソッド 返り値 since 説明
getMetaClass()
setMetaClass(MetaClass)
metaClass(Closure)
MetaClass
void
MetaClass
1.5.0
1.6.0
1.6.0
メタクラスを取得
メタクラスをセット
クロージャと ExpandoMetaClass によるメタプログラミング
hasProperty(String)
getMetaPropertyValues()
MetaProperty
List
1.6.1
1.0
指定した名前のメタプロパティを持っているか?
メタプロパティの値のリストを取得
respondsTo(String, Object[])
respondsTo(String)
List*1 1.6.0
1.6.1
指定した名前と引数の(メタ)メソッドがあるか?
use(Class, Closure)
use(List, Closure)
use(Object[])
Object 1.0 カテゴリを使う

ではそれぞれのメソッドの使い方を見ていきましょう。 以下では次のように定義された Person クラスとそのインスタンス waman があるとします:

class Person{
    String name
    int age
}

def waman = new Person(name:'倭マン', age:100)

メタクラス関連

メタクラス関連のメソッドは

  • getMetaClass()
  • setMetaClass(MetaClass)
  • metaClass(Closure)

getMetaClass(), setMetaClass() メソッドは特に問題ないと思います。 オブジェクトに対して行う(Class リテラルに対してではなく)と各インスタンス毎の設定になるところだけ注意が必要ですが。 メタクラスを使ってメタプログラミング(Java ではできないクラスの挙動の変更など)するには metaClass() メソッドを使います。

// metaClass() によるメタプログラミング
waman.metaClass{
    getSex = { -> 'male' }
    isMale = { -> sex == 'male' }

    //introduce{ "私の名前は${delegate.name}です。 年齢は${delegate.age}歳です。" }
    introduce{ "My name is $delegate.name, $delegate.age years old." }

    greet << { Person someone -> "こんにちは、${someone.name}さん。" }
    greet << { String name, int age -> greeting(new Person(name:name, age:age)) }
}

assert waman.sex == 'male'
assert waman.male
assert waman.introduce() == 'My name is 倭マン, 100 years old.'

上記の例では getSet(), isMale(), introduce(), greet() メソッド(×2)を定義しています。 getSex(), isMale() などはプロパティとして「waman.sex」、「waman.male」のようにアクセスすることができます。

メソッドを定義する場合は

  • 《メソッド名》{ メソッド本体 }
  • 《メソッド名》 = { メソッド本体 }
  • 《メソッド名》 << { メソッド本体 }

の3バージョンがありますが、「=」と「<<」の違いは同名同シグニチャのメソッドがあった場合に「=」は置き換え、「<<」は例外スローだそうです(『プログラミングGROOVY』より)。 ただ、Groovy 2.1.4 で試してみたところ、全て置き換えでした???

ちなみに、何度か言っているようにオブジェクトに対してメタクラスの変更をしても他のインスタンスには(同クラスでも)影響ありません:

def wasan = new Person(name:'倭算', age:10000)
try{
    wasan.introduce()
    assert false

}catch(MissingMethodException ex){
    assert true
}

同クラスのインスタンスに対してメタプログラミングを行いたい場合はクラス・リテラルからメタクラスを取得して設定する必要があります:

Person.metaClass{
    ...
}

ちなみに、これは(Person)インスタンス生成の前に行う必要があります。 このあたりの話は GDK の Class 編をもしやることがあれば・・・

メタプロパティ、メタメソッド

次はメタプロパティ関連のメソッド。 メタプロパティとはなんぞや?と聞かれてもお答えしかねるので(単に getter and/or setter があるプロパティみたいな感じ?)、サンプルコードの挙動で推察しておくんなまし。 以下のコードは、上記のメタクラス変更を行った後に実行してます:

// hasProperty(), getMetaPropertyValues()
assert waman.hasProperty('class')
assert waman.hasProperty('name')
assert waman.hasProperty('age')
assert waman.hasProperty('sex')    // メタクラスに追加した getSex() メソッド
assert waman.hasProperty('male')    // メタクラスに追加した isMale() メソッド

assert waman.getMetaPropertyValues().collect{ it.value }.toSet() == [Person, '倭マン', 100, 'male', true].toSet()
    // メタプロパティの値を取得。 順序はコードの通りとは限らない

次はメタメソッド。 こちらも意味はよく分からん。 responseTo() メソッドは Class クラスの getMethods() メソッドみたいなもので、指定した名前のメソッドを取得するメソッド。 パラメータの型を指定するものもあります(getMethods() にもあるように):

// respondsTo()
assert waman.respondsTo('getSex').size() == 1
assert waman.respondsTo('introduce').size() == 2    // 引数なしと引数1つ
assert waman.respondsTo('greet').size() == 2
assert waman.respondsTo('greet', Person).size() == 1
assert waman.respondsTo('greet', String, int).size() == 1
assert waman.respondsTo('greet', int, String).isEmpty()

メタクラスでクロージャによってメソッドを追加する際、クロージャに引数を指定していなかったら引数なしと引数1つのものが追加されます。 まぁ、あんまり問題はなさそうかな。

カテゴリ

最後は use() メソッドを使ったカテゴリの使用。 (Person クラスに対する)カテゴリは例えば以下のような第1引数が Person の static メソッド introdceAffectedly() (カッコつけた自己紹介)を定義したクラスのことです

class PersonCategory1{
    static introduceAffectedly(Person person){
        "『${person.introduce()}』"
    }
}

このクラスと use() メソッドを使って、あたかも Person オブジェクトに introduceAffectedly() メソッドがあるかのようにコードを書くことができます:

use(PersonCategory1){
    assert waman.introduceAffectedly() == '『My name is 倭マン, 100 years old.』'
}

static メソッドで書くのが好きじゃないという方には、@Category アノテーションを使って

@Category(Person)
class PersonCategory2{
    def introduceAAffectedly(){
        "『『${this.introduce()}』』"
    }
}

use(PersonCategory2){
    assert waman.introduceAAffectedly() == '『『My name is 倭マン, 100 years old.』』'
}

のように書くこともできます。 複数のカテゴリを使用したい場合は

// use(Object[])    配列の最後の要素がクロージャ
use(PersonCategory1, PersonCategory2){
    assert waman.introduceAffectedly() == '『My name is 倭マン, 100 years old.』'
    assert waman.introduceAAffectedly() == '『『My name is 倭マン, 100 years old.』』'
}

のようにできます。 ちなみに、カテゴリは特にそれ用のインターフェースなどがあるわけではなく、static メソッドの第1引数が使用したいオブジェクトの型ならどんなクラスでも使用できます。 特に Java ライブラリでユーティリティ・クラスとして作成されたクラスはカテゴリとして使用できるものもよくあります。 例えば java.nio.file.Files クラスは同パッケージ内の Path オブジェクトに対するユーティリティ・メソッドがたくさん定義されてますが、第1引数が Path のメソッドばかりなので、カテゴリとして使用することができます:

import java.nio.file.*
import java.nio.charset.Charset

def log = Paths.get('log.txt')
use(Files){    // Files をカテゴリとして使用
    if(log.exists())log.delete()
    log.createFile()
    log.write(['I am waman?'], Charset.defaultCharset())
}

def linesep = System.getProperty('line.separator')
assert log.toFile().text == "I am waman?$linesep"

他にも標準 APIサードパーティ製のユーティリティ・クラスでもこういった使い方ができるクラスは多いかと。

今回で GDK が Object クラスに追加しているメソッドは終了。 次回はやっと Collection かな?(予定)

プログラミングGROOVY

プログラミングGROOVY

*1:List<? extends MetaMethod>