본문 바로가기
개발새발/코틀린

[코틀린/Kotlin] 람다와 고차함수

by 조희우 2025. 8. 18.

람다

  • 이름이 없는 함수: 익명함수
  • 간단한 처리를 하므로 fun으로 함수를 만들기 번거로울 때 사용
  • filter, map, forEach 같은 함수 내에서 사용

기본 문법

val lamdaName = { arg1: Type, arg2: Type -> return }

[예시]

val result = listOf(1, 2, 3).map { it * 2 } // it * 2는 람다식
val result = listOf(1, 2, 3).map { it * 2 } // it * 2는 람다식

람다 표기법

  • 소괄호로 묶은 파라미터와 리턴, 화살표 표기법으로 연관
// 매개변수 없음
() -> A

// 매개변수 있음
(A, B) -> C
  • 파라미터 타입은 생략 가능하나 리턴은 생략 불가능
// 생략 전
(a: Int, b: Int) -> Int

// 생략 후
(a, b) -> Int

// 생략 불가능
(a, b) ->
  • 인스턴스화
// 람다식
{a, b -> a + b}

// 확장함수
String::toInt
  • 함수가 마지막 파라미터면 블록을 밖으로 내보내기 가능
// block 이 마지막 파라미터 아닐 때
fun greeting(block: (String) -> Any, value: String) {
	block(value)
}

greeting({ println(it) }, "hello world!!!") -- X

// block이 마지막 파라미터 일 때
fun greeting(value: String, block: (String) -> Any) {
	block(value)
}

greeting("hello world!!!") { println(it) }

Receiver

  • 수신객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메소드를 호출 가능: 수신객체 지정 람다
  • 계층 형태의 데이터 구조를 쉽게 생성 가능: builder
    • Kotlin DSL(Domain Specific Language) 생성 가능: gradle
  • 람다의 파라미터를 감싸던 소괄호를 수신 객체 타입의 오른쪽으로 빼낸 뒤 마침표를 삽입
T.() -> R

[예시]

  • apply
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
  • with
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

고차함수

  • 일급 함수(first-class)
    • 변수나 자료구조를 담아낼 수 있으며 파라미터로 다른 함수를 전달하거나 함수를 리턴
  • 다른 함수를 인자로 받거나, 함수를 반환하는 함수
    • 람다나 함수 참조를 사용하여 함수를 값으로 표현 가능
  • 장점
    • 재사용성이 높아짐
    • 추상화가 쉬어짐
    • 강력한 컬렉션 처리(map, filter …) 가능

함수 타입

  • 타입 추론으로 변수 타입을 지정하지 않아도 람다를 변수에 대입 가능
val sum = { x: Int, y: Int -> x + y}
val action = { println ("") }
  • 함수 타입을 정의하려면 함수 파라미터 타입을 괄호 안에 넣고, 화살표를 추가하여 리턴타입 지정
    • 반환 값이 없을 경우 Unit
    • 파라미터 지정 시 람다 식에서 파라미터 타입을 선언하지 않아도 됨
val sum: (Int, Int) -> Int = { x, y -> x + y}
    • null 타입 지정 가능
var canReturnNull: (Int, Int) -> Int? = { x, y -> null }
    • 파라미터명 지정 가능
// 자동추가 파라미터명 index, s
	stringList.forEachIndexed { index, s -> }
	
// 파라미터명 변경
	stringList.forEachIndexed { idx, value -> }

파라미터로 받은 함수 호출

  • 일반 함수 호출과 동일: 파라미터 위치와 타입 값 구분

[예시]

fun calculate(operation: (Int, Int) -> Int){
	val result = operation(2, 3)
	println("계산 결과 = ${result}")
}

calculate { a, b -> a + b }

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

  • 디폴트 값을 지정 가능

[예시]

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)
        // transform 파라미터로 받은 함수를 호출
        result.append(transform(element))
    }

    result.append(postfix)
    return result.toString()
}

val letters = listOf("Alpha", "Beta")

// 디폴트 변환 함수를 사용
println(letters.joinToString())

// 람다를 인자로 전달
println(letters.joinToString { it.lowercase() })

