[Kotlin in Action] 24. 지연 계산(lazy) 컬렉션 연산 - 시퀀스(sequence) 사용

반응형
728x90
반응형

컬렉션의 map, filter 함수

filter, map은 리스트를 반환한다. 이 함수들은 결과 컬렉션을 즉시(eagerly) 생성한다. 이는 컬렉션 함수로 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다.

package chapter5_람다로_프로그래밍._3_지연계산_컬렉션연산

data class Person(val name: String, val age: Int)

fun main() {
    val people = listOf(
        Person("Alice", 29),
        Person("Bob", 31)
    )

    val filter = people.map(Person::name).filter{ it.startsWith("김") }
}

이는 이 연쇄 호출이 리스트를 2개 만든다는 뜻이다. 한 리스트는 filter 결과를 담고, 다른 하나는 map 결과를 담는다. 원소가 수백만개가 되면 코드의 효율이 매우 떨어진다. 

 

 

시퀀스 사용

asSequence 확장 함수를 호출하면 어떤 컬렉션이든 시퀀스로 바꿀수있다. 시퀀스를 리스트로 만들때는 toList를 사용한다.

package chapter5_람다로_프로그래밍._3_지연계산_컬렉션연산

data class Person(val name: String, val age: Int)

fun main() {
    val people = listOf(
        Person("Alice", 29),
        Person("Bob", 31)
    )

    /** 성능이 더 좋다. */
    val toList = people.asSequence() // 원본 컬렉션을 시퀀스로 변환한다.
        .map(Person::name)
        .filter { it.startsWith("김") }
        .toList() //결과 시퀀스를 다시 리스트로 변환한다.
}

코틀린의 지연 계산 시퀀스는 Sequence 인터페이스에서 시작한다. 이 인터페이스는 단지 한번에 하나씩 열거될 수 있는 원소의 시퀀스를 표현할 뿐이다.

Sequence 안에는 iterator라는 단 하나의 메서드가 있다. 그 메서드를 통해 시퀀스로부터 원소 값을 얻을 수 있다.

시퀀스의 원소는 필요할때 계산된다. 따라서 중간 처리 결과를 저장하지 않고도 연산을 연쇄적으로 적용해서 효율적으로 계산을 수행할 수 있다.

* 왜 시퀀스를 다시 컬렉션으로 되돌려야할까?
컬렉션보다 시퀀스가 훨씬 더 낫다면, 그냥 시퀀스를 쓰는게 더 낫지 않을까? 라는 생각을 할 수도 있다. 답은 "항상 그렇지 않다"이다.시퀀스의 원소를 차례로 이터레이션해야 한다면 시퀀스를 직접 써도 되지만, 시퀀스 원소를 인덱스를 사용해 접근하는 등의 다른 API 메서드가 필요하다면 시퀀스를 리스트로 변환해야한다.

시퀀스에 대한 연산을 지연 계산하기 때문에 정말 계산을 실행하게 만들려면 최종 시퀀스의 원소를 하나씩 이터레이션하거나 최종 시퀀스를 리스트로 변환해야한다.

 

 

시퀀스의 연산 실행: 중간 연산과 최종 연산

시퀀스에 대한 연산은 중간연산, 최종연산으로 나뉜다.

중간 연산 다른 시퀀스를 반환한다.
최종 연산 최종 연산은 결과를 반환한다.

 

  • 중간 연산 : map, filter
  • 최종 연산 : toList
package chapter5_람다로_프로그래밍._3_지연계산_컬렉션연산

fun main() {
    val people = listOf(
        Person("Alice", 29),
        Person("Bob", 31)
    )

    val toList = people.asSequence() // 원본 컬렉션을 시퀀스로 변환한다.
        .map(Person::name)
        .filter { it.startsWith("김") }
        .toList() //결과 시퀀스를 다시 리스트로 변환한다.
}

 

최종 연산이 없는 경우

아무 내용도 출력되지 않는다.
map, filter 변환이 늦춰져서 결과를 얻을 필요가 있을때 (즉, 최종 연산이 호출될때) 적용된다는 뜻이다.

/* 최종 연산이 없는 경우 */
listOf(1, 2, 3, 4)
    .asSequence()
    .map { print("map : $it"); it * it }
    .filter { print("filter : $it"); it > 5}

 

