倭マン's BLOG

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

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 でいいんじゃね?