책 정리/코틀린 인 액션

(코틀린 인 액션) 5장 람다로 프로그래밍

긍.응.성 2022. 7. 22. 00:30
반응형

5장 람다로 프로그래밍

5.1 람다 식과 멤버 참조

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

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

람다 식의 문법

val sum = { x: Int, y: Int -> x + y }
run { println(42) }
  • 람다 식은 중괄호로 둘러싸여 있다.
  • 식이기 때문에 변수에 저장할 수 있다.
  • run 함수를 통해 인자로 받은 람다를 실행할 수 있다.
  • 실행 시점에서 람다 호출에는 아무 부가 비용이 들지 않는다.
data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31))

people.maxBy({ p: Person -> p.age }) // (1)
people.maxBy() { p: Person -> p.age } // (2)
people.maxBy { p: Person -> p.age } // (3)
people.maxBy { p -> p.age } // (4)
people.maxBy { it.age } // (5)
people.maxBy(Person::age) // (6)
  • 파라미터로 전달하는 람다식은 여러 가지 방법으로 기술 가능하다.
  • (1): 인자로 람다를 전달하는 정석적인 방법이다.
  • (2): 마지막 인자가 람다인 경우 람다를 괄호 밖으로 이동할 수 있다.
  • (3): 괄호안에 명시할 인자가 없는 경우 생략할 수 있다.
  • (4): 컴파일러의 타입 추론으로 람다의 파라미터 타임은 생략 가능하다.
  • (5): 디폴트 파라미터 it을 사용하면 더 간단해진다.
  • (6): 멤버 참조를 이용해도 된다.

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

fun printProblemCounts(responses: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    responses.forEach {
        if (it.startsWith("4") {
            clientErrors++
        } else if (it.startsWith("5")) {
            serverErrors++
        }
    }
    println("${clientErrors} client errors, ${serverErrors} server errors")
}
  • 람다는 현재 영역에 있는 변수에 접근 가능하다.
  • 자바와 달리 람다에서 final 변수도 접근 가능하다.
  • 람다 안에서 사용하는 외부 변수를 람다가 포획(capture)한 변수라고 부른다.

멤버 참조

val getAge = Person::age

fun salute() = println("Salute!")
run(::salute) // Salute!
  • 이미 선언된 함수와 동일한 기능을 하는 람다가 필요한 경우, 멤버 참조를 이용가능하다.
  • :: 를 사용하영 멤버를 참조한다.
  • 멤버 참조는 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어준다.
  • 멤버 참조를 통해 확장 함수에 대한 참조도 얻을 수 있다.

5.2 컬렉션 함수형 API

아래의 API들은 자바에서 제공하는 API와 사용성이 같다.

  • filter, map
  • any, all, count, find
  • groupBy
  • flatMap, flatten

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

일반적으로 컬렉션 함수는 결과 컬렉션을 즉시(eagerly) 생성한다. 이로 인해 연쇄된 컬렉션 함수 체인에서는 매 단계마다 임시로 계산된 중간 결과 컬렉션을 생성하게 된다. 시퀀스(sequence)를 사용하면 중간 임시 컬렉션 없이 컬렉션 연산을 연쇄할 수 있다.

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

listOf(1, 2, 3, 4)
        .map { print("map($it) "); it * it }
        .filter { print("filter($it) "); it % 2 == 0 }
// map(1) map(2) map(3) map(4) filter(1) filter(4) filter(9) filter(16)

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)
  • 컬렉션에 asSequence() 를 호출해 시퀀스를 만들 수 있다.
  • 시퀀스는 자바의 stream 처럼 중간 연산과 최종 연산으로 나뉜다
  • 중간 연산은 다른 시퀀스를 반환하며 항상 지연 계산된다.
  • 최종 연산은 연기됐던 모든 계산을 수행시켜 수행한 결과를 얻는다.
    • 최종 연산을 호출하지 않으면 중간 연산도 수행되지 않는다.

위 예제의 로깅을 통해 시퀀스를 사용하지 않았을 때와 시퀀스를 사용했을 때의 연산 순서를 확인할 수 있다.

여러 CPU에서 병렬적으로 실행하는 기능이 필요하다면 자바의 parallelStream을 사용해야 한다.

시퀀스 만들기

val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
println(numbersTo100.sum()) // 5050
  • generateSequence 함수를 통해 시퀀스를 만들 수 있다.
  • 첫번째 인자로 지정할 값, 두번째 인자로 다음 원소를 계산하는 방법을 명시한다.

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

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

/* java */
void postponeComputation(int delay, Runnable computation);
/**
 * 코틀린에서 사용
 * 마지막 인자가 함수형 인터페이스므로 괄호 바깥에 람다로 전달
 * 프로그램 전체에서 Runnable의 인스턴스는 단 하나만 만들어진다.
 */
postponeComputation(1000) { println(42) }

/**
 * 무명 객체를 사용하여 명시적으로 객체를 선언
 * 메서드가 호출될 때 마다 새로운 객체가 생성된다.
 */
postponeComputation(1000, object : Runnable {
    override fun run() {
        println(42)
    }
}

/** 
 * 주변 영역의 변수를 포획한다면
 * 컴파일러는 매번 해당 변수를 포함한 인스턴스를 매번 새로 생성한다.
 */
fun handleComputation(id: String) {
    /**
     * 람다 안에서 handleComputation method 의 id 값을 captor 했기에 
     * 호출 때마다 새로 Runnable 인스턴스를 생성한다.
     */
    postponeComputation(1000) { println(id) } 
}
  • 자바의 함수형 인터페이스는 코틀린 람다 또는 무명 객체로 대체된다.

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

fun createAllDoneRunnable(): Runnable {
    return Runnable { print("All done!") }
}

createAllDoneRunnable().run() // All done!
  • SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수이다.
  • 컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자를 사용한다.
  • SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같다.

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

수신 지정 객체 람다(lambda with receiver): 수신 객체를 명시하지 않고 람다에서 다른 객체의 메서드를 호출할 수 있게 한 람다.

with 함수

fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter) 
    }
    append("\nNow I know the alphabet!")
    toString()
}
  • with 함수는 첫번째 인자로 객체(수신 객체)를, 두번째 인자로 람다를 받는다.
  • 이때 with 함수로 전달한 객체는 람다의 파라미터로 전달된다.
  • 람다 내에서 this 키워드를 사용해서 인자로 받은 객체의 프로퍼티나 메서드에 접근이 가능하다. 이때 this 키워드는 생략 가능하다.
    • 람다 외부에 동일한 이름의 메서드가 있을 때 this를 사용해서 구분할 수 있다.
  • with 의 반환값은 람다의 결과이다.

apply 함수

fun alphabet = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter) 
    }
    append("\nNow I know the alphabet!")
}.toString()
  • applywith 과 비슷하나 결과로 수신 객체를 반환한다.
  • apply 는 확장 함수로 정의되어 있다.
fun alphabet() = buildString {
    for (letter in 'A'..'Z') {
        append(letter) 
    }
    append("\nNow I know the alphabet!")
}
  • 표준 라이브러리인 buildString 을 사용하여 간결화 할 수 있다.
  • 알아서 StringBuilder객체 생성과 toString을 호출해준다.
  • buildString 인자는 수신 객체 지정 람다이며, 수신 객체는 항상 StringBuilder이다
반응형