Effective Kotlin - 複数の処理ステップがある大きなコレクションにはSequenceを推奨

要約

Eager evaluation(先行評価) vs Lazy evaluation(遅延評価)

順序は重要です

listOfのようなIterableな構造の結果とsequenceOfの結果は異なります。


sequenceOf(1,2,3)
    .filter { print("F$it, "); it % 2 == 1 }
    .map { print("M$it, "); it * 2 }
    .forEach { print("E$it,")}

// 出力: F1, M1, E2, F2, F3, M3, E6,

listOf(1,2,3)
       .filter { print("F$it, "); it % 2 == 1 }
       .map { print("M$it, "); it * 2 }
       .forEach { print("E$it, ") }

// 出力: F1, F2, F3, M1, M3, E2, E6,

コードテスト

Sequenceは最小限の操作のみを行います

必要な最小限の作業を行います。


(1..10).asSequence()
   .filter { print("F$it, "); it % 2 == 1 }
   .map { print("M$it, "); it * 2 }
   .find { it > 5 }
// 出力: F1, M1, F2, F3, M3,

(1..10)
   .filter { print("F$it, "); it % 2 == 1 }
   .map { print("M$it, "); it * 2 }
   .find { it > 5 }
// 出力: F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, M1, M3, M5, M7, M9,

コードテスト

Sequenceは無限になり得ます

Sequenceは無限の要素を持つことができます。


val fibonacci = sequence {
   yield(1)
   var current = 1
   var prev = 1
   while (true) {
       yield(current)
       val temp = prev
       prev = current
       current += temp
   }
}
print(fibonacci.take(10).toList())
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

print(fibonacci.toList())
// 永久に実行され続けます

コードテスト

Sequenceは各処理ステップでコレクションを作成しません


numbers
   .filter { it % 10 == 0 } // ここでコレクションを1つ作成
   .map { it * 2 } // ここでコレクションを1つ作成
   .sum()
// 合計で、内部的に2つのコレクションが作成されます
numbers
   .asSequence()
   .filter { it % 10 == 0 }
   .map { it * 2 }
   .sum()
// コレクションは作成されません

Sequenceが高速にならない場合は?

現時点では、sorted関数が唯一の例外的なケースだと言われています。
無限の Sequenceに対してsortedを処理すると、無限ループに陥る可能性があるため注意してください。


generateSequence(0) { it + 1 }.take(10).sorted().toList()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
generateSequence(0) { it + 1 }.sorted().take(10).toList()
// 無限時間。終了しません。

結論

巨大なコレクションを扱い、1つ以上の処理ステップがある場合は、Sequenceを使用して処理すべきです。

AD