[Kotlin in Action] part5_람다로 프로그래밍
람다 식? 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다.
람다 식과 멤버 참조
람다 소개: 코드 블록을 하수 인자로 넘기기
람다식을 사용하면 함수를 선언할 필요가 없고 코드 블록을 직접 함수의 인자로 전달할 수 있다.
코드가 더욱 간결해지는 것이다.
자바에서 무명 내부 클래스로 리스너를 구현하는 코드이다.
/* Java */
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
/* 클릭 시 수행할 동작 */
}
});
아래는 코틀린에서 람다로 리스너를 구현하는 코드이다.
button.setOnClickListener { /* 클릭 시 수행할 동작 */ }
람다와 컬렉션
자바에서는 필요한 컬렉션 기능을 직접 작성해서 코드를 쓴다. 하지만 이 방법은 코드가 많이 들어있기 때문에 작성하다 실수를 저지르기 쉽다.
코틀린에서는 라이브러리 함수를 사용하여 더간결하고 편리하게 컬렉션을 검색한다.
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.maxBy { it.age })
Person(name=Bob, age=31)
위 코드는 maxBy 함수를 호출한 것이다. maxBy 함수는 가장 큰 원소를 찾기 위해 비교에 사용할 값을 돌려주는 함수를 인자로 받는다. it.age는 바로 이 함수인 것이다.
람다 식의 문법
함수에 인자로 넘기면서 바로 람다를 정의하는 경우가 대부분이다. 이 그림은 람다 식을 선언하기 위한 문법이다.
화살표(->)가 인자 목록과 람다 본문을 구분해준다.
변수에 람다 식을 저장할 수도 있다. 그 변수를 다른 일반 함수처럼 다루면 된다.
run 키워드는 인자로 받은 람다를 실행하는 라이브러리 함수이다.
>>> run { println(42) }
42
람다 식은 여러 가지 방법으로 쓸 수 있지만 가독성을 신경쓴다면 이렇게 쓸 수 있다.
코틀린에는 함수를 호출 할 때 맨 뒤에 있는 인자가 람다 식이라면 그 람다를 괄호 밖으로 빼낼 수 있다.
people.maxBy() { p: Person -> p.age }
이렇게! 하지만 이 빈괄호를 없애줘도 상관없다.
로컬 변수처럼 컴파일러는 람다 파라미터의 타입도 추론할 수 있다. 따라서 파라미터 타입을 굳이 명시할 필요없다.
일부만 지정해도 상관없다.
람다의 파라미터 이름을 디폴트 이름인 it으로 바꾸면 람다를 더 간편하게 쓸 수 있다. 이렇게!!
people.maxBy { it.age }
현재 영역에 있는 변수에 접근
람다 식이 편리하고 자바와 다른 것 중 하나는 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다는 것이다. 또한 람다 안에서 바깥의 변수를 변경해도 된다.
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")
}
>>> val responses = listOf("200 OK", "418 I'm a teapot",
... "500 Internal Server Error")
>>> printProblemCounts(responses)
1 client errors, 1 server errors
멤버 참조
넘기려는 코드가 이미 함수로 선언이 된 경우엔 이중 콜론(: :)을 사용한다. 이중 콜론을 사용하는 방식을 멤버 참조라고 부른다.
val getAge = { person: Person -> person.age }
:: 는 클래스 이름과 참조하려는 멤버(프로퍼티나 메서드) 사이에 위치한다. 이렇게!!
멤버 참조 뒤에는 괄호를 넣으면 안된다. 멤버 참조의 타입은 호출하는 람다와 같은 타입이다.
클래스의 최상위에 선언된 함수와 프로퍼티를 참조할 수도 있다.
람다가 인자가 여러 개인 다른 함수한테 작업을 위임하는 경우 람다를 정의하지 않고 직접 위임 함수에 대한 참조를 제공할 수도 있다.
val action = { person: Person, message: String ->
sendEmail(person, message)
}
val nextAction = ::sendEmail
생성자 참조를 사용해서 클래스 생성 작업을 연기하거나 저장할 수도 있다. :: 뒤에 클래스 이름을 넣으면 된다.
data class Person(val name: String, val age: Int)
컬렉션 함수형 API
필수적인 함수: filter 와 map
filter 함수는 컬렉션을 이터레이션하면서 주어진 람다에 각 원소를 넘겨서 람다가 true를 반환하는 원소만 모은다.
>>> val list = listOf(1, 2, 3, 4)
>>> println(list.filter { it % 2 == 0 })
[2, 4]
결과는 입력 컬렉션의 원소 중에서 주어진 술어를 만족하는 원소만으로 이뤄진 새로운 컬렉션이다.
원소를 반환하기 위해선 map 함수를 사용해야 한다.
map 함수는 주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아서 새 컬렉션을 만든다.
>>> val list = listOf(1, 2, 3, 4)
>>> println(list.map { it * it })
[1, 4, 9, 16]
all, any, count, find: 컬렉션에 술어 적용
코틀린의 all과 any는 컬렉션의 모든 원소가 어떤 조건을 만족하는지 판단하는 연산을 해준다.
count 함수는 조건을 만족하는 원소의 개수를 반환하고,
find 함수는 조건을 만족하는 첫 번째 원소를 반환한다.
all 함수
>>> val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>> println(people.all(canBeInClub27))
false
any 함수
>>> println(people.any(canBeInClub27))
true
count 함수
>>> val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>> println(people.count(canBeInClub27))
1
find 함수
>>> val people = listOf(Person("Alice", 27), Person("Bob", 31))
>>> println(people.find(canBeInClub27))
Person(name=Alice, age=27)
groupBy: 리스트를 여러 그룹으로 이뤄진 맵으로 변경
groupBy 함수는 특성을 파라미터로 전달하면 컬렉션을 자동으로 구분해준다.
>>> val people = listOf(Person("Alice", 31),
... Person("Bob", 29), Person("Carol", 31))
>>> println(people.groupBy { it.age })
flatMap과 flatten: 중첩된 컬렉션 안의 원소 처리
flatMap 함수는 먼저 인자로 주어진 람다를 컬렉션의 모든 객체에 적용하고 람다를 적용한 결과 얻어지는 여러 리스트를 한 리스트로 한데 모은다.
>>> val strings = listOf("abc", "def")
>>> println(strings.flatMap { it.toList() })
[a, b, c, d, e, f]
만약 특별히 반환할 내용이 없다면 리스트의 리스트를 평평하게 펼치기만 하면 된다. 그런 경우 flatten 함수를 사용할 수 있다.
지연 계산(lazy) 컬렉션 연산
위 함수들은 결과 컬렉션을 즉시 생성한다. 하지만 시퀀스를 사용하면 중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있다.
시퀀스 연산 실행: 중간 연산과 최종 연산
시퀀스의 중간 연산은 다른 시퀀스를 반환한다. 중간 연산은 항상 지연 계산된다.
최종 연산은 결과를 반환한다.
>>> listOf(1, 2, 3, 4).asSequence()
... .map { print("map($it) "); it * it }
... .filter { print("filter($it) "); it % 2 == 0 }
이 코드를 사용하면 결과값이 출력되지 않는다. 지연되기 때문이다.
지연된다는 것은 map과 filter 변환이 늦춰져서 결과를 얻을 필요가 있을 때(최종 연산이 호출됨) 적용된다는 것이다.
시퀀스 만들기
generateSequence 함수를 사용해서 시퀀스를 만들 수 있다.
이 함수는 이전의 원소를 인자로 받아 다음 원소를 계산한다.
>>> val naturalNumbers = generateSequence(0) { it+1}
>>> val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
>>> println(numbersTo100.sum())
5050
여기서 naturalNumbers와 numbersTo100은 모두 지연 계싼한다.
최종 연산(sum이 호출)이 될 때까지 시퀀스의 각 숫자는 계산되지 않는다.
자바 함수형 인터페이스 활용
자바 멤서드에 람다를 인자로 전달
함수형 인터페이스를 사용하면 인자로 원하는 자바 메서드에 코틀린 람다를 전달할 수 있다.
코틀린에서 람다를 자바의 메서드에 넘기면 컴파일러는 자동으로 람다를 Runnable 인스턴스로 변환해준다.
Runnable 인스턴스는 Runnable을 구현한 무명 클래스의 인스턴스라는 뜻이다.
SAM 생성자: 람다를 함수형 인터페이스로 명시적으로 변경
SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성하 함수이다.
위에서 컴파일러가 자동으로 람다를 인터페이스의 인스턴스로 변환해준다고 했는데 이를 수동으로 하는 방법이다.
fun createAllDoneRunnable(): Runnable {
return Runnable { println("All done!") }
}
>>> createAllDoneRunnable().run()
All done!
함수형 인터페이스의 인스턴스를 반환하는 메서드가 있다면 반환하고 싶은 람다를 SAM 생성자로 감싸야 한다.
수신 객체 지정 람다: with 와 apply
with 함수
with 함수는 객체의 이름을 반복하지 않고도 객체에 대해 다양한 연산을 수행하게끔 한다.
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()
}
}
with문은 파라미터가 2개 있는 함수이다. with 함수는 첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다의 수신 객체로 만든다. 인자로 받은 람다의 본문은 this를 사용해서 접근할 수 있다.
apply 함수
with와 비슷한 함수이다. 하지만 apply는 항상 자신에게 전달된 객체를 반환한다.
fun alphabet() = StringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}.toString()
apply 함수는 객체의 인스턴스를 만들면서 동시에 프로퍼티 중 일부를 초기화해야 하는 경우 유용하다. 또 더욱 간결하게 코드를 쓸 수도 있다.