최종 연산이 호출되는 경우

val list = listOf(1, 2, 3, 4)
    .asSequence()
    .map { print("map : $it"); it * it }
    .filter { print("filter : $it"); it > 5}
    .toList() // 출력된다.

 

결과

map, filter 가 각 원소마다 반복되어 호출된다.

map : 1 filter : 1 
map : 2 filter : 4 
map : 3 filter : 9 
map : 4 filter : 16

 

 

연산 순서

▶ 컬렉션에 대한 map, filter 연산 순서
1) map 함수를 각 원소에 대해 먼저 수행해서 새 시퀀스를 얻는다.
2) 그 시퀀스에 대해 다시 filter을 수행한다.

 시퀀스에 대한 map, filter 연산 순서
1) 모든 연산은 각 원소에 대해 순차적으로 적용된다.
- 즉, 첫번째 원소가 변환된 다음 걸러지는 처리가 되고, 다시 두번째 원소가 처리되어 모든 원소에 적용된다.

 

따라서 연산을 차례대로 적용하다가 결과가 얻어지면 그 이후의 원소에 대해서는 변환이 이뤄지지 않을 수도 있다.

 

 

 

예제로 이해하기

1) map으로 리스트의 각 숫자를 제곱하고 제곱한 숫자 중에서 find로 3보다 큰 첫번재 원소를 찾자.

println(listOf(1, 2, 3, 4).asSequence().map { it * it }.find { it > 3 }) // 4

같은 연산을 시퀀스가 아니라 컬렉션에 수행하면 map의 결과가 먼저 평가돼 최초 컬렉션의 모든 원소가 변환된다. 두번째 단계에서는 map을 적용해서 얻는 중간 컬렉션으로부터 술어를 만족하는 원소를 찾는다.
시퀀스를 사용하면 지연계산으로 인해 원소 중 일부의 계산은 이뤄지지 않는다.

컬렉션을 사용하면 리스트가 다른 리스트로 변환된다. 그래서 map 연산은 3과 4를 포함하여 모든 원소를 변환한다. 그 후 find가 술어를 만족하는 첫번째 원소인 4를 찾는다.

시퀀스를 사용하면 find 호출이 원소를 하나씩 처리하기 시작한다. 최초 시퀀스로부터 수를 하나 가져와서 map에 지정된 변환을 수행한 다음에 find에 지정된 술어로 만족하는지 검사한다.
이미 '2'일때 답을 찾았으므로 그 이후인 '3', '4'는 처리하지 않는다.

 

 

 

시퀀스 만들기

asSequence() 를 호출하여 시퀀스를 만들었다.
시퀀스를 만드는 다른 방법으로 generateSequence 함수를 사용할 수 있다. 이 함수는 이전의 원소를 인자로 받아 다음 원소를 계산한다.

 

1)  0부터 100까지 자연수의 합 구하기

package chapter5_람다로_프로그래밍._3_지연계산_컬렉션연산

import java.io.File

fun main() {
    /*
        numbers, numbersTo100 모두 시퀀스며 연산을 지연 계산한다.
        최종 연산을 수행하기 전까지는 시퀀스의 각 숫자는 계산되지 않는다.
     */
    val numbers = generateSequence(0) { it + 1 }
    val numbersTo100 = numbers.takeWhile { it <= 100 }
    println(numbersTo100.sum()) // 모든 연산은 "sum()"이 호출될 때 수행된다. (최종 연산)
}

 

2) 어떤 파일의 상위 디렉터리를 탐색하면서 숨김(Hidden) 속성을 가진 디렉토리가 있는지 검사함으로써 해당 파일이 숨김 디렉토리 안에 들어있는지 찾아내기

package chapter5_람다로_프로그래밍._3_지연계산_컬렉션연산

import java.io.File

fun main() {
    val file = File("/Users/.HiddenDir/a.txt")
    println(file.isInsideHiddenDirectory())
}

// 조건을 만족하는 원소를 찾은 뒤에는 더이상 찾지 않는다.
fun File.isInsideHiddenDirectory() = generateSequence(this) { it.parentFile }.any { it.isHidden } // 최종 연산

 

 

 

반응형

Designed by JB FACTORY