8장에서 다루는 내용
◼︎ 함수 타입
◼︎ 고차 함수와 코드를 구조화할 때 고차함수를 사용하는 방법
◼︎ 인라인 함수
◼︎ 비로컬 return과 레이블
◼︎ 무명 함수
고차함수는 람다를 인자로 받거나 반환하는 함수이다.
인라인 함수는 람다를 사용함에 따라 발생할 수 있는 성능상 부가비용을 없애고 람다 안에서 더 유연하게 흐름을 제어할 수 있다.
고차 함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수이다. 코틀린에서는 람다나 함수 참조를 사용해 함수를 값으로 표현할 수 있다. 따라서 고차함수는 람다나 함수 참조를 인자로 넘길 수 있거나 람다나 함수 참조를 반환하는 함수이다.(함수를 인자로 받는 동시에 반환도 가능하다)
람다를 인자로 받는 함수를 정의하려면 먼저 람다 인자의 타입을 어떻게 선언할 수 있는지 알아야 한다.
앞서 코틀린의 타입추론으로 인해 변수 타입을 지정하지 않아도 람다를 변수에 대입할 수 있음을 알게 되었다.
val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }
함수 타입을 정의하려면 함수 파라미터의 타입을 괄호 안에 넣고, 그 뒤에 화살표(→)를 추가한 다음, 함수의 반환 타입을 지정하면 된다.

그냥 함수를 정의한다면 함수의 파라미터 목록 뒤에 오는 Unit 반환 타입지정을 생략해도 되지만, 함수의 타입을 선언할 때는 반환타입을 반드시 명해야 하므로 Unit을 빼먹어서는 안된다.
다른 함수와 마찬가지로 함수 타입에서도 반환타입을 널이 될 수 있는 타입으로 지정할 수 있다.
val canReturnNull: (Int, Int) -> Int? = { x, y -> null }
널이 될 수 있는 함수 타입 변수를 정의할 수도 있다. 함수 타입 전체가 널이 될 수 있는 타입임을 선언하기 위해 함수 타입을 괄호로 감싸고 그 뒤에 물음표를 붙여야만 한다.
var funOrNull: ((Int, Int) -> Int) ?= null
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}
twoAndThree { a, b -> a * b }
}
String에 대한 filter를 구현해보자.

