코틀린
[Kotlin In Action] 8장. 고차 함수: 파라미터와 반환 값으로 람다 사용
newwisdom
2021. 12. 19. 00:08
반응형
고차 함수 정의
- 다른 함수를 인자로 받거나 함수를 반환하는 함수
- 코틀린에서는 람다나 함수 참조를 사용해 함수를 값으로 표현할 수 있음
함수 타입
(Int, String) ‐> Unit
(x: Int, y: String) ‐> Unit // 코드의 가독성 위해 파라미터에 이름 지정 가능
- 함수 파라미터의 타입을 괄호 안에 넣고, 화살표를 넣고, 반환 타입을 지정
- 반드시 반환 타입을 명시해야 함
- Unit : 의미있는 값을 반환하지 않는 함수의 반환 타입
인자로 받은 함수 호출
fun twoAndThree(operation: (Int, Int) ‐> Int) {
val result = operation(2, 3) // 함수 호출
println("The result is $result")
}
fun main(args: Array<String>) {
twoAndThree( {a, b ‐> a + b } )
}
- 일반 함수를 호출하는 구문과 같음
디폴트 값을 지정한 함수 타입 파라미터나 널이 될 수 있는 함수 타입 파라미터
- 파라미터를 함수 타입으로 선언할 때도 디폴트 값에 람다를 넣어 정의 가능
fun<T> Collection<T>.joinToString(
separator: String=",",
prefix: String="",
postfix: String=""
): String {
val result = StringBuilder(prefix)
for((index, element) in this.withIndex()){
if(index > 0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
함수를 함수에서 반환
- 함수 반환 타입으로 함수 타입을 지정해주어야 함
enum class Delivery{ STANDARD, EXPEDITED}
class Order(val itemCount: Int)
fun getShippingCostCalculator(
delivery: Delivery
): (Order) -> Double {
if(delivery == Delivery.EXPEDITED){
return { order -> 6 +1.2 * order.itemCount}
}
return {order -> 1.2 * order.itemCount }
}
- 위 예시는 Order를 받아 Double을 반환하는 함수
람다를 활용한 중복 제거
- 코드의 중복을 제거하고 싶을 경우 고차 함수를 사용할 수 있음
val averageMobileDuration = log
.filter { it.os in setOf(OS.IOS, OS.ANDROID) }
.map (SiteVisit::duration)
.average()
// 고차 함수를 사용해 중복 제거
fun List<SiteVisit>.averageDurationFor(
predicate: (SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()
인라인 함수: 람다의 부가 비용 없애기
- 람다가 변수를 포획하면 람다가 생성되는 시점마다 새로운 무명 클래스 객체가 생성되어 비용 발생
- inline 변경자가 붙은 함수는 컴파일러가 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트 코드로 변환
인라이닝이 작동하는 방식
inline fun <T> synchronized(lock : Lock, action: () -> T): T{
lock.lock()
try {
return action()
}finally {
lock.unlock()
}
}
fun foo(l : Lock){
println("before sync")
synchronized(l){
println("action")
}
println("after sync")
}
// 컴파일
fun __foo__ (l : Lock){
println("before sync")
l.lock()
try {
println("action")
}finally {
l.unlock()
}
println("after sync")
}
- inline으로 선언한 함수
- 해당 함수의 본문이 호출 위치에 인라인
- 호출시 전달한 람다도 인라이닝 됨
- 한 인라인 함수를 여러 곳에서 각각 다른 람다를 사용해 호출하면 각각 따로 인라이닝 됨
인라인 한계
- 인자로 전달된 람다식의 본문은 결과 코드에 직접 들어가기 때문에 방식이 한정적
- 함수 본문에서 파라미터로 받은 람다를 호출하면 쉽게 호출을 람다 본문으로 변경 가능
- 람다를 다른 변수에 저장 후 나중에 그 변수를 사용하면 람다를 인라인 할 수 없음
- 람다를 표현하는 객체가 어딘가는 존재해야하기 때문
inline fun <T> synchronized(lock: Lock, action: () ‐> T): T {
...
val someA = action // ‐> 컴파일 에러
...
}
Illegal usage of inline‐parameter 'action' ...
인라인 제외
- 함수 파라미터를 인라인에서 제외하려면 noinline 변경자를 붙여 인라이닝 금지시킴
inline fun foo(f: () ‐> Unit, noinline g: () ‐> Unit) {
}
컬렉션 연산 인라이닝
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.filter { it.age < 30 })
[Person(name=Alice, age=29)]
- 컬렉션의 filter, map 등 함수도 인라인 함수
- 인라인되므로 중간 리스트를 사용
- 때문에 부가비용 발생
- asSequence 사용시 시퀀스로 사용하여 중간 컬렉션을 생성하지 않을 수 있음
- 시퀀스 연산에서는 람다가 인라이닝 되지 않음
- 때문에 컬렉션 크기가 작은 경우는 시퀀스보다 컬렉션 연산이 더 성능 좋을 수 있음
함수를 인라인으로 선언해야 하는 이유
- 일반 함수 호출은 JVM이 이미 강력하게 인라인 지원
- 바이트 코드를 기계어 코드로 번역하는 JIT에서 발생
- 바이트 코드에서는 각 함수 구현이 1번만 있으면 되는데
- 코틀린 인라인 함수는 중복 발생
람다를 인자로 받는 함수를 인라인 할 경우 이익
- 부가 비용 감소
- 람다 호출 비용 감소
- 람다 위한 객체 생성 감소
- 현재 JVM은 함수 호출과 람다를 인라인할 만큼 똑똑하지 못함
- 인라이닝은 바이트코드 크기를 증가시키므로 인라인 함수 크기가 작아야 함
자원 관리를 위해 인라인된 람다 사용
use
fun readFirstLineFromFile(path: String): String {
BufferedReader(FileReader(path)).use { br ‐>
return br.readLine()
}
}
- Closeable에 대한 확장 함수로 자바의 try‐with‐resource와 같은 기능
고차 함수 안에서 흐름 제어
람다 안의 return문: 람다를 둘러싼 함수로부터 반환
fun lookForAlice(people: List<Person>) {
people.forEach { // forEach : 인라인 함수
if (it.name == "Alice") {
println("Found!")
return // lookForAlice 함수에서 리턴
}
}
println("Alice is not found")
}
- 람다 안의 return 문은 람다를 호출 하는 함수에서 return
- non-local return : 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return
람다로부터 반환 : 레이블을 사용한 return
- 람다식에서 로컬 return 사용 가능
- for 루프의 break와 비슷
- 로컬 return은 람다의 실행을 끝내고 람다를 호출했던 코드 실행을 지속
- 로컬 return과 넌 로컬 return을 구분하기 위해 label이나 함수 이름을 label로 사용
fun lookForAlice(people: List<Person>) {
people.forEach label@ { // @으로 레이블 지정
if (it.name == "Alice") return@label // 레이블 사용해서 람다에서 리턴
}
println("Alice might be somewhere")
}
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") return@forEach // 함수이름을 레이블로 사용
}
println("Alice might be somewhere")
}
무명 함수 : 기본적으로 로컬 return
// 무명 함수
fun lookForAlice(people: List<Person>) {
people.forEach(fun (person) {
if (it.name == "Alice") return // 가장 가까운 무명 함수에서 리턴
println("${person.name} is not Alice")
})
}
- return은 가장 가까운 fun 키워드를 사용해 정의된 가장 안쪽 함수를 리턴
반응형