// 이름 붙인 인자 구문을 사용해 람다를 포함하는 여러 인자를 전달
println(letters.joinToString(separator = "! ", postfix = "! ",
    transform = { it.uppercase() }))

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

  • 널이 될 수 있는 함수타입 사용 가능
  • 널이 될 수 있는 함수 타입으로 함수를 받으면 함수 직접 호출 불가능
    • 명시적으로 null 여부를 지정

[예시]

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

퀴즈

객관식

1. 다음 중 람다식의 올바른 문법은?

  1. val f = fun(a: Int) = a * 2
  2. val f = { a: Int -> a * 2 }
  3. val f = (a: Int) -> a * 2
  4. val f = [a:Int] -> a * 2

답: 2

 

2. 고차 함수란 무엇인가?

  1. 함수를 반환하는 함수만 의미한다.
  2. 함수를 인자로 받거나 반환하는 함수를 의미한다.
  3. 람다식을 의미한다.
  4. 클래스 내부에 선언된 함수를 의미한다.

답: 2

 

3. 다음 코드의 출력 결과는?

fun operate(x: Int, y: Int, op: (Int, Int) -> Int): Int {
    return op(x, y)
}

fun main() {
    val result = operate(4, 5) { a, b -> a + b }
    println(result)
}
  1. 20
  2. 9
  3. 45
  4. 컴파일 에러

답: 2

 

주관식

1. 람다식을 변수에 할당하고 호출하기

add라는 이름의 람다식을 만들어 두 정수의 합을 반환하게 해봐.

그리고 add를 호출해 3과 7의 합을 출력하시오.

 

답:

val add = { num1: Int, num2: Int -> num1 + num2 }
println(add(3, 7))

 

 

2. 고차 함수를 이용해 리스트 처리하기

applyOperation이라는 함수를 만들어보자.

  • 세 개의 매개변수를 받는다: list: List<Int>, op: (Int) -> Int, filter: (Int) -> Boolean
  • 함수는 먼저 리스트에서 filter 조건을 만족하는 요소만 걸러내고,
  • 그 후 op 함수를 적용해 변환한 결과 리스트를 반환한다.

예를 들어, 짝수만 골라 2배로 만든 리스트를 반환할 수 있어야 한다.

 

답:

fun applyOperation(list: List<Int>, 
									op: (Int) -> Int, 
									filter: (Int) -> Boolean
): List<Int>{
	val filtered = list.filter { filter }
	
	val transformed filtered.map { op }
	
	return transformed
}

fun main() {
	val result = applyOperation(listOf(1, 2, 3, 4, 5),
															filter = { it % 2 == 0 }, 
															op = { it * 2})
	println(result)
}

→ 오답 노트:

filter와 op가 호출되지 않음

fun applyOperation(list: List<Int>, 
									op: (Int) -> Int, 
									filter: (Int) -> Boolean
): List<Int>{
	val filtered = list.filter { filter(it) }
	
	val transformed filtered.map { op(it) }
	
	return transformed
}

fun main() {
	val result = applyOperation(listOf(1, 2, 3, 4, 5),
															filter = { it % 2 == 0 }, 
															op = { it * 2})
	println(result)
}

 

3. 확장 함수로 람다 활용하기

List<Int>에 대해 sumByLambda라는 확장 함수를 작성해보자.

  • 람다 (Int) -> Int를 인자로 받고,
  • 리스트 각 요소에 람다를 적용한 후, 그 합을 반환해야 한다.
fun sumByLambda(numbers: List<Int>, 
								sum: (Int) -> Int
): Int{
	val result = numbers.fold { sum }
	return result
}

fun main() {
	val result = sumByLambba(listOf(1, 2, 3, 4, 5), 
	                        sum = { acc, num -> acc + num })
	println(result)
}

오답 노트:

  • 리시버를 활용하기
  • fold는 인자가 두개가 필요함
  • 현재 문제는 “그대로” 값을 더하지만 고차함수를 이용하여 다양한 응용 후 합산이 가능
fun List<Int>.sumByLambda(transform: (Int) -> Int): Int {
    return this.map { transform(it) }.sum()
}

