다른 함수를 인자로 받거나 반환하는 함수 정의 : 고차 함수

유우선·2026년 2월 16일

Kotlin Study📚

목록 보기
26/32

고차 함수 → 다른 함수를 인자로 받거나 반환하는 함수

  • 람다 or 함수 참조를 통해 함수를 값으로 표현할 수 있음
  • 이를 통해 함수를 인자로 넘기거나 반환 받는 것이 가능

filter 함수도 고차 함수임

  • 람다를 인자로 받기 때문
list.filter { it > 0 }

이제 고차 함수를 정의하는 방법을 알아본다. 고차 함수를 정의하려면 먼저 함수 타입을 알아야 함


함수 타입은 람다의 파라미터 타입과 반환 타입을 지정한다

람다를 로컬 변수에 대입

  • 코틀린의 타입 추로으로 변수 타입을 지정하지 않아도 람다를 변수에 대입할 수 있음
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 타입을 명시해야 함
  • 함수 타입을 명시한다면 람다 내부에는 타입을 생략해도 됨

함수 타입도 반환 타입을 null이 될 수 있는 타입으로 지정할 수 있음

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

함수 타입 자체를 null이 될 수 있는 타입으로 정의할 수 있음

  • 힘수 타입을 괄호로 감싸고 그 뒤에 물음표를 붙여야 함
var funOrNull: ((Int, Int) -> Int)? = null

인자로 전달 받은 함수 호출

고차 함수를 어떻게 구현하는지 알아본다

간단한 고차 함수 정의

fun twoAndThree(operation: (Int, Int) -> Int) { // 함수 타입인 파라미터를 선언
		val result = operation(2, 3) // 함수 타입인 파라미터를 호출
		println("The result is $result")
}

fun main() { 
		twoAndThree { a, b -> a + b }
		// The result is 5
		twoAndThree { a, b -> a * b }
		// The result is 6
}
  • 인자로 받은 함수를 호출하는 방법은 일반 함수를 호출하는 구문과 같음

파라미터 이름과 함수 타입

  • 함수 타입에서 파라미터 이름을 지정할 수 있음
fun twoAndThree(operation: (operandA: Int, operandB: Int) -> Int) { // 함수 타입인 파라미터를 선언
		val result = operation(2, 3) // 함수 타입인 파라미터를 호출
		println("The result is $result")
}
fun main() { 
		twoAndThree { operandA, operandB -> operandA + operandB } // API에서 지정한 이름을 람다에 사용할 수 있음
		// The result is 5
		twoAndThree { alpha, beta -> alpha + beta } // 그냥 원하는 이름도 사용할 수 있음
		// The result is 5
}

filter 함수 구현 예제

예제를 단순하게 유지하기 위해 String에 대한 filter를 구현

fun String.filter(predicate: (Char) -> Boolean): String { 
		return buildString { 
				for (char in this@filter) { //입력 문자열 이터레이션
						if (predicate(char)) append(char) // predicte 파라미터로 전달받은 함수를 호출
				}
		}
}

fun main() { 
		println("ab1c".filter { it in 'a' .. 'z' })
		///abc
}
  • filter 함수 → 문자열의 각 문자가 술어를 만족하는지 검사
  • 술어를 만족하는 문자는 StringBuilder의 append를 사용해 결과를 만들고 반환
  • 레이블이 붙은 this → 10.6절에서 자세히 다룸

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

  • SAM 변환을 통해 코틀린 람다를 자바 메서드에 옮길 수 있음
  • 함수 타입을 사용하는 코틀린 코드를 자바에서 쉽게 호출할 수 있음’
  • 자바 람다는 자동으로 코틀린 함수 타입으로 변환됨
/* 코틀린 선언*/
fun processTheAnswer(f: (Int) -> Int) {
		println(42)
}

/* 자바 호출 */
processTheAnswer(number -> number + 1);

자바에서 코틀린 함수 타입 호출시 수신 객체를 명시해줘야 함

import kotlin.collections.CollectionsKt;

