본문 바로가기

Kotlin

Kotlin - 고차함수와 람다식 (Higher-order functions and lambdas)

https://kotlinlang.org/docs/lambdas.html

 

코틀린 함수는 일급 함수이기 때문에 함수 내부에 변수나 데이터 구조를  저장할 수 있고, 함수를 인자로 전달하거나 다른 고차 함수를 통해 반환 값이 될 수 있다. 람다 표현식을 통해 이를 편하게 사용 할 수 있다.

고차함수 (Higher-order functions)

고차 함수는 함수를 리턴하거나 함수를 파라미터로 받는 함수를 말한다.

좋은 예로 함수형 프로그래밍에서 주로 사용하는 fold 함수가 있다. 이 함수는 초기값과 결합 함수를 파라미터로 받아서 모든 원소에 대해 결합함수를 수행한다.

fun <T, R> Collection<T>.fold(
    initial: R,
    combine: (acc: R, nextElement: T) -> R // 함수를 파라미터로 받는다.
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element) // 파라미터로 받은 함수를 수행한다.
    }
    return accumulator
}

// combine으로 acc, i -> acc + " " + i 람다를 전달한다.
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })

 

함수 타입 (Function Types)

코틀린은 (Int) -> String과 같은 함수 타입을 사용한다. 함수타입을 선언해서 사용할 때는 val onClick: (Int) -> String = ... 과 같이 작성한다.

  • 모든 함수 타입은 괄호로 묶인 파라미터와 리턴타입을 갖는다 : (A, B) -> C 형태로 작성된 함수가 있다면, 이 함수는 A, B 두개의 파라미터를 받고 C 타입을 리턴한다는 의미이다. 혹시 파라미터가 없다면 빈 괄호를 작성한다. () ->C
  • 함수 타입은 추가적으로 receiver 타입을 지정할 수 있다. A.(B) -> C 형태로 작성할수 있으며, 이 함수는 A 타입에 있는 B를 파라미터로 받고 C 타입을 리턴하는 함수를 의미한다. 이후 receiver가 있는 함수 리터럴(function literals with receiver) 부분에서 자세히 보자
  • 중단 가능 함수 (suspending function): suspend () -> Unit, suspend A.(B) -> C

함수 타입은 파라미터 이름을 작성해도 되고 하지 않아도 된다. 

nullable 함수 타입은 함수타입 을 괄호로 묶고 물음표를 붙어서 작성한다. ((Int, Int) -> Int)?

(Int) -> ((Int) -> Unit) 과 같이 함수타입을 중첩해서 작성 가능하고 이 작성법은 (Int) -> (Int) -> Unit 과 동일하다.

함수 타입은 타입 별칭(type alias) 로 작성 가능하다.

typealias ClickHandler = (Button, ClickEvent) -> Unit

 

Instantiating a function type

함수 타입 구현 방법들

  • 함수 리터럴 내에 코드 블록 작성하는 방법
    • 람다 표현식: { a, b -> a + b}
    • 익명 함수: fun(s: String): Int { return s.toIntOrNull() ?: 0
  • 호출 가능한 참조 (callable reference)
    • 최상위(top-level), 지역, 멤버 또는 확장 함수: ::isOdd, String::toInt
    • 최상위(top-level), 멤버 또는 확장 속성: List<Int>::size
    • 생성자: ::Regex
  • 함수 타입을 구현한 클래스
class IntTransformer: (Int) -> Int {
    override operator fun invoke(x: Int): Int = TODO()
}

val intFunction: (Int) -> Int = IntTransformer()

val a = { i: Int -> i + 1 } // 컴파일러가 (Int) -> Int 타입으로 유추함

수신자(receiver)가 있는 함수는 수신자를 파라미터로 갖는 함수와 호환 가능하다.

예. A.(B) -> C 타입의 함수는 (A, B) -> 타입과 같은 취급을 받는다.

val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun // OK

fun runTransformation(f: (String, Int) -> String): String {
    return f("hello", 3)
}
val result = runTransformation(repeatFun) // OK

 

함수타입 인스턴스 실행하기 (Invoking a function type instance)

함수타입은 invoke(...) 연산자를 이용하거나: f.invoke(x), 그냥 함수처럼 호출하면: f(x)  된다.

수신자가 있는 함수 타입은 "수신자.f(x)" 형태로 호출하거나, f(수신자, x) 형태로 호출 할 수도 있다.

val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus

println(stringPlus.invoke("<-", "->"))
println(stringPlus("Hello, ", "world!"))

println(intPlus.invoke(1, 1))
println(intPlus(1, 2))
println(2.intPlus(3)) // extension-like call

 

람다 표현식과 익명 함수

람다 표현식과 익명함수는 함수를 선언하지 않고 표현식으로 전달 할 수 있는 함수 리터럴 이다. 

람다 문법

기본적인 람다 표현식은 아래와 같은 형식으로 작성한다.

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y}
val sum2 = { x: Int, y: Int -> x + y }
  • 람다 표현식은 기본적으로 중괄호("{}")로 감싼다.
  • 파라미터는 중과로 아에 작성하며 타입은 작성하지 않아도 된다.
  • 함수 본문은 "->" 다음에 작성한다.
  • 반환타입이 Unit이 아니라면 함수 본문의 가장 마지막 표현식이 리턴 값으로 간주 된다.

