람다
코틀린은 Java와 근본적으로 다른 한 가지가 있다.
바로 코틀린에서는 함수가 그 자체로 값이 될 수 있다는 점이다. 따라서 변수에 할당할 수도 파라미터로 넘길 수도 있다.
이 부분을 염두해두고 코틀린에서 람다를 어떻게 활용하는 지 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() {
val fruits = listOf(
Fruit("사과", 1_000),
Fruit("사과", 1_200),
Fruit("바나나", 1_400),
Fruit("수박", 10_000)
)
val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean {
return fruit.name == "사과"
}
val isApple2: (Fruit) -> Boolean = { fruit: Fruit -> fruit.name == "사과" }
// 람다의 호출 방법
isApple(Fruit("사과", 1000))
isApple.invoke(Fruit("사과", 1000))
}
- 함수의 타입
- (파라미터 타입) -> 반환 타입 과 같이 표현할 수 있다.
- fun 키워드를 사용해 함수처럼 사용해도 되고 fun 키워드를 생략하고 중괄호로 묶어 사용할 수도 있다.
- 호출 방법은 invoke()를 사용할 수도 있고 그대로 값을 넣어 호출할 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() {
// fruits, isApple ..
private fun fileterFruits(fruits: List<Fruit>,
filter: (Fruit) -> Boolean)
): List<Fruit> {
val results = mutableListOf<Fruit>()
for (fruit in fruits) {
if (filter(fruit)) {
results.add(fruit)
}
}
return results
}
filterFruits(fruits, isApple2)
filterFruits(fruits) { fruit -> fruit.name == "사과" }
filterFruits(fruits) { it.name == "사과" }
}
- 파라미터를 보면 filter라는 파라미터에 함수 자체를 받도록 해주었다.
- for문 내의 if문에서는 함수를 invoke로 받을 수도 있고 위와 같이 filter(fruit) 처럼 바로 소괄호로 부를 수 있다.
- 메서드를 사용할 때 filterFruits() 내부의 소괄호 안에 중괄호를 직접 입력해 함수를 넣어도 되지만 가독성 측면에서 불편하다.
- 중괄호를 밖으로 뺄 수도 있다.
- 단, 마지막 파라미터가 함수인 경우에만 해당한다.
- 람다는 여러줄 작성할 수 있고, 마지막 줄의 결과가 람다의 반환값이다.
1
2
3
4
5
6
7
8
9
private List<Fruit> filterFruits(List<Fruit> fruits, Predicate<Fruit>) {
List<Fruit> results = new ArrayList<>();
for (Fruit fruit : fruits) {
if (fruitFilter.test(fruit)) {
results.add(fruit);
}
}
return results;
}
- 위의 코틀린 코드를 자바로 수정한 것이다.
- 코틀린과 다르게 Predication을 사용해야 했다.
Closure
1
2
3
String targetFruitName = "바나나";
targetFruitName = "수박";
filterFruits(fruits, (fruit) -> targetFruitName.equals(fruit.getName())); // X
- 자바에서는 위와 같이 사용할 수 없다.
- (fruit) -> targetFruitName.equals(fruit.getName())
- 이 부분에서 에러가 발생한다.
- 자바에서는 람다를 사용할 때 사용할 수 있는 변수에 제약이 있다.
- final이거나 사실상 final인 변수만 사용이 가능하다.
- targetFruitName은 위 코드에서 변경되었고 final이 아니다.
그러나 코틀린에서는 문제 없이 동작한다.
1
2
3
var targetFruitName = "바나나"
targetFruitName = "수박"
filterFruits(fruits) { it.name == targetFruitName }
- 왜 가능할까?
- 똑같이 변수가 변경되었고 var이다. 왜 가능할까.
- 코틀린에서는 람다가 시작하는 지점에 참조하고 있는 변수들을 모두 포획하여 그 정보를 가지고 있다.
- 이렇게 함으로써 람다를 진정한 일급 시민으로 간주할 수 있다.
- 이 데이터 구조를 Closure라고 부른다.
일급 시민
자바에서는 메서드가 2급 시민이다.
1급 시민일 경우
- 변수에 담을 수 있다.
- 함수에 인자로 전달할 수 있다.
- 함수의 반환값으로 전달할 수 있다.
코틀린에서는 메서드가 1급 시민이다.
try with resources
1
2
3
4
5
fun readFile(path: String) {
BufferedReader(FileReader(path)).use { reader ->
println(reader.readLine())
}
}
use는 아래와 같이 구현되어 있다.
1
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
- Closeable 구현체에 대한 확장 함수인 것을 알 수 있다.
- Inline 함수이다.
- block을 보면 람다를 받도록 만들어진 것을 볼 수 있다.
- use 사용부를 보면 실제로 람다를 전달하는 것을 볼 수 있다.
컬렉션을 함수형으로 다루는 방법
자바의 Stream과 비슷하게 코틀린에서도 컬렉션을 다루는 방법이 있다. 아래와 같은 순서로 알아보자.
- 필터와 맵
- 다양한 컬렉션 처리 기능
- List를 Map으로.
- 중첩된 컬렉션 처리
필터와 맵
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val apples = fruits.filter { fruit -> fruit.name == "사과" }
val apples = fruits.filterIndexed { idx, fruit ->
println(idx)
fruit.name == "사과"
}
val applePrices = fruits.filter { fruit -> fruit.name == "사과" }
.map { fruit -> fruit.currentPrice }
// 결과: [1000, 1200, 1400]
val applePrices = fruits.filter { fruit -> fruit.name == "사과" }
.mapIndexed { idx, fruit ->
println(idx)
fruit.currentPrice
}
val values = fruits.filter { fruit -> fruit.name == "사과" }
.mapNotNull { fruit -> fruit.price }
// fruit.price의 값이 null이 아닌 요소만 필터링
- filter, map 등의 다양한 람다를 활용한 여러 기능이 있다.
다양한 컬렉션 처리 기능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val isAllApple = fruits.all { fruit -> fruit.name == "사과" }
// 반환형 Boolean
val isNoApple = fruits.none { fruit -> fruit.name == "사과" }
val isNoApple = fruits.any{ fruit -> fruit.name == "사과" }
val fruitCount = fruits.count()
val fruit = fruits.sortedBy { fruit -> fruit.price }
val fruit = fruits.sortedByDescending { fruit -> fruit.price }
val distinctFruitNames = fruits.distinctBy { fruit -> fruit.name }
.map { fruit -> fruit.name }
fruits.first()
fruits.firstOrNull()
- all
- 모두 조건에 맞는가
- none
- 모두 해당 조건에 맞지 않는다.
- any
- 하나라도 조건에 맞는가
- count
- sortedBy, sortedByDescending
- 정렬
- distinctBy
- 변형된 값을 기준으로 중복을 제거
- first
- 첫번째 값을 가져온다. (무조건 null이 아니어야 한다.)
- firstOrNull
- 첫번째 값 또는 null을 가져온다.
List를 Map으로 바꾸는 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val map: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name }
// 과일이름 -> List<과일>인 Map
// key값이 과일 이름이고 해당 과일 List가 value값으로 담긴다.
val map2: Map<Long, Fruit> = fruits.associateBy { fruit -> fruit.id }
// id -> 과일인 Map
val map3: Map<String, List<Long>> = fruits
.groupBy({ fruit -> fruit.name }, { fruit -> fruit.factoryPrice })
// 과일이름 key -> List<출고가> value
val map4: Map<Long, Long> = fruits
.associatedBy({ fruit -> fruit.id }, { fruit -> fruit.factoryPrice })
// 과일 id -> 출고가
- groupBy
- 파라미터가 하나이면 그 파라미터가 key값으로 된 value가 group인 map
- 파라미터가 두개이면 key, value에 대해 설정 가능
- associatedBy
- 파라미터가 하나이면 그 파라미터가 key값으로 된 value가 단일인 map
- 파라미터가 두개이면 key, value에 대해 설정 가능
- 위와 같이 만든 Map에 대해서도 앞서 설명한 filter 등을 덧붙여 사용할 수 있다.
중첩된 컬렉션 처리
위와 같은 컬렉션을 어떻게 처리할 수 있는지 확인해보자.
1
2
3
val samePriceFruits = fruitsInList.flatMap { list ->
list.filter { fruit -> fruit.factoryPrice == fruit.currentPrice }
}
- flatMap
- 출고가와 현재가가 동일한 과일을 고른다.
위를 아래와 같이 리팩토링할 수도 있다.
- 확장함수를 이용해 samePriceFilter 메서드를 만들었다.
- 사용하는 부분에서는 flatMap만 사용하여 간단하게 처리할 수 있다.
1
fruitsInList.flatten()
- flatten()
- 중첩된 리스트를 하나의 리스트로 풀어준다.
정리
- 함수는 Java에서 2급 시민이지만 코틀린에서는 1급 시민이다.
- 따라서 함수 자체를 변수에 넣을 수도 있고 파라미터로 전달할 수 있다.
- 코틀린에서 함수 타입은 (파라미터 타입, …) -> 반환타입 이다.
- 코틀린에서 람다는 두 가지 방법으로 만들 수 있다.
- fun 키워드를 사용한 방법보다 중괄호를 사용한 방법이 더 많이 사용된다.
- 함수를 호출하며 마지막 파라미터인 람다를 쓸 때는 소괄호 밖으로 람다를 뺄 수 있다.
- 파라미터가 한 개인 람다를 쓸 때는 그 파라미터에 it으로 접근할 수 있다.
- 하지만 it 사용보다 직접 표기해주는 방법을 권장한다.
- 람다의 마지막 expression 결과(여러줄에서 마지막 expression 결과)는 람다의 반환 값이다.
- 코틀린에서는 Closure를 사용해 non-final 변수도 람다에서 사용 가능하다.