はじめに
あまり使う機会のないと思っていたプログラミング言語GroovyのListのtransposeメソッドだけれど、とても便利だったと気づいた、というお話です。
他の言語にある、zipとかcollectionWithIndexとかを実装できます。
transposeメソッドは便利
transposeとの出会い
Groovyは、Object、Collection、ListなどのJavaのクラス・インターフェースに、たくさんの便利なメソッドが追加されています。collect,find,findAllやpermutationsなどを知った時は、「これはこのような使い方ができそうだ、便利だ!」と感動したのを覚えています。
そんな中で、あまり使い方が思い浮かばなかったメソッドがあります。Listに追加されているtransposeメソッドです。「転置行列」を作ると紹介されていました。長さNのListをM個要素に持つListを、長さMのListをN個要素に持つListに変換し、要素の位置を転置する(?)。
コードを見た方が早いですね。
assert [["A", "B"], ["C", "D"], ["E", "F"]].transpose() == [["A", "C", "E"], ["B", "D", "F"]]
assert [[1, 2, 3], [4, 5, 6]].transpose() == [[1, 4], [2, 5], [3, 6]]
「なるほど、転置行列か。」これを見たときには、「使えるとしたら画像処理や数値計算を行う時くらいかな」と考えていました。
transposeとの再会
ちょっと前に、【C#,LINQ】インデックス付きで射影(Select)と抽出(Where)【iが欲しい!?】他の言語もちょっと。というタイトルの投稿をしました。C#のLINQにはコレクションの射影と抽出を行うのに、コレクションの要素とそのインデックスを用いることができるメソッド(のオーバーロード)があります。この投稿では、Groovyについても同様のメソッドがあるかを調べました。書籍やGDKのリファレンスを調べたのですが、(eachWithIndexは見つけたのですが)、Groovyの標準APIには射影・抽出をインデックスとともにできるものは見つけられませんでした。
しかし、こちらで、自分が探していたメソッドを独自で定義し、Listクラスに追加している例を見つけることができました。C#ではインデックス付きで射影を行うメソッドはSelectというメソッド名なのですが、Groovyの場合、collectWithIndexなどがいいのでしょうかね。
def list = ["A", "B", "C", "D", "E", "F"]
assert list.collectWithIndex{ str, index -> "$str$index"} == ["A0", "B1", "C2", "D3", "E4", "F5"]
こちらでのcollectWithIndexメソッドの実装はいくつかあったのですが、その中にtransposeメソッドを使っているものがありました。そのサンプルでのtransposeメソッドの使い方を応用すれば、(他の言語にあるような)Listの便利なメソッドを奇麗に実装することができると思いました。
transposeメソッドを使う前に
長さNのListを2個要素に持つListは作りやすい
def hoge = ["h", "o", "g", "e", "h", "o", "g", "e"]
def fuga = ["f", "u", "g", "a", "f", "u", "g", "a"]
def listOfList = [hoge, fuga]
Groovyは、リテラルとしてリストが提供されています。(外側のリストの)長さが2のリストのリストは上記のように、非常に簡潔に書けますね。
長さ2のListをN個要素に持つListは扱いやすい
次のコードは、長さが2のListを4個要素に持つListの使用例です。collectメソッドを使っています。
assert [["A", 0], ["B", 1], ["C", "2"], ["D", 3], ["E", 4]].collect{ str, num -> "$str$num" } == ["A0", "B1", "C2", "D3", "E4"]
上記のように、このcollectメソッドでは、内部リストの要素が、それぞれクロージャーの引数に代入されて、処理をしやすくなっていますね。
transposeメソッドで作りやすいリストを扱いやすく変換
長さNのListを2個要素に持つListは作りやすく、長さ2のListをN個要素に持つListは扱いやすいので、transposeメソッドを用いて、長さNのListを2個要素に持つListを長さ2のListをN個要素に持つListに変換することを考えます。
def hoge = ["h", "o", "g", "e", "h", "o", "g", "e"]
def fuga = ["f", "u", "g", "a", "f", "u", "g", "a"]
[hoge, fuga].transpose().each { h, f ->
println "$h $f"
}
出力結果は、次のようになります。
h f
o u
g g
e a
h f
o u
g g
e a
下記は、Rangeも使って、transposeメソッドで、インデックスと共に要素を射影する処理です。
def list = ["h", "o", "g", "e", "f", "u", "g", "a"]
def collectedWithIndex = [list, 0..<list.size()].transpose().collect { e, i -> "$e$i"}
assert collectedWithIndex == ["h0", "o1", "g2", "e3", "f4", "u5", "g6", "a7"]
transposeメソッド使って、他の言語のListの便利メソッドを追加
metaClassを使って、Listクラスに、他の言語の良く使うListの便利なメソッドを追加してみました。
// C#のSelect(インデックス付き)
List.metaClass.collectWithIndex = { body ->
[delegate, 0..<(delegate.size())].transpose().collect(body)
}
assert list.collectWithIndex{ str, index -> "$str$index"} == ["A0", "B1", "C2", "D3", "E4"]
// ScalaのzipWithIndex
List.metaClass.zipWithIndex = {
[delegate, 0..<(delegate.size() - 1)].transpose().collect{ new Tuple(it.toArray()) }
}
assert list.zipWithIndex() == [["A", 0], ["B", 1], ["C", 2], ["D", 3], ["E", 4]]
// ScalaやC#のzip
List.metaClass.zip = { other ->
[delegate, other].transpose()
}
assert ["f", "u", "g", "a"].zip(["h", "o", "g", "e"]) == [["f", "h"], ["u", "o"], ["g", "g"], ["a", "e"]]
transposeを要素数が異なるListのListに使ったら(おまけ)
assert [[1, 2, 3],[4, 5, 6],[7, 8]].transpose() == [[1, 4, 7], [2, 5, 8]]
assert [[1, 2, 3],[4, 5, 6],[7]].transpose() == [[1, 4, 7]]
assert [[1, 2, 3],[4, 5],[7, 8]].transpose() == [[1, 4, 7], [2, 5, 8]]
まとめ
transposeメソッドは、「画像処理や数値計算だけに使えるもの」ではなくて、どのようなプログラムでもListを扱うものであれば活躍の機会がある、という考えに変わりました。
transposeメソッド便利!