관리 메뉴

나만을 위한 블로그

[코틀린 코루틴] 2. 시퀀스 빌더 본문

책/코틀린 코루틴

[코틀린 코루틴] 2. 시퀀스 빌더

참깨빵위에참깨빵 2024. 2. 12. 23:59
728x90
반응형

파이썬, 자바스크립트 등 언어에선 제한된 형태의 코루틴을 쓰고 있다.

 

  • 비동기 함수(async, await)
  • 제너레이터 함수(값을 순차적으로 리턴하는 함수)

 

코틀린에는 제너레이터 대신 시퀀스를 생성할 때 사용하는 시퀀스 빌더가 있다. 시퀀스는 List, Set 같은 컬렉션과 비슷한 개념이지만 필요할 때마다 값을 하나씩 계산하는 지연 처리를 한다. 시퀀스의 특징은 아래와 같다.

 

 

이런 특징 때문에 값을 순차 계산해서 필요할 때 리턴하는 빌더를 정의하는 게 좋다. 시퀀스는 sequence 함수로 정의하며 람다식 안에선 yield 함수를 호출해 시퀀스의 다음 값을 생성한다.

 

val seq = sequence { // this : SequenceScope<Int>
    yield(1)
    yield(2)
    yield(3)
}

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

// 123

 

sequence 함수는 짧은 DSL 코드고 인자는 수신 객체 지정 람다 함수다. 람다 안에서 수신 객체인 this는 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("Next Number : $num")
    }
}

// Generating First
// Next Number : 1
// Generating Second
// Next Number : 2
// Generating Third
// Next Number : 3
// Done!

 

 1번째 수를 요청하면 빌더 내부로 진입하고 Generating First를 출력한 뒤 1을 리턴한다. 이후 반복문에서 리턴된 값을 받고 Next Number : 1을 출력한다.

여기서 반복문과 다른 결정적 차이는, 이전에 다른 숫자를 찾기 위해 멈췄던 지점에서 재실행된다. 중단 체제가 없으면 함수가 중간에 멈췄다가 나중에 중단된 지점에서 다시 실행되는 건 불가능하다. 중단이 가능해서 메인 함수와 시퀀스 제너레이터가 번갈아가며 실행된다.

 

실제 사용 예

 

시퀀스 빌더가 쓰이는 전형적 예시는 피보나치 수열 같은 수학적 시퀀스를 만드는 것이다.

 

import java.math.BigInteger

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() {
    println(fibonacci.take(10).toList())
}

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

 

또는 난수, 임의의 문자열을 만들 때도 쓸 수 있다.

 

import kotlin.random.Random

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가 아닌 중단 함수를 쓰면 안 된다. 중단이 필요하다면 데이터를 갖고 오기 위해 Flow를 쓰는 게 낫다.

반응형
Comments