오늘도 더 나은 코드를 작성하였습니까?

람다로 프로그래밍 (지연계산(Lazy) 컬렉션 연산) 본문

Kotlin in Action/코틀린 기초

람다로 프로그래밍 (지연계산(Lazy) 컬렉션 연산)

hik14 2020. 8. 16. 20:27

컬렉션 함수를 연쇄하면서 매 단계마다 계산 중간결과를 새로운 컬렉션에 임시로 담는 건 너무 비효율적이다.

시퀀스를 사용하면 중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있다.

 

fun main() {
    val people = listOf( Person("Alice", 27), Person("Bob", 31), Person("kim", 19),
            Person("hong", 25), Person("park", 12))
    
    people.map { it.name }.filter { it.startsWith("A") }
    
    people.asSequence()
            .map { it.name }
            .filter { it.startsWith("A") }
            .toList()
}

원소가 적은 경우는 큰 차이가 없겠지만 원소가 많은 경수 성능 차이가 현저하게 난다.

 

Sequence

- 코틀린의 지연 계산 시퀀스는 Sequence 인터페이스에서 시작한다.

- 이 인터페이스는 단지 한 번에 하나씩 열거될 수 있는 원소의 시퀀스를 표현한다.

- Sequence에는 iterator라는 단 하나의 메서드가 있다. 이를 통해 원소 값을 얻을 수 있다.

 

Sequence 인터페이스 강점은 그 인터페이스 위에 구현된 연산이 계산을 수행하는 방법 때문이다.

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

 

asSequence 함수는 어떤 컬렉션이든 시퀀스로 변경 가능하며 시퀀스를 다시 다른 컬렉션으로 변경 가능하다.

 

Sequence 연산 실행: 중간 연산과 최종 연산

 

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

중간 연산은 최초 시퀀스의 원소를 변환하는 법을 안다 최종연산은 결과를 반환한다.

 

people.asSequence()

 

<중간 연산>  -  중간 연산은 최종 연산을 하기 전까지 항상 지연된다.

. { map.name }
. filter { it.startsWith("A") }

 

<최종연산> -  모든 연산이 실행된다.

. toList()

 

    people.asSequence()
            .map { println("$it") ; it.name }
            .filter {println("$it"); it.startsWith("A") }
    

    people.asSequence()
            .map { println("$it") ; it.name }
            .filter {println("$it"); it.startsWith("A") }
            .toList()

1번째는 최종 연산이 없기 때문에 어느 것도 출력되지 않는다.

 

연산의 수행 순서

 

fun main() {
    val people = listOf( Person("Alice", 27), Person("Bob", 31), Person("kim", 19),
            Person("hong", 25), Person("park", 12))

    people.map { println("$it"); it.name }.filter {println("$it"); it.startsWith("A") }

    
    people.asSequence()
            .map { println("$it") ; it.name }
            .filter {println("$it"); it.startsWith("A") }
            .toList()
}

일반 컬렉션 함수는 모든 원소를 처리하고 다음 그 중간 컬렉션을 가지고 다시 모든 원소에 대한 연산을 진행한다.

하지만 시퀀스의 경우 모든 연산은 각 원소별로 순차적으로 진행한다. 

 

즉 첫 번째 원소가 모든 연산을 다처리하고 두 번째 원소가 모든 연산을 처리하고... 이 순서다.

따라서 각 원소에 대한 연산을 처리하다 원하는 결과가 도출되면 이후 원소에 대해서는 변환이 이루어지지 않을 수도 있다.

    val list = listOf(1, 2, 3, 4 )

    println("Sequence 연산")
    list.asSequence()
            .map { println(it); it * it }
            .find{ println(it); it > 3}

    println("일반 연산")
    list .map { println(it); it * it }.find{ println(it);it > 3}

이미 조건에 만족하는 3 이상의 수를 찾았기 때문에 시퀀스는 뒤의 원소들에 대한 연산을 하지 않지만 

일반 연산은 일단 모든 원소를 맵 연산을 시킨 후 3 이상의 수를 찾고 연산을 멈춘다

컬렉션의 연산의 순서 또한 성능에 영향을 준다.

fun main() {
    val people = listOf(Person("Alice", 27), Person("Bob", 31), Person("Charles", 31),
            Person("Dan", 21))


    println("map -> filter:")
    println(
            people.asSequence()
                    .map { println("$it"); it.name }
                    .filter {println("$it"); it.length < 4}
                    .toList()
    )
    println("filter -> map:")
    println(
            people.asSequence()
                    .filter { println("${it.name}"); it.name.length < 4}
                    .map { println("$it"); it.name }
                    .toList()
    )
}

map을 먼저 연산하면 각 원소를 1번씩은 변환한다. 하지만 filter를 먼저 하면 부적절한 원소를 먼저 제외하기 때문에 이후 map연산에서 그 원소는 제외된다.

 

* 자바 스트림과 코틀린의 시퀀스

자바 8의 스트림과 코틀린의 시퀀스는 매우 비슷한 개념이지만 안드로이드에서 예전 버전 자바를 사용하는 경우 스트림을 사용할 수 없기 때문에 코틀린의 시퀀스를 사용하면 된다. 만약 자바 8을 사용한다면 필요에 따라 선택하면 된다.

 

시퀀스 만들기

generateSequence 함수를 사용한다.

이 함수는 이전의 원소를 인자로 받아 다음 원소를 계산한다

 

0을 넣고 그이후 1씩더해가면서 시퀀스를 생성하였다.

100이하일때 까지 진행한다.

그리고 합산한다.<최종연산>

 

fun main() {
    val naturalNumbers = generateSequence(0) { it+1 }
    val numberTo100 = naturalNumbers.takeWhile { it <= 100 }
    println(numberTo100.sum())
}   

 

File 클래스의 확장함수를 생성한다.

현재 파일 위치를 주고 다음 원소를 부모파일 위치로한다. 

숨긴 파일이 있는지 찾는다.

fun File.isInsideHiddenDirectory() = 
        generateSequence(this){it.parentFile}.any { it.isHidden }

fun main() {
    val file = File("filePath")
    println(file.isInsideHiddenDirectory())
}