반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

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

Kotlin/코틀린 코루틴

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

어리둥절범고래 2024. 3. 2. 13:26
반응형

코틀린의 시퀀스는 컬렉션이랑 비슷한 개념이지만, 필요할 때마다 값을 하나씩 계산하는 지연(lazy) 처리를 합니다.

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

 

  • 요구되는 연산을 최소한으로 수행한다.
  • 무한 시퀀스가 가능하다.
  • 메모리 사용이 효율적이다.

이러한 특징 때문에 값을 순차적으로 계산하여 필요할 때 반환하는 빌더를 정의하는 것이 좋습니다.

시퀀스의 람다 표현식 내부에는 yield 함수를 호출하여 시퀀스의 다음 값을 생성합니다.

val seq = sequence {
    yield(1)
    yield(2)
    yield(3)
}

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

sequence는 수신 객체 지정 람다 함수이며, 람다 내부에서 수신 객체인 this는 SequenceScope<T>를 가리킵니다.

SequenceScope의 내부는 다음과 같습니다.

public abstract class SequenceScope<in T> internal constructor() {

    public abstract suspend fun yield(value: T)

    public abstract suspend fun yieldAll(iterator: Iterator<T>)

    public suspend fun yieldAll(elements: Iterable<T>) {
        if (elements is Collection && elements.isEmpty()) return
        return yieldAll(elements.iterator())
    }
    
    public suspend fun yieldAll(sequence: Sequence<T>) = yieldAll(sequence.iterator())
}

 

각 숫자는 필요할 때마다 생성됩니다.

시퀀스 빌더 내부 그리고 시퀀스를 사용하는 곳에서 메시지를 출력하면 이러한 작동 방식을 확인해 볼 수 있습니다.

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

 

  1. 첫 번째 수를 요청하면 빌더 내부로 진입한다.
  2. "Generating first"를 출력한 뒤, 숫자 1을 반환한다.
  3. 반복문에서 반환된 값을 받은 뒤, "Next number is 1"을 출력한다.

여기서 반복문과 다른 차이가 보입니다.

이전에 다른 숫자를 찾기 위해 멈췄던 지점에서 다시 실행이 된다는 것입니다.

중단 체제가 없으면 함수가 중간에 멈췄다가, 나중에 중단된 지점에서 다시 실행되는 건 불가능합니다.

중단이 가능하기 때문에 main 함수와 시퀀스 제너레이터가 번갈아가면서 실행됩니다.

 

 

실제 사용 예

 

시퀀스 빌더는 전형적으로 피보나치 수열과 같은 수학적 시퀀스를 만들거나 난수나 임의의 문자열 만들 때도 사용될 수 있습니다.

val fibonicci: 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(fibonicci.take(10).toList())
} // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

// 난수 생성
fun randomNumbes(
    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)이 아닌 중단 함수를 사용하면 안 됩니다.

중단이 필요하다면 데이터를 가져오기 위해 플로우를 사용하는 것이 낫습니다.

플로우 빌더가 작동하는 방식은 시퀀스 빌더와 비슷하지만, 플로우는 여러 가지 코루틴 기능을 지원합니다.

fun allUsersFlow(api: UserApi): Flow<User> = flow {
    var page = 0
    do {
        val users = api.takePage(page++) // suspend 함수입니다.
        emitAll(users)
    } while (!users.isNullOrEmpty())
}

 

반응형