5장. 람다로 프로그래밍

5장. 람다로 프로그래밍

기본적인 내용은 생략한다.


키워드 #

  • 람다, 멤버 참조
  • sequence (지연 컬렉션 연산)
  • 함수형 인터페이스 (SAM 인터페이)
  • 수신 객체 지정 람다

+ 참고 : 스마트 캐스트는 val(불변)일 때에만 사용 가능하다.



5장. 람다로 프로그래밍 #

람다(= 람다 식)은 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다.



5.1 람다 식과 멤버 참조 #

5.1.1 람다 소개 : 코드 블록을 함수 인자로 넘기기 #

button.setOnClickListener {
    /* 클릭 시 수행할 동작 */
}

5.1.2 람다와 컬렉션 #

자바에서는 필요한 컬렉션 기능을 직접 작성하곤 했다. 코틀린에서는 (람다가 있기 때문에)이런 습관을 버려야 한다.

// 람다를 활용한다.
people.maxBy { it.age } // 나이로 비교하여, 가장 큰 원소를 찾는다.

// 멤버 참조를 활용한다.
people.maxBy(Person::age) // 나이로 비교하여, 가장 큰 원소를 찾는다.

코틀린 람다 식은 항상 ‘중괄호’로 둘러싸여 있다.

인자 목록 주변에 괄호가 없기도 하다. (-> 로 인자 목록과 본문을 구분할 수 있기 때문이다.)

  • { (a, b) -> ...} 가 아니고, { a, b -> ... } 로 작성한다.

코틀린에서 함수 호출 시 맨 뒤에 있는 인자가 람다 식이면, 그 람다를 괄호 밖으로 빼낼 수 있다.

따라서, 괄호 뒤에 람다 식을 둘 수 있다.

// 개선 전
people.maxBy({ p: Person -> p.age })

// 개선 후
people.maxBy() { p: Person -> p.age }

(위와 같이) 람다가 함수의 유일한 인자이고, 괄호 뒤에 람다를 썼다면 호출 시 빈 괄호를 없앨 수 있다.

people.maxBy { p: Person -> p.age }

람다의 파라미터명을 디폴트 파라미터명 it로 사용할 수 있다.

people.maxBy { it.age }

5.1.4 현재 영역에 있는 변수에 접근 #

람다의 파라미터뿐만 아니라, 람다 정의 앞에 선언된 로컬 변수까지 람다에서 모두 사용할 수 있다.

fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
    messages.foreach {
        println("$prefix $it") // prefix 변수를 사용할 수 있다.
    }
}

(자바와 다른 점은) 람다 안에서 파이널 변수가 아닌 변수에도 접근할 수 있다.

  • 람다 안에서 바깥의 변수를 변경할 수도 있다.
fun printProblemCounts(responses: Collection<String>) {
    var clientErros = 0
    var serverErros = 0
    responses.forEach {
        if(it.startWith("4")) {
            clientErros++
        }
        else {
            serverErros++;
        }
    }
}

위와 같이 람다 내부에서 사용하는 외부 변수를 ‘람다가 포획(capture)한 변수’라고 부른다.

내부적으로 아래와 같이 동작한다.

  • 파이널 변수를 포획한 경우 : 람다 코드를 변수 값과 함께 저장한다.
  • 일반 변수를 포획한 경우 : 변수를 특별한 래퍼(Ref)로 감싸서 나중에 변경하거나 읽을 수 있게 한다. (래퍼를 참조하는 것)

컴파일된 코드를 한번 살펴보자.

// (1)
class Ref<T>(var value: T)

val counter = Ref(0)
val inc = { counter.value++ }
// (2)
var counter = 0
val inc = { counter++ } 

(2) 코드의 내부 원리가 (1)이다.


" 한 가지 꼭 알아둬야 할 함정이 있다. 람다를 이벤트 핸들러나 다른 비동기적으로 실행되는 코드로 활용하는 경우 함수 호출이 끝난 다음에 로컬 변수가 변경될 수도 있다. “


5.1.5 멤버 참조 #

람다와 같이 코드를 인자로 넘길 수 있다. 다만 함수로 생성이 되어 있는 코드를 넘기려면 어떻게 해야할까?

멤버 참조를 통해 해결할 수 있다.

