[Effective Kotlin] 아이템 49. 하나 이상의 처리 단계를 가진 경우에는 시퀀스를 사용하라

0

목록 화면에서 Sequence와 Iterable이 차이가 난다고 말했었다.

interface Iterable<out T> {
    operator fun iterator() : Iterator<T>
}

interface Sequence<out T> {
    operator fun iterator() : Iterator<T>
}

둘다 정의는 비슷한데 어떤 차이가 발생하는것일까?

정답은 연산 방식이다.

  • Kotlin Collections : Eager evalutaion
  • Kotlin Sequence : Lazy evaluation
  • Java Stream : Lazy evalutaion

Lazy evaluation은 연산을 최대한 뒤로 미루고, 연산이 필요한 순간에 연산을 수행하는 방식이다.
Eager evaluation은 연산을 미루지 않고, 바로 처리하는 방식이다.

val words = "The quick brown fox jumps over the lazy dog".split(" ")
val lengthsList = words.filter { println("filter: $it"); it.length > 3 }
    .map { println("length: ${it.length}"); it.length }
    .take(4)

println("Lengths of first 4 words longer than 3 chars:")
println(lengthsList)

/**
filter: The
filter: quick
filter: brown
filter: fox
filter: jumps
filter: over
filter: the
filter: lazy
filter: dog
length: 5
length: 5
length: 5
length: 4
length: 4
Lengths of first 4 words longer than 3 chars:
[5, 5, 5, 4]
**/

위의 코드는 아래와 같은 다이어그램으로 동작한다.

이제 Squence를 확인해보자.

val words = "The quick brown fox jumps over the lazy dog".split(" ")
//convert the List to a Sequence
val wordsSequence = words.asSequence()

val lengthsSequence = wordsSequence.filter { println("filter: $it"); it.length > 3 }
    .map { println("length: ${it.length}"); it.length }
    .take(4)

println("Lengths of first 4 words longer than 3 chars")
// terminal operation: obtaining the result as a List
println(lengthsSequence.toList())

/**
Lengths of first 4 words longer than 3 chars
filter: The
filter: quick
length: 5
filter: brown
length: 5
filter: fox
filter: jumps
length: 5
filter: over
length: 4
[5, 5, 5, 4]
**/

Sequnce는 Lazy 하게 실행되는것을 알수있고, filter 되자마자 map이 실행된다.

Iterable를 사용하는 Collection은 연산자마다 전부 계산을 한다면,
Sequence는 최종적인 계산이 만났을때 수행이 된다.

그래서 Sequnce는 최소한의 연상이 가능하다.

자 이외의 시퀀스의 장점을 알아보자.

  • 최소한의 연산
  • 무한 시퀀스 형태로 사용할 수 있습니다.
  • 각각의 단계에서 컬렉션을 발생시키지 않습니다.
  • 자연스러운 처리순서를 유지합니다.

🧖🏻‍♀️ 순서의 중요성

방금 시퀀스는 요소 하나하나에 지정한 연산을 한꺼번에 적용한다고 했는데 이를 lazy order라고 부르고,
이터러블은 요소 전체를 대상으로 연산을 차근차근 적용해 나간다.

그래서 둘이 다른 순서를 보여주기도 하는데, 아래의 코드를 확인해보자.

    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, 

    println()
    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,

하지만 반복문과 조건문을 사용한 코드는 Sequnce처럼 동작하므로, Sequnce가 좀더 자연스러운 처리이다.

🚊 최소 연산

시퀀스는 중간연산이라는 개념을 갖고있기때문에, 최소 연산을 하게 된다.
이러한 이유로 모든 요소에 적용할 필요가 없을땐 시퀀스를 사용하는것이 좋다.
first, take, any, all, none, indexOf 가 있다.

🥰 무한 시퀀스

시퀀스는 최종 연산이 일어기 전에는 컬렉션에 어떠한 처리도 하지 않는다.
따라서 무한 시퀀스를 만들어, 필요한 부분까지만 값을 추출하는 방법도 가능하다.

아래는 Generate를 통한 무한 시퀀스이다.

    generateSequence(1) { it + 1 }
        .map { it * 2 }
        .take(10)
        .forEach { println("$it, ") }
    val fibonacci = sequence<Int> {
        yield(1)
        var current = 1
        var prev = 1
        while (true) {
            yield(current)
            val temp = prev
            prev = current
            current += temp
        }
    }
    print(fibonacci.take(100).toList())

시퀀스는 이터러블보다 훨씬 효율적으로 작동하지만, 둘다 종결연산자를 사용하지 않으면 무한하게 반복되게 된다.
심지어 all과 none, any는 true나 false를 리턴하지 않으면 무한 루프로 빠지므로 take와 first정도만 사용하는것은 권장한다.

각각의 단계에서 컬렉션을 만들어내지 않음

표준 컬렉션 처리함수는 각각의 단계에서 대부분 새로운 컬렉션을 만들어낸다.
각각의 단계에서 만들어진 결과를 활용하는것은 컬렉션의 장점이나, 큰비용이 든다.
특히 무거운 컬렉션을 처리하는데 굉장히 큰 비용이 들어가기 때문에, 파일을 처리할때는 시퀀스를 활용한다.

// 1.53기가의 컬렉션을 3개나 새로 만들어서 메모리가 터진다.
    File("Chicaco.csv").readLines()
        .drop(1) // 묘사 제거
        .mapNotNull { it.split(",").getOrNull(6) }
        // 탐색한다.
        .filter { "Cannbis" in it }
        .count()
        .let(::print)
// 컬렉션을 하나밖에 만들어내지 않는다.
    File("Chicaco.csv").useLines {
        it.drop(1) // 묘사 제거
            .mapNotNull { it.split(",").getOrNull(6) }
            // 탐색한다.
            .filter { "Cannbis" in it }
            .count()
            .let(::print)
    }

🚧 참고

코틀린 랭 - 시퀀스
시퀀스에 관한 정리

profile
쉽게 가르칠수 있도록 노력하자

0개의 댓글