본문 바로가기

Kotlin

Kotlin In Action - 5장. 람다로 프로그래밍

  • 코틀린 표준 컬렉션 라이브러리 함수에서 람다를 넘기는 방식 알아보기
  • 자바 라이브러리와 람다를 함께 사용하는 방식 알아보기
  • 수신 객체 지정 람다 ( lambda with receiver )

에 대해 알아보자!

 

람다식과 멤버 참조

람다는 일련의 동작을 다른 함수에 넘겨야 하는 경우 코드를 간결하게 구현할 수 있다

val sum = { x:Int, y:Int -> x+y }
println(sum(1, 2))

{ println(42) } ()
run { println(42) }
실행 시점에 코틀린 람다 호출에는 아무 부가 비용이 들지 않음. 이유는? 8.2장 TODO link 달기
people.maxBy({ p: Person -> p.age })

// 마지막 인자가 람다라면 람다를 괄호 밖으로 뺄 수 있음
people.maxBy() { p -> p.age }

// 람다가 유일한 함수 인자인 경우, 호출 괄호도 없앨 수 있음
people.maxBy { p -> p.age }
val people = listOf(Person("Ted", 31))

val name = people.joinToString(" ", transform = { p -> p.name })

val name2 = people.joinToString(" ") { p -> p.name }
val sum = { x:Int, y:Int ->
	println("sum of {$x} and {$y}")
    x + y // 마지막에 있는 식이 람다의 결과 값
}

현재 영역에 있는 변수에 접근 - 람다가 capture한 변수

fun printProblemCounts(responses: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0

    responses.forEach {
        if (it.startsWith("4")) {
            clientErrors++
        } else if (it.startsWith("5")) {
            serverErrors++;
        }
    }
    
}
  • 람다 내부에서 바깥 변수에 접근 가능하고, 변경도 가능함
  • client/server Errors 변수와 같이 람다 안에서 사용하는 외부 변수 = 람다가 포획한 함수
  • 기본적으로 함수가 끝나면 erros변수들의 생명주기가 끝나지만, 포획하는 경우 달라짐
  • 람다가 함수가 끝난 뒤 실행된다고 해도 포획한 함수는 읽거나 쓸 수 있음

멤버 참조 ( member reference )

val getAge = Person::age

people.maxBy { it.age }
people.maxBy { p -> p.age }
people.maxBy(Person::age)

1. 람다가 인자가 여럿인 다른 함수한테 작업을 위임하는 경우 람다를 정의하지 않고, 직접 위임 함수에 대한 참조를 제공해줌

var nextAction = ::sendEmail // 람다 대신 멤버 참조를 쓸 수 있음

var action = { person:Person, message: String ->
	sendEmail(person, message)
}

2. 생성자 참조를 사용하면 클래스 생성작업을 연기하거나 저장해 둘 수 있음

data class Person(val name:String, val age:Int )

val createPerson = ::Person // 인스턴스를 만드는 동작을 값으로 저장
val p = createPerson("Ted",31)
println(p)

3. 확장 함수도 멤버 함수와 똑같은 방식으로 참조 가능

fun Person.isAudlt() = age >= 21

val predicate = Person::isAdult

바운드 멤버 참조 ( bound member reference )

// Kotlin 1.0
val p = Person("Ted",31)
val personAgeFun = Person::age
println(personAgeFun(p))

// kotlin 1.1 이후 - 바운드 멥버 참조 지원
val p = Person("Ted",31)
val personAgeFun = p::age
println(personAgeFun())
  • 멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장한 다음 나중에 그 인스턴스에 대해 멤버를 호출

컬렉션 함수형 API

1. 목록에서 나이가 제일 많은 사람들을 가져오고 싶은 경우

// 방법 1 - maxBy N번 호출...
people.filter { it.age == people.maxBy(Person::age)!!.age }

// 방법 2
val maxAge = people.maxBy(Person::age)!!.age
people.filter { it.age == maxAge }

2. map

val numbers = mapOf(0 to "zero")

println(numbers.mapValues { it.value.toUpperCase() } )
// 0=ZERO

all, any, count, find

val list = listOf(1, 2, 3)
println(list.any { it !=3 }) // true
count vs size ?
결과는 같지만
people.filter(canBeInClube27).size 와 같은 코드를 작성하면
조건을 만족하는 모든 원소가 들어가는 중간 컬렉션이 발생해버린다. 그렇기 때문에 count를 쓰는게 더 효율적이다

groupBy

flatMap, flattern

class Book(val title: String, val authors: List<String>)


fun test() {
    val books = listOf("Kotlin in Action", listOf("Toss", "KakaoBank"))
    books.flatMap { b -> b.authors }.toSet()
}
  • list의 list를 펼쳐야 하는 경우는 listOfLists.flatten() 함수 이용

지연 계산 컬렉션 연산 - lazy

    people.asSequence()
        .map(Person::age)
        .filter( { it.startsWith("a")})
        .toList()
  • 중간 결과를 저장하는 컬렉션이 생기지 않기 때문에 원소가 많은 경우 성능이 좋아짐
  • 연산 계산 수행 방법
    • 중간 연산 : map, filter
    • 최종 연산 : 결과 반환 ( toList() )
  • 실행 순서
    • person1이 map, filter -> person2이 map,filter 쳐렴 각 원소에 대해 순차적으로 적용
