filter, map 등의 몇 가지 컬렉션 함수는 결과 컬렉션을 즉시 생성한다. 이는 컬렉션 함수를 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다는 의미이다.
시퀀스 sequence 를 사용하면 중간 임시 컬렉션을 사용하지 않고 컬렌션 연산을 연쇄할 수 있다. 중간 결과를 저장하는 컬렉션이 생기지 않기 때문에 원소가 많을 경우 연산에 대한 성능이 개선된다.
시퀀스 Sequence
코틀린의 지연 계산 시퀀스는 Sequence 인터페이스에서 시작한다. 이 인터페이스는 단지 한 번에 하나씩 열거될 수 있는 원소의 시퀀스를 표현할 뿐이며, Sequence 안의 iterator 라는 단 하나의 메서드를 통해 시퀀스로부터 원소 값을 얻을 수 있다.
- asSequence 확장 함수를 호출하면 어떤 컬렉션이든 시퀀스로 변환 가능
- 시퀀스를 리스트로 만들 때는 toList 함수를 사용
- 큰 컬렉션에 대해서 연산을 연쇄시킬 때는 시퀀스를 사용
- 중간 원소를 재배열하는 비용이 커지기 때문에 시퀀스를 통한 지연 계산이 효과적
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31), Person("Minux", 29))
// map, filter 의 연쇄 호출이 리스트를 2개 생성
people.map(Person::name).filter { it.startsWith("A") }
people.asSequence() // 원본 컬렉션을 시퀀스로 변환
.map(Person::name) // 시퀀스도 컬렉션과 똑같은 API를 제공
.filter { it.startsWith("A") }
.toList() // 결과 시퀀스를 다시 리스트로 변환
중간 연산과 최종 연산
시퀀스에 대한 연산은 중간 intermediate 연산과 최종 terminal 연산으로 나뉜다.
- 중간 연산 : 다른 시퀀스를 반환하며, 이 시퀀스는 최초 시퀀스의 원소를 변환하는 방법을 인지하고 있다.
- 최종 연산 : 최초 컬렉션에서 일련의 계산을 수행해 얻은 컬렉션, 원소, 숫자 또는 객체인 결과를 반환한다.
중간 연산만으로는 결과를 얻을 수 없으며, 최종 연산이 수행되어야 결과를 얻을 수 있다.
// sequence. map ~ 중간연산 ~ filter. 최종연산 toLsit()
sequence.map { ... }.filter { ... }.toList()
- 시퀀스는 최종 연산을 하기 전까지 각 원소는 계산되지 않는다.
- 이 예제에서 toList() 가 최종 연산이며, 이 때 시퀀스의 연산이 계산되어 반환된다.
listOf(1, 2, 3, 4).asSequence()
.map { print("map($it) "); it * it }
.filter { print("filter($it) "); it % 2 == 0 }
출력결과 없음
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)
- toList 등의 최종 연산이 없다면 결과를 얻지 못한다.
- map, filter 의 변환이 늦춰져서 결과를 얻을 필요가 있을 때 (최종 연산이 호출될 때) 적용되므로 지연 계산이라 한다.
- 직접 연산을 구현한다면 map 함수를 각 원소에 대해 먼저 수행해서 중간 임시 컬렉션을 얻고, 그 컬렉션에 대해 다시 filter 를 수행하게 된다.
- 시퀀스의 경우 모든 연산은 각 원소에 대해 순차적으로 적용된다.
EX. 원소에 연산을 차례대로 적용하다가 결과가 얻어지면 그 이후의 원소에 대해서 변환이 이뤄지지 않는 경우
listOf(1, 2, 3, 4).asSequence()
.map { it * it }
.find { it > 3}
4
- 시퀀스를 사용한다면 1, 2 원소에 대해 map, find 연산이 차례로 진행되며, 2 원소가 4 로 변환되고 find 술어 함수에 의해 찾아졌을 때, 나머지 3, 4 원소의 연산은 이뤄지지 않는다.
- 시퀀스가 아닌 컬렉션으로 즉시 계산을 수행한다면 1, 4, 9, 16 의 중간 결과 컬렉션이 생성되고, 그 다음에 find 함수가 실행된다.
EX. 연산의 순서가 성능에 영향을 끼치는 경우
data class Person(val name: String, val age: Int)
val people = listOf(
Person("Alice", 29),
Person("Bob", 31),
Person("Dan", 29),
Person("Charles", 43),
)
// map 을 먼저 하는 경우
people.asSequence()
.map(Person::name)
.filter { it.length < 4 }
.toList()
[Bob, Dan]
// filter 를 먼저 하는 경우
people.asSequence()
.filter { it.name.length < 4 }
.map(Person::name)
.toList()
[Bob, Dan]
- 두 연산 모두 결과는 동일하나, map 을 먼저 할 경우
- [”Alice”, “Bob”, “Dan”, “Charles”] 중간 임시 컬렉션이 생성되고 filter 가 수행
- 4번 + 4번 = 8번
- filter 를 먼저 할 경우
- [Person(“Bob”, 31), Person(”Dan”, 29)] 중간 임시 컬렉션이 생성되고 map 이 수행
- 4번 + 2번 = 6번
시퀀스 만들기
generateSequence 함수로 시퀀스를 만들 수 있다.
EX. 자연수의 시퀀스를 생성하고 사용하기
val naturalNumbers = generateSequence(0) { it + 1 } // 'it' is the previous element
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
println(numbersTo100.sum()) // sum() 이 최종연산
5050
// val generateSequence(0) { it + 1 }
// naturalNumbers.toList() // error: the sequence is infinite
- generateSequence() 의 첫 번째 인자는 초기값 seed 이며, 뒤의 람다 함수(중간 연산)가 다음 원소를 만드는 식이 된다.
- takeWhile 은 술어 함수를 통해 생성할 원소 범위를 설정할 수 있다.
- generateSequence 를 통해 시퀀스를 생성할 때 null 반환, takeWhile, take 등을 설정하지 않고 최종 연산을 수행하면 무한대의 시퀀스를 가지므로 런타임 에러가 발생한다.
generateSequence(0) { if( it < 5 ) it + 1 else null }.toList()
[0, 1, 2, 3, 4, 5]
- 중간 연산에서 null 을 반환하면 시퀀스 생성이 종료된다.
- EX. generateSequence(0) { if ( it < 10 ) it + 1 else null }
naturalNumbers.take(5).toList()
[0, 1, 2, 3, 4]
- take 는 시퀀스의 원소 개수를 특정해 생성시킨다.
728x90
'Kotlin' 카테고리의 다른 글
Coroutine (0) | 2024.02.13 |
---|---|
변수와 상수 (0) | 2023.11.02 |
수신 객체 지정 람다, with 와 apply (0) | 2023.09.08 |
자바 함수형 인터페이스 활용 (0) | 2023.09.07 |
컬렉션 함수형 API (0) | 2023.09.05 |