val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }
컴파일러는 sum과 action이 함수 타입임을 추론
val sum: (Int, Int) -> Int = { x, y -> x + y } // Int 파라미터를 2개 받아서 Int 값을 반환하는 함수
val action: () -> Unit = { println(42) } // 아무 인자도 받지 않고 아무 값도 반환하지 않는 함수
함수 타입을 정의하려면 함수 파라미터의 타입을 팔호 안에 넣고 그 뒤에 화살표(->)를 추가한 다음, 함수의 반환 타입을 지정
var canReturnNu11: (Int, Int) -> Int? = { x, y -> null }
다른 함수와 마찬가지로 함수 타입에서도 반환 타입을 널이 될 수 있는 타입으로 지정할 수 있다.
var funOrNull: ((Int, Int) -> Int)? = null
함수 전체가 널이 될 수 있는 타입임을 선언하기 위해 함수 타입을 괄호로 감싸고 그 뒤에 물음표를 붙여야만 한다.
🔖 10.1.2 인자로 전달 받은 함수 호출
fun twoAndThree(operation: (Int, Int) -> Int) {
val result = operation(2, 3)
println("The result is $result")
}
fun main() {
twoAndThree { a, b -> a + b }
// The result is 5
twoAndThree { a, b -> a * b }
// The result is 6
}
인자로 받은 함수를 호출하는 구문은 일반함수를 호출하는 구문과 같다.
함수 타입에서 파라미터 이름을 지정할 수 있다.
fun String.filter(predicate: (Char) -> Boolean): String {
return buildString {
for (char in this@filter) {
if (predicate(char)) append(char)
}
}
}
filter 함수는 술어를 파라미터로 받는다.
확장 함수와 buildString 함수 모두 수신 객체를 정의하기 때문에 this 앞에 레이블읇 붙여 buildString 람다의 수신 객체가 아니라 filter의 바깥쪽 수신 객체에 접근해야 한다.
🔖 10.1.3 자바에서 코틀린 함수 타입 사용
fun processTheAnswer (f: (Int) -> Int) {
printin(f(42))
}processTheAnswer(number -> number + 1);
자동 SAM 변환을 통해 코틀린 람다를 함수형 인터페이스를 요구하는 자바 메서드에게 넘길 수 있다. 즉, 자바에서 정의된 고차 함수를 아무 문제 없이 사용할 수 있다는 의미다.
자바에서 람다를 인자로 받는 코틀린 확장함수를 사용할 때는 첫 번째 인자로 수신 객체를 명시적으로 전달해야 한다.
🔖 10.1.4 함수 타입의 파라미터에 대해 기본값을 지정할 수 있고, 널이 될 수도 있다.
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()
}
이 구현은 유연하지만 핵심 요소인 컬렉션의 각 원소를 문자열로 변환하는 방법을 제어할 수 없다는 단점이 있다.
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))
}
result.append(postfix)
return result.toString()
}
함수 타입에 대한 기본값을 선언할 때 특별한 구문이 필요하지는 않다.
= 뒤에 람다를 넣으면 된다.
fun <T> Collection<T>.joinToString(separator: String = ", ", prefix: String = "", postfix: String = "", transform: ((T) -> String)? = null): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
val str = transform?.invoke(element) ?: element.toString()
result.append(str)
}
result.append(postfix)
return result.toString()
}
transform은 넘이 될 수 있는 함수 타입이지만 그 반환 타입은 널이 될 수 없는 타입이다.
transform은 자신이 null이 아니면 String 타입의 널이 아닌 값을 반환한다는 사실을 보장한다.
🔖 10.1.5 함수를 함수에서 반환
fun getShippingCostCalculator(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 = getShippingCostCalculator(Delivery.EXPEDITED)
println("Shipping costs ${calculator(Order(3))}")
}
다른 함수를 반환하는 함수를 정의하려면 함수의 반환 타입으로 함수 타입을 지정하면 된다.
class ContactListFilters {
var prefix: String = ""
var onlyWithPhoneNumber: Boolean = false
fun getPredicate(): (Person) -> Boolean {
val startWithPrefix = { p: Person -> p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)}
if (!onlyWithPhoneNumber) {
return startWithPrefix
}
return { startWithPrefix(it) && it.phoneNumber != null }
}
}
getPredicate method는 filter 함수에 인자로 넘길 수 있는 함수를 반환한다.
고차 함수는 코드 구조를 개선하고 중복을 없앨 때 쓸 수 있는 아주 강력한 도구다.
🔖 10.1.6 람다를 활용해 중복을 줄여 코드 재사용성 높이기
data class SiteVisit(
val path: String,
val duration: Double,
val os: OS
)
enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }
val log = listOf(
SiteVisit("/", 34.0, OS.WINDOWS),
SiteVisit("/", 22.0, OS.MAC),
SiteVisit("/login", 12.0, OS.WINDOWS),
SiteVisit("/signup", 8.0, OS.IOS),
SiteVisit("/", 16.3, OS.ANDROID),
)
val averageWindowsDuration = log
.filter { it.os == OS.WINDOWS }
.map(SiteVisit::duration)
.average()
윈도우 사용자의 평균 방문 시간 출력은 average 함수로 쉽게 표현할 수 있다.
fun List<SiteVisit>.averageDurationFor(os: OS) = filter { it.os == os }.map(SiteVisit::duration).average()
중복을 피하기 위해 OS를 파라미터로 뽑아낼 수 있다.
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) = filter(SiteVisit).map(SiteVisit::duration).average()
코드 중복을 줄일 때 함수 타입이 상당히 도움이 된다.
📖 10.2 인라인 함수를 사용해 람다의 부가 비용 없애기
inline 변경자를 어떤 함수에 붙이면 컴파일러는 그 함수가 쓰이는 위치에 함수 호출을 생성하는 대신에 함수를 구현하는 코드로 바꿔치기 해준다.
🔖 10.2.1 인라이닝이 작동하는 방식
inline fun <T> synchronized(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
fun main() {
val l = ReentrantLock()
synchronized(1) {
// ...
}
}
어떤 함수를 inline으로 선언하면 그 함수의 본문이 인라인된다.
함수를 호출하는 코드를 함수를 호출하는 바이트코드 대신에 함수 본문을 번역한 바이트코드로 컴파일한다는 뜻
synchronized 함수를 inline으로 선언했으므로 synchronized를 호출하는 코드는 모두 자바의 synchronized 문과 같아진다.
class LockOwner(val lock: Lock) {
fun runUnderLock(body: () -> Unit) {
synchronized(lock, body)
}
}
인라인 함수를 호출하면서 람다를 넘기는 대신에 함수 타입의 변수를 넘길 수도 있다.
함수에 더해 프로퍼티 접근자에도 inline을 붙일 수 있다.
🔖 10.2.2 인라인 함수의 제약
class FunctionStorage {
var myStoredFunction: ((Int) -> Unit)? = null
inline fun storeFunction(f: (Int) -> Unit) {
myStoredFunction = f // error
}
}
함수가 인라이닝될 때 그 함수에 인자로 전달된 람다식의 본문은 결과 코드에 직접 들어갈 수 있다. 하지만 이렇게 람다가 본문에 직접 펼쳐지기 때문에 함수가 파라미터로 전달받은 람다를 본문에 사용하는 방식이 한정될 수밖에 없다.
transform 파라미터로 전달받은 함수 값을 호출하지 않는 대신, TransformingSequence라는 클래스의 생성자에게 그 함수 값을 넘긴다.
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
//..
}
인라이닝하면 안 되는 람다를 파라미터로 받는다면 noinline 변경자를 파라미터 이름 앞에 붙여 인라이닝을 금지할 수 있다.
🔖 10.2.3 컬렉션 연산 인라이닝
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
fun main() {
println(people.filter { it.age < 30 })
}
위 예제를 람다식을 사용하지 않게 다시 쓰면 아래와 같다.
fun main() {
val result = mutableListOf<Person>()
for (person in people) {
if (person.age < 30) {
result.add(person)
}
}
println(result)
}
use는 Closeable과 함께 쓰이지만 useLines는 File과 Path 객체에 정의돼 있고, 람다가 문자열 시퀀스에 접근하게 해준다.
📖 10.3 람다에서 반환: 고차 함수에서 흐름 제어
🔖 10.3.1 람다 안의 return 문: 람다를 둘러싼 함수에서 반환
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
fun lookForAlice(people: List<Person>) {
for (person in people) {
if (person.name == "Alice") {
println("Found!")
return
}
}
println("Alice is not found")
}fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") {
println("Found!")
return
}
}
println("Alice is not found")
}
위 두 예제는 같은 의미다.
람다 안에서 return을 사용하면 람다에서만 반환되는 것이 아니라 그 람다를 호출하는 함수가 실행을 끝내고 반환된다.
비로컬 return: 자신을 둘러쌓고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return 문
return이 바깥쪽 함수를 반환시킬 수 있는 때는 람다를 인자로 받는 함수가 인라인 함수인 경우다.
인라이닝되지 않는 함수에 전달되는 람다 안에서 return을 사용할 수는 없다.
🔖 10.3.2 람다로부터 반환: 레이블을 사용한 return
로컬 return: 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 계속 이어간다.
로컬 return과 비로컬 return을 구분하기 위해 레이블을 사용한다.
fun lookForAlice(people: List<Person>) {
people.forEach label@{
if (it.name != "Alice") return@label
println("Found Alice")
}
}
return으로 실행을 끝내고 싶은 람다식 앞에 레이블을 붙인다.
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name != "Alice") return@forEach
println("Found Alice!")
}
}
람다식의 레이블을 명시하면 함수 이름을 레이블로 사용할 수 없다는 점에 유의
🔖 10.3.3 익명 함수: 기본적으로 로컬 return
fun lookForAlice(people: List<Person>) {
people.forEach (fun (person) {
if (person.name == "Alice") return
println("${person.name} is not Alice")
})
}
return은 가장 가까운 함수를 가리킨다.
익명 함수 안에서 레이블이 붙지 않은 return 식은 익명 함수 자체를 반환시킬 뿐 익명 함수를 둘러싼 다른 함수를 반환시키지 않는다.