앞 절에서 살펴본 map이나 filter와 같은 컬렉션 함수는 결과 컬렉션을 즉시(eagerly) 생성한다. 이는 컬렉션 함수를 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다는 말이다.
시퀸스(sequence)를 사용하면 중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있다.
people.map(Person::name).filter { it.startsWith("A") }
위 코드는 더 효율적으로 만들기 위해서는 각 연산이 컬렉션을 직접 사용하는 대신 시퀸스를 사용하게 만들어야 한다.
people.asSequence()
.map(Person::name)
.filter { it.startsWith("A")}
.toList()
시퀸스를 사용하면 중간 결과를 저장하는 컬렉션이 생기지 않기 때문에 원소가 많은 경우 성능이 눈에 띄게 좋아진다.
코틀린 지연 계산 시퀸스는 Sequence 인터페이스에서 시작한다. 이 인터페이스는 단지 한 번에 하나씩 열거될 수 있는 원소의 시퀸스를 표현할 뿐이다. Sequence 안에는 iterator라는 단 하나의 메소드가 있다. 그 메소드를 통해 시퀸스로부터 원소 값을 얻을 수 있다.
Sequence 인터페이스의 강점은 그 인터페이스 위에 구현된 연산이 계산을 수행하는 방법 때문에 생긴다. 시퀸스의 원소는 필요할 때 비로소 계산된다.
asSequnece 확장 함수를 호출하면 어떤 컬렉션이든 시퀸스로 바꿀 수 있다. 시퀸스를 리스트로 만들 때는 toList를 사용한다.
왜 시퀸스를 다시 컬렉션으로 되돌려야 할까? 시퀸스의 원소를 차례로 이터레이션해야 한다면 시퀸스를 직접 써도 된다. 하지만 시퀸스 원소를 인덱스를 사용해 접근하는 등의 다른 API 메소드가 필요하다면 시퀸스를 리스트로 변환해야 한다.
8.2절에서는 중간 컬렉션을 생성함에도 불구하고 코틀린에서 즉시 계산 컬렉션에 대한 연산이 더 효율적인 이유를 설명한다. 하지만 컬렉션에 들어가는 원소가 많으면 중간 원소를 재배열하는 비용이 커지기 때문에 지연 계산이 더 낫다.
시퀸스에 대한 연산은 중간(intermediate) 연산과 최종(terminal) 연산으로 나뉜다. 중간 연산은 다른 시퀸스를 반환한다. 그 시퀸스는 최초 시퀸스의 원소를 변환하는 방법을 안다. 최종 연산은 결과를 반환한다.
중간 연산은 항상 지연 계산된다. 최종 연산이 없는 예제를 살펴보자.
listOf(1,2,3,4,).asSequence()
.map { print("map ($it) "); it*it }
.filter { print("filter($it) "); it % 2 == 0 }
이 코드를 실행하면 아무 내용도 출력되지 않는다. 이는 map과 filter 변환이 늦춰져서 결과를 얻을 필요가 있을 때 적용된다는 뜻이다.
fun main() {
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를 수행할 것이다. 컬렉션에 대한 map과 filter는 그런 방식으로 작동한다.