Kotlin 람다 식(Lambda expression)

개요

여기서는 익명 함수를 다루는 람다 식과 클로저에 대해서 알아 보겠다. 비슷한거 같으면서도 다른 점이 있다. 여기서 알아보도록 하겠다.

익명 함수 (Anonymouse function)

먼저 익명 함수(Anonymouse function)가 무엇인지 예제를 살펴 보면서 알아보겠다.

미리 정의된 함수를 :: 사용하면 함수 객체로 변환하는 일련의 흐름을 거치지 않고 함수 객체를 직접 생성 할 수 있다. 아래 예제는 (1), (2), (3)은 같은 결과를 얻을 수 있다.

예제: 함수 객체의 리터럴 표현

// (1)
fun succ(n: Int): Int = n + 1
map(list, ::succ)

// (2)
map(list, fun(n: Int): Int { return n + 1 })

// (3)
map(list, {n: Int -> n + 1})

(2)나 (3)과 같이 함수 객체를 직접 생성하는 기법을 함수 리터럴(Function literal)이라고 한다. 익명 함수(Anonymouse function고도 한다.

(3)과 같은 함수 리터럴의 서식을 일반화하면 다음과 같다.

{인수 목록 -> 함수 본문}

람다 식(Lambda expression)

람다 식(람다표현식:Lambda expression)은 간단히 람다라고도 하고, 익명 함수(Anonymouse function)를 의미한다. 익명 함수는 함수의 이름이 없는 혹은 생략한 함수를 말한다. 보통 한번 사용되고 재사용되지 않는 함수를 만들때 익명 함수로 만드는데, 변수에 대입하여 이용하거나 콜백 함수에 지정할 수 있다. 그러기에 함수를 따로 생성하지 않고, 코드에 익명 함수를 만들어 코드 가독성을 높일 수 있다. 이는 함수형 프로그래밍에서 자주 사용하는 패턴이다.

fun main() {
    var add1 = fun(a: Int, b: Int): Int = a + b

    println(add1(3, 5))
}

Output:

8

클로저 (closure)

함수 리터럴에는 괄호가 필요하다는 점에 주의하자. 그리고 문맥에 따라 괄호를 생략할 수도 있다.

예제: 함수 리터럴 표기법

// 함수 리터럴 내에서 타입 추론이 가능하다.
val foo: (Int) -> Int = { n ->
    n + 1
}

// 인수가 1개인 경우는 암묵적으로 it를 사용할 수 있다.
val bar: (Int) -> Int = {
    it + 1
}

// 여러 문장이 있는 함수 리터럴
val baz: (Int) -> Int = {
    var sum = 0
    for (e in 1..it) {
        sum += e
    }
    sum
}

// 고층 함수에 전달하는 특수한 기법
map(listOf(1, 2, 3)) {
    it + 1
}

그런데 Kotlin 함수 리터럴은 클로저(closure)이라고 한다. 즉, 인수에 주어진 변수 이외의 변수를 코드를 작성했을 때의 범위로 해결할 수 있다. 글로 설명하는 것보다는 간단한 예제 코드를 보도록 하자.

아래 예제에 클로저를 반환하는 함수 counter 이다.

fun counter(): () -> Int {
    var count = 0
    return {
        count++
    }
}

이 함수는 “Int을 반환하는 함수"를 반환한다. “Int를 반환하는 함수"를 함수 A라고 부른다. 함수 A는 { count++ }이다. 변수 count는 함수 A 외부에서 선언되지만 함수 A가 정의 된 곳에서 이를 참조하고 갱신할 수 있다. 함수 A를 호출하면 count 값을 반환하고 count는 1씩 증가한다.

함수 counter의 사용 예는 아래와 같다. 호출할 때마다 1씩 증가한다.

val counter1 = counter()
println(counter1()) // => 0
println(counter1()) // => 1
println(counter1()) // => 2

val counter2 = counter()
println(counter2()) // => 0
println(counter2()) // => 1

counter()에 의해 함수 A를 얻고 있다. 함수 A(여기서는 변수 counter1으로 선엄됨)가 호출 될 때마다 반환되는 값이 증가하고 있는 것을 알 수 있다.

람다 식과 클로저의 차이점

그렇다면 람다 식과 클로저의 차이는 무엇인가? 일단 람다와 클로저는 모두 익명의 특정 기능 블록이다.

차이점으로는 클로저는 외부 변수를 참조하고, 람다는 매개변수만 참조한다는 점이 다르다.

Closure 예제) 외부의 count 라는 변수를 참조

val count = 1

fun increase(): Int {
    return count + 1
}

Lambda 예제) 외부에서 count라는 매개변수를 전달 받음

  fun increase(count: Int): Int {
      return count + 1
  }

클로저는 외부에 의존성이 있고, 람다는 외부에 의존성이 없는 스태틱 메서드와 비슷하다.

익명 함수 생성

간단히 익명 함수를 사용 예제를 보자면 아래와 같다. 익명 함수는 아래처럼 이름없이 정의되는 함수(fun(str: String))를 말한다.

fun main() {
    // 익명 함수를 생성하여 greeting에 할당한다.
    val greeting1 = fun() { println("Hello world!") }

    // 익명 함수 호출한다.
    greeting1()
}

Output:

Hello world!

위 예제에서는 greeting 변수에는 에는 익명 함수가 할당 되었고, greeting 익명 함수 변수를 호출하였다.

이를 람다를 이용하면 더 간단히 익명 함수를 정의할 수 있다. 아래 예제는 위의 예제를 람다로 다시 작성한 코드이다.

fun main() {
    val greeting2: () -> Unit = { println("Hello world!") }

    greeting2()
}

Output:

Hello world!

인자를 받고, 값을 반환하는 익명 함수

