이 글은 Kotlin In Action을 참고 하였습니다.
더욱 자세한 설명이나 예제는 직접 책을 구매하여 확인 하시기 바랍니다
8. 고차함수
val add: (Int, Int) -> Int = {a,b -> a + b}val action: () -> Unit = { println("짜란~") }
함수타입은 생략할 수도 있으나, 명시하려면 (인자1:타입, 인자2:타입....) -> 반환타입 으로 표기할 수 있습니다.
인자는 괄호로 묶여야 합니다.
Unit은 반환값이 없음을 나타내는 타입입니다. 실제 타입없이 사용할때는 없어도 되지만 타입을 명시할 때는 필요합니다.
val returnNull = (Int, String) -> String? = { null }
val lambdaNull : ((Int, String) -> String)? = null
위 두개의 type은 return에 null을 허용하거나, 인자 자체에 null을 허용할때를 표현한 예제 입니다.
※ 함수 타입에 인자를 넣더라도 컴파일시 무시됩니다. 하지만 인자가 있음으로 해서 가독성을 높일 수 있습니다. (타입의 인자와 사용된 인자의 이름역시 달라도 상관 없습니다.)
8.1.2 인자로 받은 함수 호출
fun twoAndThree(operation: (Int, Int) -> Int) {
val result = operation(2, 3)
println("The result is $result")
}
fun main(args: Array) {
twoAndThree { a, b -> a + b }
twoAndThree { a, b -> a * b }
}
호출하는 방법은 일반 함수 호출하는것과 같습니다.
8.1.3 자바에서 코틀린 고차함수 호출
- Function0<R> : 인자가 없을때
- Function1<T,R>: 인자가 하나일때
- Function2<T,V,R>: 인자가 두개일때
- FunctionN<N1,N2...,R> 인자가 N개일때
//자바
twoAndThree(x -> x+1)
// 또는
twoAndThree(new Function1 {
@Override
public Integer invoke(Integer number) {
System.out.println(number)
return number +1
}
}
또한 코틀린의 기본 라이브러리 확장 함수도 자바에서 호출할 수 있습니다.
다만 이런경우 receiver object를 같이 넘겨줘야 합니다.
//자바
List lists = new ArrayList<>();
list.add("abc");
list.add("def");
CollectionKt.forEach(lists, str -> {
System.out.prinlnt(str);
return Unit.INSTANCE;
}
또한 람다의 반환값이 Unit은 반드시 반환해야 합니다. (자바의 void를 return할 수 없습니다.)
8.1.4 lambda 인수에 default 값 지정
다른 parameter와 마찬가지로 함수타입 인자로 기본값을 지정해 줄수 있습니다.
fun Collection.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 main(args: Array) {
val letters = listOf("Alpha", "Beta")
println(letters.joinToString())
println(letters.joinToString { it.toLowerCase() })
println(letters.joinToString(separator = "! ", postfix = "! ",
transform = { it.toUpperCase() }))
}
만약 null이 될수있는 함수타입이라면 내부적으로 null check을 한 후에 사용해야 합니다.
8.1.5 함수를 반환
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) {
val calculator = getShippingCostCalculator(Delivery.EXPEDITED)
println("Shipping costs ${calculator(Order(3))}")
}
8.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()
fun main(args: Array) {
println(averageWindowsDuration)
}
위 함수는 특정 OS의 평균 접속시간을 구하는 코드 입니다.
이를 Top-level function으로 바꾸고 OS도 인자로 넣을수 있도록 바꿔보겠습니다.
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)
)
fun List.averageDurationFor(os: OS) =
filter { it.os == os }.map(SiteVisit::duration).average()
fun main(args: Array) {
println(log.averageDurationFor(OS.WINDOWS))
println(log.averageDurationFor(OS.MAC))
}
8.2 inline function
inline func <T> synchronized(lock: Lock, action: () -> T) :T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
fun foo(string: Array<String>) {
val lock = Lock()
println("start")
synchronized(lock) {
println("lambda")
}
println("end")
}
실제 위 함수가 컴파일되면 아래와 같이 됩니다.
fun __foo__(l: lock) {
println("start")
try {
println("lambda")
}
finally {
lock.unlock()
}
println("end")
}
즉 함수 본문 뿐만 아니라 람다의 구문도 그대로 녹아 들어갑니다.
단! 람다 구문이 외부 변수(lambda capturing)을 한다면, 람다부분의 코드는 호출함수에 녹아들지는 않습니다. (그냥 람다식을 호출 하도록 바이트 코드가 변환됨)
8.2.2 Inline function limitation
inline으로 사용하려는 함수가 수신된 함수타입 인자를 바로 실행하지 않고, 다른 변수에 저장하고 나중에 그 변수를 사용하는경우에는 inline 함수로 만들 수 없습니다.
즉, 함수타입 인자를 바로 호출하지 않고, 어딘가에 저장하여 사용한다면, 그 함수는 inline 함수가 될 수 없습니다.
이때 컴파일러는 관련 에러를 출력합니다.
list를 operation 할때 sequence로 만들면 filter, map 함수는 더이상 inline 함수가 아닙니다.
sequence는 최종 연산자를 만나야만 연산이 시작되므로 그전까지 해당 동작을 변수나, 다음 객체 생성자에 담아두고 있어야 합니다.
따라서 sequence의 filter, map 등의 함수는 inline 될수 없습니다.
또한 함수의 인자로 여러개의 함수타입을 받을때 특정 람다만 inline을 하고 싶지 않다면 noinline 키워드를 붙이면 됩니다.
inline fun foo(a: () -> Unit, noinline b: () -> String) {...}
위 코드의 경우 컴파일시 a()는 코드는 inlining 되고, b()는 함수 호출로 변경됩니다.
8.2.3 Collection 연산 inlining
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
fun main(args: Array) {
println(people.filter { it.age > 30 }
.map(Person::name))
}
filter와 map은 inline 함수이기 때문에 해당 코드가 호출된 코드로 녹아들어갑니다.
따라서 직접 for문을 돌리는것과 비용면에서는 차이가 없습니다.
다만 두 함수 모두, 중간연산으로 값을 내어 놓습니다.
filter 수행의 결과를 list로 반환되며, 그 결과로 map을 수행하고 또다른 list를 반환합니다.
따라서 중간 수행 결과를 생성하는것에 대한 비용은 발생합니다.
이를 방지하기 위해 asSequnce()를 사용하면 중간 리스트를 생성하는 비용을 아낄수 있습니다.
중간에 수행해야할 람다식은 객체의 필드에 저장되면 최종 연산자를 만났을때 호출되면서 사용됩니다.
따라서 중간생성비용은 아낄수 있지만 람다가 inline되지는 않습니다.
결론적으로 원소의 개수가 작을경우 중간결과 리스트 생성 비용보다 inline되지 않는 함수를 호출하는 비용이 더 들수도 있습니다.
따라서 asSequence() 원소가 많은 경우에만 사용해야 합니다.
8.2.4 inline으로 선언해야 함수
- 일반함수의 경우 JVM의 바이트 코드를 기계어로 변경하는 JIT compiler가 inlining을 강력하게 지원 합니다. 따라서 bytecode에는 함수호출로 보일지라도 실제 JIT까지 컴파일되면 코드가 호출됨에 있어 가장 이익이 되는 형태로 inlining을 진행합니다.
- inline 함수는 코드 내용이 호출함수쪽으로 녹아들어 갑니다. 따라서 여기저기 호출부분마다 같은 코드가 추가되면서 중복이 생깁니다. 따라서 내용이 길고 여러곳에서 호출되는 inline 함수는 바이트 코드량을 증가시킵니다.
- inline 하지 않으면 함수를 직접 호출하므로 stack trace가 깔끔합니다.
- 람다가 있는 함수는 JVM이 아직 똑똑하게 inlining을 지원하지 않고 함수로 호출합니다. 따라서 적절한곳에 inline을 사용한다면 부가적인 객체생성을 막고 함수 호출 비용을 줄일 수 있습니다.
- inline 함수는 non-local return이 가능합니다. 이는 8.3에서 설명합니다.
8.2.5 resource 자동관리
fun readFirstLineFromFile(path: String): String {
BufferedReader(FileReader(path)).use { br ->
return br.readLine()
}
}
use를 쓰게되면 finally에서 close를 강제로 해야 된다든가 하는 번거로움이 없어집니다.
안드로이드에서는 cursor를 쓸때 정말 유용합니다.
use는 exception이 발생하여 비정상 종료 되더라고 close를 호출하도록 되어있으니 안심하고 사용해도 됩니다.
8.3 High order function에서의 흐름제어
8.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) {
people.forEach {
if (it.name == "Alice") {
println("Found!")
return
}
}
println("Alice is not found")
}
fun main(args: Array) {
lookForAlice(people)
}
forEach를 사용하여 람대 내부에 return을 넣었습니다.
이 결과는 "Found" 입니다. ("Alice is not found"는 출력되지 않습니다.)
사실 forEach 대신에 그냥 for문을 사용했더라도 같은 동작이 일어납니다.
즉 람다 내부에서 return을 사용하면 람다뿐만 아니라 외부의 function까지도 종료됩니다. 이런걸 non-local return 이라고 합니다.
(자신의 외부함수까지 종료함)
이는 자바와 같은 return 동작을 하도록 코틀린 compile가 만들어주기 때문이며, 이렇게 람다 내부에 return을 쓸수 있는건 inline 함수뿐입니다.
8.3.2 label을 이용한 local return
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
fun lookForAlice(people: List) {
people.forEach label@{
if (it.name == "Alice") return@label
}
println("Alice might be somewhere")
}
fun main(args: Array) {
lookForAlice(people)
}
람다 뒤에 @라벨 을 붙이고, return뒤에 @라벨명을 다시 붙이면 해당 람다만 빠져 나오는 return이 됩니다.
라벨을 선언하지 않는나면 람다식 이름으로 사용이 가능합니다.
...
fun lookForAlice(people: List) {
people.forEach {
if (it.name == "Alice") return@forEach
}
println("Alice might be somewhere")
}
...
this에도 lable을 붙여서 사용할 수 있습니다.
println(StringBuilder().apply sb@ {
listOf (12,3).apply {
// this로 reciever object(listOf)에 접근하고 @sb로 다시 StringBuilder에 접근했다.
this@sb.append(this.toString())
}
}
// 결과
[1,2,3]
8.3.3 무명 함수의 local return
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
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>) {
lookForAlice(people)
}
무명 함수는 함수명과 타입을 생략할 수 있습니다.
코드 작성중간에 local return을 많이 해야하는 경우 무명함수를 이용하면 편리합니다.
'개발이야기 > Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린 constructor vs init block (2) | 2018.05.29 |
---|---|
[Kotlin] 코틀린 Generic #1 (1) | 2018.05.12 |
[Kotlin] 코틀린 연산자 오버로딩 #2 컬렉션, in, rangeTo, iterator, destructuring, Property delegation, by (0) | 2018.05.06 |
[Kotlin] 코틀린 연산자 오버로딩 #1 - 산술연산자, 비트연산자, equals, compareTo (3) | 2018.05.03 |
[Kotlin] 코틀린 Collection과 배열 (3) | 2018.05.02 |