본문 바로가기
Kotlin

[Kotlin in Action] 8.1 고차 함수 정의

by Nhahan 2025. 3. 27.

고차 함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수다. 코틀린에서는 람다나 함수 참조를 사용해 함수를 값으로 표현할 수 있다. 따라서 고차 함수는 람다나 함수 참조를 인자로 넘길 수 있거나 람다나 함수 참조를 반환하는 함수다. 예를 들어, 표준 라이브러리 함수인 filter는 술어 함수를 인자로 받으므로 고차 함수다.

list.filter { x > 0 }

 

 

8.1.1 함수 타입

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

이 경우 컴파일러는 sum과 action이 함수 타입임을 추론한다. 각 변수에 구체적인 타입 선언을 추가하면 어떻게 될까.

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

함수 타입을 정의하려면 함수 파라미터의 타입을 괄호 안에 넣고, 그 뒤에 화살표를 추가한 다음, 함수의 반환 타입을 지정하면 된다.

Unit 타입은 의미 있는 값을 반환하지 않는 함수 반환 타입에 쓰는 특별한 타입이다. 그냥 함수를 정의한다면 함수의 파라미터 목록 뒤에 오는 Unit 반환 타입 지정을 생략해도 되지만, 함수 타입을 선언할 때는 반환 타입을 반드시 명시해야 하므로, Unit을 빼먹어서는 안된다.

 

다른 함수와 마찬가지로 함수 타입에서도 반환타입을 null이 될 수 있는 타입으로 지정할 수 있다.

var canReturnNull: (Int, Int) -> Int? = { null }

null이 될 수 있는 타입 변수를 정의할 수도 있다. 다만 함수의 반환 타입이 아니라 함수 타입 전체가 null이 될 수 있는 타입임을 선언하기 위해 함수 타입을 괄호로 감싸고 그 뒤에 물음표를 붙여야만 한다.

var funOrNull: ((Int, Int) -> Int)? = null

 

8.1.2 인자로 받은 함수 호출

// 간단한 고차 함수 정의하기
fun twoAndThree(operation: (Int, Int) -> Int) {
    val result = operation(2, 3)
    println("The result is $result")
}

>>> twoAndThree { a, b -> a + b }
The result is 5
>>> twoAndThree { a, b -> a * b }
The result is 6

인자로 받은 함수를 호출하는 구문은 일반 함수를 호출하는 구문과 같다. 함수 이름 뒤에 괄호를 붙이고, 괄호 안에 원하는 인자를 콤마로 구분해 넣는다.

 

표준 라이브러리 고차 함수인 filter를 String 타입에 대하여 구현해보자.

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

>>> pritln("ab1c".filter { it in 'a'..'z' })
abc

 

 

8.1.3 자바에서 코틀린 함수 타입 사용

컴파일된 코드 안에서 함수 타입은 일반 인터페이스로 바뀐다. 즉, 함수 타입의 변수는 FunctionN 인터페이스를 구현하는 객체를 저장한다. 코틀린 표준 라이브러리는 함수 인자의 개수에 따라 Function0<R>(인자가 없는 함수), Function1<P1, R>(인자가 하나인 함수) 등의 인터페이스를 제공한다. 각 인터페이스에는 invoke 메소드 정의가 하나 들어있다. invoke를 호출하면 함수를 실행할 수 있다. 함수 타입인 변수는 인자 개수에 따라 적당한 FunctionN 인터페이스를 구현하는 클래스의 인스턴스를 저장하며, 그 클래스의 invoke 메소드 본문에는 람다의 본문이 들어간다.

함수 타입을 사용하는 코틀린 함수를 자바에서도 쉽게 호출할 수 있다. 

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

/* Java */
>>> processTheAnswer(number -> number + 1);
43

 

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

 

반환 타입이 Unit인 함수나 람다를 자바로 작성할 수도 있다. 하지만 코틀린 Unit 타입에는 값이 존재하므로 자바에서는 그 값을 명시적으로 반환해줘야 한다. (String) -> Unit처럼 반환 타입이 Unit인 함수 타입의 파라미터 위치에 void를 반환하는 자바 람다를 넘길 수 없다.

 

8.1.4 디폴트 값을 지정한 함수 타입 파라미터나 null이 될 수 있는 함수 타입 파라미터

파라미터를 함수 타입으로 선언할 때도 디폴트 값을 정할 수 있다.

// 함수 타입의 파라미터에 대한 디폴트 값 정하기
fun <T> Colletion<T>.joinToString(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = "",
    transform: (T) -> String = { it.toString() } // 함수 타입 파라미터를 선언하면서 람다를 디폴트 값으로 지정한다.
) : String {
    // ...
}

 

8.1.5 함수를 함수에서 반환

함수가 함수를 반환할 필요가 있는 경우보다는 함수가 함수를 인자로 받아야 할 필요가 있는 경우가 훨씬 더 많다. 하지만 함수를 반환하는 함수도 여전히 유용하다.

문자열과 같은 일반 타입의 값을 함수가 쉽게 반환할 수 있는 것처럼, 함수 타입을 사용하면 함수에서 함수를 쉽게 반환할 수 있다.

고차 함수는 코드 구조를 개선하고 중복을 없앨 수 있는 강력한 도구다.

 

8.1.6 람다를 활용한 중복 제거

함수 타입과 람다 식은 재활용하기 좋은 코드를 만들 때 쓸 수 있는 훌륭한 도구다. 람다를 사용할 수 없는 환경에서는 아주 복잡한 구조를 만들어야만 피할 수 있는 코드 중복도 람다를 활용하면 간결하고 쉽게 제거할 수 있다.

 

반응형

댓글