-
[kotlin in action] 고차함수 : 파라미터와 반환 값으로 람다 사용카테고리 없음 2021. 8. 18. 11:29728x90반응형
인라인 함수
비로컬 return과 레이블
무명 함수
람다를 인자로 받거나 반환하는 고차함수
람다 안에서 더 유연하게 흐름을 제어할 수 있는 코틀린 특성인 인라인 함수
8.1 고차 함수 정의
고차 함수: 다른 함수를 인자로 받거나 반환하는 함수
코틀린에서는 람다나 함수 참조를 통해 인자로 넘기거나 반환할 수 있다.
ex) filter 함수
list.filter { x > 0 }
함수 타입
람다를 로컬 변수에 대입하는 경우
val sum = { x: Int, y: Int -> x + y } val action = { println(42) }
각 변수에 구체적인 타입 선언 추가하기
val sum: (Int, Int) -> Int = { x, y -> x + y } val action: () -> Unit = { println(42) }
var canReturnNull: (Int, Int) -> Int? = { x, y -> null } var funOrNull: ((Int, Int) -> Int)? = null
함수 타입에서 파라미터 이름을 지정할 수도 있다.
fun performRequest( url: String, callback: (code: Int, content: String) -> Unit ) { /* ... */ }
인자로 받은 함수 호출
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 } }
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'}) }
자바에서 코틀린 함수 타입 사용
함수 타입인 변수는 인자 개수에 따라 적당한 FunctionN 인터페이스를 구현하는 클래스의 인스턴스를 저장하며, 그 클래스의 invoke 메소드 본문에는 람다의 본문이 들어간다.
자바에서 코틀린 표준 라이브러리가 제공하는 람다를 인자로 받는 확장함수를 쉽게 호출할 수 있다. 하지만, 수신 객체를 확장 함수의 첫 번째 인자로 명시적으로 넘겨야 한다.
// Java List<String> strings = newArrayList(); strings.add("42"); CollectionsKt.forEach(strings, s -> { System.out.println(s); return Unit.INSTANCE; // Unit타입의 값 명시적으로 반환 });
디폴트 값을 지정한 함수 타입 파라미터나 널이 될 수 있는 함수 타입 파라미터
함수를 함수에서 반환
예제1 ) 사용자가 지정한 배송 수단에 따라 배송비를 다르게 계산하는 로직을 함수로 반환하는 함수
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.EXPEDITED) val calculator2 = getShippingCostCalculator(Delivery.STANDARD) println("Shipping costs ${calculator(Order(3))}") // 12.3 println("Shipping costs ${calculator2(Order(3))}") // 3.5999999999999996 }
예제2 ) GUI 연락처 관리 앱을 만드는 데 UI의 상태에 따라 어떤 연락처 정보를 표시할지 결정해야 할 필요가 있다고 가정하자.
사용자가 UI의 입력창에 입력한 문자열과 매치되는 연락처만 화면에 표시하되 설정에 따라 전화번호 정보가 없는 연락처를 제외시킬 수도 있고 포함시킬 수도 있다.
data class Person( val firstName: String, val lastName: String, val phoneNumber: String? ) class ContactListFilters { var prefix: String = "" // 입력창에 입력된 문자열과 매치되는 연락처만 화면에 표시 var onlyWithPhoneNumber: Boolean = false // 전화번호 정보가 없는 연락처 제외 가능 fun getPredicate(): (Person) -> Boolean { // 연락처 목록을 필터링하는 술어 함수 val startsWithPrefix = { p: Person -> p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix) } if (!onlyWithPhoneNumber) { // return startsWithPrefix } return { startsWithPrefix(it) && it.phoneNumber != null } } } fun main(args: Array<String>) { val contacts = listOf(Person("Dmitry", "Jemerov", "123-4567"), Person("Svetlana", "Isakova", null)) val contactListFilters = ContactListFilters() with (contactListFilters) { prefix = "Dm" onlyWithPhoneNumber = true } println(contacts.filter( contactListFilters.getPredicate())) // getPredicate()가 반환한 함수를 filter 에게 인자로 넘긴다. }
람다를 활용한 중복 제거
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) ) // STEP 1 // 윈도우 사용자의 평균 방문시간 val averageWindowsDuration = log .filter { it.os == OS.WINDOWS } .map(SiteVisit::duration) .average() // STEP 2 // 만약 맥 사용자의 평균 방문시간을 구하고 싶다면? 위 코드와 중복되는 코드가 많을 것임. // 중복 코드를 별도의 함수로 추출한다. fun List<SiteVisit>.averageDurationFor(os: OS) = filter { it.os == os }.map(SiteVisit::duration).average() // STEP 3 // 만약 모바일 사용자의 평균 방문시간을 구하고 싶다면? // 더 복잡한 질의를 통해 방문 기록을 분석하고 싶다면? // 고차 함수를 사용하여 중복 제거한다. fun List<SiteVisit>.averageDurationFor2(predicate: (SiteVisit) -> Boolean) = filter(predicate).map(SiteVisit::duration).average() fun main(args: Array<String>) { println(averageWindowsDuration) println(log.averageDurationFor(OS.MAC)) println(log.averageDurationFor2 { it.os == OS.IOS && it.path == "/signup" }) }
8.2 인라인 함수: 람다의 부가 비용 없애기
람다를 무명 클래스로 컴파일하지만 그렇다고 람다 식을 사용할 때마다 새로운 클래스가 만들어지는건 아니다.
람다가 변수를 포획하면 람다가 생성되는 시점마다 새로운 무명클래스 객체가 생긴다.
-> 실행 시점, 무명 클래스 생성에 따른 부가 비용이 든다.
그렇다면, 반복되는 코드는 별도의 라이브러리로 빼내되 컴파일러가 자바의 일반 명령문만큼 효율적인 코드를 생성하게 만들 수는 없을까?
inline 변경자를 어떤 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔치기 해준다.
인라이닝이 작동하는 방식
인라인 함수의 한계
인라인 함수의 본문에서 람다식을 바로 호출하거나, 람다 식을 인자로 전달받아 바로 호출하는 경우에는 그 람다를 인라이닝 할 수 있다.
그런 경우가 아니라면 컴파일러는 인라이닝을 금지시킨다.
컬렉션 연산 인라이닝
data class Person2(val name: String, val age: Int) val people = listOf(Person2("Alice", 29), Person2("Bob", 31)) fun main(args: Array<String>) { // 컬렉션 크기가 큰 경우 -> 시퀀스를 사용해서 지연 계산 // 컬렉션 크기가 작은 경우 -> 일반 컬렉션 연산이 나을 수도 (시퀀스는 람다가 인라이닝되지 않는다) println(people.filter { it.age < 30 } .map(Person2::name)) // filter, map 는 인라인 함수 }
중간 시퀀스는 람다를 필드에 저장하는 객체로 표현되며, 최종 연산은 중간 시퀀스에 있는 여러 람다를 연쇄호출한다. 따라서, 시퀀스는 람다를 인라이닝하지 않는다.
함수를 인라인으로 선언해야하는 경우
inline 키워드는 람다를 인자로 받는 함수만 성능 향상을 가져올 가능성이 크다.
일반 함수 호출의 경우에는, 이미 JVM이 강력하게 인라이닝을 지원한다. (바이트코드 -> 실제 기계어 코드 번역 과정에서)
반면 코틀린 인라인 함수는 바이트코드에서 각 함수 호출 지점을 함수 본문으로 대치해서 코드 중복이 생긴다. 그냥 함수를 직접 호출하면 스택트레이스가 더 깔끔해지고 더 낫다. 하지만 람다를 인자로 받는 함수를 인라이닝할 경우 많은 이점이 있다.
(1) 함수 호출 비용 줄일 수 있다. 람다를 표현하는 클래스와 람다 인스턴스에 해당하는 객체를 만들 필요가 없어진다.
(2) 현재의 JVM은 함수 호출과 람다를 인라이닝해 주지 못한다.
(3) 일반 람다에서 사용할 수 없는 몇 가지 기능을 제공한다 ( ex. non-local 반환 )자원 관리를 위해 인라인된 람다 사용
자원 관리 패턴 : try/finally문 사용 ( try 블록 시작 직전 자원 획득, finally 블록에서 자원 해제 )
withLock은 Lock 인터페이스의 확장함수다.
fun <T> Lock.withLock(action: () -> T): T { lock() try { return action() } finally { unlock() } }
- 자바 try-with-resource 문
static String readFirstLineFromFile(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } }
- 코틀린의 use 함수
closeable 자원에 대한 확장 함수이며, 람다를 인자로 받는다. 람다를 호출한 다음, 자원을 닫는다. 인라인 함수이다.
fun readFirstLineFromFile(path: String): String { BufferedReader(FileReader(path)).use { br -> return br.readLine() } }
8.3 고차 함수 안에서 흐름 제어
람다 안의 return문: 람다를 둘러싼 함수로부터 반환
람다 안에서 return 을 사용하면 람다로부터만 반환되는게 아니라 그 람다를 호출하는 함수가 실행을 끝내고 반환하게 된다.
자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return문을 넌로컬 return 이라고 한다.
자바 메소드 안에 있는 for루프나 synchronized 블록 안의 return 키워드는 메소드를 반환시킨다.
코틀린에서는 람다를 받는 함수로 for 나 synchronized 같은 기능을 구현한다. 그런 함수 안에서 쓰이는 return이 자바의 return과 같은 의미를 갖게 허용한다.
람다를 인자로 받는 함수가 인라인 함수인 경우에만 return이 바깥쪽 함수를 반환시킬 수 있다.
람다로부터 반환: 레이블을 사용한 return
람다식에서도 로컬 return 사용할 수 있다. for 루프의 break 같은 역할. 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 계속 이어나간다. 넌로컬리턴과 구분하기 위해서 레이블(label)을 사용해야 한다.
fun lookForAlice(people: List<Person5>) { people.forEach label@{ if (it.name == "Alice") return@label } println("Alice might be somewhere") }
람다 식에는 레이블이 2개 이상 붙을 수 없으므로, 람다 식의 레이블을 명시하면 함수 이름을 레이블로 사용할 수 없다.
무명 함수: 기본적으로 로컬 return
코드 블록을 함수로 넘길 때 사용할 수 있는 다른 방법이다.
fun lookForAlice(people: List<Person6>) { people.forEach(fun (person) { // 람다 식 대신 무명함수 사용. 함수 이름, 파라미터 생략 가능 if (person.name == "Alice") return // 가장 가까운 함수를 가리킴 println("${person.name} is not Alice") }) } fun main(args: Array<String>) { lookForAlice(people6) // filter에 무명함수 넘기기 people6.filter (fun(person6) : Boolean { return person6.age < 30 }) // 식이 본문인 무명함수 people6.filter(fun (person6) = person6.age < 30) }
return 의 규칙을 정리하면, fun 키워드를 사용해 정의된 가장 안쪽 함수를 반환시킨다는 것이다. 무명 함수는 일반 함수처럼 보이지만, 람다식에대한 문법적 편의이다.
728x90