Kotlinでのfoldとreduceの違い
1. 概要
このチュートリアルでは、Kotlinの fold()メソッドと reduce()メソッドの違いを探ります。
両方の関数がコレクションをトラバースして特定の操作を適用するという事実にもかかわらず、それらはまったく異なります。
2. reduce()
reduce()メソッドは、指定されたコレクションを単一の結果に変換します。要素のペアをいわゆる累積値に結合するにはラムダ関数演算子が必要です。
次に、コレクションを左から右にトラバースし、累積値を次の要素と段階的に結合します。
これが実際に動作することを確認するために、 reduce を使用して、数値のリストの合計を計算してみましょう。
val numbers: List<Int> = listOf(1, 2, 3)
val sum: Int = numbers.reduce { acc, next -> acc + next }
assertEquals(6, sum)
空のリストの場合はどうなりますか? 実際には、返す正しい値がないため、 reduce()はRuntimeExceptionをスローします。
val emptyList = listOf<Int>()
assertThrows<RuntimeException> { emptyList.reduce { acc, next -> acc + next } }
この場合、0を返すことが有効な結果であると主張することができます。 これから説明するように、 fold()を使用すると、柔軟性が高まります。
別の特性を理解するために、 reduce()のシグネチャを見てみましょう。
inline fun <S, T : S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S
この関数は、ジェネリック型SとSのサブ型Tを定義します。最後に、 reduce()は型の1つの値を返します。 ]S。
前の例で、合計がIntの範囲を超える可能性があると仮定します。 そのため、結果タイプをLongに変更します。
// doesn't compile
val sum: Long = numbers.reduce<Long, Int> { acc, next -> acc.toLong() + next.toLong() }
明らかに、LongはIntのスーパータイプではないため、コンパイルされません。 コンパイルエラーを修正するには、代わりにLongタイプを
ただし、結果タイプを変更するという一般的な問題は解決されません。
3. fold()
したがって、 fold()を使用して前述の合計の例を実装することにより、これらの問題に対処できるかどうかを見てみましょう。
val sum: Int = numbers.fold(0){ acc, next -> acc + next }
assertEquals(6, sum)
ここでは、初期値を提供しました。 reduce()とは対照的に、コレクションが空の場合、初期値が返されます。
さらに深く掘り下げるために、 fold()の署名を見てみましょう。
inline fun <T, R> Iterable<T>.fold(initial: R, operation: (acc: R, T) -> R): R
reduce()とは対照的に、2つの任意の汎用タイプTとRを指定します。
したがって、結果タイプをLongに変更できます。
val sum: Long = numbers.fold(0L){ acc, next -> acc + next.toLong() }
assertEquals(6L, sum)
一般に、結果タイプを変更する機能は非常に強力なツールです。たとえば、適切な結果タイプを使用すると、コレクションを偶数と奇数に簡単に分割できます。
val (even, odd) = numbers.fold(Pair(mutableListOf<Int>(), mutableListOf<Int>())) { eoPair, number ->
eoPair.apply {
when (number % 2) {
0 -> first += number
else -> second += number
}
}
}
assertEquals(listOf(2), even)
assertEquals(listOf(1, 3), odd)
4. foldとreduceのバリエーション
両方の関数の基本的なバリエーションを見てきました。 ただし、Kotlin標準ライブラリにはそれらのバリエーションもいくつか用意されています。
コレクションを右から左に逆の順序でトラバースする必要がある場合、 foldRight()を使用できます。
val reversed = numbers.foldRight(listOf<Int>()) { next, acc -> acc + next }
assertEquals(listOf(3,2,1), reversed)
foldRight()を使用すると、ラムダのパラメーターの順序が逆になることに注意してください。foldRight(…){next、acc->…}
同様に、 reduceRight()を使用できます。
さらに、コレクション内の各要素のインデックスにもアクセスしたい場合があります。
val reversedIndexes = numbers.foldRightIndexed(listOf<Int>()) { i, _, acc -> acc + i }
assertEquals(listOf(2,1,0), reversedIndexes)
同様に、 reduceRightIndexed()を使用できます。
さらに、 foldIndexed()および reduceIndexed()は、基本的な左から右の順序でインデックスへのアクセスを提供します。
5. 結論
したがって、 fold()と reduce()の違いを確認しました。
一方では、空でないコレクションのみを操作し、すべての要素を同じタイプの単一の結果に結合する場合は、 reduce()が適切な選択です。 一方、初期値を指定したり、結果タイプを変更したりする場合は、 fold()を使用すると柔軟性が得られます。
さらに深く掘り下げるには、Kotlinのコレクション変換を確認することをお勧めします。
そして、いつものように、コード例はGitHubのにあります。