/ *... */
public static void main(String[] arg) {
		List<String> strings = new ArrayList();
		strings.add("42");
		CollectionsKt.forEach(strings, s -> { // 코틀린 표준 라이브러리 함수 호출 가능
				System.out.println(s);
				return Unit.INSTANCE; // Unit 타입의 값을 명시적으로 반환해야 함
		});
}
  • Unit을 반환하는 함수나 람다를 호출할 때는 Unit 타입의 값을 명시적으로 반환해줘야 함
    • Unit 타입에는 값이 존재하기 때문
  • 값이 존재하기 때문에 Unit 타입을 반환하는 코틀린 함수에 void 타입 자바 람다를 넘길 수 없음

함수 타입의 자세한 동작 방식

  • 함수 타입의 변수 → FunctionN 인터페이스를 구현
    • 인자의 개수에 따라 FunctionN의 N값이 바뀜
  • 각 인터페이스에는 invoke라는 유일한 메서드가 있음
    • 이 메서드를 호출하면 함수가 호출됨
    • invoke에 대해선 13장에 자세히 다룸
interface Function1<P1, out R>{
		operator fun invoke(p1: P1): R
}
  • 함수 타입의 변수 → 함수에 대응하는 FunctionN 인터페이스를 구현하는 클래스의 인스턴스
    • invoke 메서드에는 람다 본문이 들어감
fun processTheAnswer(f: Function1<Int, Int>) {
		println(f.invoke(42))
}
  • FunctionN 인터페이스 → 컴파일러가 생성한 합성 타입으로 코틀린 표준 라이브러리에서 이들의 정의를 찾을 수 없음
    • 컴파일러가 필요할 때 이런 함수를 생성
    • 파라미터의 개수 제한 없이 파라미터를 사용하는 함수에 대한 인터페이스를 사용할 수 있음

함수 타입의 파라미터 기본값 지정과 null 이 될 수 있는 타입

함수 타입에 기본값 지정

함수 타입의 파라미터에 대한 기본값으로 람다식을 지정해놓으면 호출할 때마다 람다를 넘겨주지 않아도 되고 필요 시 람다를 넘겨 원하는 기능을 추가할 수 있음

fun <T> Collection<T>.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)) // transform 파라미터에 대한 인자로 받은 함수 호출
		}
		
		result.append(postfix)
		return result.toString()
}

fun main() {
		val letters = listOf("Alpha", "Beta")
		println(letters.joinToString()) //transform 기본 함수 사용
		// Alpha, Beta
		println(letters.joinToString { it.lowercase() }) // 람다를 인자로 전달
		// alpha, beta
		println(letters.joinToString(seperator="! ", postfix="! ", transform={ it.uppercase() }))
		// 이름 붙은 인자 구문을 사용해 람다를 포함하는 여러 인자를 전달
		// ALPHA! BETA!
}
  • 함수 타입에 대한 기본값 선언도 = 뒤에 람다를 넣으면 됨

null이 될 수 있는 함수 타입

  • null이 될 수 있는 함수 타입으로 하수를 받으면 그 함수를 직접 호출할 수 없음
    • null 여부를 명시적으로 검사하면 호출은 할 수 있음
    • invoke를 사용하여 간결하게 해결할 수도 있음
  • 함수 타입 → invoke 메서드를 구현하는 인터페이스
    • 일반 메서드처럼 invoke도 안전한 호출을 사용할 수 있음