listOf(1,2,3,4).asSequence().map{it*it*}.find { it>3 }) // 4
  • collection이였다면? 1,2,3,4 -> 1,4,9,16 컬렉션 반환 후 find 함
  • sequence인 경우? 1,2,3,4 -> 1,4 -> find! 즉 3,4는 제곱 수행도 되지 않는다
자바 stream VS 코틀린 sequence
자바 stream과 거의 같아 보인다. 그런데 왜 굳이 코틀린에서 따로 구현한 것일까?
안드로이드 등에서 여전히 예전 버전 자바를 사용하는 경우 자바8에 있는 스트림이 없기 때문

자바8을 채택하면 Kotlin sequence, collection에서 제공하지 않는 스트림 연산을 여러 CPU에서 병렬적으로 실행할 수 있는 기능을 사용할 수 있다.
즉, 자바 버전에 따라 시퀀스와 스트림 중에 적절하게 사용하면 된다.

시퀀스 만들기

상위 디렉토리를 뒤지면서 hidden 속성을 가진 디렉터리가 있는지 검사하는 코드

fun File.isInsideHiddenDirectory() =
    generateSequence(this) { it.parentFile }.any { it.isHidden }

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

// java
button.setOnClickListener( new OnClikListener() {
    @Override
    public void onClick(View v){
     ...
    }
}

// kotlin
button.setOnClickListener { view-> ... }
  • 이런 코드가 동작하는 이유? 추상 메소드가 단 하나만 있기 때문 ( functional interface, single abstract method )
자바와 달리 코틀린은 제대로 된 함수 타입이 존재
-> 함수를 인자로 받을 필요가 있는 함수는 함수형 인터페이스가 아니라 함수 타입을 인자 타입으로 사용해야 한다.
선언 방법은 8.1장

자바 메소드에 labmda를 인자로 전달

// java
void postponeComputation(int delay, Runnable computation)


// kotlin에서 java call
postponseComputation(1000) { println(42) }
  • 컴파일러는 자동으로 무명  클래스와 인스턴스를 만들어준다
  • println 메소드는 반복 사용 됨

람다 vs 무명 객체

// 무명 객체 - 메소드 호출할때마다 새로운 객체 생성
postponeComputation(1000, object: Runnable {
	override fun run() {
    	println(42)
    }
})

// 람다처럼 인스턴스 재활용하도록 구현
val runnable = Runnable { println(42) } // 전역 변수로 컴파일 됨
fun handleComputation() {
	postponeComputation(1000, runnable)
}
  • 호출할때 마다 새로운 객체 생성됨

람다가 주변 영역의 변수를 포획한다면 매 호출마다 인스턴스 새로 생성

fun handleCompuation(id: String) {
	postponeCompuation(1000) { println(id) } // 새로 생성
}

수신 객체 지정 람다 : with, apply

class Test {

    fun alphabet(): String {
        val result = StringBuilder()

        for (letter in 'A'..'Z') {
            result.append(letter)
        }

        result.append("\n Alphabet!")

        return result.toString()
    }

    fun alphabetWith(): String {
        val stringBuilder = StringBuilder()

        return with(stringBuilder) {// 메소드를 호출하려는 수신 객체를 지정
            for (letter in 'A'..'Z') {
                this.append(letter) // this 명시해서 함수 호출
            }

            append("\b alphabet!") // this 생략하고 호출 가능
            this.toString() // 람다에서 값을 반환

        }
    }
}
  • 약간의 문제(?) : 위의 함수를 보면 result의 함수를 반복적으로 호출하고 있음! -> 더 간결하게 안되나?
  • with가 코틀린에서 제공하는 특별한 구문처럼 보이지만 아니다
    • 첫번째 파라미터 : StringBuilder
    • 두번째 파라미터 : 람다
    fun alphabetWith2() = with(StringBuilder()) {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\b alphabet!")
        toString()
    }
  • toString() 이라는 함수가 외부 클래스 안에도 있다면? 그렇다면 this@OuterClass.toString() 이렇게 호출하면 됨

apply 함수

  • 자신에게 전달된 수신 객체를 반환함
  • apply 함수는 객체의 인스턴스를 만들면서 즉시 프로퍼티 중 일부를 초기화 해야하는 경우 유용 ( Builder 객체같은 )
fun alphabetApply() = StringBuilder().apply {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\b alphabet!")
    }.toString()
    
    fun createViewWithCustom(context : Context){
        TextView(context).apply {
            text="custom"
            setPadding(10,0,0,0)
        }        
    } 

표준 라이브러리에서 apply를 사용한 예제이다.

fun alphabet() = buildString {
	for (letter in 'A'..'Z') {
    	append(letter)
    }
    append("\b alphabet!")
}
11장 DSL에서 이를 이용하여 더 흥미로운 예제를 만들 수 있다고 한다.