람다 소개 ( 코드 블록을 함수 인자로 넘기기)
"이벤트가 발생하면 이 핸들러를 실행하자", "데이너 구조의 모든 원소에 이 연산을 적용하자"와 같은 생각을 코드로 표현하기 위해 일련의 동작을 변수에 저장하거나 다른 함수에 넘겨야 하는 경우가 자주 있다.
이전 자바에서는 무명 내부 클래스를 통해 목적을 달성했으며, 코드를 함수에 넘기거나 변수에 저장할 수 있는 장점이 있지만 번거롭다.
함수형 프로그래밍에서는 함수를 값처럼 다루는 접근 방법을 선택함으로써 이 문제를 해결했다. 함수형 언어에서는 하수를 직접 다른 함수에 전달할 수 있으며, 람다 식을 사용하면 코드가 더 간결해진다.(함수를 선언할 필요 없고 코드 블록을 직접 함수 인자로 전달)
JAVA
button.setOnClickListener(new OnClickListener)_ {
@Override
public void onClick(View view){
...
}
}
- 무명 내부 클래스를 쓰느라 코드가 복잡하다
Kotlin
button.setOnclickListener{
...
}
- 자바 무명 내부 클래스와 비교하여 더 간결하고 읽기 쉽다.
람다와 컬렉션
사람들로 이뤄진 리스트에서 가장 연장자 찾기
data class Person(val name: String, val age:Int)
fun findTheOldSet(people: List<Person>){
var maxAge = 0 //가장 많은 나이
var theOldSet: Person? = null
for(person in people){
if(person.age > maxAge){//현재까지 발견한 최 연장자보다 나이가 많은사람을 찾으면 최댓값 변경
maxAge = person.age
theOldSet = person
}
}
println(theOldSet)
}
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
findTheOldSet(people)
}
- 이러한 루프 문은 상당히 많은 코드가 들어있어 개발자가 실수할 가능성이 있다
- 예를 들어 비교문을 반대로 사용하면 가장 작은 값을 가지고 오는 실수를 하게 된다
- 코틀린에서는 라이브러리 함수를 사용하면 된다.
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy { it.age })// 나이 프로퍼티를 비교해서 사용할 값을 돌려주는 함수를 인자로 받는다
}
people.maxBy(Person::age)
자바 컬렉션에 대해 수행하던 대부분의 작업은 람다나 멤버 참조를 인자로 취하는 라이브러리 함수를 통해 개선할 수 있다.
개선된 코드는 더 짧고 이해하기 쉽다
람다 식의 문법
람다는 값처럼 여기저기 전달할 수 있는 동작의 모음이다.
//파라미터 //본문
{ x: Int, y: Int -> x + y }
- 코틀린 람다식은 항상 중괄호로 둘러싸여 있다.
- 인자 목록 주변에 괄호가 없다
- 화살표가 인자 목록과 람다 본문을 구분해준다.
람다식을 변수에 저장, 직접 호출
fun main(args: Array<String>) {
val sum = {x: Int, y: Int -> x + y}
println(sum(1, 2))
//{ println(42) } //직접 호출
}
- 이와 같은 방식은 읽기 어렵고 쓸모도 없다. (람다를 만들자마자 바로 호출하느니 람다 본문을 직접 실행하는 편이 낫다)
- 코드의 일부분을 블록으로 둘러싸 실행할 필요가 있다면 run 라이브러리를 사용해보자
fun main(args: Array<String>) {
run{println(42)}
}
- 실행 시점 코틀린 람다 호출에는 아무 부가 비용이 들지 않으며, 프로그램의 기본 구성요소와 비슷한 성능을 낸다.
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy({p:Person -> p.age}))
}
- 중괄호 안에 있는 코드는 람다식이고, 람다식을 maxBy 함수에 넘긴다. 람다식은 Person 타입의 값을 인자로 받아서 인자의 age를 반환한다
- 해당 코드는 너무 복잡하다
- 구분자가 너무 많이 쓰여 가독성이 떨어진다.
- 컴파일러가 문맥으로부터 유추할 수 있는 인자 타입을 굳이 적을 필요 없다.
- 인자가 단 하나뿐인 경우 굳이 인자에 이름을 붙이지 않아도 된다
중괄호 제거(코틀린에서는 함수 호출 시 맨 뒤에 있는 인자가 람다식이라면 그 람다를 괄호 밖으로 뺄 수 있다.)
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy() { p:Person -> p.age })
}
- 람다가 유일한 인자이자 마지막 인자이므로 괄호 뒤에 람다를 둘 수 있다.
빈 괄호 제거 (어떤 함수의 유일한 인자이고 괄호 뒤에 람다를 썼다면 호출 시 빈 괄호를 없애도 된다)
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy { p:Person -> p.age })
}
3가지 형태 모두 같은 의미이며 마지막 문장이 가장 가독성이 좋다
- 람다가 함수의 유일한 인자라면 괄호 없이 람다를 사용할 수 있다
- 인자가 여럿 있는 경우 람다를 밖으로 뺄 수 있고, 괄호 안에 유지해서 함수의 인자임을 분명히 할 수 있다
- 둘 이상을 인자더라도 맨 마지막 인자가 람다라면 밖으로 뺄 수 있다.
EX) JointoString
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
val names = people.joinToString(separator = " ", transform = { p:Person -> p.name})
println(names)
}
-> 리펙토링 : 함수 호출에서 함수를 괄호 밖으로 뺸다
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
val names = people.joinToString(" "){ p:Person -> p.name}
println(names)
}
- 람다의 용도를 분명히 알아볼 수 없는 단점이 있다.
파라미터 타입 생략
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
people.maxBy { p:Person -> p.age } //파라미터 타입 명시
people.maxBy { p -> p.age }//파라미터 타입을 생략 (컴파일러가 추론)
}
- 람다 파라미터의 타입을 추론할 수 있다.(파라미터 타입을 생략할 수 있다)
- maxBy 함수의 경우 파라미터의 타입은 항상 컬랙션 원소 타입과 같다
- 컴파일러는 Person타입의 객체가 들어있는 컬렉션에 대해 maxBy함수를 호출한다는 사실을 알고 있으므로 람다의 파라미터도 Person타입인 것을 추론할 수 있다.
- 처음에는 타입을 쓰지 않고 람다를 작성하고 컴파일러가 타입을 모른다고 오류를 내줄 경우 타입을 명시하자
- 파라미터 중 일부의 타입은 지정하고 나머지 파라미터는 타입을 지정하지 않고 이름만 남겨둬도 된다
- 컴파일러가 파라미터 타입 중 일부만 추론하지 못하거나, 타입 정보가 코드를 읽을 때 도움이 된다면 일부 타입만 표시하자
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
people.maxBy { it.age }
}
- 람다의 파라미터가 하나뿐이고 그 타입을 컴파일러가 추론할 수 있다면 it를 바로 쓸 수 있다.
- 람다 파라미터 이름을 따로 지정하지 않은 경우에만 it라는 이름이 자동으로 만들어진다
- it를 사용하는 관습은 코드를 간결하게 만들어 주지만 남용하면 안 된다.
- 특히, 람다 안에 람다가 중첩되는 경우 각 람다의 파라미터를 명시하는 편이 좋다
- 각 람다의 it파라미터가 어떤 람다에 속했는지 파악하기 어렵기 때문이다.
- 람다를 변수에 저장할 때는 파라미터의 타입을 추론할 문맥이 존재하지 않기 때문에 파라미터 타입을 명시해야 한다.
fun main(args: Array<String>) {
val sum = { x: Int, y: Int ->
println("Computing the sum of $x and $y ...")
x + y
}
println(sum(1, 2))
}
- 본문이 여러 줄로 이뤄진 경우 본문의 마지막 있는 식이 람다의 결괏값이 된다.
현재 영역에 있는 변수에 접근
람다를 함수 안에서 정의하면 함수의 파라미터뿐 아니라 람다 정의의 앞에 선언된 로컬 변수까지 람다에서 사용할 수 있다.
fun main(args: Array<String>) {
val errors = listOf("403 Forbidden", "404 Not Found")
printMessagesWithPrefix(errors, "Error:")
}
fun printMessagesWithPrefix(messages: Collection<String>, prefix: String){
messages.forEach{//각 원소에 대해 수행할 작업을 람다로 받는다
println("$prefix $it") //람다 안에서 함수의 prefix 파라미터를 사용
}
}
- 자바와 다르게 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다
- 람다 안에서 바깥의 변수를 변경해도 된다.
fun main(args: Array<String>) {
val responses = listOf("200 OK", "418 I'm a teapot", "500 Internal Server Error")
printProblemCounts(responses)
}
fun printProblemCounts(responses:Collection<String>){
var clientErrors = 0
var serverErrors = 0
responses.forEach{
if(it.startsWith("4")){
clientErrors++//람다 안에서 람다 밖의 변수를 변경
}else if(it.startsWith("5")){
serverErrors++
}
}
println("$clientErrors client errors, $serverErrors server errors")
}
- 코틀린에서는 람다에서 람다 밖 함수에 있는 final이 아닌 변수에 접근할 수 있고, 변수를 변경할 수 있다
- 람다 안에서 사용하는 외부 변수를 가리켜 "람다가 포획한 변수"라고 부른다
- 기본적으로 함수 안에 정의된 로컬 변수의 생명주기는 함수가 끝나면 반환되지만, 어떤 함수가 자신의 로컬 변수를 포획한 람다를 반환하거나, 다른 변수에 저장한다면 로컬 변수의 생명주기와 함수의 생명주기가 달라질 수 있다.
- 포획한 변수가 있는 람다를 저장해서 함수가 끝난 뒤에 실행해도 람다의 본문 코드는 여전히 포획한 변수를 읽거나 쓸 수 있다.
- final 변수를 포획한 경우 람다 코드를 변수 값과 함께 저장한다.
- final이 아닌 변수를 포획한 경우 변수를 특별한 래퍼로 감싸 나중에 변경하거나 읽을 수 있게 한 다음 래퍼에 대한 참조를 람다 코드와 함께 저장한다.
Tip : 변경 가능한 변수 포획하는 방법
자바에서는 final 변수만 포획할 수 있는데 변경 가능한 변수를 저장하는 원소가 단 하나뿐인 배열을 선언하거나, 변경 가능한 변수를 필드로 하는 클래스를 선언하여 변경가능한 변수를 포획할 수 있다.
주의할 점
fun tryToCountButtonClicks(button: Button): Int{
var clicks = 0
button.onClick(clicks++)
return clicks
}
- 이 함수는 항상 0을 반환한다.
- onclick 핸들러는 호출될 때마다 clicks 값을 증가시키지만 그 값의 변경을 관찰할 수는 없다.
- 핸들러는 tryToCountButtonClicks가 clicks를 반환한 다음 호출하기 때문이다
- 이 함수를 제대로 구현하기 위해서는 clicks를 함수 내부가 아닌 클래스의 프로퍼티나, 전역 프로퍼티 등의 위치로 빼내서 나중에 변수 변화를 살펴볼 수 있게 해야 한다
멤버 참조
넘기려는 코드가 이미 함수로 호출된 경우 어떻게 해야 할까? 함수를 직접 넘길 수 없을까?
코틀린에서는 자바 8과 마찬가지로 함수를 값으로 바꿀 수 있고 이중 콜론(::)을 사용한다.
fun main(args: Array<String>) {
val age = Person::age
println(age)
}
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
//모두 같다
people.maxBy(Person::age)
people.maxBy { p -> p.age }
people.maxBy { it.age }
}
- "::"를 사용하는 식을 멤버 참조라고 부른다.
- 멤버 참조는 프로퍼티나 메서드를 단 하나만 호출하는 함숫값을 만들어 준다
- :: 는 클래스 이름과 참조하려는 멤버 이름 사이에 위치한다
- Person(클래스)::age(멤버) //::으로 구분
- 참조 대상이 함수인지 프로퍼티인지와는 관계없이 멤버 참조 뒤에는 괄호를 넣으면 안 된다.
fun main(args: Array<String>) {
run(::salute) //최상위 함수를 참조한다
}
fun salute() = println("Salute!")
- 최상위에 선언된 함수나 프로퍼티를 참조할 수도 있다
- REPL, 스크립트에서는 최상위 함수를 참조할 수 없다
- 클래스 이름을 생략하고 ::로 참조를 바로 시작한다.
- ::salute라는 멤버 참조를 run 라이브러리 함수에 넘긴다
fun main(args: Array<String>) {
val action = {person:Person, message: String -> //sendEmail 함수에 작업을 위임
sendEmail(person, message)
}
val nextAction = ::sendEmail //위 방식 대신 멤버 참조를 사용할수 있다
}
- 인자가 여럿인 다른 함수에게 작업을 위임하는 경우 람다를 정의하지 않고 직접 위임 함수에 대한 참조를 제공할 수 있다
fun main(args: Array<String>) {
val createPerson = ::Person
val p = createPerson("Alice", 29)
println(p)
}
- 생성자 참조를 사용하면 클래스 생성 작업을 연기하거나, 저장해둘 수 있다.
- :: 뒤에 클래스 이름을 넣으면 생성자 참조를 만들 수 있다
fun main(args: Array<String>) {
val predicate = Person::isAdult
println(predicate)
}
fun Person.isAdult() = age >= 21
- 확장 함수도 멤버 함수와 같은 방식으로 참조할 수 있다.
- isAdult는 Person 클래스의 멤버가 아닌 확장 함수이다
- isAdult를 호출할 때 인스턴스 멤버 호출 구문처럼 쓸 수 있는 것처럼 멤버 참조 구문을 사용해 확장 함수에 대한 참조도 얻을 수 있다
Tip : 바운드 멤버 참조
코틀린 1.0에서는 클래스의 메서드나 프로퍼티에 대한 참조를 얻은 다음에 그 참조를 호출할 때 항상 인스턴스 객체를 제공해야 했다.
하지만 1.1부터는 바운드 멤버 참조를 지원하여 멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장한 다음 나중에 그 인스턴스에 대한 멤버를 호출해 준다.
따라서 호출 시 수신 대상 객체를 별도로 지정해 줄 필요가 없다
fun main(args: Array<String>) {
val p = Person("Dmitry", 34)
val personsAgeFunction = Person::age
println(personsAgeFunction(p))
val dmitrysAgeFunction = p::age //바운드 멤버 참조
println(dmitrysAgeFunction())
}
'KotlinInAction > 람다로 프로그래밍' 카테고리의 다른 글
| 수신 객체 지정 람다 (with와 apply) (0) | 2022.08.03 |
|---|---|
| 자바 함수형 인터페이스 활용 (0) | 2022.08.03 |
| 지연 계산 컬렉션 연산 (0) | 2022.08.02 |
| 컬렉션 함수형 API (0) | 2022.08.01 |