Lambda
1 Lambda
- 람다 식(lambda expression)은 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 의미합니다.
- 람다를 따로 선언해서 변수에 저장할 수 있지만 대부분 함수에 인자로 넘기면서 바로 람다를 정의하는 경우가 많습니다.
1.1 코드 블록을 함수 인자로 넘기기
- "이벤트가 발생하면 이 핸들러를 실행하자" 또는 "데이터 구조의 모든 원소에 이 연산을 적용하자"와 같은 생각을 코드로 구현할 때 일련의 동작을 변수에 저장하거나 다른 함수에 넘기는 경우가 있습니다.
- 자바 8 이전에는 익명 클래스를 통해 코드를 함수에 넘기거나 변수에 저장할 수 있었습니다.
- 클래스를 선언하고 클래스의 인스턴스를 함수에 넘기는 방식으로 상당히 번거로운 작업이었습니다.
- 함수형 프로그래밍 언어에서는 함수를 값처럼 다루는 접근 방식을 택해 이 문제를 해결합니다.
2 람다 식의 문법
- 코틀린 람다 식은 항상 중괄호로 둘러싸여 있습니다.
->가 인자 목록과 분문을 구분합니다.
2.1 람다 예시
{x: Int, y:Int -> x + y}
- 위 예시는 두 개의 Int형 인자를 받아서 그 합을 반환하는 람다 식입니다.
- 인자 목록 주변에 괄호가 없다는 점을 꼭 유의하세요.
val sum = { x: Int, y: Int ->
println("Computing the sum of $x and $y...")
x + y
}
println(sum(1, 2))
- 위와 같이 람다식을 변수에 저장할 수 있습니다.
- 람다를 변수에 저정할 때는 파라미터의 타입을 추론할 문맥이 없기 때문에 타입을 명시해야 합니다.
2.1 람다식 줄여 쓰기
원본
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy({person -> person.age}))
people리스트에서 나이(age)가 가장 많은 사람을 찾는 예시입니다.- 여기서 중괄호
{}안에 있는 코드가 람다식입니다. maxBy함수에 람다식을 전달하여, 각Person객체의age속성을 기준으로 최대값을 찾습니다.- 여기서 코드를 더 간결하게 줄여보겠습니다.
2.1.1 람다식 소괄호에서 빼기
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy() { person -> person.age })
- 함수 호출 시 맨 뒤에 있는 인자가 람다식이면 그 람다를 괄호 밖으로 빼낼 수 있는 문법적인 관습이 있습니다.
maxBy함수의 람다식을 괄호 밖으로 이동시켰습니다.- 이는 가독성을 높이고 코드 작성을 간편하게 만듭니다.
2.1.2 람다식을 함수의 유일한 인자로 썼을 때
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy { person -> person.age })
- 람다가 함수의 유일한 인자라면 빈 괄호
()를 생략할 수 있습니다. - 이렇게 하면 코드가 더 간결해집니다.
2.1.3 it 사용하기
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy { it.age })
- 파라미 터 이름을 지정하지 않으면
it이라는 이름의 디폴트 파라미터가 만들어집니다. - 람다의 파라미터가 하나뿐이고 그 타입을 컴파일러가 추론할 수 있는 경우 it을 사용할 수 있습니다.
2.2 람다식의 결과 값
- 람다식의 본문이 여러줄로 이루어진 경우 본문의 맨 마지막에 있는 식이 람다의 결과 값이 됩니다.
- 이때 명시적인 return이 필요하지 않습니다.
fun main() {
val sum = { x: Int, y: Int ->
println("Computing the sum of $x and $y...")
x + y // 마지막 식이 람다의 결과 값
}
println(sum(1, 2))
// Computing the sum of 1 and 2...
// 3
}
- 위 예시에서
x + y가 람다의 결과 값입니다.
3 람다가 캡쳐한 변수: 현재 영역에 있는 변수 접근
- 람다를 함수 안에서 정의하면 함수의 파라미터뿐 아니라 람다 정의보다 앞에 선언된 로컬 변수까지 람다에서 접근할 수 있습니다.
- 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다는 점이 자바와 다릅니다.
- 따라서 람다 안에서 바깥 변수를 변경할 수 있습니다.
- 자바는 람다 안에서 접근할 수 있는 변수가 final로 선언된 변수만 가능합니다.
3.1 예시
fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
messages.forEach { message ->
println("$prefix $it")
}
}
fun main() {
val errors = listOf("403 Forbidden", "404 Not Found")
printMessagesWithPrefix(errors, "Error:")
// Error: 403 Forbidden
// Error: 404 Not Found
}
- forEach는 각 원소에 대해 수행할 작업을 람다로 받습니다.
- forEach에 넘겨주는 람다는 자신을 둘러싼 영역에 정의된 prefix 변수와 다른 변수를 접근할 수 있습니다.
3.2 바깥 함수 변수 변경하기
fun printProblemCounts(reseponse: Collection<String>) {
var clientErrors = 0 // 람다에서 접근할 수 있는 바깥 변수
var serverErrors = 0 // 람다에서 접근할 수 있는 바깥 변수
response.forEach { response ->
if (response.startsWith("4")) {
clientErrors++ // 바깥 변수를 변경
} else if (response.startsWith("5")) {
serverErrors++ // 바깥 변수를 변경
}
}
println("$clientErrors client errors, $serverErrors server errors")
}
fun main() {
val responses = listOf("200 OK", "418 I'm a teapot", "500 Internal Server Error")
printProblemCounts(responses)
// 1 client errors, 1 server errors
}
- 위 예시에서
clientErrors와serverErrors는 람다 안에서 접근할 수 있는 바깥 변수입니다. - 위 예제에서 prefix, clientErrors, serverErrors와 같이 람다 안에서 접근할 수 있는 외부 변수를
람다가 캡쳐한 변수라고 부릅니다.
3.3 람다가 캡쳐한 변수
- 기본적으로 함수 안에 정의된 로컬 변수의 생명주기는 함수의 생명주기와 같습니다.
- 하지만 어떤 함수가 자신의 로컬 변수를 캡처한 람다를 반환하거나 다른 변수에 저장한다면 로컬 변수의 생명주기와 함수의 생명주기가 달라질 수 있습니다.
- 캡처한 변수가 있는 람다를 저정한 후 함수가 끝난 뒤에 실행해도 람다의 본문 코드는 여전히 캡처한 변수를 사용할 수 있습니다.
- 파이널 변수를 캡처한 경우에는 람다 코드를 변수 값과 함께 저장합니다. 파이널이 아닌 경우 변수를 특별한 래퍼로 감싸고 래퍼에 대한 참조를 람다 코드와 함께 저장합니다.
4 멤버 참조
- 람다를 사용해 코드 블록을 넘길 수 있습니다. 그런데 이러한 코드 블록이 이미 함수로 정의된 경우 어떻게 할까요?
- 그 함수를 호출하는 람다를 만들면 되지만 이는 중복된 코드입니다.
- 이런 경우 멤버 참조를 이용하면 이미 정의된 함수를 직접 넘길 수 있습니다.