-
Kotlin in Action: 5.3 지연계산(lazy) 컬렉션 연산Book/Kotlin in Action 2022. 11. 18. 00:50
5.3 지연계산 컬렉션 연산
map이나 filter같은 컬렉션 함수는 결과 컬렉션을 "즉시" 생성한다. 이는 컬렉션 함수를 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다는 말이다. 시퀀스를 사용하면 중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있다.
아래 부분은 책에 있는 그대로 적은 건데 번역본이라서 그런지 이해가 잘 되지 않는다.
더보기코틀린 지연 계산 시퀀스는 Sequence 인터페이스에서 시작한다. 이 인터페이스는 단지 한 번에 하나씩 열거될 수 있는 원소의 시퀀스를 표현할 뿐이다. Sequence안에는 iterator라는 단 하나의 메소드가 있다. 그 메소드를 통해 시퀀스로부터 원소 값을 얻을 수 있다.
시퀀스 원소는 필요할 때 비로소 계산된다.
따라서 중간 처리 결과를 저장하지 않고도 연산을 연쇄적으로 적용해서 효율적으로 계싼을 수행할 수 있다.
asSequence 확장 함수를 호출하면 어떤 컬렉션이든 시퀀스로 바꿀 수 있다. 시퀀스를 리스트로 만들 때는 toList를 사용한다.
더보기✅큰 컬렉션에 대해서 연산을 연쇄시킬 때는 시퀀스를 사용하는 것을 규칙으로 삼아야 한다.
시퀀스에 대한 연산을 지연 계산하기 때문에 정말 계산을 실행하게 만들려면 최종 시퀀스의 원소를 하나씩 이터레이션하거나 최종 시퀀스를 리스트로 변환해야 한다.
1. 시퀀스 연산 실행: 중간 연산과 최종 연산
시퀀스에 대한 연산은 중간 연산과 최종 연산으로 나뉜다.
- 중간 연산은 다른 시퀀스를 반환한다. 그 시퀀스는 최초 시퀀스의 연산을 변환하는 방법을 안다.
- 최종 연산은 결과를 반환한다. 결과는 최초 컬렉션에 대해 변환을 적용한 시퀀스로부터 일련의 계산을 수행해 얻을 수 있는 컬렉션이나 원소, 숫자 또는 객체다.
중간 연산은 항상 지연 계산 된다. 최종 연산을 호출하면 연기되었던 모든 계산이 수행된다.
//최종 연산이 없는 예제 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)
위의 코드를 보면, 최종 연산이 없을 때는 아무 내용도 출력되지 않는다. 이는 map과 필터 면환이 늦춰져서 결과를 얻을 필요가 있을 때(즉 최종 연산이 호출된 때) 적용된다는 뜻이다.
실행 순서🚨
직접 연산을 구현한다면, map함수를 각 원소에 대해 먼저 수행해서 새 시퀀스를 얻고, 그 시퀀스에 대해 다시 filter를 수행할 것이다. 컬렉션에 대한 map과 filter는 그런 방식으로 작동한다.
하지만 시퀀스에 대한 map과 filter는 그러지 않다. 시퀀스의 경우 모든 연산은 각 원소에 대해 순차적으로 적용된다. 즉 첫 번째 원소가 처리되고, 다시 두 번째 원소가 처리되며, 이런 처리가 모든 원소에 적용된다. (그래서 마지막 출력문을 보면 map과 filter가 번갈아가면서 나옴)
따라서 원소에 연산을 차례대로 적용하다가 결과가 얻어지면 그 이후의 원소에 대해서는 변환이 이뤄지지 않을 수도 있다.
println(listOf(1,2,3,4).asSequence() .map( it*it ).find(it>3))
이와 같은 연산을 컬렉션에서 수행하면 map의 결과가 먼저 평가돼 최초 컬렉션의 모든 원소가 변환된다. 두 번째 단계에서는 map을 적용해서 얻은 중간 컬렉션으로부터 술어를 만족하는 원소를 찾는다. 시퀀스를 사용하면 지연 계산으로 인해 원소 중 일부의 계산은 이뤄지지 않는다.
컬렉션을 사용하면 리스트가 다른 리스트로 변환된다.
- 그래서 map 연산은 3과 4를 포함해 모든 원소를 변환한다.
- 그 후 find가 술어를 만족하는 첫 번째 원소인 4를 찾는다.
시퀀스를 사용하면 find호출이 원소를 하나식 처리하기 시작한다.
- 최초 시퀀스로부터 수를 하나 가져와서 map에 지정된 변환을 수행한 다음에 find에 지정된 술어를 만족하는지 검사한다.
- 최초의 시퀀스에서 2를 가져오면 제곱값이 3보다 커지기 때문에 그 제곱값의 결과를 반환한다.
- 이미 답을 찾았으므로 3,4를 처리할 필요가 없다.
컬렉션에 대해 수행하는 연산의 순서도 성능에 영향을 끼친다. map과 filter의 순서에 따라서 결과는 같아도 수행해야 하는 변환의 전체 횟수는 ㄴ다르다.
map을 먼저 하면 모든 원소를 변환한다. 하지만filter를 먼저 하면 부적절한 원소를 먼저 제외하기 때문에 그런 원소는 변환되지 않는다.
여기까지 보면 우리는 Java의 스트림이 생각날 것이다. 사실 기능이 아예 동일하다고(Java8이상 스트림에서는 코틀린의 시퀀스보다 다양한 기능이 있다) 할 수 있는데, 예전 버전 자바를 사용하는 경우 자바 8에 있는 스트림이 없기 때문에 코틀린에서 같은 개념을 따로 구현해 제공한다.
그래서 자바의 스트림을 제대로 이해할 수 있게 관련 블로그를 가져왔다. 참고하시길..
https://velog.io/@cham/JAVA-%EC%8A%A4%ED%8A%B8%EB%A6%BCStream
2. 시퀀스 만들기
지금까지 살펴본 시퀀스 예제는 모두 컬렉션에 대해 asSequence()를 호출해 시퀀스를 만들었다. 시퀀스를 만드는 다른 방법으로 generateSequence함수를 사용할 수 있다. 이 함수는 이전의 원소를 인자로 받아 다음 원소를 계산한다.
시퀀스를 사용하는 일반적인 용례 중 하난는 객체의 조상으로 이뤄진 시퀀스를 만들어내는 것이다. 어떤 객체의 조상이 자신과 같은 타입이고 모든 조상의 시퀀스에서 어떤 특성을 알고 싶을 때가 있다.
'Book > Kotlin in Action' 카테고리의 다른 글
Kotlin in Action: 5.5 수신 객체 지정 람다 : with와 apply (0) 2022.11.18 Kotlin in Action: 5.4 자바 함수형 인터페이스 활용 (0) 2022.11.18 Kotlin in Action: 5.2 컬렉션 함수형 API (0) 2022.11.17 Kotlin in Action: 5.1 람다로 프로그래밍 (0) 2022.11.15 Kotlin in Action: 클래스, 객체, 인터페이스 (0) 2022.11.11