아래 예제는 인자를 받아 값을 반환하는 익명 함수이다.

fun main() {
    val greeting3 = { name: String, age: Int -> "Hello. My name is $name. I'm $age year old." }

    val result3 = greeting3("devkuma", 21)

    println(result3)
}

Output:

Hello. My name is devkuma. I'm 21 year old.

-> 왼쪽에는 인자 이름(name, age)과 데이터 타입(String, Int)이 명시되어 있다. -> 오른쪽에는 반환값("Hello My name...")이 작성되어 잇다. return은 암시적으로 작성하지 않는다.

인자 타입을 생략할 수 있는 익명 함수

아래 예제에서는 인자 타입을 생략한 익명 함수이다.

fun main() {
    val greeting4: (String, Int) -> String = { name, age -> "Hello. My name is $name. I'm $age year old." }

    val result4 = greeting4("devkuma", 21)

    println(result4)

Output:

Hello. My name is devkuma. I'm 21 year old.

위에 예제에서는 greeting4 변수에 (String, Int) -> String 함수를 타입으로 정의하고 있어서, 임명 함수에서는 name, age와 같이 인자 타입을 생략할 수 있다.

인자 이름을 생략할 수 있는 익명 함수

익명함수의 인자가 1개인 경우, 인자 이름을 생략할 수 있다.

아래 예제는 name 변수를 1개를 받는 익명 함수이다.

fun main() {
    val greeting5: (String) -> String = { name -> "Hello. My name is $name." }

    val result5 = greeting5("devkuma")

    println(result5)

Output:

Hello. My name is devkuma.

위와 같이 명시적으로 1개뿐인 변수(name)인 경우에는 변수명을 생략할 수도 있다. 생략을 하게 되면 iterator의 축약형인 it이라는 이름으로 접근을 해야 한다. 그 사용 예제는 아래와 같다.

fun main() {
    val greeting6: (String) -> String = { "Hello. My name is $it." }

    val result6 = greeting6("devkuma")

    println(result6)

Output:

Hello world!

위 예제에서는 name을 생략했고, name 대신에 it을 사용하고 있는 것을 볼 수 있다.

이 모든 것들을 생략할 수 있었던 이유는 변수에 이미 인자와 리턴 타입이 정의되어 있기 때문이다. 컴파일러에서는 이 모든 정보를 알고 있기 때문에 생략해도 빌드 에러가 발생하지 않는다.

라이브러리에 사용되는 익명 함수

코틀린에서 여러 객체들은 함수를 인자로 받는 함수들이 많다. 익명함수를 사용하면 아래처럼 짧은 코드로 쉽게 구현할 수 있다.

fun main() {
    val numbers = listOf(5, 1, 3, 2, 9, 6, 7, 8, 4)
    println(numbers)

    val sorted = numbers.sortedBy { it }
    println(sorted)

    val biggerThan5 = numbers.sortedBy { it }.filter { it > 5 }
    println(biggerThan5)

Output:

[5, 1, 3, 2, 9, 6, 7, 8, 4]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[6, 7, 8, 9]

sortedBy { it }에서의 익명 함수는 { it } 이것이다. sortedBy 함수의 정의를 보면 인자가 (T) -> R?로 정의 되어 있는데, numbers가 이미 Int라는 것을 알 수 있기에 생략할 수 있는 것이다.

public inline fun <T, R : Comparable<R>> Iterable<T>.sortedBy(crossinline selector: (T) -> R?): List<T> {
    return sortedWith(compareBy(selector))
}

.filter { it > 5 }도 내부도 익명 함수입니다.

이렇게 익명 함수를 사용하게 되면 중복을 제거 하룻 있게 때문에 코드가 간결해지는 장점이 있다.

SAM(Single Abstract Method)

추상 메소드에 하나의 메소드만 있는 것을 SAM(Single Abstract Method)은 이라고 한다. 람다 표현식으로 SAM의 익명객체를 만들어 인자로 넘길 수 있는데, 이는 익명 함수와 유사하다.

아래 코드는 안드로이드에서 흔하게 쓰이는, 익명 객체 패턴인데, setOnClickListener는 익명 클래스를 인자로 받는다.

익명 클래스 코드에서 실질적인 구현 코드는 doSomething뿐이지만 클래스를 정의하기 위해 형식적인(boilerplate) 코드를 작성해줘야 한다.

button.setOnClickListener(object : OnClickListener{
  override fun onClick(view: View){
    doSomething()
  }
})

람다를 이용하면 구현 코드만 작성하여 인자로 넘겨줄 수 있다. 인자의 타입은 이미 정의되어있기 때문에, 타입에 맞게 람다 표현식만 작성하면 컴파일러가 알아서 익명 객체로 만들어 준다.

아래 코드는 람다로 익명클래스로 인자를 생성하는 예제이다.

fun setOnClickListener(listener: (View) -> Unit)

위에 같은 함수를 아래와 같이 호출할 수 있다.

button.setOnClickListener({ view -> doSomething() })

여기서 컴파일러에서 인자의 타입을 이미 알고 있기 때문에 아래와 같이 인자를 생략할 수 있다.

button.setOnClickListener() { doSomething() }

또 여기서 인자가 익명함수 또는 익명 클래스이면, 아래와 같이 함수의 괄호(())도 생략할 수 있다.

button.setOnClickListener { doSomething() }

생략할 수 있는 부분을 제거하다 보니 코드가 한결 간결해 졌다.

정리를 하자면, 위에서 볼수 있는 것 같이 익명 함수를 인자로 전달하는 것이 아니라 함수의 구현 내용 처럼 보여, 가독성을 높여줄 수 있는 문법이다. 그런데, 함수 구조에 따라서 가독성을 떨어 틀이는 경우도 있을 수 있으니 사용시 주의를 해야 한다.

참조