OSS로 공부 :: Kotlin - Function literals with receiver

똑같은 코드는 이제 그만

전편에 이어 이번에도 똑같은 코드를 다시 한 번 볼 것이다.

1
2
3
4
5
6
7
8
fun bot(body: Bot.Builder.() -> Unit): Bot = Bot.Builder().build(body)

fun main(args: Array<String>) {
val bot = bot {
token = "YOUR_API_KEY"
}
bot.startPolling()
}

몇 번이고 봐서 지겨운 이 코드에는 Trailing Lambda가 포함돼 있다. 그리고 지난 편 마무리에서처럼 또 다른 무언가가 숨겨져 있다.

함수 타입, 그리고 람다

코틀린에서 함수는 일급 시민(first-class)이다. 즉, 함수를 변수에 대입할 수도 있는데 이때 해당 변수의 타입이 바로 함수 타입이다. 자바를 생각해보면 모든 것은 클래스로 시작해서 클래스로 끝나고 함수는 클래스의 멤버로서 실행되는 하나의 기능이었으나 코틀린에서는 함수 자체가 하나의 변수가 되어 또 다른 함수의 인수가 될 수 있다.

코틀린의 공식 문서(링크)에서는 함수 타입은 특별한 표기법을 갖고 있는데 다음과 같다고 한다.

  • All function types have a parenthesized parameter types list and a return type: (A, B) -> C denotes a type that represents functions taking two arguments of types A and B and returning a value of type C. The parameter types list may be empty, as in () -> A. The Unit return type cannot be omitted.
  • Function types can optionally have an additional receiver type, which is specified before a dot in the notation: the type A.(B) -> C represents functions that can be called on a receiver object of A with a parameter of B and return a value of C. Function literals with receiver are often used along with these types.
  • Suspending functions belong to function types of a special kind, which have a suspend modifier in the notation, such as suspend () -> Unit or suspend A.(B) -> C.
1
2
3
- 모든 함수 타입은 괄호화된 매개 변수 유형 목록과 반환 유형이 있다. (A, B) -> C
- 간혹 리시버가 추가 된 표기법도 갖고 있다. A.(B) -> C
- 특별한 종류의 지연 함수도 있다. suspend () -> Unit 이나 suspend A.(B) -> C

여기서 제일 먼저 눈에 띄는 건 바로 함수의 형태. 이 전에 봤었던 것처럼 람다의 모습이다. 함수 타입은 람다 유형의 함수를 저장하는 타입인 것이다. 그리고 두 번째로는 A.(B) -> C형태의 리시버가 추가 된 표현이다.

일반 람다와 수신 객체 지정 람다, 그리고 리시버(수신 객체)

우선 설명에 들어가기 앞서 일반 람다를 받는 buildString 함수를 정의해보자.

1
2
3
4
5
6
7
8
9
10
11
12
fun buildString(buildAction: (StringBuilder) -> Unit): String {
val sb = StringBuilder()
buildAction(sb)
return sb.toString()
}

>>> val s = buildString {
it.append("Hello, ")
it.append("World!")
}
>>> println(s)
Hello, World!

이 코드는 이해하기 쉬우나 사용자가 사용하기에는 편하지 않다. (오.. 후미 람다..) 람다 본문에서 매번 it을 사용해 StringBuilder 인스턴스를 참조해야 한다. 우리가 원하는 것은 StringBuilder에 텍스트를 채우는 것이므로 it을 일일이 넣지 않고 싶다. 이럴 때 코틀린에서 제공하는 수신 객체 지정 람다(lambda with a receiver)를 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
fun buildString(buildAction: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.buildAction()
return sb.toString()
}

>>> val s = buildString {
this.append("Hello, ")
append("World!")
}
>>> println(s)
Hello, World!

위 코드를 보면 달라진 점은 buildString의 buildAction의 선언이다. 파라미터 타입을 선언할 때 이전엔 일반 함수타입이었다면 이번엔 확장 함수 타입이다. 여기서 .(마침표) 앞에 오는 타입(StringBuilder)를 수신 객체 타입이라 부르며, 람다에 전달되는 그런 타입의 객체를 수신 객체라고 부른다. (공식문서 참고)

