Kotlin 람다 파헤치기

Betalabs·2022년 7월 19일
5

들어가며

안녕하세요! Betalabs 백엔드 개발팀의 Woogie입니다.
이번에는 코틀린 공식 문서Kotlin In Action을 참고해서 코틀린 람다에 대해 알아보겠습니다. :)


아래에 대해서 알아볼 것입니다.

첫째, 람다란?

둘째, 람다를 왜 사용하는가?

셋째, 람다 표기법

넷째, 컬렉션에서 람다 활용하기

다섯째, Receiver 지정 람다 그리고

여섯째, 마치며

람다란?

Kotlin In Action
람다 식 lambda expression 또는 람다는 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다.

저는 간단한 표현식으로 함수를 전달할 수 있는 표현 방법이라고 이해했습니다.

고차함수 (High-order function)

기본적으로 코틀린의 함수는 일급 함수(first-class)입니다.

간단히 일급 함수라는 것은 함수를 변수나 자료구조로 담아낼 수 있고, 파라미터로 다른 함수로 전달하거나 함수를 리턴 가능한 것입니다.

쉽게 예를 들면,

// 함수를 변수로 저장
val apple = { fruit: String -> println("fruit: $fruit") }

apple.invoke("🍎")

// 함수를 파라미터로 전달
val bucket = listOf("🍏", "🍌", "🍊", "🍇").fold("🍎") { acc: String, next: String -> 
	"$acc, $next" 
}.also { println(it) }

와 같이 활용할 수 있겠습니다.

람다를 왜 사용하는가?

람다를 사용하는 이유는 무엇보다 간결함에 있다고 생각합니다.

간결하게 표현함으로써 가독성이 좋아지고 그것은 곧 생산성과도 직결될 수 있다는 생각입니다.

우리는 읽기 쉽고 이해하기 좋은 코드를 선호합니다. 그렇기 때문에 람다를 적극적으로 사용하고 있습니다.

아래는 오라클 공식 문서에서 발췌한 내용입니다.

One issue with anonymous classes is that if the implementation of your anonymous class is very simple, such as an interface that contains only one method, then the syntax of anonymous classes may seem unwieldy and unclear.

간단한 구현을 위해서 익명 클래스를 이용하는 건 장황하고 그런 구문은 불분명한다는 것을 이야기하고 있습니다.

람다 표기법

코틀린의 람다 표기법은 다음과 같습니다.

  • 소괄호로 묶은 파라미터와 리턴으로 이루어져 있으며, 화살표 표기법으로 연관 지어줍니다.
// 매개변수 없음
() -> A

// 매개변수 있음
(A, B) -> C
  • 파라미터의 타입은 생략이 가능하지만, 리턴은 생략할 수 없습니다.
// 생략 전
(a: Int, b: Int) -> Int -- O

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

// 생략 불가능
(a, b) ->  -- X
  • 인스턴스화
// 람다식
{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) }

컬렉션에서 람다 활용하기

Java 8부터 도입된 stream 을 이용하면, 컬렉션을 명령형이 아닌 선언형으로 제어할 수 있어 더욱 가독성 좋은 코드 작성이 가능해졌습니다.

코틀린 은 컬렉션을 더욱 편하게 제어할 수 있게 기본으로 많은 확장 함수를 제공하고 있고, 람다를 이용해 더욱 간결하게 표현 가능합니다.

최대값 구하기

  • Java의 stream 이용
class Person {
    private final String name;
    private final int age;

    public Person(final String name, final int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }
}

final Person billGates = new Person("Bill Gates", 12);
final Person markZuckerberg = new Person("Mark Zuckerberg", 15);
final Person elonMusk = new Person("Elon Musk", 4);

final int max = Stream.of(billGates, markZuckerberg, elonMusk)
                      .map(Person::getAge)
                      .max(Comparator.comparingInt(age -> age)).orElse(0);

System.out.println(max); -- 15
  • Kotlin 이용
// kotlin
class Person(
	val name: String,
	val age: Int
)

val billGates = Person("Bill Gates", 12)
val markZuckerberg = Person("Mark Zuckerberg", 15)
val elonMusk = Person("Elon Musk", 4)

val maxAge = listOf(billGates, markZuckerberg, elonMusk).maxOf { person -> person.age }

println(maxAge) -- 15

위와 같이 코틀린은 컬렉션을 제어하는 유용한 확장 함수들을 제공하고 있습니다.

Receiver 지정 람다 그리고

Receiver 란?

Kotlin In Action
수신객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메소드를 호출 할 수 있게 하는 것이다.
그런 람다를 수신객체 지정 람다 lambda with receiver 라고 부른다.

Receiver 지정 방법

수신 지정 객체 람다를 선언하는 방법은 람다의 파라미터를 감싸던 소괄호를 수신 객체 타입의 오른쪽으로 빼낸 후 마침표(.)를 넣는 형태입니다.

예시로 보면, 다음과 같습니다.

T.() -> R

Receiver 지정 람다 예시

이런 lambda with receiver는 코틀린이 제공하는 스코프 함수 withapply를 알아보면 자세히 알 수 있습니다.

"hello world".apply {
println("$this!!") -- "hello world!!"
}

applywith의 비슷하지만 한 가지 다른 점이 있습니다.

예시를 통해 알아보겠습니다.

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

위와 같이 apply는 수신 객체를 return 합니다.

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

with 는 람다를 호출해서 얻은 결과를 return 합니다.

그리고 ...

위와 같은 receiver를 이용해 계층 형태의 데이터 구조를 손쉽게 만들어 낼 수 있고, 이를 builder라고 합니다.

빌더를 이용해 Kotlin DSL(Domain Specific Language)를 만들어 낼 수 있고, 이를 이용한 대표적인 예시가 바로 gradle입니다.

아래는 Kotlin DSL 사용의 예입니다.

  • Kotlin DSL gradle
dependencies {
    testImplementation(kotlin("test"))
}
  • Typesafe HTML DSL (공식문서 참고)
import kotlinx.browser.*
import kotlinx.html.*
import kotlinx.html.dom.*

fun main() {
    document.body!!.append.div {
        h1 {
            +"Welcome to Kotlin/JS!"
        }
        p {
            +"Fancy joining this year's "
            a("https://kotlinconf.com/") {
                +"KotlinConf"
            }
            +"?"
        }
    }
}

Kotlin DSL 을 이용하면, 간결하고 타입 안전한 builder를 손쉽게 만들어 낼 수 있고, 구조화된 집합을 만들어내기 용이합니다.

마치며

개발을 하며 대표적으로 람다가 쓰이는 범위는 역시 컬렉션과 스코프 함수라는 생각이 들었습니다. 물론, 컬렉션과 스코프 함수를 사용할 때에는 람다에 대해 별생각 없이 접근해도 충분히 사용 가능합니다.

다만, 람다의 특성과 활용 범위에 대해 알고 있다면 더욱 간결하고 생산성 높은 코드 생성을 위한 도구를 만들어 낼 수 있겠다는 생각이 들었습니다.

참고로 베타 랩스에서는 JDSL이라는 JPA Criteria API를 만들어주는 DSL 오픈소스를 적극 활용하고 있습니다.

그 편리함과 간결함 덕에 생산성과 코드 품질 두 마리 토끼를 잘 잡고 있습니다.

1개의 댓글

comment-user-thumbnail
2022년 8월 2일

잘 보고 갑니다!

답글 달기