Edit Page

集約オペレーション(aggregate)

Kotlinのコレクションは良く使われる集約(aggregate)オペレーションの関数を含んでいる。 集約オペレーションとは、コレクションの中身を元に単一の値を返すオペレーションの事です。 多くのものは良く知られていて、他のプログラム言語のものと同様に振る舞います:

  • minOrNull()maxOrNull() は最小と最大の要素をそれぞれ返す。空のコレクションに対して行うとnullを返す。
  • average()は数値のコレクションの平均値を返す。
  • sum()は数値のコレクションの合計を返す。
  • count()コレクションの要素数を返す。
fun main() { val numbers = listOf(6, 42, 10, 4) println("Count: ${numbers.count()}") println("Max: ${numbers.maxOrNull()}") println("Min: ${numbers.minOrNull()}") println("平均: ${numbers.average()}") println("合計: ${numbers.sum()}") }

最小と最大の要素を、何らかのセレクタ関数やカスタムなComparatorを元にして返す関数もある:

  • maxByOrNull()minByOrNull()はセレクタ関数を引数にとり、そのセレクタが返した値が最大の要素と最小の要素をそれぞれ返す。
  • maxWithOrNull()minWithOrNull()Comparatorオブジェクトを引数に取り、そのComparatorによる最大と最小の要素をそれぞれ返す。
  • maxOfOrNull()minOfOrNull()はセレクタ関数を引数に取り、セレクタが返した最大の値か最小の値をそれぞれ返す。
  • maxOfWithOrNull()minOfWithOrNull()Comparatorオブジェクトを引数に取り、セレクタの返した値をComparator基準で最大の値か最小の値を返す。(訳注:Comparatorとセレクタの2つの関数を引数に取る)

これらの関数は、空のコレクションに対してはnullを返す。 この挙動だけが違う代替バージョンもある。 maxOfminOfmaxOfWithminOfWithなどです。 これらの関数はそれぞれの対応するものと同じ挙動をするけれど、空のコレクションに対してはNoSuchElementExceptionを投げます。

fun main() { //sampleStart val numbers = listOf(5, 42, 10, 4) val min3Remainder = numbers.minByOrNull { it % 3 } println(min3Remainder) val strings = listOf("one", "two", "three", "four") val longestString = strings.maxWithOrNull(compareBy { it.length }) println(longestString) //sampleEnd }

普通のsum()の他に、より高度なsum関数、sumOf()関数もあります。 これはセレクタ関数を引数に取り、それを全要素に適応した結果の合計を返します。 セレクタは異なる数値型を返しても構いません:Int, Long, Double, UInt, ULongなど (JVMの場合は BigIntegerBigDecimalもOK)。

fun main() { //sampleStart val numbers = listOf(5, 42, 10, 4) println(numbers.sumOf { it * 2 }) println(numbers.sumOf { it.toDouble() / 2 }) //sampleEnd }

foldとreduce

より特殊なケースのためには、reduce()fold()関数があります。これは渡されたオペレーションをコレクションの要素に順番に適用していって、累積した結果を返します。 オペレーションは引数2つを取ります:前回までに累積した値と、コレクションの要素です。

reduceとfoldの違いは、fold()は初期値を引数に取り、それを最初のステップでの累積値として用いるのに対し、 reduce()は最初のステップとしては1つ目と2つ目の要素を引数に実行する所が違いです。

fun main() { //sampleStart val numbers = listOf(5, 2, 10, 4) val simpleSum = numbers.reduce { sum, element -> sum + element } println(simpleSum) val sumDoubled = numbers.fold(0) { sum, element -> sum + element * 2 } println(sumDoubled) //間違い: 結果を見ると最初の要素は2倍されていないのが分かる。 //val sumDoubledReduce = numbers.reduce { sum, element -> sum + element * 2 } //println(sumDoubledReduce) //sampleEnd }

上の例は両者の違いを示しています: fold()を使って要素を二倍したものの合計を計算しています。 同じ関数をreduce()に渡すと、異なった結果を返します。 なぜならリストの最初と二番目の要素を最初のステップの引数にしますが、 そのため最初の要素が2倍されない為です。

逆順に関数を適用していきたければ、 reduceRight()foldRight()関数を使いましょう。 これらの関数はfold()reduce() と似たように振る舞いますが、 最後の要素から始まって一つ前、その一つ前、と続いていく所が違います。 reduceRightとfoldRightはオペレーションの引数の順番が変わる事に注意してください: 1つ目の引数が要素で二つ目の引数が累積値となります。

fun main() { //sampleStart val numbers = listOf(5, 2, 10, 4) val sumDoubledRight = numbers.foldRight(0) { element, sum -> sum + element * 2 } println(sumDoubledRight) //sampleEnd }

要素のインデックスも引数に取るオペレーションを適用する事も出来ます。 この場合はreduceIndexed()foldIndexed()関数を使います。 要素のインデックスはオペレーションには最初の引数として渡されます。

最後に、そのようなオペレーションを右から左に適用する関数もあります。reduceRightIndexed()foldRightIndexed()です。

fun main() { //sampleStart val numbers = listOf(5, 2, 10, 4) val sumEven = numbers.foldIndexed(0) { idx, sum, element -> if (idx % 2 == 0) sum + element else sum } println(sumEven) val sumEvenRight = numbers.foldRightIndexed(0) { idx, element, sum -> if (idx % 2 == 0) sum + element else sum } println(sumEvenRight) //sampleEnd }

すべてのreduce系の関数は空のコレクションには例外を投げます。代わりにnullを受け取りたければ、それらの対応する*OrNull()バージョンを使ってください:

途中の累積値を保存したい場合は、 runningFold() (またはその省略名の scan()) とrunningReduce()関数があります。

fun main() { //sampleStart val numbers = listOf(0, 1, 2, 3, 4, 5) val runningReduceSum = numbers.runningReduce { sum, item -> sum + item } val runningFoldSum = numbers.runningFold(10) { sum, item -> sum + item } //sampleEnd val transform = { index: Int, element: Int -> "N = ${index + 1}: $element" } println(runningReduceSum.mapIndexed(transform).joinToString("\n", "runningReduceによる最初のN要素の合計:\n")) println(runningFoldSum.mapIndexed(transform).joinToString("\n", "runningFoldによる最初のN要素の合計:\n")) }

オペレーションにインデックスのパラメータが必要な場合は、runningFoldIndexed()runningReduceIndexed()を使ってください。