1
2
String.(Int, Int) -> Unit
- String : 수신 객체

왜 확장 함수 타입일까

코틀린에서 확장 함수를 사용한다는 것은 확장 대상 클래스에 정의 된 메소드를 마치 그 클래스의 내부에서 호출하듯이 사용할 수 있는 것이다. 즉 그렇기 때문에 일반 함수처럼 객체를 수신 받는 것이 아니라 수신 객체를 지정함으로 해당 객체의 내부 함수를 그대로 사용할 수 있게 하여 불필요한 it과 같은 구문을 삭제할 수 있게 된 것이다.

또한 sb.buildAction()에서 buildAction은 StringBuilder 클래스 안에 정의 되어있는 함수가 아니며, StringBuilder 인스턴스인 sb는 확장 함수를 호출할 때와 동일한 구문으로 호출할 수 있는 함수 타입(따라서 확장 함수 타입)의 인자일 뿐이다.

그렇다면 다시 코드를 보자

1
2
3
4
5
6
7
8
fun bot(body: Bot.Builder.() -> Unit): Bot = Bot.Builder().build(body)

fun main(args: Array<String>) {
val bot = bot {
token = "YOUR_API_KEY"
}
bot.startPolling()
}

오.. 몇 번이나 보는 것이냐.

지금까지 설명한 내용을 토대로 위 코드를 분석 해보자.

  1. bot 객체를 생성하기 위한 bot 함수는 1개의 매개 변수로 정의돼 있고 이 매개 변수는 람다 타입이다. 즉 Trailing Lambda를 통해 함수 호출 시 람다 본문으로 정의하며 호출할 수 있다.

    1
    val bot = bot { .. }
  2. 이 매개 변수는 수신 객체 지정 람다로 인수로 넘어오는 람다 본문에서 객체를 특별히 지정하지 않아도 해당 객체의 내부 변수나 함수를 사용할 수 있다.

    1
    2
    3
    val bot = bot {
    token = "YOUR_API_KEY"
    }

    여기서 token 변수는 Bot 클래스의 멤버 변수라는 것을 알 수 있다.

결론

이 코드는 코틀린의 정신이 굉장히 많이 담겨 있는 코드이다.

해당 코드의 내부를 더 뜯어보면 결국 Builder 패턴을 제공하기 위해 이와 같은 여러가지 기능들을 녹인 것이다. 즉 Bot 객체를 생성하기 위해선 Bot()과 같이 직접 생성을 할 수 없고 꼭 bot 함수를 통해 객체를 생성하도록 유도한다.(물론 Bot 클래스의 기본 생성자에 디폴트 파라미터값을 지정하면 어차피 코틀린에서 이름으로 인자를 지정할 수 있으므로 Builder 패턴처럼 사용 가능하다)

처음 해당 Github의 사용법을 보는데 의아해 했었다. 내가 알고 있는 코틀린의 모습이랑은 뭔가 달라보인다고 할까. 책으로 공부하고 간단한 프로젝트를 진행하면서 사용했던 모습과는 뭔가 달라보였다. 그래서 더더욱 궁금했고 이제서야 모든 걸 알게 되니 지금까지 내가 사용한 코틀린은 어찌보면 코틀린이 아닌 자바였다는 것을 깨닫게 됐다.

물론 꼭 이처럼 사용해야지 코틀린이라고 정의할 순 없으나 코틀린이라는 언어를 만들며 고민하고 고려했던 기능적인 편의성이라고 생각이 충분히 들었다.

끝난 게 아니다.

이 OSS는 텔레그램 API를 쉽게 사용할 수 있도록 개발 됐다. 그럼 얼마나 편하게 제공하는지 더 봐야 하지 않나. 통신은 어떻게 하는지 메세지가 오면 어떤식으로 사용자에게 알려주는지 말이다.

다음에는 바로 이와 같은 내용으로 작성해보려고 한다.

댓글

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×