trailing 람다 

관습적으로, 함수의 마지막 매개변수가 함수라면 람다 식을 괄호 밖에 작성 할 수도 있는데, 이를 trailing lambda라고 한다.

val product = items.fold(1) { acc, e -> acc * e }

어떤 함수가 1개의 함수 매개변수만 필요하다면 괄호 마저 생략 할 수 있다.

run { println("...") }

it: implicit name of a single parameter

람다 표현식이 단 하나의 파라미터만 받는다면, "->" 포함 왼쪽의 파라미터 선언 부분을 생략 할 수 있으며, 이때 생략된 파라미터는 it 이라는 이름으로 함수 본문에서 사용 할 수 있다.

ints.filter { it > 0 } // '(it: Int) -> Boolean' 함수를 왼쪽 처럼 작성할 수 있다.

Returngin a value from a lambda expression

람다식에 return 문을 명시적으로 작성 할 수도 있는데, 이 때는 qualified return 문(라벨을 지정해서 어느 코드 블럭의 리턴인지 명시)으로 작성해야 한다.

// 아래 두 코드는 동일하게 동작함.
ints.filter {
    val shouldFilter = it > 0
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0
    return@filter shouldFilter // qualified return 문(return@filter)을 작성해서 어드 코드 블록에 대한 리턴인지 명시함.
}

 Underscore for unused variables

람다 파라미터 중 사용하지 않는 파라미터가 있다면 underscore("_") 로 대체 할 수 있다.

map.forEach { (_, value) -> println("$value!") }

 

익명함수 (Anonymous function)

람다식에서는 return 문이 생략된다. 대부분의 경우 큰 문제는 없지만 특별히 return 문을 명시하고 싶은 경우 익명 함수를 사용한다.

// 함수 명을 따로 작성할 필요없다.
// 표현식의 반환 타입은 자동을 유추한다.
fun(x: Int, y: Int): Int = x + y

// 위 코드를 아래와 같이 작성 할 수도 있다.
// 코드 블록의 반환 타입은 명시 해 줘야 한다.
fun(x: Int, y: Int): Int {
    return x + y
}

Closure

람다식이나 익명함수는 함수 범위 외부에 선언되고 함수내부에서 사용중인 변수인 클로저에 접근 할 수 있다.

var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)

수신자가 있는 함수 리터럴

A.(B) -> C 와 같이 수신자(A)가 있는 함수 타입은 수신자 타입의 오브젝트를 이용해 함수를 호출 할 수 있다. 수신자가 있는 함수 타입의 함수 본문에는 수신자 객체를 표현할때 this 를 사용하고, 이를 생략 할 수도 있다. 마치 확장 함수와 비슷한 느낌을 받는다.

수신자 타입이 Int 인 sum 함수를 정의 한다고 했을때 아래와 같이 2가지 방법 (람다, 익명함수)으로 작성할 수 있다.

// 람다
val sum: Int.(Int) -> Int = { other -> plus(other) } // this.plus 에서 this(수신자, Int) 가 생략됨


// 익명함수
val sum = fun Int.(other: Int): Int = this + other // this는 수신자(Int)

// 사용 방법
val a: Int = 3;
println(a.sum(5)) // Int의 확장 함수 처럼 사용 할수도 있고,
println(sum(a, 5)) // 첫번째 파라미터로 수신자를 전달 할 수도 있다.

 

'Kotlin' 카테고리의 다른 글

Kotlin - Inline functions  (1) 2024.04.21
Kotlin - 함수 (Functions)  (0) 2024.04.09
Kotlin - 타입 별칭 (Type aliases)  (0) 2024.03.29
Kotlin - Delegated properties  (0) 2024.03.28
Kotlin - Object 표현과 정의  (0) 2024.03.23