시퀀스는 List나 Set과 같은 컬렉션이랑 비슷한 개념이지만, 필요할 때마다 값을 하나씩 계산하는 지연(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)이 아닌 중단 함수를 사용하면 안 된다.
중단이 필요하다면 플로우를 사용하는 것이 낫다.
플로우 빌더의 작동 방식은 시퀀스 빌더와 비슷하지만, 플로우는 여러가지 코루틴 기능을 지원한다.