코틀린 코루틴 1.2 - 시퀀스 빌더

Seogi·2025년 6월 26일

Kotlin

목록 보기
5/27

시퀀스는 ListSet과 같은 컬렉션이랑 비슷한 개념이지만, 필요할 때마다 값을 하나씩 계산하는 지연(lazy) 처리를 한다.

시퀀스의 특징은 다음과 같다.

  • 요구되는 연산을 최소한으로 수행한다.
  • 무한정이 될 수 있다.
  • 메모리 사용이 효율적이다.
val seq = sequence {
    yield(1)
    yield(2)
    yield(3)
}

fun main() {
    for (num in seq) {
        print(num)
    } // 123
}

시퀀스는 sequence라는 함수를 이용해 정의한다.
시퀀스의 람다 표현식 내부에서는 yield 함수를 호출하여 시퀀스의 다음 값을 생성한다.

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

람다 내부의 수신 객체는 SequenceScope<T>이다. 이 객체는 yield 함수를 가지고 있다.

동작 방식

여기서 반드시 알아야 하는 것은 시퀀스는 각 숫자가 미리 생성되는 대신, 필요할 때마다 생성된다는 점이다.

val seq = sequence {
    println("Generating first")
    yield(1)
    println("Generating second")
    yield(2)
    println("Generating third")
    yield(3)
    println("Done")
}

fun main() {
    for (num in seq) {
        println("The next number is $num")
    }
}

// Generating first
// The next number is 1
// Generating second
// The next number is 2
// Generating third
// The next number is 3
// Done

우선 첫 번째 수를 요청하면 빌더 내부로 진입한다. 그리고 "Generating first"를 출력한 뒤, 숫자 1을 반환한다.

여기서 반복문과 다른 차이를 알 수 있다.
이전에 다른 숫자를 찾기 위해 멈췄던 지점에서 다시 실행이 된다는 것이다.
중단이 없으면 함수가 중간에 멈췄다가, 나중에 중단된 지점에서 다시 실행되는 것은 불가능하다.

실제 사용 예

val fibonacci: Sequence<BigInteger> = sequence {
    var first = 0.toBigInteger()
    var second = 1.toBigInteger()
    while (true) {
        yield(first)
        val temp = first
        first += second
        second = temp
    }
}

fun main() {
    print(fibonacci.take(10).toList())
}

// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

피보나치 수열과 같은 수학적 시퀀스를 만들 수 있다.

fun randomNumbers(
    seed: Long = System.currentTimeMillis()
): Sequence<Int> = sequence {
    val random = Random(seed)
    while (true) {
        yield(random.nextInt())
    }
}

fun randomUniqueStrings(
    length: Int,
    seed: Long = System.currentTimeMillis()
): Sequence<String> = sequence {
    val random = Random(seed)
    val charPool = ('a'..'z') + ('A'..'Z') + ('0'..'9')
    while (true) {
        val randomString = (1..length)
            .map { i -> random.nextInt(charPool.size) }
            .map(charPool::get)
            .joinToString("")
        yield(randomString)
    }
}.distinct()

난수나 임의의 문자열을 만들 때도 사용될 수 있다.

주의점

시퀀스 빌더는 반환(yield)이 아닌 중단 함수를 사용하면 안 된다.
중단이 필요하다면 플로우를 사용하는 것이 낫다.
플로우 빌더의 작동 방식은 시퀀스 빌더와 비슷하지만, 플로우는 여러가지 코루틴 기능을 지원한다.

0개의 댓글