[Kotlin in Action] part8_고차함수: 파라미터와 반환 값으로 람다 사용
고차 함수 정의
고차 함수 : 다른 함수를 인자로 받거나 함수를 반환하는 함수
=> 코틀린에서는 람다나 함수 참조를 사용해 함수를 값으로 표현할 수 있다.
함수 타입
람다를 인자로 받는 함수를 정의하려면 -> 람다 인자의 타입을 어떻게 선언?
=>변수 타입을 지정하지 않아도 람다를 변수에 대입할 수 있기 때문에
함수 타입 정의: 함수 파라미터의 타입을 괄호 안에 넣고, 그 뒤에 화살표(->) 추가, 함수의 반환 타입 지정.
=> 변수 타입을 함수 타입으로 지정하면 -> 파라미터로부터 람다의 파라미터 타입을 유추할 수 있다.
인자로 받은 함수 호출
//인자로 받은 함수를 호출
fun twoAndThree(operation: (Int, Int) -> Int) {
val result = operation(2, 3)
println("The result is $result")
}
>>> twoAndThree { a, b ->a+b}
The result is 5
>>> twoAndThree { a, b ->a*b}
The result is 6
인자로 받은 함수를 호출하는 구문:
(=일반 함수를 호출하는 구문) 함수 이름 + (인자 , 인자)
자바에서 코틀린 함수 타입 사용
코틀린에서 함수 인자에 따라 Function<> 인터페이스를 제공, 그 중 invoke 메서드를 호출하면 함수 실행 가능.
함수 타입을 사용하는 코틀린 함수를 자바에서도 호출 가능
=> 자바8 람다를 넘기면 됨.
/* Kotlin declaration */
fun processTheAnswer(f: (Int) -> Int) {
println(f(42))
}
/* Java */
>>> processTheAnswer(number -> number + 1);
43
-> 자바에서 코틀린 라이브러리의 람다를 인자로 받는 확장 함수를 쉽게 호출 가능
ㄴ but, 코틀린에서처럼 깔끔하지는 않음.
디폴트 값을 지정한 함수 타입 파라미터나 널이 될 수 있는 함수 타입 파라미터
<파라미터를 함수 타입으로 할 때 디폴트 값 설정>
=> 디폴트 값으로 람다 식을 넣으면 됨.
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
transform: (T) -> String = { it.toString() } //함수 타입 파라미터 선언, 람다를 디폴트 값 지정
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(transform(element)) //"transform" 파라미터로 받은 함수를 호출
}
result.append(postfix)
return result.toString()
}
>>> val letters = listOf("Alpha", "Beta")
>>> println(letters.joinToString())
Alpha, Beta
>>> println(letters.joinToString { it.toLowerCase() })
alpha, beta
>>> println(letters.joinToString(separator = "! ", postfix = "! ",
... transform = { it.toUpperCase() }))
ALPHA! BETA!
=> 디폴트 값 설정하는 방법 : = 뒤에 람다를 넣으면 됨.
<파라미터를 함수 타입으로 할 때 널이 될 수 있는 함수 타입>
*** 널이 될 수 있는 함수 타입으로 함수를 받으면 그 함수를 직접 호출할 수는 없다.
=> 해결: 널 여부를 명시적으로 검사
fun foo(callback: (() -> Unit)?) {
// ...
if (callback != null) {
callback()
}
}
함수를 함수에서 반환
함수가 함수를 반환해야 할 때 -> 적절한 로직을 사용한다.
enum class Delivery { STANDARD, EXPEDITED }
class Order(val itemCount: Int)
fun getShippingCostCalculator(
delivery: Delivery): (Order) -> Double { //함수를 반환하는 함수를 선언
if (delivery == Delivery.EXPEDITED) {
return { order -> 6 + 2.1 * order.itemCount } // 함수에서 람다 반환
}
return { order -> 1.2 * order.itemCount } // 함수에서 람다 반환
}
>>> val calculator = // 반환받은 함수를 변수에 저장
... getShippingCostCalculator(Delivery.EXPEDITED)
>>> println("Shipping costs ${calculator(Order(3))}") // 반환받은 함수를 호출
Shipping costs 12.3
함수의 반환 타입으로 함수 타입을 지정해야 한다.
=> return 식에 람다나 멤버 참조나 함수 타입의 값을 계산하는 식 등을 넣으면 된다.
람다를 활용한 중복 제거
람다를 활용하면 코드가 중복이 될 때 간결하게 만들 수 있다.
// 윈도우 사용자의 평균 방문 시간 출력
val averageWindowsDuration = log
.filter { it.os == OS.WINDOWS }
.map(SiteVisit::duration)
.average()
>>> println(averageWindowsDuration)
23.0
-> 중복을 피하기 위해 OS를 파라미터로 설정한다.
//맥 사용자의 평균 방문 시간 출력
fun List<SiteVisit>.averageDurationFor(os: OS) = //중복 코드를 별도 함수로 추출한다.
filter { it.os == os }.map(SiteVisit::duration).average()
>>> println(log.averageDurationFor(OS.WINDOWS))
23.0
>>> println(log.averageDurationFor(OS.MAC))
22.0
=> 람다를 이용해서 함수 타입을 사용 -> 더 복잡한 중복을 제거할 수 있다.
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()
>>> println(log.averageDurationFor {
... it.os in setOf(OS.ANDROID, OS.IOS) })
12.15
>>> println(log.averageDurationFor {
... it.os == OS.IOS && it.path == "/signup" })
8.0
인라인 함수: 람다의 부가 비용 없애기
람다가 변수를 포획하면 람다가 생성되는 시점마다 무명 클래스 객체가 생기므로 람다 사용은 조금 효율적이지 못하다는 단점이 있다.
=> 해결: inline 변경자를 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔준다.
인라이닝이 작동하는 방식
함수가 inline 되면 그 함수를 호출하는 코드는 함수 본문을 번역한 바이트코드로 컴파일된다.
-> 이 함수는 Lock 객체를 잠그고 주어진 코드 블록을 실행한 다음 Lock 객체에 대한 잠금을 해제한다.
// 인라인 함수 정의
inline fun <T> synchronized(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
val l = Lock()
synchronized(l) {
// ...
}
ㄴ synchronized 함수를 inline으로 선언
-> synchronized를 호출하는 코드는 모두 자바의 synchronized와 같아진다.
synchronized()를 사용하는 코드:
fun foo(l: Lock) {
println("Before sync")
synchronized(l) {
println("Action")
}
println("After sync")
}
=> synchronized()의 본문 뿐만 아니라 synchronized에 전달된 람다의 본문도 함께 인라이닝된다.
-> 인라인 함수를 호출 할 때 람다를 넘기는 대신 함수 타입의 변수를 넘길 수도 있다.
class LockOwner(val lock: Lock) {
fun runUnderLock(body: () -> Unit) {
synchronized(lock, body) //람다 대신 함수 타입 변수를 넣는다.
}
}
인라인 함수의 한계
람다를 사용하는 모든 함수를 인라이닝하지는 못한다.
-> 파라미터로 전달받은 람다를 본문에 사용하는 방식에 한계가 있다.
함수를 인라인으로 선언해야 하는 경우
람다를 인자로 받는 함수를 인라이닝하는 것이 다른 경우보다 더 효율적이다.
-> 인라이닝을 통해 효율을 높일 수 있다.
-> 일반 람다에서는 사용할 수 없는 몇 가지 기능을 더 사용할 수 있다. (넌로컬 반환 등)
자원 관리를 위해 인라인된 람다 사용
자원 관리: 어떤 작업을 하기 전에 자원을 획득하고 작업을 마친 후 자원을 해제
ㄴ 람다로 중복을 없앨 수 있는 일반적인 패턴
ㄴ 주로 사용하는 방식: try/finally 문 -> try문 이전에 자원 획득, finally 블록에서 자원 해제
->use 함수를 자원 관리에 활용하기
fun readFirstLineFromFile(path: String): String {
BufferedReader(FileReader(path)).use { br ->
return br.readLine()
}
}
use 함수: closable 자원에 대한 확장 함수, 람다를 인자로 받는다. 람다를 호출한 다음에 자원을 닫아준다.
고차 함수 안에서 흐름 제어
람다 안의 return 문: 람다를 둘러싼 함수로부터 반환
넌로컬(non-local) 리턴: 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return 문
-> return 이 바깥쪽 함수를 반환시킬 수 있는 때는 람다를 인자로 받는 함수가 인라인 함수인 경우 뿐
but, 인라이닝되지 않는 함수에 전달되는 람다 안에서 return을 사용할 수는 없음.
람다로부터 반환: 레이블을 사용한 return
람다 식에서의 로컬 return: for 루프의 break와 비슷한 역할을 한다.
로컬 return은 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 이어간다.
-> 로컬 리턴과 넌로컬 리턴을 구분하려면: 레이블 사용
=> 리턴으로 끝내고 싶은 람다 식 앞에 레이블을 붙이고, return 키워드 뒤에 그 레이블 추가.
fun lookForAlice(people: List<Person>) {
people.forEach label@{
if (it.name == "Alice") return@label
}
println("Alice might be somewhere")
}
>>> lookForAlice(people)
Alice might be somewhere
-> 람다 식에 레이블을 넣으려면 이름 뒤에 @문자를 추가한 것을 람다를 여는 { 앞에 붙이며 된다.
무명 함수: 기본적으로 로컬 return
무명 함수: 코드 블록을 함수에 넘길 때 사용
fun lookForAlice(people: List<Person>) {
people.forEach(fun (person) {
if (person.name == "Alice") return
println("${person.name} is not Alice")
})
}
>>> lookForAlice(people)
Bob is not Alice
-> 무명 함수와 일반 함수의 차이: 무명 함수는 함수 이름, 파라미터 타입을 생략할 수 있다.
- 블록이 본문인 무명 함수 -> 반환 타입 명시해야 함.
- 식이 본문인 무명 함수 -> 반환 타입 생략 가능
people.filter(fun (person) = person.age < 30)
//식이 본문인 무명 함수