시퀀스

홍승범·2023년 4월 30일
0

Kotlin

목록 보기
8/8

컬렉션에서의 처리는 즉시 발생한다. 즉 컬렉션의 map이나 filter가 호출될 때 컬렉션의 모든 원소는 즉각 처리된다.

반면 시퀀스는 지연처리된다. 데이터를 처리하기 위해 시퀀스를 사용하면 각각의 원소는 자신의 다음 우너소가 처리되기 전에 전체 파이프라인을 완료한다.

이러한 지연처리방식은 데이터가 많거나, first 같은 숏서킷 연산의 경우에 도움이 되고 원하는 값을 찾았을 때 시퀀스를 종료할 수 있게 도와준다.

fun main() {
   val result =  (100 until 200).map { it * 2 }
        .filter { it % 3 == 0 }
        .first()
   print(result)
}

위 코드는 범위 안의 100개의 숫자를 모두 2배를 하고, 그다음 100개의 숫자에 나머지연산을 하여 그중 첫번째 원소만을 취하는 방법. 즉 연산이 200번이라는 것.

이것을 개선하기 위해 filter대신 조건을 받는 first함수를 사용할 수 있다.

val result =  (100 until 200).map { it * 2 }
       .first { it % 3 == 0}
   print(result)

위의 코드는 100번의 2배 연산을 한 후 3번의 first연산수행으로 끝마칠 수 있다. 하지만 시퀀스를 이용하면 더 좋은 방법을 사용할 수 있다.

(100 until 2_000_000).asSequence()
        .map { println("doubling $it"); it * 2 }
        .filter { println("filtering $it"); it % 3 == 0}
        .first()

위 코드는 범위의 상한이 2백만임에도 6번의 연산만으로 결과를 추출해낸다.

doubling 100
filtering 200
doubling 101
filtering 202
doubling 102
filtering 204

시퀀스 API는 컬렉션에 들어있는 함수와 동일한 함수를 가지고 있지만, 시퀀스에 대한 연산은 중간연산 / 최종연산이라는 범주로 나뉜다. map, filter과 같은 중간연산은 새로운 시퀀스를 리턴하고, first, toList등과 같은 최종연산은 시퀀스가 아닌 다른 것을 리턴한다.

중요한것은 최종연산이 없이는 시퀀스는 데이터를 처리하지 않는다라는 점이다.

시퀀스 생성

이미 원소가 있다면 sequenceOf를, Iterable이 있다면 asSequence를 사용한다.

val numSeq = sequenceOf(3,1,4,1,5,9)
val numSeq2 = listOf(3,1,4,1,5,9).asSequence()

무한개의 원소를 가지는 시퀀스도 생성할 수 있다.

아래 예제는 주어진 정수의 다음 소수를 판별하는 예제이다. 이를 위해 먼저 Int에 확장함수를 구현한다.

fun Int.isPrime() = 
    this == 2 || (2..ceil(sqrt(this.toDouble())).toInt()).none { divisor -> this % divisor == 0 }

다음 소수를 알아내기 위해 몇번의 숫자를 반복해야할지는 모른다. 따라서 시퀀스를 생성하는 함수를 사용할 수 있다.

fun nextPrime(num: Int) = generateSequence(num + 1) { it + 1 }.first(Int::isPrime)

generateSequence 함수는 초기값과 시퀀스에 속하게 될 다음 값을 생산하는 함수를 인자로 받는다.

nextPrime 함수는 시퀀스를 생성해 무한대의 정수를 생산할 수 있으나, first 터미널 연산자에 의해 생성된 정수가 평가된다.

다른 무한시퀀스를 생성하는 예제를 보자. n개의 소수의 리스트를 생성하는 예제이다.

fun firstNPrimes(count: Int) = generateSequence(2, ::nextPrime)
    .take(count) // 중간연산자임
    .toList()

무한대의 원소를 가지는 시퀀스를 잘라내는 다른 방법은 마지막에 null을 리턴하는 생성함수를 사용하는 것이다.

fun primesLessThan(max: Int): List<Int> = generateSequence(2) { n -> if (n < max) nextPrime(n) else null}
    .toList()
    .dropLast(1) // 다음 소수가 한계값보다 큰 값인지 알수 없으므로 실제로는 max보다 큰 소수를 하나 포함한다. 따라서 마지막 하나는 뺀다

fun primesLessThan2(max: Int): List<Int> = generateSequence(2, ::nextPrime)
    .takeWhile { it < max }
    .toList()

sequence 함수는 주어진 람다 블록에서 평가되는 시퀀스를 생성한다

fun<T> sequence(block: suspend SequenceScope<T>.() -> Unit): Sequence<T>

이 함수를 이용해 필요할 때 yield를 통해 값을 생성하는 시퀀스를 만들 수 있다.

피보나치 수를 생성하는 예제이다.

fun fibonacciSequence() = sequence {
    var terms = Pair(0, 1)
    while(true) {
        yield(terms.first)
        terms = terms.second to terms.first + terms.second
    }
}

yield, yieldAll 함수는 중단함수로서 이터레이터에 값을 전달하고 다음 값을 요청할 때까지 값 생성을 중단한다. yield는 단일 값을, yieldAll은 다수의 값을 이터레이터에 넘겨준다.

다음 예제는 yieldAll 의 사용예이다

val seq2 = sequence {
    val start = 0
    yield(start)
    yieldAll(1..5 step 2)
    yieldAll(generateSequence(8) {  it * 3 })
}

위 시퀀스는 0 단일값을 yield한 후 범위를 통해 1,3,5의 값을 yield, 그리고 8부터 시작하는 각 원소에 3을 곱한 무한 시퀀스를 yield한다.

즉 yield와 yieldAll을 조합하여 시퀀스를 원하는 값을 생성할 수 있도록 구현할 수 있다는 이야기이다.

profile
그냥 사람

0개의 댓글