안드로이드 todo 아키텍처 샘플 프로젝트를 보면서 난생처음 보는 문법이 있어서 궁금해서 찾아보았습니다. 그런데 이것이 자바와 다른 코틀린만의 중요한 특징과도 연결이 되어 있는, 안드로이드 개발자라면 반드시 알아야 하는 부분이라는 것을 알게 되어 이렇게 정리해보게 되었습니다!
코틀린 함수는 일급시민(first-class) 입니다. 즉, 변수에 대입되어서 다른 함수의 인자가 될 수도 있고 반환값이 될 수도 있습니다. 자바의 경우 함수는 클래스의 멤버의 역할만을 수행했지만, 코틀린에서는 함수 자체가 하나의 변수가 될 수 있습니다. 이러한 함수를 표현하기 위해 함수타입 계열(family of function type)을 사용합니다.
쉽게 말해서
val exampleVar : String = "This is an example string"
우리가 변수에 형식(타입)을 정해서 값을 할당하듯이
어떤 변수에 함수를 할당할 때, 그 변수는 함수 타입을 가진다는 것을 명시해야 되는 것이지요.
코틀린은 함수 선언과 관련해서 (Int) -> String 와 같은 함수 타입을 사용합니다.
모든 함수 타입은 괄호로 묶인 파라미터 타입 리스트와 리턴 타입을 가지고 있습니다.
() -> A 처럼 파라미터 타입 리스트는 비어 있을 수 잇습니다. 하지만 리턴 타입은 생략될 수 없습니다.
Function types can optionally have an additional receiver type, which is specified before the dot in the notation: the type A.(B) -> C represents functions that can be called on a receiver object A with a parameter B and return a value C. Function literals with receiver are often used along with these types.
함수 타입은 선택적으로 리시버 타입을 가질 수 있습니다. 점(.) 앞에 명시하면 되는데요,
A.(B) -> C 는 파라미터 B로 리시버 객체 A에 부를 수 있고, c를 반환하는 함수를 나타냅니다. Fuction literals with receiver 는 이러한 타입과 종종 함께 쓰입니다.
서스펜드 함수는 함수 타입의 특별한 종류 중 하나로, suspend () -> Unit or suspend A.(B) -> C. 와 같이 suspend 를 붙여서 표현합니다.
함수 타입은 (x: Int, y: Int) -> Point 와 같이 함수 파라미터에 이름을 붙일 수도 있습니다. 이 이름은 파라미터의 의미를 기록하기 위한 용도로 사용될 수 있습니다.
Function types with receiver, such as A.(B) -> C, can be instantiated with a special form of function literals – function literals with receiver.
A.(B) -> C 와 같은 리시버를 가진 함수 타입(Function type with receiver)는 특별한 형식의 함수 리터럴(function literals)로 인스턴스화 -리시버를 가진 함수 리터럴(function literals with reciever) -될 수 있습니다.
쉽게 말해서
val sum: Int.(Int) -> Int = { other -> plus(other) }
Int.(Int) -> Int , 즉 리시버를 가지는 함수 타입으로 정의된 sum 변수에는
{ other -> plus(other) } 리시버를 가지는 함수 리터릴이 할당되어 있습니다.
다시 말해 리시버를 가지는 함수 리터럴은 변수에 할당된 함수 그 자체라고 볼 수 있습니다.
As mentioned above, Kotlin provides the ability to call an instance of a function type with receiver while providing the receiver object.
코틀린은 리시버 객체를 제공하면서 리시버를 가지는 함수 타입의 인스턴스를 콜할 수 있습니다.
함수 리터럴의 바디 안에서, 콜에 의해 전달된 리시버 객체는 암시된 this가 되어서 리시버 객체를 다른 한정자들 없이, this 없이 그 멤버들에 접근할 수 있습니다. 위의 예시에서도 plus 가 리시버 객체의 멤버 메서드 이고 this 없이 호출할 수 있는 것을 알 수 있습니다.
이는 함수 바디 안에서 리시버 객체의 멤버들에 접근할 수 있는 확장함수와도 유사합니다.
위에서 본 것과 같이 람다식은 컨텍스트에서 리시버 타입을 추론할 수 있을 때, 리시버를 가지는 함수 리터럴로 사용될 수 있습니다. 이들이 활용된 중요한 예시로 type-safe builder 가 있습니다. type-sage builder 로 DSL(Domain Specific Language) 을 만들 수 있습니다. 이것에 대해서는 나중에 다뤄보도록 하겠습니다!
fun Int.withoutReceiver(block: (num: Int) -> Unit) = block(this)
fun main(args: Array<String>) {
100.withoutReceiver {
print(it.hashCode())
}
}
block 의 function type 을 보면 리시버가 없기 때문에 this로 int 를 넘겨주고, 람다의 매개변수 num 으로 받아서 it 으로 접근하는 것을 알 수 있습니다.
fun Int.withReceiver(block: Int.() -> Unit) = block()
fun main(args: Array<String>) {
100.withReceiver {
print(hashCode())
}
}
그런데 위 코드에서는 block 을 호출하면 implicit한 this 가 자연스럽게 따라가는 것을 알 수 있습니다.
그럼 이들이 어디에 쓰일까요?
바로 많이 쓰이는 apply, run, with 와 같은 코틀린 표준함수인 scope function 이 이렇게 구현되어 있습니다!
public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
그 중 apply() 함수와 also() 함수를 비교해보겠습니다. 이 둘이 다른 점은 T.()와 같은 표현에서 람다식이 확장 함수로 처리된다는 것입니다.
fun main() {
data class Person(var name: String, var skills: String)
var person = Person("Ejjjang", "Kotlin")
person.also { it.skills = "Java" }
person.apply { skills = "Swift" }
}
apply() 가 also() 함수와 다른 점은 객체를 넘겨 받는 방식입니다.
also() 함수는 it 을 통해 멤버에 접근합니다. 하지만 apply() 에서는 함수 타입에 리시버가 있기 때문에 this 를 생략하고 멤버 이름만 사용하고 있습니다.
그럼 처음에 말한 안드로이드 샘플 아키텍처 코드를 살펴봅시다.
class TasksActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.tasks_act)
// Set up the toolbar.
setupActionBar(R.id.toolbar) {
setHomeAsUpIndicator(R.drawable.ic_menu)
setDisplayHomeAsUpEnabled(true)
}
...
//util.kt
fun AppCompatActivity.setupActionBar(@IdRes toolbarId: Int, action: ActionBar.() -> Unit) {
setSupportActionBar(findViewById(toolbarId))
supportActionBar?.run {
action()
}
}
AppCompatActivity 를 상속받고 있는 TaskActivity 에서
AppCompatActivity 의 확장함수인 setupActionBar 을 부르고 있습니다. AppCompatActivity 를 상속받았으니 AppCompatActivity.setupActionBar 로 부르지 않아도 되는 것이지요.
첫 번째 인자로 toolbar 의 id 를, 두 번째 인자인 람다 매개변수 action 에 setHome.. 을 넣었습니다. 여기서 바로 setHome.. 으로 넣을 수 있는 이유는 ActionBar 를 리시버 객체로 지정했기 때문이고 따라서 ActionBar 의 멤버 메서드인 setHomeAsUpIndicator 과 setDisplayHomeAsUpEnabled 에 this 없이 접근할 수 있는 것이지요.
https://kmdigit.github.io/2020/05/19/study-with-oss-kotlin-function-literals-with-receiver/
https://kotlinlang.org/docs/lambdas.html
https://kotlinexpertise.com/function-literals-with-receiver/
https://medium.com/til-kotlin-ko/kotlin%EC%9D%98-extension%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B0%80-part-3-587cc37e7337
https://stackoverflow.com/questions/48244734/what-does-classname-mean-in-kotlin
감사합니다 :)