val getAge = Person::age

// val getAge = { person:Person -> person.age }  와 같다.

:: 를 사용하는 식을 멤버 참조 라고 부른다.

  • 멤버 참조는 프로퍼티나 메서드를 ‘단 하나’만 호출하는 함수 값으로 만들어준다.
  • 멤버 참조 뒤에는 () 를 붙여서는 안된다.

최상위에 선언된 함수/프로퍼티도 참조할 수 있다.

fun salute() = println("salute")

run(::salute) // 최상위 함수를 호출한다.

생성자 참조(constructor reference)를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있다.

:: 뒤에 클래스 이름을 넣으면 생성자 참조를 만들 수 있다.

val createPerson = ::Person

createPerson("alice", 29)

확장함수도 멤버 함수와 똑같은 방식으로 참조할 수 있다.

fun Person.isAudit() = age >= 21

val predicate = Person::isAudit

바운드 멤버 참조

코틀린 1.0에서는 클래스의 메서드, 프로퍼티에 대한 참조를 얻은 다음에 그 참조를 호출할 때 항상 인스턴스 객체를 제공해야 했다.

코틀린 1.1부터는 바운드 멤버 참조(bound member reference)를 지원한다. 바운드 멤버 참조를 사용하면 멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장한 다음 나중에 그 인스턴스에 대해 멤버를 호출한다.

val p = Person("Dmitry", 34)

val personAgeFunction = Person::age 
println(personAgeFunction(p)) // 인스턴스를 넘긴다.

val dmitryAgeFunction = p::age  // 인스턴스를 함께 저장한다.
println(dmitryAgeFunction()) // 인스턴스를 넘기지 않는다.



5.2 컬렉션 함수형 API #


5.2.1 필수적인 함수 : filter, map #

생략


5.2.2 all, any, count, find : 컬렉션에 술어 적용 #

all : 모든 술어를 만족하는지 확인한다.

val canBeInClub27 = { p: Person -> p.age <= 27 }

people.all(list)

any : 하나라도 만족하는지 확인한다.

val canBeInClub27 = { p: Person -> p.age <= 27 }

people.any(list)

count : 술어를 만족하는 원소의 개수를 센다.

val canBeInClub27 = { p: Person -> p.age <= 27 }

people.count(list)

find : 술어를 만족하는 원소를 찾는다.

  • 가장 먼저 발견된 원소 ‘하나’를 반환한다.
  • 발견되는 원소가 없다면 null 을 반환한다.
val canBeInClub27 = { p: Person -> p.age <= 27 }

people.find(list)

5.2.3 groupBy: 리스트를 여러 그룹으로 이뤄진 맵으로 변경 #

컬렉션의 모든 원소를 어떤 특성에 따라 여러 그룹으로 나눌 때 사용할 수 있다.

val people = listOf(...)

println(people.groupBy { it.age })

// {
//    29 = [Person(...), Person(...)], 
//    31 = [Person(...), Person(...)]
// }

5.2.4 flatMap, flatten : 중첩된 컬렉션 안의 원소 처리 #

flatMap : 주어진 람다를 컬렉션의 모든 객체에 적용하고(또는 매핑하고), 람다를 적용한 결과로 얻어지는 여러 리스트를 한 리스트로 모은다.

  • 즉, map → flat 한다?
val strings = listOf("abc", "def")
strings.flatMap { it.toList() }     // [a, b, c, d, e, f]

// 1. string -> list mapping
// 2. flat 
val books = lostOf(Book(...), Book(...), Book(...), ...)

books.flatMap { it.authors }.toSet()

// [길동, 철수, ...]

flatten : (flatMap과 같이 매핑할 게 없고) 펼치기(flat)만 필요하다면 flatten 을 사용한다.



5.3 지연 계산(lazy) 컬렉션 연산 #

위에서 살펴본 map, filter 등의 연산은 즉시(eager) 연산한다.
= 체이닝 연산을 할 때 계산의 중간 결과를 새로운 컬렉션에 담는다는 의미이다.

시퀀스(sequence) 를 사용하면 중간의 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 체이닝(연쇄)할 수 있다.

  • 시퀀스의 원소는 필요할 때 비로소 계산된다.
  • asSequence 확장 함수를 호출하면 어떤 컬렉션이든 시퀀스로 바꿀 수 있다.

