[Kotlin] Sequence

케니스·2023년 1월 13일
0

Kotlin

목록 보기
1/5

Kotlin standard library는 Collection과 함께 또 다른 유형인 Sequences(Sequence<T>) 를 포함하고 있습니다. 컬렉션과는 달리 시퀀스는 엘리먼트를 포함하지 않으며 반복하는 동안 엘리먼트를 생성합니다. 시퀀스는 Iterable과 동일한 기능을 제공하지만 여러 단계 처리에 대한 다른 접근방식을 구현합니다.

Iterable은 여러 과정을 처리할 때 각 단계를 완료하고 그 결과인 중간 컬렉션을 반환합니다. 시퀀스는 여러 과정의 처리에서 가능한 Lazily(나중에)하게 처리합니다. 이 의미는 시퀀스는 여러 단계의 처리는 바로 실행하지 않고 전체 단계가 처리된 결과가 요청되었을 때 실제 시퀀스 연산이 일어나면서 Lazily(나중에)하게 처리됩니다.

동작 실행 순서도 다르다고 할 수 있습니다. Sequence는 모든 단일 요소에 대해 처리를 One-By-One 형태로 수행하는 반면 Iterable 은 전체 컬렉션의 각 단계를 완료하고 그 다음 단계로 진행합니다.

따라서 시퀀스를 사용하면 중간 단계의 결과에 대한 처리를 피할 수 있습니다. 그러므로 전체 컬렉션의 체이닝 동작에서 퍼포먼스 향상을 기대할 수 있습니다. 그러나 이런 시퀀스의 Lazy 특성은 오히려 간단한 계산이나 작은 컬렉션을 처리할 때 불필요한 오버헤드가 발생할 수 있습니다. 이런 이유로 SequenceIterable 사이에서 어떤것이 해당 케이스에서 더 효율적인지를 고려해야합니다.

Construct

From elements

시퀀스를 만들기 위해서는 sequenceOf() 함수를 호출하고 인자들을 나열하면 됩니다.

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

From an Iterable

Iterable객체(ListSet등)를 asSequence()를 호출해서 시퀀스로 만들 수 있습니다.

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

From a Function

시퀀스의 요소들을 계산 할 수 있는 방식인 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

From chunk

마지막으로 시퀀스를 임의의 크기의 덩어리로 생성할 수 있는 기능이 있습니다. 이 기능은 람다 함수내에서 호출할 수 있는 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())

Sequence operations

시퀀스 동작은 상태요구에 따라서 그룹별로 분류될 수 있습니다.

  • map()이나 filter()과 같은 스테이트리스(Stateless) 동작은 상태나 각 요소들간의 의존적인 과정을 요구하지 않지만 take()drop()처럼 적은 양을 가진 상수의 상태는 요구할 수 있습니다.
  • 스테이트풀(Stateful) 동작은 시퀀스에서 비례하는 요소들의 숫자만큼 상당한 양의 상태가 필요합니다.

시퀀스 동작이 Lazliy하게 생성된 다른 시퀀스를 리턴하면 그것을 itermediate라 하고 다른 동작은 terminal이라고 합니다.terminal의 수행 동작의 예로는 toList() 또는 sum()이 있습니다. 시퀀스 요소들은 오직 terminal이라는 동작으로만 얻어질 수 있습니다.

시퀀스는 여러번의 Iteration이 가능하지만 일부 시퀀스의 구현은 한번만 반복으로 되도록 제한되어 있습니다. 해당 내용은 문화되어있습니다.

Sequence processing example

IterableSequnce의 차이를 예제로 살펴보겠습니다.

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)
// 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이 수행됩니다.

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())
// 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개가 되었을 때 수행은 정지된다.

profile
노력하는 개발자입니다.

0개의 댓글