Kotlin standard library는 Collection과 함께 또 다른 유형인 Sequences(Sequence<T>)
를 포함하고 있습니다. 컬렉션과는 달리 시퀀스는 엘리먼트를 포함하지 않으며 반복하는 동안 엘리먼트를 생성합니다. 시퀀스는 Iterable
과 동일한 기능을 제공하지만 여러 단계 처리에 대한 다른 접근방식을 구현합니다.
Iterable
은 여러 과정을 처리할 때 각 단계를 완료하고 그 결과인 중간 컬렉션을 반환합니다. 시퀀스는 여러 과정의 처리에서 가능한 Lazily(나중에)하게 처리합니다. 이 의미는 시퀀스는 여러 단계의 처리는 바로 실행하지 않고 전체 단계가 처리된 결과가 요청되었을 때 실제 시퀀스 연산이 일어나면서 Lazily(나중에)하게 처리됩니다.
동작 실행 순서도 다르다고 할 수 있습니다. Sequence
는 모든 단일 요소에 대해 처리를 One-By-One 형태로 수행하는 반면 Iterable
은 전체 컬렉션의 각 단계를 완료하고 그 다음 단계로 진행합니다.
따라서 시퀀스를 사용하면 중간 단계의 결과에 대한 처리를 피할 수 있습니다. 그러므로 전체 컬렉션의 체이닝 동작에서 퍼포먼스 향상을 기대할 수 있습니다. 그러나 이런 시퀀스의 Lazy 특성은 오히려 간단한 계산이나 작은 컬렉션을 처리할 때 불필요한 오버헤드가 발생할 수 있습니다. 이런 이유로 Sequence
와 Iterable
사이에서 어떤것이 해당 케이스에서 더 효율적인지를 고려해야합니다.
시퀀스를 만들기 위해서는 sequenceOf()
함수를 호출하고 인자들을 나열하면 됩니다.
val numbersSequence = sequenceOf("four", "three", "two", "one")
Iterable
객체(List
와 Set
등)를 asSequence()
를 호출해서 시퀀스로 만들 수 있습니다.
val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()
시퀀스의 요소들을 계산 할 수 있는 방식인 generateSequence()
함수와 함께 구축하는 방법도 있습니다. 선택적으로 첫번째 인자를 명시적으로 선언할 수 있습니다. 이런 시퀀스 생성은 null
을 반환하면 중지됩니다. 아래 예제를 보면 무한한 시퀀스가 있습니다.
val oddNumbers = generateSequence(1) { it + 2 } // `it` is the previous element
//println(oddNumbers.take(5).toList()) // Result : [1, 3, 5, 7, 9]
println(oddNumbers.count()) // error: the sequence is infinite
// Result : Evaluation stopped while it's taking too long️
generateSequence()
를 사용할 때 유한한 시퀀스를 만들기 위해서는 마지막 요소 다음에 null을 반환해야 합니다.
val oddNumbersLessThan10 = generateSequence(1) { if (it < 8) it + 2 else null }
println(oddNumbersLessThan10.count()) // Result: 5
마지막으로 시퀀스를 임의의 크기의 덩어리로 생성할 수 있는 기능이 있습니다. 이 기능은 람다 함수내에서 호출할 수 있는 yield()
그리고 yieldAll()
함수입니다. 이 함수는 시퀀스 소비자에게 요소들을 반환하고 소비자가 다음 요소를 요청할 때 까지 sequence()
실행을 일시중단 합니다. yield()
는 단일 요소를 인자로 가지고 yieldAll()
은 Iterable
객체를 인자로 가지고 Iterator
나 다른 Sequence
도 가질 수 있습니다. yieldAll
에 인자로 들어가는 Sequence
는 무한할 수 있지만 모든 후속 호출은 실행되지 않기 때문에 마지막에만 호출해야 합니다.
val oddNumbers = sequence {
yield(1)
yieldAll(listOf(3, 5))
yieldAll(generateSequence(7) { it + 2 })
}
println(oddNumbers.take(5).toList())
시퀀스 동작은 상태요구에 따라서 그룹별로 분류될 수 있습니다.
map()
이나 filter()
과 같은 스테이트리스(Stateless) 동작은 상태나 각 요소들간의 의존적인 과정을 요구하지 않지만 take()
나 drop()
처럼 적은 양을 가진 상수의 상태는 요구할 수 있습니다.시퀀스 동작이 Lazliy하게 생성된 다른 시퀀스를 리턴하면 그것을 itermediate
라 하고 다른 동작은 terminal
이라고 합니다.terminal
의 수행 동작의 예로는 toList()
또는 sum()
이 있습니다. 시퀀스 요소들은 오직 terminal
이라는 동작으로만 얻어질 수 있습니다.
시퀀스는 여러번의 Iteration이 가능하지만 일부 시퀀스의 구현은 한번만 반복으로 되도록 제한되어 있습니다. 해당 내용은 문화되어있습니다.
Iterable
과 Sequnce
의 차이를 예제로 살펴보겠습니다.
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)
// Result
// 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]
위의 예제를 보면 단어 리스트에서 길이가 3을 초과하는 filter()
와 필터링된 문자열 길이를 map()
이 순서대로 호출되는걸 볼 수 있습니다. 순차적으로 filter의 모든 요소들이 수행되고 리턴된 결과로 map이 수행됩니다.
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())
// Result
// 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()
에서 요소를 리턴하자 마자 실행됩니다. 그리고 take(4)
로 4개의 요소만 필요하기 때문에 시퀀스 처리가 만족하는 개수가 4개가 되었을 때 수행은 정지된다.