오늘도 더 나은 코드를 작성하였습니까?

고차 함수 : 파라미터와 반환 값으로 람다 사용하기. 본문

Kotlin in Action/코틀린 답게 사용하기

고차 함수 : 파라미터와 반환 값으로 람다 사용하기.

hik14 2021. 2. 24. 19:53

고차함수의 정의

 - 다른 함수를 인자로 받거나 함수를 반환하는 함수.

 

함수타입

val sum = {x:Int, y: Int -> x+y}
val action = {println(14)}
val sum: (Int, Int) -> Int  = {x:Int, y: Int -> x+y}
val action: () -> Unit = {println(14)}

*Unit은 값을 반환 하지 않는 함수의 반환타입인데, 일반적인 값을 반환하지 않는 함수는 정의할 때는 생략이 가능하지만, 함수타입을 선언 할때는 생략을 해서는 안된다.

 

* 괄호에 주의하며 아래코드를 보면된다

    // 반환값이 널값이될 수 있다.
    val canReturnNull: (Int, Int) -> Int? = {x , y -> null}
    // 함수자체가 널이될 수 있다.
    val funOrNull: ((Int, Int) -> Int)? = null

.*함수타입에서 파라미터 이름을 지정가능하다

fun performRequest(url: String, callback: (code: Int, content: String) -> Unit){
    /*
    ...
    * */
}

fun main() {

    val url = "http://kotlin.com"
    performRequest(url){code, content ->  /* .... */ }
}

 

filter 확장함수 구현하기

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() {
    println("abc12dceg24".filter { it in 'a'..'z' })
}

자바에서 함수타입 사용하기

 

함수타입은 컴파일 코드 안에서 일반 인터페이스로 변화한다.  즉, 함수타입 변수는 FunctionN 인터페이스를 구현한 객체를 저장하는것,

인자의 개수에 따라 Function0<R>(인자 없음), Function2<P1, R>(인자 1개) 등의 인터페이스를 제공한다.

 

각, 인터페이스에는 invoke 메소드 정의가 하나들어 있다. invoke 함수를 호출하면 함수를 실행할수있다. 

 

함수타입변수는 FunctionN 인터페이스를 구현하는 클래스 인스턴스를 저장하여 invoke 메소드 본문에 람다 본문이 들어간다.

 

코틀린

fun processTheAnswer(f:(Int) -> Int) {
    println(f(42))
}

자바8 이후

processTheAnswer(number -> number+1)

자바8 이전

processAnswer(
	new Function1<Integer, Integer>(){
    	@Overide
        public Integer invoke(Integer number){
        	System.out.println(number);
            return number + 1;
        }
    }
);

자바에서 코틀린 표준라이브러리가 제공하는 람다를 인자로 받는 확장함수를 호출할수 있지만, 수신객체를 첫번째 인자로 반드시 명시적으로 넘겨야 하기 때문에 코드가 깔끔하지 못하다. 

List<String> strings = new ArrayList();
strings.add("42");


CollectionsKt.forEach(strings, s -> {  // 수신객체 인자로 넘기기.
	System.out.println(s);
    return Unit.INSTANCE;  // 코틀린의 Unit타입을 명시적 반환
})

 

디폴트 값을 지정한 함수타입 파라미터 

fun <T> Collection<T>.jointToString(
    separator: String = ",",
    prefix: String = "",
    postfix: String = "",
    transform: (T) -> String = { it -> it.toString() }
): String {

    val result = StringBuilder(prefix)

    for ((index, element) in withIndex()) {
        if (index > 0) result.append(separator)
        result.append(transform(element))
    }

    result.append(postfix)

    return result.toString()
}



fun main() {
    val letters = listOf("a", "B", "c", "D")
    println(letters.jointToString { it.toUpperCase() })
    println(letters.jointToString { it.toLowerCase() })
    println(letters.jointToString("/"))
}

StringBuilder.append( o: Any? ) 이 함수를 내부적으로 이용하는데 항상 toString을 사용하여 문자열을 변환한다.

대부분의 경우 잘 작동하지만, toString 아닌 다른 원소를 바꿔서 나누고 싶은 경우는,

그러면 {원소 -> 문자열} 하는 람다를  joinToString으로 넘겨주면 되는데 매번 넘기기보단 default 값의 람다를 인자로 만든다.

 

널이될 수 있는 함수 타입 파라미터

 

- 널이 될수 있는 함수를 파라미터로 받는다면 호출이 불가능할 수 있다 --> NPE

- null 검사를 명시적으로 해주거나

- ?. 안전호출을 이용하자

fun <T> Collection<T>.jointToString(
    separator: String = ",",
    prefix: String = "",
    postfix: String = "",
    transform: ((T) -> String)? = null
): String {

    val result = StringBuilder(prefix)

    for ((index, element) in 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 }
}

사용자가 입력 창에 입력한 문자열과 매치되는 사람의 연락처만 화면에 표시하되, 연락처 정보가 있는 사람과 없는 것을 구별하기

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() {

    val contacts = listOf(
        Person("Dmitry", "Jemerov", "123-4567"),
        Person("Svetlana", "Isacova", null)
    )

    val contactListFilters = ContactListFilters()

    with(contactListFilters){
        prefix = "Sv"
        onlyWithPhoneNumber = false
    }
    
    println(contacts.filter( contactListFilters.getPredicate() ))
}

 

람다를 활용한 중복 제거

 

각 사이트의 페이지 마다 사용자가 있던 시간과 접속한 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 main() {

    val averageWindowsDuration = log
        .filter { it.os == OS.WINDOWS }
        .map (SiteVisit::duration)
        .average()

    println(averageWindowsDuration)
}

 

일반 확장함수를 생성해 중복을 제거하기.

fun List<SiteVisit>.averageDuration(os: OS) =
        filter { it.os == os }
        .map { it.duration }
        .average()

모바일 디바이스 사용자의 평균방문시간 구하기(하드 코딩)

fun main() {

    val averageMobileDuration = log.filter { it.os in setOf(OS.IOS, OS.ANDROID) }
        .map { it.duration }
        .average()

    println(averageMobileDuration)
}

고차함수를 이용하여 중복을 줄여보기.

fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) = 
    filter(predicate)
    .map(SiteVisit::duration)
    .average()

fun main() {
    println(log.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) })
    println(log.averageDurationFor { it.os == OS.IOS && it.path == "/signup"})
}

 

코드의 중복을 줄여주는데 함수타입과 람다는 매우 도움을 준다.

코드의 일부분을 복사 붙혀넣기 하기전에 한 번 람다를 생성해 고차함수를 이용한다면 코드의 중복을 줄일 수 있다.