이동식 저장소

[Kotlin] Sequence 본문

Primary/Kotlin

[Kotlin] Sequence

해스끼 2021. 1. 15. 11:22

Sequence

Sequence는 독특한 형태의 container이다. 기본적인 형태는 List 등의 Iterable과 같지만, 세부적인 동작 방법이 다르다.

 

Iterablemap(), filter(), take()을 적용한다고 해 보자. 우선 원본의 모든 원소에 map()이 적용되고, 그 결과에 filter()가 적용되고, 마지막으로 take()가 적용된다. 즉 코드에서 함수를 적용한 순서대로 중간 결과가 반환된다.

 

Sequence에 같은 함수를 적용한다고 해 보자. SequenceIterable과는 다르게 각 원소마다 모든 함수를 적용한다. 각 원소마다 map(), filter(), take()을 모두 적용해 본다는 뜻이다. 만약 원소가 filter()의 조건에 맞지 않는다면 filter()까지만 함수가 적용되고 뒤에 있는 take()은 적용되지 않는다. 차이가 느껴지는가?

 

Sequence의 또다른 특징으로는, Sequence의 연산이 최대한 늦게(lazily) 적용된다는 것이다. Iterable은 연산 함수를 호출한 즉시 결과가 반환된다. 그러나 Sequence는 가능한 한 연산을 늦게 적용한다. 예컨대 for문에서 연산 결과를 요청하기 전까지는 연산하지 않고, 그마저도 원소 하나씩만 적용한다.

 

이러한 특징으로 인해 Sequence는 대량의 데이터에 연산을 적용할 때 더 효율적이다. 하지만 데이터가 작다면 lazy 성질로 인해 오버헤드가 생길 수 있으니 상황에 맞게 SequenceIterable을 적절히 활용하자.

Sequence 생성

다른 컬렉션처럼 sequenceOf() 함수를 이용하여 Sequence를 만들 수 있다.

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

또는 다른 Iterable로부터 asSequence()를 호출하여 만들 수도 있다.

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

또는 람다식으로 만들 수도 있다. generateSequence()의 첫 번째 매개변수에는 초기값을, 두 번째 매개변수에는 다음 값을 계산할 람다식을 넘겨줘야 한다. Sequence를 중간에 끝내고 싶을 경우 람다식에서 null을 반환하도록 해야 한다. 그렇지 않은 경우에는 Sequence가 무한히 이어질 수 있다.

val oddNumbers = generateSequence(1) { it + 2 } // it: 이전 값
println(oddNumbers.take(5).toList())
//println(oddNumbers.count())     // error: null이 없으므로 sequence가 무한히 이어진다.
[1, 3, 5, 7, 9]

또는 chunk로부터 만들 수도 있다. chunkyield() 또는 yieldAll()을 말한다. yield()는 원소 하나를 인자로 받고, yieldAll()Iterable 또는 Sequence 객체를 인자로 받을 수 있다. yieldAll()은 원소를 무한히 제공할 수 있으므로 웬만하면 가장 마지막에 부르는 게 좋다.

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

Sequence 연산

Sequence의 연산은 다음의 두 가지로 나눌 수 있다.

  • Stateless: Sequence의 상태가 거의 필요 없는 연산. map(), filter(), take() 등이 있다.
  • Stateful: Sequence의 원소의 수 등 상태 정보가 많이 필요한 연산.

다음의 기준으로도 나눌 수 있다.

  • intermediate: Sequence 연산이 다른 Sequence를 반환할 때.
  • terminal: Sequence 연산이 Sequence가 아닌 값을 반환할 때. toList(), sum() 등이 있다.

일반적으로 Sequence는 여러 번 반복될(iterated) 수 있으나, 특정 클래스의 경우 단 한 번만 반복될 수 있다. 레퍼런스 문서에 자세히 설명되어 있다.

Sequence 연산 예시

IterableSequence의 차이점을 살펴보자.

Iterable

여러 개의 단어 중 길이가 3 이상인 처음 4개의 단어를 찾고자 한다.

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]

실행 결과를 보면, filter()가 모두 적용된 다음 map()이 적용되고, 그 후에 take()가 적용되었다. 코드에 적힌 순서대로 연산이 수행되었다.

Iterable

Sequence

Sequence로 같은 연산을 수행해 보자.

val words = "The quick brown fox jumps over the lazy dog".split(" ")
// List를 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: 결과를 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]

원소 하나하나마다 filter(), map(), take()가 적용되고 있다. 4개의 원소를 가져온 후에는 연산이 중단된다. 이렇게 하면 Iterable보다 적은 연산으로 결과를 얻을 수 있다.

Sequence


앱이나 다른 프로그램에서 쏠쏠하게 써먹을 수 있을 것 같다.

'Primary > Kotlin' 카테고리의 다른 글

[Kotlin] Coroutines - Cancellation and Timeouts  (0) 2021.01.20
[Kotlin] Coroutines - Basics  (0) 2021.01.19
[Kotlin] Thread 생성 및 실행  (0) 2021.01.14
[Kotlin] Collections 확장 함수  (0) 2021.01.13
[Kotlin] 데이터 클래스  (0) 2021.01.08
Comments