아래 예시를 살펴보자.

참고로, map, filter 는 연산 후 리스트를 반환한다.

people.map(Person::name).filter { it.startsWith("A") }

위 코드는 리스트를 2개 만든다.

원소가 많을 수록, 체이닝이 많을 수록 문제가 심해진다.


이를 더 효율적으로 개선하기 위해서 시퀀스를 사용해볼 수 있다.

people.asSequence()
        .map(Person::name)
        .filter { it.startsWith("A") }
        .toList()

위 코드는 중간 결과를 저장하는 컬렉션이 생기지 않기 때문에, 성능이 눈에 띄게 좋아진다.


코틀린 지연 계산 시퀀스는 Sequence 인터페이스에서 시작한다.

  • 이 인터페이스는 단지 한 번에 하나씩 열거될 수 있는 원소의 시퀀스를 표현한다.
    • Sequence 안에는 iterator 라는 단 하나의 메서드가 있다.
    • 이 메서드를 통해 시퀀스로부터 원소 값을 얻을 수 있다.

왜 시퀀스를 다시 리스트로 변경해야할까?
꼭 변경하지 않아도 된다.
이터레이터를 통해 접근해도 된다면 시퀀스를 그대로 써도 된다.
하지만 대부분의 경우 인덱스를 통해 접근하거나 다른 기능들을 사용하기 편리한 리스트를 다루기 때문에 대부분의 예시에서 변경하는 것이다.


” 큰 컬렉션에 대해서 연산을 체이닝할 때는 시퀀스를 사용하는 것을 규칙으로 삼아라 “


5.3.1 시퀀스 연산 실행: 중간 연산과 최종 연산 #

Java Stream 쪽 내용과 비슷해서 생략한다.


5.3.2 시퀀스 만들기 #

  • asSequence()
  • generateSequence()

생략



5.4 자바 함수형 인터페이스 활용 #

코틀린은 ‘함수형 인터페이스’를 인자로 취하는 자바 메서드를 호출할 때 람다를 넘길 수 있게 해준다.

” 자바와 달리 코틀린에서는 제대로 된 함수 타입이 존재한다. 따라서 코틀린에서 함수를 인자로 받을 필요가 있는 함수는 함수형 인터페이스가 아니라 함수 타입을 인자 타입으로 사용해야 한다. 다만, 컴파일러가 이 함수 타입을 함수형 인터페이스로 변환해주지는 않는다. (이에 대한 내용은 8.1 절에서 자세하게 다룬다.) “


5.4.1 자바 메서드에 람다를 인자로 전달 #

함수형 인터페이스를 인자로 원하는 자바 메서드에 코틀린 람다(혹은 익명객체)를 전달할 수 있다.

void postponeComputation(int delay, Runnable computation);

postponeComputation(1000) { println(42) }

postponeComputation(1000, object: Runnable {
    override fun run() {
        println(42)
    }
})

람다와 익명객체 사이에는 차이가 있다.

  • 익명 객체 : 메서드를 호출할 때마다 새로운 객체가 생성된다.
  • 람다 : (내부적으로 생성한 익명 객체를) 재사용한다.
    • 다만, 주변 영역의 변수를 포획한다면 매 호출마다 같은 인스턴스를 사용할 수 없기 때문에 새로 객체를 생성한다.

위 코드에서 postponeComputation(1000) { println(42) } 코드는 다음과 동일하다.

val runnable = Runnable { println(42) }
postponeComputation(1000, runnable)

람다의 자세한 구현

코틀린 1.0에서 인라인(inline) 되지 않은 모든 람다 식은 무명 클래스로 컴파일된다. (?)

생략


  • 코틀린 inline 으로 표시된 코틀린 함수에게 람다를 넘기면 아무런 무명 클래스도 만들어지지 않는다.
  • 대부분의 코틀린 확장 함수는 inline 표시가 붙어 있다.

inline 함수에 대해 찾아볼 것


5.4.2 SAM 생성자: 람다를 함수형 인터페이스로 명시적으로 변경 #

‘SAM 생성자’에 대해 찾아볼 것



5.5 수신 객체 지정 람다 : with 와 apply #

생략



5.6 요약 #

생략