6장 시퀀스

코틀린 쿡북 6장을 요약한 내용 입니다.

지연 시퀀스 사용하기

특정 조건을 만족시키는 데 필요한 최소량의 데이터만 처리하고 싶을 경우에 코틀린 시퀀스를 쇼트 서킷 함수와 함께 사용하라

코틀린은 기본 컬렉션에 확장 함수를 추가해 두었기 때문에 List에는 map과 filter 같은 함수가 있다. 이러한 함수는 즉시 처리되는데, 다시 말하면 컬렉션의 모든 원소를 처리해야 한다는 의미다.

만약 100부터 200까지의 숫자를 각각 2배로 만든 다음 3으로 나눠 딱 떨어지는 첫 번째 값을 찾고 싶다고 하자.

(100 until 200).map { it * 2 } // 100개 계산
	.filter { it % 3 == 0 } // 또 다른 100개 계산
	.first()

다행스럽게도 서술 조건자(predicate)를 받는 first 함수 중복이 있다.

(100 until 200).map { it * 2 } // 100개의 계산
    .first { it % 3 == 0 } // 3개의 연산만 수행

쇼트 서킷이란?

특정 조건에 다다를 때까지 오직 필요한 데이터만을 처리하는 방식을 쇼트 서킷이라 부른다.

(100 until 200).asSequence()    // 범위를 시퀀스로 변경
    .map { println("doubling $it"); it * 2 }
    .filter { println("filtering $it"); it % 3 == 0 }
    .first()

// result

doubling 100
filtering 200
doubling 101
filtering 202
doubling 102
filtering 204

시퀀스의 각 원소는 다음 원소로 진행하기 전에 완전한 전체 파이프라인에서 처리되기 때문에 오직 6개의 연산만이 수행된다.

시퀀스 API는 컬렉션에 들어 있는 함수와 똑같은 함수를 가지고 있지만 시퀀스에 대한 연산은 중간 연산과 최종 연산이라는 범주로 나뉜다.

시퀀스 생성하기

값으로 이뤄진 시퀀스를 생성하고 싶을 경우 이미 원소가 있다면 sequenceOf를 사용하고 Iterable이 있다면 asSequence를 사용하라

val numSequence1 = sequenceOf(3, 1, 4, 1, 5, 9)
val numSequence2 = listOf(3, 1, 4, 1, 5, 9).asSequence()

sequence는 언제 사용하면 좋을까?

어떤 수가 소수인지 여부를 결정하기 위해 해당 수를 2부터 해당 수의 제곱근까지 나누어 떨어지는 수를 찾는 Int의 확장 함수를 살펴보자

import kotlin.math.ceil
import kotlin.math.sqrt

fun Int.isPrime() =
	this == 2 || (2..ceil(sqrt(this.toDouble())).toInt())
	.none { divisor -> this % divisor == 0 }

none 함수는 주어진 수를 이 범위의 각각의 수로 나누어 정확히 떨어지는 수를 이 범위에서 찾을 수 없다면 true를 리턴한다.

이제 어떤 정수가 있고 해당 정수의 다음에 나오는 소수를 알고 싶다고 하자. 6 다음에 소수는 7이다. 주어진 수가 182라면 그 다음 소수는 191이다. 주어진 수가 9,973이면 그다음 소수는 10,007이다. 여기서 문제는 다음 소수를 찾기 위해서 얼마나 많은 수를 확인해야 하는지 알 수 없다는 것이다. 이럴 경우 시퀀스를 사용하기 좋은 예이다.

// 주어진 정수 다음에 나오는 소수 찾
fun nextPrime(num: Int) =
    generateSequence(num + 1) { it + 1 } // 주어진 수보다 1 큰 수에서 시작하고 1증가 반복
        .first(Int::isPrime)

nextPrime 함수는 시퀀스 안에서 무한대의 정수를 생성하고 첫 번째 소수를 찾을때까지 생성한 정수를 하나씩 평가한다. 이때 first 함수는 시퀀스 대신 하나의 값을 리턴하기 때문에 이는 최종 연산이다. 최종 연산 없이는 시퀀스는 아무 값도 처리하지 않는다.

무한 시퀀스 다루기

