Kotlin의 람다 및 멤버 참조
람다(Lambda expression)은 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다. 자바에서는 오랜 기간 람다를 지원하지 않다가 자바 8에 와서 람다를 지원하기 시작했다. 코틀린 표준 라이브러리는 람다를 많이 사용하기 때문에 람다를 더욱 유용하게 활용할 수 있다.
코드 블록을 함수 인자로 넘기기
“이벤트가 발생하면 이 핸들러를 실행하자”, “데이터 구조의 모든 원소에 이 연산을 적용하자” 같은 아이디어를 코드로 표현하려면 어떻게 할까? 자바에서는 무명 내부 클래스를 활용하여 이 목적을 달성했다.
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
//클릭 시 수행할 동작
}
});
그러나 함수형 프로그래밍은 함수를 값처럼 다루는 접근 방법을 통해 이를 해결한다. -> 함수형 언어는 함수를 직접 다른 함수에 전달할 수 있다. 람다 식을 사용하면 함수를 선언할 필요가 없고 코드 블록을 직접 함수의 인자로 전달할 수 있다.
button.setOnClickListener { /*클릭 시 수행할 동작 */ }
코틀린에서 위 코드는 자바의 무명 내부 클래스와 같은 역할을 하지만 훨씬 간결하고 읽기 쉽다.
람다와 컬렉션
코틀린은 컬렉션에 다룰 때 수행하는 여러 일반적인 작업을 모아놓은 라이브러리가 있다. 이런 패턴은 람다가 없으면 제공하기 어렵기 때문에 자바 8 이전에는 자바에서 쓰기 편한 컬렉션 라이브러리가 적었으며 직접 구현해 사용하곤 했다.
data class Person(val name: String, val age: Int)
Person으로 만들어진 리스트가 있다고 가정하자. 여기서 가장 연장자를 찾고 싶을 땐 어떻게 해야 할까. 보통은 이렇게 할 것이다.
fun findTheOlder(people: List<Person>) {
var maxAge = 0
var theOldest: Person? = null
for (person in people) {
if(person.age > maxAge) {
maxAge = person.age
theOldest = person
}
}
println(theOldest)
}
구현 난이도가 높지는 않지만 코드량이 많아 귀찮다. 코틀린에서는 이런 기능을 제공하는 라이브러리 함수를 사용하면 된다.
println(people.maxByOrNull { it.age })
모든 컬렉션에 대해 이 함수를 사용할 수 있다. 멤버 참조를 사용하면
println(people.maxByOrNull(Person::age))
다음과 같이 쓸 수도 있다. 이는 멤버 참조 절에서 자세히 다룰 예정이다.
코틀린의 버전 업데이트에 따라 글 작성 시점 기준 최신 버전 코틀린은maxBy
함수가 Deprecated 되고maxByOrNull
함수가 그 위치를 대신하고 있습니다.
람다 식의 문법
람다 식의 문법은 다음과 같다.
코틀린의 람다 식은 항상 중괄호로 둘러싸여 있다. 화살표( ->
)가 파라미터와 본문을 구분한다.
람다 식을 변수에 저장할 수 있다.
val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2))
람다 식을 직접 호출할 수 있지만 굳이 쓸모는 없다. 코드의 일부분을 블록으로 둘러싸 실행하려면 run을 사용한다.
{ println(42) }()
run { println(42) }
람다 호출은 부가 비용이 들지 않으며 비슷한 성능을 낸다.
앞서 작성했던 컬렉션 함수를 다시 풀어서 쓰면 다음과 같다.
people.maxByOrNull({ person: Person -> person.age })
중괄호 안의 코드는 람다 식이고 이 람다 식을 maxByOrNull
함수에 넘기는 것으로 해석할 수 있다. 이제 이 코드를 간단하게 해보자. 먼저 코틀린은 함수 호출 시 맨 뒤 인자가 람다면 람다를 괄호 밖으로 빼는 관습이 있다. 람다가 유일한 인자일 경우 빈 괄호는 생략할 수 있다.
people.maxByOrNull { person: Person -> person.age }
컴파일러는 람다 파라미터 타입도 추론한다. 따라서 파라미터 타입을 명시할 필요가 없다. 컴파일러는 Person
타입 컬렉션에서 maxByOrNull
함수를 호출하는 것을 알고 있으므로 람다의 파라미터도 Person
이라는 사실을 알 수 있다. 다만 타입 정보가 코드 해석에 도움이 된다면 남겨놔도 괜찮다.
people.maxByOrNull { person -> person.age }
파라미터의 디폴트 이름 it
을 사용하면 파라미터 이름도 생략해 람다 식을 더 간결하게 표현할 수 있다. 람다의 파라미터가 하나뿐이고 그 타입을 컴파일러가 추론할 수 있으면 it
을 사용할 수 있다.
people.maxByOrNull { it.age }
it
은 코드를 간단하게 만들어주지만 남용하면 안된다. 람다 안에 람다가 중첩되는 경우 각각의it
이 어느 람다에 속해있는지 알기 어렵다. 문맥상 파라미터의 의미나 타입을 쉽게 알 수 없는 경우에도 이를 명시하는 것이 도움이 된다.
파라미터의 타입을 추론할 수 없는 경우에는 타입을 명시해야 한다.
val getAge = { p: Person -> p.age }
여러 줄로 이뤄진 람다의 경우 맨 마지막의 식이 람다의 결과 값이 된다.
val sum = { x: Int, y: Int ->
println("$x + $y")
x + y
}
변수에 접근
람다를 함수 안에서 정의한다면 함수의 파라미터 뿐 아니라 람다 정의의 앞에 선언된 로컬 변수도 람다에서 사용할 수 있다.
fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
messages.forEach { //For-Each Lambda
println("$prefix $it")
}
}
자바와 비슷하지만 자바와 다른 점은 코틀린 람다 내에서 final이 아닌 변수에도 접근할 수 있다. 또한 람다 안에서 바깥 변수를 변경할 수 있다. 이처럼 람다가 사용할 수 있는 변수를 람다가 포획한(captured) 변수라고 한다.
멤버 참조
::
를 사용하는 식을 멤버 참조(Member reference)라고 한다. 멤버 참조는 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어준다.
val getAge = Person::age
위 멤버 참조는 다음 람다 식을 간략하게 표현한 것이다.
val getAge = { person: Person -> person.age }
멤버 참조 뒤에는 괄호를 넣으면 안된다. 또한 멤버 참조는 그 멤버를 호출하는 람다와 같은 타입이다. 따라서 람다와 자유롭게 바꿔 쓸 수 있다.
최상위에 선언된 함수나 프로퍼티를 참조하려면 다음과 같이 한다.
fun salute() = println("Salute!")
>>> run(::salute)
생성자 참조(Constructor reference)를 사용하여 클래스 생성 작업을 연기하거나 저장할 수 있다.
val createPerson = ::Person
val p = createPerson("Alice", 29)
확장 함수도 멤버 함수와 같은 방식으로 사용할 수 있다.