본문 바로가기

코틀린

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

람다 식과 멤버 참조

람다식

함수 타입에 넘길 수 있는 코드 조각. 
항상 중괄호로 둘러싸여 있으며, 인자 목록 주변에 괄호가 없다. 
람다에서 화살표가 인자 목록과 람다 본문을 구분한다.

{ x: Int, y: Int ‐> x + y }

람다가 마지막 인자라면 괄호 밖에 위치시킬 수도 있다. 
또한 인자가 한개이고 타입 추론이 가능하다면 default 명인 it을 사용할 수 있다. 

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

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

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

자바와 달리 final 변수가 아닌 변수에도 접근이 가능하며, 수정 또한 가능하다 !

fun printProblemCounts(response: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    response.forEach {
        if (it.startsWith("4")) {
            // 람다 밖의 변수 접근/수정 가능
            clientErrors++
        } else {
        }
    }
}

멤버 참조

이미 선언된 함수를 값으로 사용해야 할 경우 멤버 참조를 사용한다. 

val getAge = Person::age // 멤버 참조, 확장 함수도 동일
getAge(somePerson)
run(::salute) // 최상위 함수 참조
// Person::age(person)는 안 됨

fun sendMail(p: Person, msg: String): Unit = ...
val nextAction = ::sendMail // (p: Person, msg: String): Unit 타입 함수
val createPerson = ::Person // 생성자 참조

코틀린 1.1부터는 바운드 멤버 참조도 가능하다. 
(멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장하기 때문에 객체를 넘길 필요가 없어짐)

val p = Person("Dmitry", 34)
val ageFunc = p::age
println(ageFunc())

컬렉션 함수형 API

filter와 map

  • filter : 조건을 충족하는 것만 걸러냄
  • filterNot 도 사용 가능
  • map : 값을 변환해서 새로운 컬렉션 생성
list.filter { it % 2 == 0} 
list.map { it * it }

all, any, find(null 가능 타입, 조건 충족하는 첫번째 원소)로 조건 검사를 걸 수 있으며 count로 갯수를 셀 수 있다.

groupBy

group을 키 값으로, 이에 해당하는 객체 list들을 값으로 가진 map을 반환한다.

val strs = listOf("12", "345", "11", "456")
val grouped: Map<Int, List<String>> = strs.groupBy { it.length }

flatMap과 flatten

인자로 주어진 람다를 컬렉션 모든 객체에 적용하고, 적용한 결과로 얻어지는 여러 리스트를 한 리스트로 모은다.

val books = listOf(
Book("소설", listOf("작가1", "작가2")),
Book("시집", listOf("작가3", "작가1")))

val authors = books
.flatMap { it.authros } 
.toSet()

지연 계산 lazy 컬렉션 연산

map과 filter는 즉시 연산을 수행한다. 
즉 계산 중간 결과를 바로 새로운 컬렉션에 임시로 담는다. 
원소 개수가 많고 변환 연산이 많으면 성능상 주의가 필요하다. 

list.filter { it % 2 == 0 } // 새로운 컬렉션 생성
      .map { it * 2 } // 새로운 컬렉션 생성

자바의 스트림과 동일하게 시퀀스를 사용해서 최종 연산에 대해서만 결과를 생성하게 할 수 있다. 

people.asSequence() 
        .map(Person::name)
        .filter { it.startsWith("A") } 
        .toList() // 다시 콜렉션으로 변환

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

인터페이스에 추상 메서드가 하나인 경우 함수형 인터페이스 혹은 sam 인터페이스라고 한다. 
함수형 인터페이스를 인자로 받는 자바의 메서드를 호출할 때 람다를 넘길 수 있다. 
이는 컴파일러가 알아서 무명 클래스와 인스턴스를 만들어주기 때문이다.

// java 코드
void postponeComputation(int delay, Runnable computation)
postponeComputation(10) { println(42) } 
postponeComputation(10, object: Runnable { // 객체 식을 함수형 인터페이스 구현으로 넘김
  override fun run() {
    println(42)
  }
})

// SAM 생성자 : 람다를 함수형 인터페이스의 인스턴스로 변환
val comp: Runnable = Runnable { println(42) }

람다와 무명 객체

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

val runnable = Runnable { println(42) }
fun handleComputation() {
	postponeComputation(1000, runnable) // 모든 handleComputation 호출에 같은 객체를 사용
}

만약 여기서 람다가 바깥 변수를 포획한다면 매 호출마다 새로운 인스턴스를 생성한다.

SAM(Single Abstract Method)

자바로 작성된 메서드가 하나의 인터페이스를 구현할 때는 좀 더 간편하게 대신 함수를 작성할 수 있는데 이를 SAM 변환이라 한다.

val listener = OnClickListener { view ->
    val text = when (view.id) {
        R.id.button1 -> "First button"
        R.id.button2 -> "Second button"
        else -> "Unknown button"
    }
    toast(text)
}
button1.setOnClickListener(listener)

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

람다 내부에서 수신 객체를 지정하지 않고, 람다 바디 안에서 다른 객체의 메서드를 호출할 수 있게 하는 람다를 수신 객체 지정 람다라고 한다.

with

첫 번째 파라미터로 받은 객체를 두 번째 파라미터로 받은 람다의 수신 객체로 만든다.
마지막 반환 값이 람다의 결과이다.

fun alphabet(): String {
    val sb = StringBuffer()
    return with(sb) {
        ('A'..'Z').forEach { ch ‐> this.append(ch) }
        append("\nNow I know tthe alphabet!") // this를 생략해도 sb.append 호출
        this.toString() // this는 with의 첫 번째 인자로 전달한 sb
    }
}

>>> println(alphabet())
ABCDEFGHIJKLMNOPQRSTUVWXYZ

// 일케도 쓸 수 있음
fun alphabet(): String {
    val stringBuilder = StringBuilder()
    return with(stringBuilder) {
        for (letter in 'A'..'Z') {
            this.append(letter)
        }
        append("\nNow I know the alphabet!")
        this.toString()
    }
}

apply

반환 값이 자신을 전달한 수신 객체라는 것만 빼면 with와 거의 동일하다. 

fun alphabet() = StringBuilder().apply {
    for (letter in 'A'..'Z'){
        append(letter)
    }
    append("\nNow I know the alphabet!")
}

apply는 확장 함수로 사용된다.