무한대의 원소를 갖는 시퀀스의 일부분이 필요할 경우 널을 리턴하는 시퀀스 생성기를 사용하거나 시쿠너스 확장 함수 중에서 takeWhile 같은 함수를 사용하자

이전의 예에서 다수의 소수를 찾기 위해 얼마나 많은 숫자를 검사해야 하는지 미리 알 수 있는 방법을 구현해보자

// 처음 N개의 소수 찾기
fun firstPrimes(count: Int) =
    generateSequence(2, ::nextPrime) // 2부터 시작하는 소수의 무한 시퀀스
    .take(count)    // 요청한 수만큼만 원소를 가져오는 중간 연
    .toList()    // 최종 연산

take 함수는 상태가 없는 중간 연산이며 처음 count 개수의 값만으로 구성된 시퀀스를 리턴한다.

무한대의 원소를 갖는 시퀀스를 잘라내는 다른 방법은 마지막에 널을 리턴하는 생성 함수를 사용하는 것이다.

// 주어진 수보다 작은 모든 소수
fun primeLessThan(max: Int): List<Int> =
    generateSequence(2) { n -> if (n < max) nextPrime(n) else null }
        .toList()
        .dropLast(1)

primeLessThan 함수는 여기서 generateSequence를 호출해 현재 값이 제공된 한계 값보다 작은값인지 여부를 확인한다. 현재 값이 한계 값보다 작으면 다음 소수를 계산한다. 현재 값이 한계값보다 크면 null을 리턴하고 리턴된 null은 시퀀스를 종료한다.

다음 소수가 한계 값보다 큰 값인지 여부를 미리 알수 없기 때문에 이 함수는 실제로 한계 값을 넘어가는 소수를 하나 포함하는 리스트를 생성한다. 그런 다음에 dropList 함수는 생성된 결과 리스트에서 한계 값을 넘는 소수를 리턴하기 전에 잘라낸다.

하지만 이는 takeWile을 사용하면 더 간단하게 작성할 수 있다.

fun primeLessThan(max: Int): List<Int> =
    generateSequence(2, ::nextPrime)
        .takeWhile { it < max }
        .toList()

takeWhile 함수는 시퀀스에서 제공된 술어가 true를 리턴하는 동안 시퀀스에서 값을 추출한다.

시퀀스에서 yield하기

구간을 지정해 시퀀스에서 값을 생성하고 싶을 경우 yield 중단(suspend) 함수와 함께 sequence 함수를 사용하라

컬렉션을 시퀀스로 변환하려면 asSequence를 사용하거나 generateSequence에 함수를 제공해 값을 생성한다. 하지만 sequence 함수의 경우에는 필요한 때 yield해 값을 생성하는 람다를 제공해야 한다.

피보나치 수를 생성하는 작업을 구현해보자. 이 예제는 코틀린 라이브러리 공식 문서의 예제를 바탕으로 한다.

// sequence를 사용해 피보나치 수 생성하기
fun fibonacciSequence() = sequence {
    var terms = Pair(0, 1)
    
    while (true) {
        yield(terms.first)
        terms = terms.second to terms.first + terms.second
    }
}

위 함수는 새로운 원소가 생성될 때마다 yield 함수는 결과 Pair의 첫 번째 원소를 리턴한다.

yield 함수는 sequence 연산에 제공된 람다를 받는 SequenceScope의 일부로서 SequenceScope를 가진 비슷한 두 함수 중 하나다.

yield가 중단 함수라는 사실은 yield가 코루틴과도 잘 동작한다는 의미이다.

예상하다시피 yeildAll은 다수의 값을 이터레이터에 넘겨준다.

val sequence = sequence {
    val start = 0
    yield(start)    // 단일 값(0)을 yield
    yieldAll(1..5 step 2)    // 범위를 통해 순회 가능 (1, 3, 5)를 yield
    yieldAll(generateSequence(8) { it * 3 })    // 8부터 시작해 3을 곱한 값을 원소로 갖는 무한 시퀀스를 yield
}

이 코드는 결과적으로 0, 1, 3, 5, 8, 24, 72 ... 을 원소로 갖는 시퀀스다. 중단 함수 안의 yield와 yieldAll의 조합을 사용하면 시퀀스에 생성된 값을 원하는 조합으로 쉽게 바꿀 수 있다.

Last updated