fun <T> Collection<T>.joinToString(
		separator: String= ", ",
		prefix: String="",
		postfix: String = "",
		transform: ((T) -> String)? = null
): String {
		val result = StringBuilder(prefix)
		
		for((index, element) in this.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 getShppingCostCalculator(delivery: Delivery): (Order) -> Double { // 함수를 반환하는 함수 선언
		if(delivery == Delivery.EXPEDITED) {
				return { order -> 6 + 2.1 * order.itemCount } // 함수에서 람다를 반환
		}
		return { order -> 1.2 * order.itemCount } // 함수에서 람다를 반환
}

fun main() {
		val calculator = getShippingCostCalculator(Delivery.EXPEDITED) // 함수를 반환받아 저장
		println("Shipping costs ${calculator(Order(3))}") // 반환받은 함수 호출
		// Shipping costs 12.3
}
  • 함수에서 함수를 반한하려면 반환 타입으로 함수 타입을 지정해줘야 함
    • getShppingCostCalculator는 Order 객체를 받아 Double 타입을 반환하는 함수를 반환함
  • 함수를 반환하려면 return식에 람다, 맴버 참조, 함수 타입의 값을 계산하는 식을 넣으면 됨

함수를 반환하는 함수를 UI 코드에서 사용하기

data class Person(
    val firstName: String,
    val lastName: String,
    val phoneNumber: String?
)

class ContactListFilters {
    var prefix:String = ""
    var onlyWithPhoneNumber: Boolean = false

    fun getPrediction(): (Person) -> Boolean { // 함수를 반환하는 함수 정의
        val startWithPrefix = { p: Person ->
            p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)
        }
        if(!onlyWithPhoneNumber) {
            return startWithPrefix // 함수 타입의 변수 반환
        }
        return { startWithPrefix(it) && it.phoneNumber != null } // 람다를 반환
    }
}

fun main() {
    val contact = listOf(
        Person("Dmitry", "Jemerov", "123-4567"),
        Person("Svetlana", "Isakova", null)
    )
    val contactListFilters = ContactListFilters()
    with (contactListFilters) {
        prefix = "Dm"
        onlyWithPhoneNumber = true
    }

    println(
        contact.filter(contactListFilters.getPrediction()) // 반환한 함수를 filter에 인자로 넘김
    )
    // [Person(firstName=Dmitry, lastName=Jemerov, phoneNumber=123-4567)]
}
  • getPredicate 메서드 → filter 함수에 인자로 넘길 수 있는 함수를 반환함

람다를 통한 코드 재사용성 높이기

람다식 → 재사용성을 높이는 훌륭한 도구

  • 반복적으로 사용되는 코드를 람다로 추출해 함수에 인자로 넘기면 코드를 재사용 하면서 코드 중복을 제거할 수 있음

웹 사이트 방문 기록 분석 예시

데이터 정의

data class SiteVisit(
    val path: String,
    val duration: Double,
    val os: OS
)

enum class OS { WINDOW, LINUX, MAC, IOS, ANDROID }

val log = listOf(
    SiteVisit("/", 34.0, OS.WINDOW),
    SiteVisit("/", 22.0, OS.MAC),
    SiteVisit("/login", 12.0, OS.WINDOW),
    SiteVisit("/signup", 8.0, OS.IOS),
    SiteVisit("/", 16.3, OS.ANDROID)
)
  1. 사이트 방문을 하드 코딩한 필터

    val averageWindowsDuration = log
        .filter { it.os == OS.WINDOW }
        .map(SiteVisit::duration)
        .average()
    
    fun main() {
        println(averageWindowsDuration)
        //23.0
    }
  2. 일반 함수를 통해 코드 중복 제거

    fun List<SiteVisit>.averageDurationFor(os: OS) =
        filter { it.os == os }.map(SiteVisit::duration).average()
    
    fun main() {
        println(log.averageDurationFor(OS.WINDOW))
        // 23.0
        println(log.averageDurationFor(OS.MAC))
        // 22.0
    }
    • OS를 파라미터로 뽑아내 코드 중복을 해결
  3. 하드 코딩된 필터를 로컬 함수로 정의하기

fun main() {
    val averageMobileDuration = log
        .filter { it.os in setOf(OS.IOS, OS.ANDROID) }
        .map(SiteVisit::duration)
        .average()
    println(averageMobileDuration)
    //12.15
}
  1. 고차 함수를 사용해 중복 제거하기
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) }
    )
    // 12.15
    println(
        log.averageDurationFor { it.os == OS.IOS && it.path == "/signup" }
    )
    // 8.0
}
  • 중복되는 코드를 고차 함수로 뽑아냄으로써 코드 중복을 제거할 수 있음
  • 뿐 만 아니라 함수 타입을 사용하면 필요한 조건을 파라미터로 뽑아낼 수 있음
    • 복잡한 로직을 간단하게 구현 가능

0개의 댓글