[코틀린 코루틴] 2. 시퀀스 빌더
파이썬, 자바스크립트 등 언어에선 제한된 형태의 코루틴을 쓰고 있다.
- 비동기 함수(async, await)
- 제너레이터 함수(값을 순차적으로 리턴하는 함수)
코틀린에는 제너레이터 대신 시퀀스를 생성할 때 사용하는 시퀀스 빌더가 있다. 시퀀스는 List, Set 같은 컬렉션과 비슷한 개념이지만 필요할 때마다 값을 하나씩 계산하는 지연 처리를 한다. 시퀀스의 특징은 아래와 같다.
- 요구되는 연산을 최소한으로 수행
- 무한정이 될 수 있음
- 메모리 사용이 효율적 (이펙티브 코틀린 아이템 49 참고)
이런 특징 때문에 값을 순차 계산해서 필요할 때 리턴하는 빌더를 정의하는 게 좋다. 시퀀스는 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를 쓰는 게 낫다.