KotlinのgroupingByについて調べてみた
KotlinにgroupingBy
なる関数があることを知った。
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/grouping-by.html
Groupingソースなるものを作成するための拡張関数で、listとか配列で使うことができる。
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-grouping/index.html
例えば”I have a pen”で各アルファベットが何回出現しているかを調べるのに使える。
val result = "I have a pen".groupingBy { it }.eachCount()
println(result) // {i=1, =3, h=1, a=2, v=1, e=2, p=1, n=1}
groupingBy
自体は続く関数オブジェクトで求められるkeyを元にしたMapへ集計できるようにするためのインターフェースで、これ自体呼び出しても何も起こらない。上記の例でいうとeachCount()
を呼び出して初めて集計が行われる。
keySelectorを引数に取るのはgroupBy
もgroupingBy
も同じ。
groupBy
だと指定したkeyごとの要素をListにもつMapを返す。イメージ的にはmap
に近い処理。
groupingBy
はそれ自体は何もしない。keyを元に集計処理を行うインターフェースを用意するだけのメソッドなので、その後に別途集計処理が必要になる。forEach
を拡張したものと考えるといいかもしれない。
両者の使い分けは、keyを元にした要素のリストがほしいのか、それともその要素を何らかの処理をして集計した結果だけが欲しいのかで使い分けることになるだろう。集計した結果のみが必要なのであれば、その中間形態であるkeyごとの要素リストは不要なので、groupingBy
を使ったほうが効率的である。
groupingBy
だけでは集計処理は行われないので、その後に以下のいずれかを利用して集計を行う。
- aggregate
- fold
- reduce
- eachCount
それぞれ別にxxxToという処理も用意されていて、違いは集計先のMapが指定できるかどうか。Toがついている方は、既存のMapが集計先に利用されるので、前の集計結果にさらに付け足すのに使える。Toがつかない方は空のMapが集計先として利用される。
groupingBy
自体は要素のグルーピングを行うわけではなく、aggregate
などを呼び出すことで初めて要素のグルーピングと集計が行われる。
よほど特殊な事情がない限り、aggregate
を直接使うことはないと思われる。大体のケースでfold
を使ったほうが便利だろう。
というのも、aggregate
は要素がkeyによるグルーピングを行った最初の要素かどうかを判定したりするのも全て自分で書く必要があるからだ。要素が初出の場合に初期値を用意し、そうでない場合に集計処理をするというのがfold
なので、大抵のケースでfold
で事足りるはず。
最終的にはどれを使ってもList<何らかの型>
がMap<指定したキー, 集計後の結果>
に集計される。(元のデータがList
とは限らないけれど、最終的にMap
に集約されるのは変わらない)
keyごとの要素の個数が欲しい場合にこれを使う。引数もいらないので最もシンプル。
要素の集計にロジックが必要な場合にこちらを利用する。オブジェクトの特定のフィールドだけが集計対象であるときなどに利用することが考えられるだろうか。
どちらも集計処理を行う関数オブジェクトを引数にとるのは同じ。
違いはfold
は集計値の初期値を設定する必要があるが、reduce
は初期値すら省略できるというところ。reduce
はkeyごとに出現した最初の要素が初期値に使われるからである。
集計処理を行う関数オブジェクトは、集計後の値を返すような関数オブジェクトにすればいい。この関数オブジェクトの戻り値が、次の要素のaccumulatorの値になる。
集計結果が要素と同じ型になるのかどうかが使い分けの分岐点となる。集計結果が元の要素と同じ型ならreduceを使ったほうが便利(初期値の指定がいらないので)。
というのもreduce
の初期値はkeyごとに最初に出現した要素になるからである。だから型の変換が行えない。
data class SalesInfo(val id: String, val date: Date, val sales: Int)
val dailySales = listOf(...) // 日々の売上データ
// 商品IDごとに売上を集計
val sum = dailySales.groupingBy { it.id }
.fold(0) { _, acc, element ->
acc + element.sales
}
println(sum["hoge"]) // 商品ID"hoge"の1ヶ月分の売上を表示
上記の例で言えばSalesInfo型の要素をInt型に集計している。このケースではreduce
は使えない。
data class ShootInfo(val name: String, val try: Int, val hit: Int)
val total = listOf(..ShootInfoのリスト..)
.groupingBy { it.name }
.reduce { key, accumulator, element ->
ShootInfo(key, accumulator.try + element.try, accumulator.hit + element.hit)
}
println(total["hoge"]) // hogeさんの試行回数と命中数を表示
この場合、各要素と集計後の型は同じなのでreduce
が使えるということ。
reduce
は要素をグルーピングして1つの要素に集約するイメージ。別の型への変換を伴うならfold
を使うというスタンスで使い分けたらいいのかなと理解した。