filter 함수는 술어를 파라미터로 받는다. predicate 파라미터는 문자(Char 타입)를 파라미터로 받고 불리언(Boolean 타입) 결과 값을 반환한다. 술어는 인자로 받은 문자가 filter 함수가 돌려주는 결과 문자열에 남아 있기를 바란다면 true를 반환하고 문자열에서 사라지기를 바라면 false를 반환하면 된다. ????
fun String.filter(predicate: (Char) -> Boolean) : String{
val sb = StringBuilder()
for(index in 0 until length){
val element = get(index)
if(predicate(element)) sb.append(element)
}
return sb.toString()
}
fun main(args: Array<String>) {
println("ab1c".filter { it in 'a'..'z' })
}
💡 인텔리J 아이디어는 디버깅할 때 람다 코드 내부를 한단계씩 실행해 볼수 있는 스마트 스테핑을 제공한다!
fun processTheAnswer(f: (Int) -> Int){
println(f(42))
}
/*
public final class ProcessKt {
public static final void processTheAnswer(@NotNull Function1 f) {
Intrinsics.checkNotNullParameter(f, "f");
int var1 = ((Number)f.invoke(42)).intValue();
boolean var2 = false;
System.out.println(var1);
}
}
*/
public class Main {
public static void main(String[] args) {
ProcessKt.processTheAnswer(number-> number+1);
}
}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()
}
위 함수는 제네릭 함수다. 컬렉션의 원소 타입을 표현하는 T를 타입 파라미터로 받는다. transform 람다는 그 T 타입의 값을 인자로 받는다.
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()
}
함수가 함수를 반환할 필요가 있는 경우보다는 함수가 함수를 인자로 받아야 할 필요가 훨씬 더 많다. 아래 코드는 사용자가 선택한 배송 수단에 따라 배송비를 계산하는, 적절한 로직을 선택해서 함수로 반환하는 함수이다.
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 }
}
fun main(args: Array<String>) {
val calculator = getShippingCostCalculator(Delivery.STANDARD)
println("Shipping costs ${calculator(Order(3))}")
}
다른 함수를 반환하는 함수를 정의하려면 함수의 반환 타입으로 함수 타입을 지정해야 한다. 위 getShippingCostCalculator 함수는 Order를 받아서 Double을 반환하는 함수를 반환한다. 함수를 반환하려면 return 식에 람다나 멤버 참조나 함수 타입의 값을 계산하는 식 등을 넣으면 된다.
data class SiteVisit(
val path: String,
val duration: Double,
val os: OS
)
enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }
fun List<SiteVisit>.averageDurationFor(os: OS) =
filter { it.os == os }.map(SiteVisit::duration).average()
fun main(args: Array<String>) {
val log = listOf(
SiteVisit("/", 34.0, OS.WINDOWS),
SiteVisit("/", 22.0, OS.MAC),
SiteVisit("/", 12.0, OS.WINDOWS),
SiteVisit("/", 8.0, OS.IOS),
SiteVisit("/", 16.3, OS.ANDROID)
)
val averageWindowsDuration = log
.filter { it.os == OS.WINDOWS }
.map(SiteVisit::duration)
.average()
val averageMobileDuration = log
.filter { it.os in setOf(OS.IOS, OS.ANDROID) }
.map(SiteVisit::duration)
.average()
println(log.averageDurationFor(OS.WINDOWS))
println(log.averageDurationFor(OS.MAC))
println(averageMobileDuration)
println(averageWindowsDuration)
}
5장에서 코틀린이 보통 람다를 무명 클래스로 컴파일하지만 그렇다고 람다식을 사용할 때마다 새로운 클래스가 만들어지는 것은 아니다. 람다가 변수를 포획하면 람다가 생성되는 시점마다 새로운 무명 클래스 객체가 생긴다. 이런 경우 실행 시점에 무명 클래스 생성에 따른 부가 비용이 든다. 이때 inline 변경자를 어떤 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트 코드로 바꿔치기 해준다.
어떤 함수를 inline으로 선언하면 그 함수의 본문이 인라인(inline)된다. 다른 말로 하면 함수를 호출하는 코드를 함수를 호출하는 바이트코드 대신에 함수 본문을 번역한 바이트 코드로 컴파일 된다.
inline fun <T> synchronized(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
val l = Lock()
synchronized(l) {
/ ...
}
inline 키워드의 이점을 배우고 나면 코드를 더 빠르게 만들기 위해 코드 여기저기에서 inline을 사용하고 싶어질 것이다. inline 키워드를 사용해도 람다를 인자로 받는 함수만 성능이 좋아질 가능성이 높기 때문에 좋지 못하다.
fun readFirstLineFromFile(path: String): String {
BufferedReader(FileReader(path)).use { br ->
return br.readLine()
}
}
//fun lookForAlice(people: List<Person>){
// for(person in people){
// if(person.name == "Alice"){
// println("Found!")
// return
// }
// }
//}
fun lookForAlice(people: List<Person>){
people.forEach {
if(it.name == "Alice"){
println("Found!")
return
}
}
}
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
lookForAlice(people)
}람다 식에서도 로컬(local) return을 사용할 수 있다.
람다 안에서 로컬 return은 for 루프의 break와 비슷한 역할을 한다.
로컬 return은 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 계속 이어간다.
로컬 return과 넌로컬 return을 구분하기 위해 레이블(label)을 사용해야 한다.
return으로 실행을 끝내고 싶은 람다 식 앞에 레이블을 붙이고, return 키워드 뒤에 그 레이블을 추가하면 된다.

fun lookForAlice(people: List<Person>){
people.forEach label@{
if(it.name == "Alice") return@label
}
println("Alice might be somewhere")
}
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
lookForAlice(people)
}
람다에 레이블을 붙여서 사용하는 대신 람다를 인자로 받는 인라인 함수의 이름을 return 뒤에 레이블로 사용해도 된다.
fun lookForAlice(people: List<Person>){
people.forEach{
if(it.name == "Alice") return@forEach
}
println("Alice might be somewhere")
}
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
lookForAlice(people)
}
fun lookForAlice(people: List<Person>){
people.forEach(fun (person){
if( person.name == "Alice") return
println("${person.name} is not Alice")
})
}
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
lookForAlice(people)
}println(people.filter(fun (person): Boolean{
return person.age < 30
}))무명함수도 일반 함수와 같은 반환 타입 지정 규칙을 따른다. 위 코드처럼 블록이 본문인 무명 함수는 반환 타입을 명시해야 하지만, 식을 본문으로 하는 무명 함수의 반환 타입은 생략할 수 있다.
println(people.filter(fun (person) = person.age < 30))