본문 바로가기
2023년 이전/kotlin

kotlin - 시퀀스(Sequence)

by JeongUPark 2020. 3. 2.
반응형

[출처 -  Kotlin In Action] [아래 내용들은 Kotlin In Action을 공부하면서 스스로 정리한 내용입니다] 


시퀀스(Sequence)

다음 코드를 봅시다

people.filter{ it.age== people.maxBy(Person::age)!!.age}.map(Person::name)

이 코드는 filter와 map을 연쇄 호출하고 있습니다. 이 연쇄 호출시 filter와 map이 각각이 리스트를 만들게 되며, 이는 리스틀르 2개를 만든다는 뜻이 됩니다. 즉, 하나는 filter의 결과를 담고, 하나는 map의 결과를 담게 됩니다. 원소가 몇개 없을 때는 문제가 안되지만, 원소가 수백개, 더 많아질수록 효율이 떨어지게 될 것입니다.

이를 효율적으로 사용하기 위해서 시퀀스를 사용하여 만들어야 합니다.(시퀀스를 사용하게 되면 중간에 임시 컬랙션을 사용하지 않고 컬렉션 연산을 연쇄할 수 있습니다.)

data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
    val people = listOf(
        Person("Alice", 31),
        Person("Bob", 29), Person("Carol", 31)
    )
    val k =   people.asSequence()
        .filter{ it.age== people.maxBy(Person::age)!!.age}
        .map(Person::name)
    k.iterator().forEach {
        println(it)
    }
}

이렇게 할 경우 결과는

Alice
Carol

이렇게 나옵니다. 만일 println(k)를 할 경우에는 k는 List가 아니고 Sequence 이기 때문에 kotlin.sequences.TransformingSequence@4459eb14 이런 식의 결과를 확인하게 될 것입니다.

 

시퀀스를 사용했기 때문에 중간 결과를 저장하는 컬렉션이 없기 때문에 원소가 많을 수록 성능이 더 좋은것을 확인 할 수 있을 것입니다. 그리고 시퀀스 연산은 시퀀스안에는 단 하나의 iterator라는 메소드가 있는데, 이 메소드를 통해 시퀀스로부터 원소 값을 얻을 수 있습니다. (위의 코드에서도 확인 할 수 있습니다.)

 

즉, 시퀀스 인터페이스의 장점은 시퀀스의 원소를 필요할때 사용할 수 있고, 중간 처리 컬렉션이 없이 연산을 연쇄적으로 적용해서 효율적으로 계산을 수행 합니다.

 

그리고 이런 시퀀스를 리스트로 다시 만들때는 .toList를 사용하면 됩니다.

data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {

    val people = listOf(
        Person("Alice", 31),
        Person("Bob", 29), Person("Carol", 31)
    )
    val k =   people.asSequence()
        .filter{ it.age== people.maxBy(Person::age)!!.age}
        .map(Person::name)
   println(k.toList())
}
//결과
[Alice, Carol]

시퀀스 연산 실행

시퀀스 연산은 중간 연산과 최종 연산으로 나뉩니다.

asSequence().map { .... }.filter {....}.toList()
            |------ 중간 연산 ---------|-최종연산-|

중간 연산은 항상 지연계산됩니다. 최종 연산이 없는 예제를 보면

fun main(args: Array<String>) {
    listOf(1, 2, 3, 4).asSequence()
            .map { print("map($it) "); it * it }
            .filter { print("filter($it) "); it % 2 == 0 }
}

결과가 출력되지 않습니다. 그 이유는 map과 filter의 변환이 늦춰져서 최종연산이 호출될 때 적용되기 때문입니다.

그래서 최종연산을 추가하면

fun main(args: Array<String>) {
    listOf(1, 2, 3, 4).asSequence()
            .map { print("map($it) "); it * it }
            .filter { print("filter($it) "); it % 2 == 0 }
            .toList()
}
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16) 

결과를 확인 할 수 있습니다.

그리고 여기 결과에서 확인 할 수 있는 내용은 시퀀스가 아니고 일반 컬렉션일 경우에는 map연산 후 임시 컬렉션을 만들고 그에 대한 filter를 적용하여 결과를 반환 했을 것입니다. 하지만 시퀀스는 원소에 대해 순차적으로 연산되는 것을 확인 할 수 있습니다.

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

fun main(args: Array<String>) {
    val k = listOf(1, 2, 3, 4).asSequence()
            .map { print("map($it) "); it * it }
            .find {print("find($it)"); it>3}
    print("|  final result: $k")
}
map(1) find(1)map(2) find(4)|  final result: 4

이렇게 원소 2까지만 연산이 되고 결과를 반환하고 끝이 나게 됩니다.

만일 위의 코드를 Sequence가 아닌 일반 컬렉션으로 연산했다면

fun main(args: Array<String>) {
    val k = listOf(1, 2, 3, 4)
            .map { print("map($it) "); it * it }
            .find {print("find($it)"); it>3}
    print("|  final result: $k")
}
map(1) map(2) map(3) map(4) find(1)find(4)|  final result: 4

map은 모든 원소가 동작하고, find는 조건이 만족하는 2번째 원소에서 멈추게 될 것입니다.

그리고 filter를 먼저쓰느냐 map을 먼저 쓰느냐에 따라 그 연산 횟수가 다른 경우가 발생하기도 합니다.

map을 먼저할 경우

data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31), Person("Charles",31),Person("Dan",21))
    println(people.map{println("map(${it.name})");it.name}.toList().filter{println("filter($it)"); it.length<4})
} 
map(Alice)
map(Bob)
map(Charles)
map(Dan)
filter(Alice)
filter(Bob)
filter(Charles)
filter(Dan)
[Bob, Dan]

filter를 먼저할 경우

data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31), Person("Charles",31),Person("Dan",21))
    println(people.filter{println("filter(${it.name})"); it.name.length<4}.map{println("map(${it.name}");it.name}.toList())
}
filter(Alice)
filter(Bob)
filter(Charles)
filter(Dan)
map(Bob)
map(Dan)
[Bob, Dan]

이렇게 더 적은 연산을 확인 할 수 있습니다. 

중요한 것은 어떻게 코드를 작성했을 때 더 적은 연산으로 좋은 결과를 내는지 고민하여 코딩을 해야할 것 같습니다.

 

시퀀스 만들기

지금 까지는 asSequence를 사요해서 시퀀스를 만들었습니다. 시퀀스를 만드는 다른 방법으로는 generateSequence을 사용하여 시퀀스를 만들 수 있습니다. generateSequence은 이전 원소를 인자로 받아 다음 원소를 계산합니다.

다음 코드는 0~100 까지 자연수의 합을 구하는 코드입니다.

fun main(args: Array<String>) {
    val naturalNumbers = generateSequence(0) { it + 1 }
    val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
    println(numbersTo100.sum())
}

결과는 당연히 5050입니다.

위 코드에서 naturalNumbers와 numbersTo100은 둘다 시퀀스이며 연산을 지연 계산합니다. 그래서 최종 연산을 할때까지 시퀀스의 각 숫자는 연산되지 않습니다.

 

반응형

'2023년 이전 > kotlin' 카테고리의 다른 글

kotlin - 수신 객체 지정 람다  (0) 2020.03.10
kotlin - 자바 함수형 인터페이스 활용  (2) 2020.03.10
kotlin - 컬렉션 함수형 API  (1) 2020.03.02
kotlin - 람다식  (0) 2020.02.26
kotlin - object 키워드  (0) 2020.02.25