倭マン's BLOG

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

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:全体集合を元のコレクションとする。