2023 01 15 코틀린 공부 - 고차함수
8장 고차 함수 : 파라미터와 반환 값으로 람다 사용
고차함수는 다른 함수를 인자로 받거나 반환하는 함수이다. 코틀린에서 고차 함수는 람다나 함수 참조를 인자로 넘길 수 있거나 람다나 함수 참조를 반환하는 함수를 의미한다.
람다로 함수를 만드는 방법은 {파라미터, 파라미터, .... -> 반환값}의 형태로 만든다. 아래의 코드를 보면 이해가 될 것이다.
val sum: (Int, Int) -> Int = { x, y -> x + y }
// val sum = {x:Int, y:Int -> x+y}
첫 줄 처럼 :를 사용해서 파라미터와 반환값의 타입을 지정해도 되고 마지막줄처럼 바로 사용해도 된다
고차함수는 (함수의 파라미터들...)-> 반환값 의 형태를 사용하여 함수를 받을 수 있다. 람다로 이러한 함수를 넘겨 줄 수 있으며 간단한 예제는 아래의 코드를 보자. 아래의 코드에서 (Int, Int)->Int 형식의 함수를 받고 결과를 출력하는 twoAndThree고차함수를 람다식을 이용해서 호출했다.
fun twoAndThree(operation: (Int, Int) -> Int) {
val result = operation(2, 3)
println("The result is $result")
}
twoAndThree { a, b -> a + b }
twoAndThree { a, b -> a * b }
.
컴파일된 코드에서 함수타입은 일반 인터페이스로 바뀌며 함수타입의 변수는 FunctionN 인터페이스를 구현하는 객체를 저장한다. 인자의 개수에 따라 0....N등의 인터페이스를 제공한다. 각 인터페이스는 invoke메서드 정의가 하나 들어있다.
이 메서드로 함수를 실행 할 수 있다. 함수타입인 변수는 FunctionN인터페이스를 구현하는 클래스의 객체를 저장하며 invoke메서드 본문에 람다의 본문이 들어간다. 자바 8람다를 넘기면 함수 타입을 사용하는 코틀린 함수를 자동으로 함수 타입의 값으로 변환된다.
또한 함수 타입 인자에 default값을 줄 수 있고 null이 될 수 있다.
다른 함수를 반환하는 함수는 함수의 반환 타입으로 함수 타입을 지정해야 한다. 아래의 코드의 경우 Order라는 클래스를 받아서 Double을 반환하는 함수를 반환한다.
fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double {
if (delivery == Delivery.EXPEDITED) {
return { order -> 6 + 2.1 * order.itemCount }
}
return { order -> 1.2 * order.itemCount }
}
val calc = getShippingCostCalculator(Delivery.EXPEDITED)
println(calc(Order(4)))
람다를 사용하면 람다가 생성되는 시점마다 새로운 무명 클래스 객체가 생성되어 일반 함수를 사용한 구현보다 비효율적이다. 람다처럼 반복되는 코드를 별도의 라이브러리 함수로 빼내면서 성능도 일반 함수와 비슷하게 만드는 방법이 코틀린 컴파일러에 가는한데 inline변경자를 함수에 붙이면 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔치기 해준다. 컬렉션의 map, filter도 아니라인 함수이다.
이러한 인라인 함수는 일반 함수 호출에 쓸 경우 효율이 더 좋아지는지는 확인해 보아야 한다. 하지만 람다를 인자로 받는 함수는 함수 호출 비용을 줄이고 람다를 표현하는 클래스와 그 객체를 만들 필요가 없어져 효율이 높아진다. 하지만 바이트코드가 전체적으로 커질 수 있다.
람다로 중복을 없앨 수 있는 일반적인 패턴 중 하나는 작업 전에 자원 획득, 작업, 자원을 해제하는 자원 관리다. try, finally전에 자원을 획득하고 try에서 작업, finally에서 자원을 해제한다. 코틀린의 use함수는 닫을 수 있는 자원에 대한 확장함수로 람다를 인자로 받는다. use는 람다를 호출 한 후 자원을 닫아준다. 정상 종료, 예외가 발생해도 확실히 닫아준다. use도 inline함수여서 성능에는 별 지장이 없다고 한다.
람다안에서 return은 람다를 둘러싼 함수로부터 반환하는 것이다. 아래의 코드에서 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")
}
}
lookForAlice(people)
이렇듯 자신을 둘러싸고 있는 블록보다 더 바깥의 블록을 반환하게 하는 return문을 non local return 이라고 한다. 이는 람다를 인자로 받는 함수가 인라인 함수인 경우이다.
이러한 람다의 특징때문에 local return을 위한 label을 사용해야한다 람다 앞에 label이름과 @를 붙이면 return에 @label이름을 붙여서 람다로부터 반환할 수 있다.
val people = listOf(Person("Alice", 29), Person("Bob", 31))
fun lookForAlice(people: List<Person>) {
people.forEach label@ {
if (it.name == "Alice") {
println("Found!")
return@label
}
println("Alice is not found")
}
}
lookForAlice(people)
.다른 방법으로 forEach를 label로 사용할 수 있다.
val people = listOf(Person("Alice", 29), Person("Bob", 31))
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") {
println("Found!")
return@forEach
}
println("Alice is not found")
}
}
lookForAlice(people)
또한 무명함수는 return은 기본적으로 local return 한다.