람다는 값처럼 여기저기 전달할 수 있는 코드 블록이다. 변수에 저장할 수도 있지만, 대부분 함수에 인자로 넘길 때 그 자리에서 람다를 정의하는 경우가 대부분이다.
아래는 람다 식의 형태이다.
코틀린의 람다는 중괄호로 둘러쌓여 있다. 파라미터와 본문으로 구성되어 있으며, 화살표(->)가 둘을 구분한다. 그리고 자바와 달리 파라미터 주변에 ()가 없다.
val maxAge = people.maxByOrNull { it.age }
람다 문법을 이야기하기 위해 컬렉션 확장 함수 maxByOrNull을 가져왔다. 위 형태는 람다 문법을 이용하여 람다 식을 간략하게 표현한 것으로, 정식 문법으로 적으면 다음과 같다.
people.maxByOrNull({ p: Person -> p.age })
간략화하지 않으니 어떤 일이 벌어지고 있는지 명확하다.
함수에 람다를 인자로 넣어 호출하고 있으며, 람다는 Person 타입의 객체를 입력 받아서 age 필드 값을 반환한다.
정식 문법은 명확하지만 불필요한 정보들이 있다. 코틀린은 람다를 간략하게 사용할 수 있도록 여러 문법을 지원하는데, 위 코드를 하나씩 바꿔보며 각 문법을 살펴보자.
잠시 람다 문법을 살펴보기 앞서서 함수 타입을 표현하는 방식을 살펴보자. 코틀린은 함수를 값처럼 다룰 수 있으며, 함수 타입이 존재한다.
표현 방식은 다음과 같다
(변수1 이름: 타입, 변수2 이름: 타입, ...) -> 리턴 값 타입
매개변수 정보와 리턴 타입으로 구성되며, 둘을 화살표(->)로 구분한다. 또한, 람다 내부와 달리 매개변수 정보를 괄호로 둘러 싼다.
val sumFunction : (Int, Int) -> Int = { x: Int, y: Int -> x + y }
(Int, Int) -> Int가 함수 타입에 해당한다.
설명과 달리 변수 이름을 나타내지 않았는데, 변수 이름은 생략이 가능하다. 즉, (num1: Int, num2: Int) -> Int도 가능하다.
람다가 함수의 마지막 인자라면, 람다를 함수 호출 괄호 뒤로 뺄 수 있다.
people.maxByOrNull() { p: Person -> p.age }
그래서 함수 타입은 보통 파라미터의 마지막에 정의한다.
만약 함수 타입의 인자를 여러 개 입력 받는 함수라면, 람다를 괄호 안에 넣어서 호출할 수도 있고 뒤로 뺄 수도 있다. 하지만 마지막 람다만 뺄 수 있기 때문에, 둘 다 괄호 안에 넣는 편이 낫다.
// 일반적인 호출 방식
func({ /* 람다1 */ }, { /* 람다2 */ })
// vs
// 마지막 람다 뒤로 빼기
func({ /* 람다1 */ }) { /* 람다2 */ }
람다가 함수의 유일한 인자일 경우, 함수의 호출 괄호()를 없앨 수 있다.
people.maxByOrNull { p: Person -> p.age }
람다의 파라미터도 일반 변수처럼 컴파일러가 타입을 추론할 수 있다. 그래서 파라미터 타입을 생략할 수 있다. 위 코드는 컬렉션의 타입이 Person이라는 것이 명확하므로 굳이 명시할 필요가 없다.
people.maxByOrNull { p -> p.age }
람다의 변수가 하나이고 타입 추론이 가능한 경우, 디폴트 변수명 it을 사용할 수 있다.
위의 경우 람다에 입력이 하나(컬렉션의 모든 원소 하나씩)이고 타입도 명확하므로, it을 사용할 수 있다.
people.maxByOrNull { it.age }
it은 코드를 간결하게 만들지만 남용할 경우 가독성을 해칠 수 있다. 예를 들어, 람다가 중첩되어 있으면 it이 어느 블록의 변수인지 헷갈릴 것이다.
people.map { p ->
p.friends.joinToString(" and ") { f -> f.name }
}
// it의 소속이 분명치 않다
people.map {
it.friends.joinToString(" and ") { it.name }
}
이처럼 타입이나 변수명 생략이 가독성을 해친다면 정보를 명시하자.
지금까지 본문이 한 줄인 람다를 살펴봤다.
만약 람다가 여러 줄로 구성되어 있는 경우, 본문의 마지막 라인이 람다의 결과(리턴) 값이 된다.
val sumFunc = { x: Int, y: Int ->
println("$x + $y = ${x + y}")
x + y // 리턴 값
}
val sum = sumFunc(3, 5) // sum: 8
(※ 변수에 람다를 정의하여 대입할 때는 파라미터를 추론할 문맥이 없기 때문에 이름과 타입을 명시할 수 밖에 없다.)
지금까지 살펴본 람다의 사용 형태는 간단했다. 좀 더 복잡한 식에 적용해보자.
joinToString 함수는 리스트를 문자열로 변환할 때 각 원소를 원하는 방식으로 조작하여 변환하기 위해 사용한다.
val people = listOf(Person("윤동주", 20), Person("이육사", 30))
val name = people.joinToString(
separator = " ",
transform = { p: Person -> p.name }
)
println(names) // 출력: 윤동주 이육사
transform 매개 변수는 함수 타입이고 마지막에 위치한다. 따라서, 람다를 괄호 뒤로 빼서 아래와 같이 바꿀 수 있다.
val name = people.joinToString(" ") { it.name }
첫 번째 방식은 괄호 안에 람다를 넣되, named argument를 사용하여 인자의 의미가 명확하고, 방금의 축약 방식은 간결하다.
각 방식은 장단점이 있으므로, 상황과 기호에 맞게 람다 문법을 사용하자!