-
5장. 람다로 프로그래밍 - (3) 지연 계산(lazy) 컬렉션 연산Kotlin 2022. 3. 11. 12:07
컬렉션 함수는 결과 컬렉션을 즉시(eagerly) 생성한다.
이는 컬렉션 함수를 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다는 말이다.
시퀀스(sequence)를 사용하면 중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있다.
val people = listOf(Person("Alice", 27), Person("Bob", 31)) println(people.map(Person::name).filter{it.startsWith("A")})//[Alice]
- filter와 map이 리스트를 반환한다. 이는 이 연쇄 호출이 리스트를 2개 만든다는 뜻이다.
- 한 리스트 filter의 결과를 담고, 다른 하나는 map의 결과를 담는다.
각 연산이 컬렉션을 직접 사용하는 대신 시퀀스를 사용하면 더 효율적이다.
// -- 원본 컬렉션을 시퀀스로 변환한다. people.asSequence() // -- 시퀀스도 컬렉션과 똑같은 API를 제공한다. .map(Person::name) .filter{it.startsWith("A")} // -- 결과 시퀀스를 다시 리스트로 변환한다. .toList()
Sequence 인터페이스
- 코틀린 지연 계산 시퀀스는 Sequence 인터페이스에서 시작한다.
- 이 인터페이스는 단지 한 번에 하나씩 열거될 수 있는 원소의 시퀀스를 표현할 뿐이다.
- Sequence 안에는 iterator라는 단 하나의 메소드가 있다. 그 메소드를 통해 시퀀스로부터 원소 값을 얻을 수 있다.
- 시퀀스의 원소는 필요할 때 비로소 계산된다. 따라서 중간 처리 결과를 저장하지 않고도 연산을 연쇄적으로 적용해서 효율적으로 계산을 수행할 수 있다.
- asSequence 확장 함수를 호출하면 어떤 컬렉션이든 시퀀스로 바꿀 수 있다.
- 시퀀스를 리스트로 만들 때는 toList를 사용한다.
- 큰 컬렉션에 대해서 연산을 연쇄시킬 때는 시퀀스를 사용하는 것을 규칙으로 삼아라.
- 시퀀스에 대한 연산을 지연 계산하기 때문에 정말 계산을 실행하게 만들려면 최종 시퀀스의 원소를 하나씩 이터레이션하거나 최종 시퀀스를 리스트로 변환해야 한다.
시퀀스 연산 실행 : 중간 연산과 최종 연산
시퀀스에 대한 중간 연산과 최종 연산 중간 연산
다른 시퀀스를 반환한다. 그 시퀀스는 최초 시퀀스의 원소를 변환하는 방법을 안다.
- 중간 연산은 항상 지연 계산된다.
- 아래 코드를 실행해도 아무 내용도 출력되지 않는다.
- 즉, 결과를 얻을 필요가 있을 때(즉 최종 연산이 호출될 때) 적용된다는 뜻이다.
- 최종 연산이 호출하면 연기됐던 모든 계산이 수행된다.
- 연산 수행 순서
- 직접 연산 : map 함수를 각 원소에 대해 먼저 수행해서 새 시퀀스를 얻고, 그 시퀀스에 대해 다시 filter를 수행할 것이다.
- 시퀀스 : 시퀀스의 경우 모든 연산은 각 원소에 대해 순차적으로 적용된다. 즉, 첫 번째 원소가 (변환된 다음에 걸러지면서) 처리되고, 다시 두 번째 원소가 처리되며, 이런 처리가 모든 원소에 대해 적용된다.
- 원소에 연산을 차례대로적용하다가 결과가 얻어지면 그 이후의 원소에 대해서는 변환이 이뤄지지 않을 수도 있다.
listOf(1,2,3).asSequence() .map{ print("map($it"); it*it } .filter { print("filter($it"); it % 2 == 0 } //map(1)filter(1) map(2)filter(4) map(3)filter(9) map(4)filter(16)
최종 연산
결과를 반환한다. 결과는 최초 컬렉션에 대해 변환을 적용한 시퀀스로부터 일련의 계산을 수행해 얻을 수 있는 컬렉션이나 원소, 숫자 또는 객체다.
예시
println(listOf(1,2,3,4).asSequence() .map{ it*it } .find {it > 3}) //4
즉시 계산은 전체 컬렉션에 연산을 적용하지만 지연 계산은 원소를 한 번에 하나씩 처리한다.
val people2 = listOf(Person("Alice", 29), Person("Bob", 31), Person("Charles", 31), Person("Dan", 21)) println(people2.asSequence().map(Person::name) .filter{ it.length < 4 } .toList())//[Bob, Dan] println(people2.asSequence().filter{ it.name.length < 4 } .map(Person::name) .toList())//[Bob, Dan]
결과는 같지만 filter를 먼저 적용하면 전체 변환 횟수가 줄어든다.
map을 먼저하면 모든 원소를 변환한다. 하지만 filter를 먼저 하면 부적절한 원소를 먼저 제외하기 때문에 그런 원소는 변환되지 않는다.
시퀀스 만들기
1. asSequence()를 호출해 시퀀스 생성
2. generateSequence 함수 시퀀스 생성 - 이전의 원소를 인자로 받아 다음 원소를 계산한다.
val naturalNumbers = generateSequence(0){ it + 1 } val numbersTo100 = naturalNumbers.takeWhile{it<=100} println(numbersTo100.sum())//모든 지연 연산은 최종 연산인 "sum"이 결과를 계산할 때 수행된다. //5050
시퀀스를 사용하는 일반적인 용례 중 하나는 객체의 조상으로 이뤄진 시퀀스를 만들어내는 것이다.
fun File.isInsideHidedenDirectory() = generateSequence(this) {it.parentFile}.any {it.isHidden } val file = File("/Users/svtk/.HiddenDir/a.txt") println(file.isInsideHidedenDirectory())//true
'Kotlin' 카테고리의 다른 글
5장. 람다로 프로그래밍 - (5) 수신 객체 지정 람다 : with와 apply (0) 2022.03.17 5장. 람다로 프로그래밍 - (4) 자바 함수형 인터페이스 활용 (0) 2022.03.17 5장. 람다로 프로그래밍 - (2) 컬렉션 함수형 API (0) 2022.03.11 5장. 람다로 프로그래밍 - (1) 람다 식과 멤버 참조 (0) 2022.03.09 4장. 클래스, 객체, 인터페이스 - (4) object 키워드 : 클래스 선언과 인스턴스 생성 (0) 2022.03.08