fun main() {
    val result = listOf(1, 2, 3, 4, 5).sumByLambda { it } // 각 요소를 합산
    //응용
    listOf(1, 2, 3, 4, 5).sumByLambda { it * 2 }
    listOf(1, 2, 3, 4, 5).sumByLambda { if (it % 2 == 0) it else 0 }
    
    println(result)
}

추가 학습

람다 정의해보기

1. 두 숫자를 곱한 결과를 반환하는 람다

val multiply: (Int, Int) -> Int = { a, b -> a * b }

2. 문자열을 받아 길이를 반환하는 람다

val strLength: (String) -> Int = { str -> str.length }

3. 정수를 받아 짝수인지 Boolean으로 반환하는 람다

val numJud: (Int) -> Boolean = { num -> if num%2==0 true else false

    [보충]

val numJud: (Int) -> Boolean = { num -> num % 2 == 0 }

함수를 인자로 받는 고차함수

정수를 하나 받고, 그 정수에 어떤 연산(함수)을 적용한 결과를 반환하는 함수를 만들기

예시

fun applyOperation(x: Int, operation: (Int) -> Int): Int {
    return operation(x)
}

사용 예

val square = { num: Int -> num * num }
println(applyOperation(4, square))  // 16

1. 위와 비슷하게 applyOperation 함수를 만들기

fun applyOperation(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
	return operation(x, y)
}

 

2. 인자로 넘길 함수도 만들어서 테스트 (예: 제곱, 2배 등)

val involution = { num1: Int, num2: Int -> num1.pow(num2) }

 

    [보충] pow()는 Double용 함수이므로 반환형에 위배

    2-1. toInt()

val involution = { num1: Int, num2: Int -> 
	num1.pow(num2).toInt()
}

    2-2. 재귀/반복

val involution = { num1: Int, num2: Int ->
	var result = 1
	repeat(num2) { result *= num1 }
	result
}

 

3. println()으로 결과 출력

println(applyOperation(3, 4, involution))

함수를 만들어서 리턴하는 함수

1. 곱셈 함수 리턴하기

[문제]

함수 getMultiplier를 작성하세요.

이 함수는 정수 n을 입력받아, 또 다른 정수 x를 입력받으면 x * n을 반환하는 함수를 리턴해야 합니다.

 

[조건]

  • 함수 이름: getMultiplier
  • 매개변수: n: Int
  • 반환값: (Int) -> Int 타입의 함수

[예시]

val double = getMultiplier(2)
println(double(10))  // 20

val triple = getMultiplier(3)
println(triple(10))  // 30

 

답:

fun getMultiplier (n: Int): (Int) -> Int{
	return { x: Int -> x * n}
}

 

2. 조건 함수 리턴하기

[문제]

함수 getGreaterThan을 작성하세요.

이 함수는 기준값 n을 입력받고, 어떤 정수가 그 값보다 큰지를 판별해주는 함수를 반환합니다.

 

[조건]

  • 함수 이름: getGreaterThan
  • 매개변수: n: Int
  • 반환값: (Int) -> Boolean 타입의 함수

[예시]

val isAdult = getGreaterThan(19)
println(isAdult(20)) // true
println(isAdult(18)) // false

 

답:

fun getGreaterThan(n: Int): (Int) -> Boolean{
	return { x: Int -> x > n }
}

 

3. 고차함수 조합하기

[문제]

숫자 리스트와 조건 함수를 받아서 조건에 맞는 숫자만 출력하는 filterAndPrint 함수를 작성하세요.

 

[조건]

  • 함수 이름: filterAndPrint
  • 매개변수:
    • numbers: List<Int>
    • condition: (Int) -> Boolean
  • 동작: condition을 만족하는 숫자만 출력 (println() 사용)

[예시]

val numbers = listOf(1, 2, 3, 4, 5, 6)
val evenCondition = { x: Int -> x % 2 == 0 }
filterAndPrint(numbers, evenCondition)
// 출력: 2, 4, 6

 

답:

fun filterAndPrint(numbers: List<Int>, condition: (Int) -> Boolean){
	numbers.forEach{ number ->
		if(condition(number)){
			println(number)
		}
	}
}

연산을 매핑한 Map<String, (Int, Int) -> Int>으로 관리하는 방식

1. 연산 기호에 따라 수식 처리하기

[문제]

  • 연산 기호(String)와 실제 연산 함수(Int, Int) -> Int를 Map으로 관리한다.
  • getOperationFromMap(op: String): (Int, Int) -> Int 함수를 작성해서,
    • Map에서 해당 연산을 찾아 반환
    • 없으면 예외를 던지거나 기본 연산(예: 덧셈) 반환

[힌트]

val operations = mapOf<String, (Int, Int) -> Int>(
    "+" to { a, b -> a + b },
    "-" to { a, b -> a - b },
    "*" to { a, b -> a * b },
    "/" to { a, b -> a / b }
)

fun getOperationFromMap(op: String): (Int, Int) -> Int {
    return operations[op] ?: throw IllegalArgumentException("Unknown operation")
}
  • getOperationFromMap 함수를 완성해보고,
  • main에서 "+" 와 "*" 연산을 각각 가져와 호출해 출력해봐.

 

답:

val operations = mapOf<String, (Int, Int) -> Int>(
    "+" to { a, b -> a + b },
    "-" to { a, b -> a - b },
    "*" to { a, b -> a * b },
    "/" to { a, b -> a / b }
)

fun getOperationFromMap(op: String): (Int, Int) -> Int {
    return operations[op] ?: throw IllegalArgumentException("Unknown operation")
}

fun main() {
	val plus = getOperationFromMap("+")
	val multiply = getOperationFromMap("*")
	
	println(plus(3, 4))
	println(multiply(2, 5))
}

 

2. 사용자로부터 직접 연산 기호 (+, -, *, /) 입력받아서 처리하기

[문제]

  • 사용자에게 연산 기호를 입력받고,
  • 위 Map에서 연산 함수를 가져와서 적용하는 코드를 작성해보자.

[힌트]

  • readLine() 으로 입력받기 (터미널 환경 가정)
  • 입력받은 연산 기호를 getOperationFromMap에 넘겨서 함수 받아 사용하기

[예시 흐름]

연산 기호를 입력하세요 (+, -, *, /): *
3 5
결과: 15

 

답:

val operations = mapOf<String, (Int, Int) -> Int> (
	"+" to { a, b -> a + b },
    "-" to { a, b -> a - b },
    "*" to { a, b -> a * b },
    "/" to { a, b -> a / b }
)

fun getOperationFromMap(op: String): (Int, Int) -> Int{
	return operations[op] ?: throw IllegalArgumentException("Unkown operation")
}

fun main() {
	print("연산 기호를 입력하세요 (+, -, *, /)")
	
	val inputOperation = readLine()!!
	val numbers = readLine()!!.split(" ").map{ it.toInt()}
	
	val operation = getOperationFromMap(inputOperation)
	
	println("결과: " + operation(numbers[0], numbers[1]))
}

 

[보충]

operation과 관련된 변수를 두개나 설정해야하는게 어색함

1. 변수 이름 중복을 줄여서 만들기

val opSymbol = readLine()!!
val operate = getOperationFromMap(opSymbol)

2. 입력과 동시에 함수처리하기

val operate = getOperationFromMap(readLine()!!)

커링(Currying)이란?

  • 여러 개의 인자를 한 번에 받는 함수를
  • 하나의 인자만 받는 함수들의 연쇄로 바꾸는 기법

-> (A, B) -> C 형태 함수를 (A) -> (B) -> C 형태로 변환

 

[예시]

fun add(a: Int): (Int) -> Int = { b -> a + b }

fun main() {
    val addFive = add(5)
    println(addFive(3))  // 8 출력
}
  1. multiply 커링 함수를 작성
    • multiply(a: Int): (Int) -> Int 형태
    • 반환 함수는 a와 b를 곱한 결과를 반환
  2. multiply를 이용하여 구현하기
    • val timesTen = multiply(10)을 작성하기
    • timesTen(5) 결과를 출력하기

답:

fun multiply(a: Int): (Int) -> Int = { b -> a * b}

fun main(){
	val timesTen = multiply(10)
	println(timesTen(5))
}