8.1 고차 함수란?
- 다른 함수를 인자로 받거나 함수를 반환하는 함수
- 해당 언어에서 함수가 일급 객체여야한다.
일급객체
- 변수에 할당할 수 있다.
- 다른 함수를 인자로 전달 받는다.
- 다른 함수의 결과로서 리턴될 수 있다.
예를 들어서 filter 함수도 고차함수다
public inline fun <T> Iterable<T>.filter(**predicate: (T) -> Boolean**): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
8.1.1 함수 타입
- 람다를 인자를 받는 함수를 정의하려면, 먼저 람다 인자의 타입을 어떻게 선언할 수 있는지 알아야한다.
- 변수 타입을 함수 타입으로 지정하면 함수 타입에 있는 파라미터로부터 람다의 파라미터 타입을 유추할 수 있다.
- 그러므로 람다 식 안에서 굳이 파라미터 타입을 적을 필요가 없다
fun main() {
val sum: (Int, Int) -> Int = { x, y -> x + y }
val action: () -> Unit = { println(12) }
println(sum(1,5)) // 6
action() // 12
}
- 다른 함수와 마찬가지로 함수 타입에서도 반환 타입을 널이 될 수 있는 타입으로 지정할 수 있다.
val canReturnNull: (Int) -> Int? = { x -> null }
val funOrNull: ((Int) -> Int)? = null
8.1.2 인자로 받은 함수 호출
- 인자로 받은 함수를 호출하는 구문은 일반 함수를 호출하는 구문과 같다.
fun twoAndThree(operation: (Int, Int) -> Int) {
val result = operation(2, 3)
println(result)
}
fun main() {
twoAndThree { x, y -> x + y } // 5
twoAndThree { x, y -> x * y } // 6
}
8.1.3 자바에서 코틀린 함수 타입 사용
- 컴파일된 코드 안에서 함수 타입은 일반 인터페이스로 바뀐다.
- 각 인터페이스에는 invoke 메소드 정의가 들어있다
- 함수 타입의 변수는 인자 개수에 따라 적당한 FuntionN 인터페이스를 구현하는 클래스의 인스턴스를 저장하며, 그 클래스의 invoke 메소드 본문에는 람다의 본문이 들어간다.
// 코틀린 선언
fun processTheAnswer(f: (Int) -> Int) {
println(f(42))
}
// 자바
processTheAnswer(number -> number + 1)
processTheAnswer(
new Function1<Integer, Integer>() { // 자바 코드에서 코틀린 함수 타입을 사용한다
@override
public Integer invoke(Integer number) {
system.out.println(number);
return number + 1;
}
})
- 반환 타입이 Unit인 함수나 람다를 자바로 작성할 수 있다.
- 하지만 코틀린 Unit타입에는 값이 존재하므로 자바에서는 그 값을 명시적으로 반환해줘야한다.
CollectionKt.forEach(strings, s -> {
System.out.println(s);
return **Unit.INSTANCE**;
});
8.1.4 디폴트 값을 지정한 함수 타입 파라미터나 널이 될 수 있는 함수 타입 파라미터
- 파라미터를 함수 타입으로 선언할 때도 디폴트 값을 줄 수 있다.
fun <T> Collection<T>.joinToString(
separator: String = ",",
**transform: (T) -> String = { it.toString() }** // 함수 타입 파라미터를 선언하면서 람다를 디폴트 값으로 지정한다.
): String {
val result = StringBuilder()
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(transform(element))
}
return result.toString()
}
fun main() {
val letters = listOf("Alpha", "Beta")
println(letters.joinToString { it.lowercase() }) // alpha,beta
}
- 널이 될 수 있는 함수 타입으로 함수를 받으면 그 함수를 직접 호출할 수 없다는 점에 유의하자.
- 코틀린은 NPE가 발생한 가능성이 있으므로 그런 코드의 컴파일을 거부할 것이다.
fun <T> Collection<T>.joinToString(
separator: String = ",",
**transform: ((T) -> String)? = null //널이 될 수 있는 함수 타입의 파라미터를 선언한다.**
): String {
val result = StringBuilder()
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
val str = transform?.invoke(element)
?: element.toString() // 람다를 인자로 받지 않은 경우를 처리한다.
result.append(str)
}
return result.toString()
}
fun main() {
val letters = listOf("Alpha", "Beta")
println(letters.joinToString { it.lowercase() }) // alpha,beta
}
8.1.5 함수를 함수에서 반환
enum class Delivery { STANDARD, EXPEDITED }
class Order(val itemCount: Int)
fun getShippingCostCalcaulator(
delivery: Delivery
): (Order) -> Double {
if (delivery == Delivery.EXPEDITED) {
return { order -> 6 + 2.1 * order.itemCount }
}
return { order -> 1.2 * order.itemCount }
}
fun main() {
val calculator = getShippingCostCalcaulator(Delivery.EXPEDITED)
println(calculator(Order(3))) // 12.3
}
8.1.6 람다를 활용한 중복 제거
- 함수 타입과 람다 식은 재활용하기 좋은 코드를 만들 때 유용하다
- 하드코딩 된 예제
- 중복을 피하기 위해 OS를 파라미터로 뽑아낸다.
data class SiteVisit( val path: String, val duration: Double, val os: OS ) enum class OS { WINDOWS, LINUX, IOS, ANDROID } **fun List<SiteVisit>.averageDurationFor(os: OS) = filter { it.os == os }.map(SiteVisit::duration).average()** fun main() { val log = listOf( SiteVisit("/", 34.0, OS.WINDOWS), SiteVisit("/login", 24.0, OS.LINUX), SiteVisit("/", 14.0, OS.WINDOWS), ) println(log.averageDurationFor(OS.WINDOWS)) // 24.0 }
- data class SiteVisit( val path: String, val duration: Double, val os: OS ) enum class OS { WINDOWS, LINUX, IOS, ANDROID } fun main() { val log = listOf( SiteVisit("/", 34.0, OS.WINDOWS), SiteVisit("/login", 24.0, OS.LINUX), SiteVisit("/", 14.0, OS.WINDOWS), ) val averageWindowsDuration = log .filter { it.os == OS.WINDOWS } .map(SiteVisit::duration) .average() println(averageWindowsDuration) // 24.0 }
- 하드코딩 된 예제
- 고차 함수를 사용해 중복을 제거한다.
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) = filter(predicate).map(SiteVisit::duration).average() fun main() { val log = listOf( SiteVisit("/", 34.0, OS.IOS), SiteVisit("/login", 24.0, OS.LINUX), SiteVisit("/", 14.0, OS.ANDROID), ) println(log.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) }) // 24.0 }
- fun main() { val log = listOf( SiteVisit("/", 34.0, OS.IOS), SiteVisit("/login", 24.0, OS.LINUX), SiteVisit("/", 14.0, OS.ANDROID), ) val averageWindowsDuration = log .filter { it.os in setOf(OS.IOS, OS.ANDROID) } .map(SiteVisit::duration) .average() println(averageWindowsDuration) // 24.0 }
- 변수, 프로퍼티, 파라미터 등을 사용해 데이터의 중복을 없앨 수 있는 것처럼 람다를 사용하면 코드의 중복을 없앨 수 있다.
8.2 인라인 함수: 람다 부가 비용 없애기
- 람다가 변수를 포획하면, 람다가 생성되는 시점마다 새로운 익명 클래스 객체가 생긴다.
- 이런 경우 런타임에 익명클래스 생성에 따른 부가 비용이 든다.
- inline 변경자를 어떤 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔치기 해준다
8.2.1 인라이닝이 작동하는 방식
- 어떤 함수를 inline으로 선언하면 함수 호출 코드를 함수를 호출하는 바이트코드 대신에 함수 본문을 번역한 바이트 코드로 컴파일한다
- 다중 스레드 환경에서 어떤 공유 자원에 대한 동시 접근을 막기 위한 것이다.
- 이 함수는 Lock 객체를 잠그고 주어진 코드 블록을 실행한 다음에 Lock 객체에 대한 잠금을 해제한다.
inline fun <T>synchronized(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
fun main() {
val l = Lock()
synchronized(l) {
//...
}
}
- 코틀린 표준 라이브러리는 아무 타입의 객체나 인자로 받을 수 있는 synchronized 함수를 제공한다.
- synchronized 함수를 inline으로 선언했으므로 synchronized를 호출하는 코드는 모두 자바의 synchronized문과 같아진다.
**inline** fun <T>synchronized(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
fun foo(l: Lock) {
println("before lock")
synchronized(l) {
println("action") // **람다의 본문**
}
println("after lock")
}
- 인라이닝 된 결과물이다.
- 위의 코드와 같은 바이트 코드를 만들어낸다.
- synchronized 함수의 본문뿐 아니라 synchronized에 전달된 람다의 본문도 함께 인라이닝된다.
fun __foo__(l: Lock) {
println("before lock")
l.lock()
try {
**println("action")**
}
finally {
l.unlock()
}
println("after lock")
}
- 람다의 본문에 의해 만들어지는 바이트코드는 그 람다를 호출하는 코드 정의의 일부분으로 간주하기 때문에 코틀린 컴파일러는 그 람다를 함수 인터페이스를 구현하는 무명 클래스로 감싸지 않는다.
- 인라인 함수를 호출하면서 람다를 넘기는 대신에, 아래와 같이 함수 타입의 변수를 넘길 수도 있다.
inline fun <T>synchronized(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
class LockOwner(val lock: Lock) {
fun runUnderLock(body: () -> Unit) {
synchronized(lock, body)
}
}
- 이 경우에는 인라인 함수를 호출하는 코드 위치에서는 변수에 저장된 람다의 코드를 알 수 없다.
- 따라서 람다 본문은 인라이닝되지 않고 synchronized 함수의 본문만 인라이닝된다.
inline fun <T>synchronized(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
class LockOwner(val lock: Lock) {
fun __runUnderLock__(body: () -> Unit) {
lock.lock()
try {
body() // synchronized를 호출하는 부분에서 람다를 알 수 없으므로 본문은 인라이닝되지 않는다
}
finally {
lock.unlock()
}
}
}
- 한 인라인 함수를 두 곳에서 각각 다른 람다를 사용해 호출한다면 그 두 호출은 각각 따로 인라이닝된다
- 인라인 함수의 본문 코드가 호출 지점에 복사되고 각 람다의 본문이 인라인 함수의 본문 코드에서 람다를 사용하는 위치에 복사된다.
8.2.2 인라인 함수의 한계
- 파라미터로 받은 람다를 다른 변수에 저장하고 나중에 그 변수를 사용한다면 람다를 표현하는 객체가 어딘가는 존재해야 하기 때문에 람다를 인라이닝할 수 없다.
- 일반적으로 인라인 함수의 본문에서 람다 식을 바로 호출하거나 람다 식을 인자로 전달받아 바로 호출하는 경우에는 그 람다를 인라이닝할 수 있다.
- 이 경우가 아니라면 컴파일러는 인라이닝을 금지시킨다.
- 둘 이상의 람다를 인자로 받는 함수에서 일부 람다만 인라이닝하고 싶을때는 notinline 변경자를 파라미터 이름 앞에 붙여서 인라이닝을 금지할 수 있다.
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
//
}
- 코틀린에서는 어떤 모듈이나 서드파티 라이브러리 안에서 인라인 함수를 정의하고 그 모듈이나 라이브러리 밖에서 해당 인라인 함수를 사용할 수 있다.
8.2.3 컬렉션 연산 인라이닝
- 람다 사용
data class Person(val name: String, val age: Int)
fun main() {
val people = listOf(Person("alice", 30), Person("bob", 20))
println(people.filter { it.age < 30 }) // [Person(name=bob, age=20)]
}
- 람다 미사용
data class Person(val name: String, val age: Int)
fun main() {
val people = listOf(Person("alice", 30), Person("bob", 20))
val result = mutableListOf<Person>()
for (person in people) {
if (person.age < 30) result.add(person)
}
println(result) // [Person(name=bob, age=20)]
}
- 코틀린의 filter함수는 인라인 함수다.
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
- filter함수의 바이트코드는 그 함수에 전달된 람다 본문 바이트코드와 함께 filter를 호출한 위치에 들어간다.
- 그 결과 앞 예제에서 filter를 써서 생긴 바이트코드와 뒤 예제에서 생긴 바이트코드는 거의 같다.
println(people.filter { it.age < 30 }.map(Person::name)) // [bob]
- filter, map은 둘 다 inline 함수이다. 두 함수의 본문은 인라이닝되며, 추가 객체나 클래스 생성은 없다.
- 그러나 리스트를 걸러낸 결과를 저장하는 중간 리스트를 만든다.
- 처리할 원소가 많아지면 중간 리스트를 사용하는 부가 비용도 걱정할 만큼 커진다.
- asSequence를 통해 리스트대신 시퀀스를 사용하면 중간 리스트로 인한 부가 비용은 줄어든다.
- 그러나 시퀀스는 람다를 저장해야 하므로 람다를 인라인하지 않는다.
- 따라서 지연 계산을 통해 성능을 향상시키려는 이유로 모든 컬렉션 연산에 asSequence를 붙여서는 안 된다.
- 시퀀스를 통해 성능을 향상시킬 수 있는 경우는 컬렉션 크기가 큰 경우다.
8.2.4 함수를 인라인으로 선언해야 하는 경우
- 일반 함수 호출의 경우 JVM은 코드 실행을 분석해서 가장 이익이 되는 방향으로 호출을 인라이닝한다.
- 반면에 코틀린 인라인 함수는 바이트코드에서 각 함수 호출 지점을 함수 본문으로 대치하기 떄문에 코드 중복이 생긴다.
람다를 인자로 받는 함수를 인라이닝하면 얻는 이득
- 함수 호출 비용을 줄일 수 있을 뿐만 아니라 람다를 표현하는 클래스, 람다 인스턴스에 해당하는 객체를 만들 필요도 없어진다.
- 현재의 JVM은 함수 호출과 람다를 인라이닝해 줄 정도로 똑똑하지 못하다.
- 인라이닝을 사용하면 일반 람다에서는 사용할 수 없는 몇 가지 기능을 사용할 수 있다.
주의 할 점
- 인라이닝하는 함수가 큰 경우 함수의 본문에 해당하는 바이트코드를 모든 호출 지점에 복사해 넣고 나면 바이트코드가 전체적으로 아주 커질 수 있다.
8.3 고차 함수 안에서 흐름 제어
8.3.1 람다 안의 return 문: 람다를 둘러싼 함수로부터 반환
data class Person(val name: String, val age: Int)
fun lookForAlice(people: List<Person>) {
people.forEach { // 람다의 본문
if (it.name == "Alice") {
println("found")
return;
}
}
println("not found")
}
fun main() {
val people = listOf(Person("Alice", 29), Person("Bob", 39))
lookForAlice(people) // found
}
- 람다 안에서 return을 사용하면 그 람다를 호출하는 함수가 실행을 끝내고 반환된다.
- 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 블록을 반환하게 만드는 return문을 넌로컬 return이라 부른다.
- 이 경우 return문은 for 루프나 synchronized 블록을 끝내지않고 메소드를 반환시킨다.
- 하지만 인라이닝되지 않는 함수에 전달되는 람다 안에서 return을 사용할 수는 없다.
8.3.2 람다로부터 반환: 레이블을 사용한 return
- 로컬 return은 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 계속 이어간다.
- 로컬 return과 넌로컬 return을 구분하기 위해 레이블을 사용해야 한다.
fun lookForAlice(people: List<Person>) {
people.forEach label@ {
if (it.name == "Alice") return@label;
}
println("Alice might me somewhere")
}
fun main() {
val people = listOf(Person("Alice", 29), Person("Bob", 39))
lookForAlice(people) // Alice might me somewhere
}
- 무명 함수를 이용해서 장황한 넌로컬 반환문이나 람다 안에서 여러 위치에 return 문이 들어가는 경우에도 코드를 쉽게 작성할 수 있다.
8.3.3 무명 함수: 기본적으로 로컬 return
- return 식은 fun 키워드로 정의된 함수를 반환시킨다.
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") return // lookForAlice 반환
}
}
fun lookForAlice(people: List<Person>) {
people.forEach(fun (person) {
if (person.name == "Alice") return // 바로 위의 무명 함수 반환
})
}
8.4 요약
- 인라인 함수를 컴파일할 때 컴파일러는 그 함수의 본문과 그 함수에게 전달된 람다의 본문을 컴파일한 바이트코드를 모든 함수 호출 지점에 삽입해준다.
- 이렇게 만들어지는 바이트코드는 람다를 활용한 인라인 함수 코드를 풀어서 직접 쓴 경우와 비교할 때 아무 부가 비용이 들지 않는다.
- 인라인 함수안에서는 람다 안에 있는 return문이 바깥쪽 함수를 반환시키는 넌로컬 return을 사용할 수 있다.
- 무명 함수는 람다 식을 대신할 수 있으며 return 식을 처리하는 규칙이 일반 람다 식과는 다르다.
'kotlin' 카테고리의 다른 글
[jackson] 데이터가 null일때 empty list로 바꾸는 옵션 (0) | 2022.09.19 |
---|---|
11. DSL 만들기 (0) | 2022.04.28 |
10. 애노테이션과 리플렉션 (0) | 2022.03.26 |
9. 제네릭스 (0) | 2022.02.28 |
7. 연산자 overloading과 관례 (0) | 2022.01.23 |