조용한 담장

코틀린(Kotlin) Collections : 시퀀스(sequences) 본문

kotlin

코틀린(Kotlin) Collections : 시퀀스(sequences)

iosroid 2020. 1. 2. 18:39

코틀린(kotlin) 의 시퀀스(sequence) 를 살펴보자.

원문 https://kotlinlang.org/docs/reference/sequences.html 을 보며 정리.

kotlin standard library 는 collection 과 함께 또다른 container type 인 sequences (Sequence<T>) 를 가지고 있다.

여러 단계를 포함하는 Iterable 을 수행할 때에는 각 단계의 처리는 바로 끝나고 즉각 그 결과를 리턴한다.

sequences 의 여러 단계 처리는 전체 단계가 처리된 결과가 요구됬을 때에 실제 연산이 일어나며 느리게(나중에) 처리된다. (executed lazily)

동작 수행의 순서 또한 다르다. Sequence 은 각각 하나의 element 에 대해 모든 단계를 수행한다.

Iterable 은 전체 collection 에 대해 각 단계의 수행을 완료하고 다음 단계로 넘어간다.

따라서, sequence 는 중간 단계의 결과에 대한 처리를 피할 수 있게 해주며, collection 전체 처리에 대한 수행 성능이 향상된다.

하지만 크기가 작은 collection 이나 단순한 연산 동작에 대해서는 오히려 불필요한 오버헤드가 생길 수 있다.

그러므로 어느 경우에 SequenceIterable 이 나을지 적절한 선택을 해야 한다.

Constructing

From elements

sequence 를 생성하려면 argument 로 element 를 나열하는 sequenceOf() 함수를 호출한다.

val numbersSequence = sequenceOf("four", "three", "two", "one")

From Iterable

ListSet 같은 Iterable object 를 이미 가지고 있다면 asSequence() 함수를 통해 sequence 를 생성할 수 있다.

val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()

From function

element 를 생성하는 함수를 argument 로 사용해서 sequence 를 생성하는 방법으로 generateSequence() 함수를 사용한다.

첫번째 element 를 특정 값으로 지정 하거나 함수 호출의 결과를 지정하는 것도 가능하다.

함수가 null 을 리턴하면 sequence 생성은 멈추게 된다.

아래의 코드는 무한히 생성되는 오류를 가진다.

val oddNumbers = generateSequence(1) { it + 2 } // `it` is the previous element
println(oddNumbers.take(5).toList()) // [1, 3, 5, 7, 9]
//println(oddNumbers.count())     // error: the sequence is infinite

generateSequence() 함수로 유한한 sequence 를 만드려면 마지막 element 뒤에 null 을 리턴하는 함수를 제공해야 한다.

val oddNumbersLessThan10 = generateSequence(1) { if (it < 10) it + 2 else null }
println(oddNumbersLessThan10.count()) // 6

From chunks

sequence() 함수를 사용하여 임의의 크기의 chunk 의 각각의 element 로 sequence 를 생성할 수 있다.

이 함수는 yield()yieldAll() 함수 호출을 포함하는 람다 표현식을 가진다.

두 함수는 element 를 sequence consumer 에게 리턴하며 다음 consumer 의 다음 element 에 대한 요청이 있을 때까지 sequence() 의 실행은 중단된다.

yield() 는 한개의 element 를 argument 로 가지고, yieldAll()Iterable object, Iterator 또는 다른 Sequence 를 가진다.

yieldAll() 의 argument 가 되는 Sequence 는 무한할 수 있으나 이때의 호출은 항상 마지막이 되어야 하며 그렇지 않으면 그 뒤의 정의된 argument 들은 호출되지 않는다.

val oddNumbers = sequence {
    yield(1)
    yieldAll(listOf(3, 5))
    yieldAll(generateSequence(7) { it + 2 })
}
println(oddNumbers.take(5).toList())

// output:
// [1, 3, 5, 7, 9]

Sequence operations

sequence 수행 동작은 state 요구사항에 따라 아래처럼 나뉠 수 있다.

  • Stateless 수행 동작은 state 를 요구하지 않고 element 각각을 독립적으로 처리한다. 예를들어, map() 이나 filter() 이 있다. Stateless 수행 동작은 또한 element 를 처리하기 위한 작은 일정 양의 state 를 요구할 수 있다. 예를들어, take(), drop() 이 있다.
  • Stateful 수행 동작은 보통 sequence 의 element 개수에 비례하여 많은 양의 state 를 요구한다.

sequence 수행 동작이 늦게 생성되는(produced lazily) 다른 sequence 를 리턴 한다면 그것은 intermediate 라 하고 다른 동작은 terminal 이라 한다.

terminal 수행 동작의 예로는 toList()Sum() 이 있다.

Sequence element 는 오직 terminal 수행 동작으로 부터만 얻어질 수 있다.

sequence 는 여러번의 iteration 동작이 가능하나 어떤 sequence 구현은 단 한번으로 제한될 수 있는데 이는 해당되는 것들에 문서화 되어 있다.

Sequence processing example

Iterable

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)

// output:
// 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]

filter() 에서 모든 element 에 대해 동작이 수행되고 나서 map() 에서 filter() 의 결과로 리턴된 element 에 대해 수행된다.

https://kotlinlang.org/assets/images/reference/sequences/list-processing.png

Sequence

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

// output:
// 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]

프린트 문 이 먼저 호출된것을 통해 filter()map() 이 실제 필요로 하는 때에 호출되는 것을 볼 수 있다.

또한 map()filter() 가 element 를 리턴하자 마자 바로 실행되는 것을 볼 수 있다.

그리고 take() 에 의해 최종 결과의 개수가 4개를 만족하면 나머지 수행은 멈추고 모든 동작이 마무리 된다.

https://kotlinlang.org/assets/images/reference/sequences/sequence-processing.png

위 예제를 보면 같은 크기의 list 에 대해 iterable 은 23 단계를 수행하지만 sequence 는 18 단계만 수행을 하는 것을 알 수 있다.

 

Comments