- 람다 식과 멤버 참조
- 함수형 스타일로 컬렉션 다루기
- 시퀀스: 지연 컬렉션 연산
- 자바 함수형 인터페이스를 코틀린에서 사용
- 수신 객체 지정 람다 사용
람다 식/람다
기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각
-> 공통 코드 구조를 라이브러리 함수로 뽑아낼 수 있다
수신 객체 지정 람다
-> 람다 선언을 둘러싸고 있는 환경과는 다른 상황에서 람다 본문을 실행할 수 있다
무명 내부 클래스를 사용하면 코드를 함수에 넘기거나 변수에 저장할 수 있지만 번거로움
-> 함수형 프로그래밍에서는 함수를 값으로 다루는 접근 방법을 택하면 해결
람다 식을 사용하면 함수를 선언할 필요가 없고 코드 블록을 직접 함수의 인자로 전달 가능
// 람다로 리스너 구현
button.setOnClickListener { }
ex) 람다를 사용해서 컬렉션 검색하기 ( 연장자 찾기 )
data class Person(val name: String, val age:Int)
val people = listOf(Person("Kim",29), Person("Park",30))
>>>println(people,maxBy { it.age }) <- 나이 프로퍼티를 비교해서 원소 찾기
Person(name=Park,age=30)
↪ maxBy는 가장 큰 원소를 찾기 위해
비교에 사용할 값을 돌려주는 함수로 인자를 받는다
↪ { it.age } 는 비교에 사용할 값을 돌려주는 함수
// 멤버 참조를 사용해 컬렉션 검색
people.maxBy(Person::age) <- 위와 동일한 결과 도출
val sum = { x: Int, y: Int -> x + y }
↪ 파라미터 ↪ 본문
println(sum(1, 2)) <- 변수에 저장된 람다를 호출한다
3
>>> run { println(42) } <- 람다 본문에 있는 코드를 실행한다
42
↪ 람다 호출에는 부가 비용 X
people.maxBy { p:Person -> p:age }
// 람다 파라미터 타입 제거하기
people.maxBy { p:Person -> p.age } <- 파라미터 타입 명시
people.maxBy { p -> p.age } <- 파라미터 타입 생략
로컬 변수처럼 컴파일러는 람다 파라미터의 타입도 추론 가능
-> 파라미터 타입을 명시할 필요 X
파라미터 중 일부의 타입은 지정하고 나머지 파라미터는 타입을 지정하지 않고 이름만 남겨둬도 된다
// 디폴트 파라미터 이름 it 사용하기
people.maxBy { it.age } <- "it"은 자동 생성된 파라미터 이름
val getAge = { p:Person -> p.age }
people.maxBy(getAge)
람다를 함수 안에서 정의하면 함수의 파라미터뿐 아니라 람다 정의의 앞에 선언된 로컬 변수까지 람다에서 모두 사용할 수 있다
코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다
-> 람다 안에서 바깥의 변수를 변경해도 된다
람다에서 람다 밖 함수에 있는 파이널이 아닌 변수에 접근할 수 있다
-> 변수를 변경할 수도 있다
넘기려는 코드가 이미 함수로 선언된 경우는 어떻게 해야 할까?
-> 함수를 직접 넘긴다
-> 함수를 값으로 바꿀 수 있는데, 이중 콜론(::)을 사용한다
:: 를 사용하는 식을 멤버 참조라고 부른다
멤버 참조
프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어준다
// 둘은 같은 역할
val getAge = Person::age
val getAge = { person:Person -> person.age }
// 다양한 방식으로 사용 가능
people.maxBy(Person::age)
people.maxBy { p -> p.age }
people.maxBy { it.age }
fun salute() = println("Salute!")
>>> run(::salute) <- 최상위 함수를 참조한다
Salute!
↪ 클래스 이름을 생략하고 ::로 참조를 바로 시작
// 이 람다는 sendEmail 함수에게 작업을 위임한다
val action = { person: Person, message: String ->
sendEmail(person, message)
}
val nextAction = ::sendEmail <- 람다 대신 멤버 참조를 쓸 수 있다
data class Person(val name: String, val age: Int)
// "Person"의 인스턴스를 만드는 동작을 값으로 저장한다
>>> val createPerson = ::Person
>>> val p = createPerson("Kim",29)
>>> println(p)
Person(name="Kim",age=29)
확장 함수도 멤버 함수와 같은 방식으로 참조 가능
fun Person.isAdult() = age >= 21
val predicate = Person::isAdult
바운드 멤버 참조
멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장한 다음 나중에 그 인스턴스에 대해 멤버를 호출해준다
호출 시 수신 대상 객체를 별도로 지정해 줄 필요가 없다>> val p = Person("kim", 29) >> val personAgeFunction = Person::age >> println(personAgeFunction(p)) 29 >> val umbumAgeFunction = p::age // bound member reference >> println(umbumAgeFunction()) 29
val list = listOf(1, 2, 3, 4)
println(list.filter { it % 2 == 0 }
[2, 4]
val list = listOf(1, 2, 3, 4)
println(list.map { it * it })
[1, 4, 9, 16]
----------------------------------------------
// 사람의 리스트가 아닌 이름의 리스트를 출력하고 싶다면
data class Person(val name: String, val age:Int)
val people = listOf(Person("Kim",29), Person("Park",30))
println(people.map { it.name })
[Kim, Park]
// 멤버 참조를 사용해 더 멋지게 사용 가능
people.map(Person::name)
// filter 와 map 혼용 사용
people.filter { it.age >= 30 }.map(Person::name)
[Park]
----------------------------------------------
연산 중요성
// 좋지 못한 코드
people.filter { it.age == people.maxBy(Person::age)!!.age }
// 좋은 코드
val maxAge = people.maxBy(Person::age)!!.age
people.filter { it.age == maxAge }
all, any
컬렉션의 모든 원소가 어떤 조건을 만족하는지 판단하는 연산
count
조건을 만족하는 원소의 개수를 반환
find
조건을 만족하는 첫 번째 원소를 반환
// 어떤 사람의 나이가 29살 이하인지 판단하는 술어
val canBeInClub29 = { p: Person -> p.age <= 29 }
// 모든 원소가 이 술어를 만족하는지 궁금하면 all을 사용
val people = listOf(Person("kim", 29), Person("Park", 30))
println(people.all(canBeInClub29))
false
// 하나라도 이 술어를 만족하는지 궁금하면 any를 사용
println(people.any(canBeInClub29))
true
어떤 조건에 대해 !all을 수행한 결과와 그 조건의 부정에 대해 any를 수행한 결과는 같다
어떤 조건에 대해 !any를 수행한 결과와 그 조건의 부정에 대해 all을 수행한 결과는 같다
// 술어를 만족하는 원소의 개수를 구하려면 count를 사용
println(people.count(canBeInClub29))
1
// 술어를 만족하는 원소를 하나 찾고 싶으면 find를 사용
println(people.find(canBeInClub29))
Person(name=kim, age=29)
count와 size
count가 있다는 사실을 잊어버리고, 컬렉션을 필터링한 결과의 크기를 가져오는 경우가 있다
people.filter(canBeInClub29).size
이렇게 처리하면 조건을 만족하는 모든 원소가 들어가는 중간 컬렉션이 생긴다
count는 조건을 만족하는 원소의 개수만을 추적하지 조건을 만족하는 원소를 따로 저장하지 않는다
(count가 훨씬 효율적)
val people = listOf(Person("kim",29), Person("Park",30),
Person("Han",29))
println(people.groupBy { it.age })
{29=[Person(name=Kim,age=29), Person(name=Han,age=29)],
30=[Person(name=Park,age=30)]}
↪ groupBy의 결과 타입은 Map<Int, List<Person>>
이다
↪ 맵을 mapKeys or mapValues 등을 사용해 변경할 수 있다
val strings = listOf("abc","def")
println(strings.flatMap { it.toList() })
[a, b, c, d, e, f]
↪ toList 함수를 문자열에 적용하면 그 문자열에 속한 모든 문자로 이뤄진 리스트가 만들어진다
↪ map과 toList를 함께 적용하면 문자로 이뤄진 리스트로 이뤄진 리스트가 생긴다
↪ flatMap 함수는 리스트의 리스트에 들어있던 모든 원소로 이뤄진 단일 리스트를 반환한다
map이나 filter 같은 컬렉션은 즉시 생성한다
-> 컬렉션 함수를 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다
시퀀스
중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있다
people.map(Person::name).filter { it.startsWith("A") }
-> filter와 map이 리스트를 반환하므로 연쇄 호출이 리스트를 2개 만든다
people.asSequence() <- 원본 컬렉션을 시퀀스로 변환한다
.map(Person::name)
.filter { it.startsWith("A") }
.toList() <- 시퀀스 결과를 다시 리스트로 변환한다
중간 연산
다른 시퀀스를 반환
-> 항상 지연 계산된다
최종 연산
결과를 반환
-> 연기됐던 모든 계산이 수행된다
// 중간 연산이 없는 시퀀스 연산
>>> listOf(1, 2, 3, 4).asSequence()
.map { print("map($it) "); it * it }
.filter { print("filter($it) "); it % 2 == 0 }
.toList()
아무 내용도 출력되지 않는다
-> map과 filter 변환이 늦춰져서 결과를 얻을 필요가 있을 때 적용되기 때문
>>> listOf(1, 2, 3, 4).asSequence()
.map { print("map($it) "); it * it }
.filter { print("filter($it) "); it % 2 == 0 }
.toList()
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)
연산 수행 순서의 중요성 1
직접 구현을 한다면
map 함수를 통해서 각 원소에 대해 먼저 수행해서 새 시퀀스를 얻고
그 시퀀스에 대해 다시 filter를 수행할 것이다시퀀스에 대한 map과 filter의 경우에는
모든 연산은 각 원소에 대해 순차적으로 적용된다
즉, 첫 번째 원소가 처리되고, 다시 두 번째 원소가 처리되며,
이런 처리가 모든 원소에 대해 적용된다원소에 연산을 차례대로 적용하다가 결과가 얻어지면 그 이후의 원소에 대해서는 변환이 이뤄지지 않을 수도 있다
// 숫자를 제곱하고 제곱한 숫자 중에서 3보다 큰 첫 번째 원소 찾기
>>> println(listOf(1, 2, 3, 4).asSequence()
.map { it * it }.find { it > 3 })
4
↪ 시퀀스를 사용하면 find 호출이 원소를 하나씩 처리
연산 수행 순서의 중요성 2
map과 filter를 어떤 순서로 수행해도 되지만,
변환의 전체 횟수는 달라진다
element 를 생성하는 함수를 argument 로 사용해서 sequence 를 생성하는 방법으로 generateSequence() 함수를 사용한다
첫번째 element 를 특정 값으로 지정 하거나
함수 호출의 결과를 지정하는 것도 가능하다
함수가 null 을 리턴하면 sequence 생성은 멈추게 된다
fun main(args: Array<String>) {
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
println(numbersTo100.sum())
}
아래의 코드는 무한히 생성되는 오류를 가진다.
val oddNumbers = generateSequence(1) { it + 2 } // `it` is the previous element
println(oddNumbers.take(5).toList()) // [1, 3, 5, 7, 9]
//println(oddNumbers.count()) // error: the sequence is infinite
generateSequence()
함수로 유한한 sequence 를 만드려면element 뒤에 null 을 리턴하는 함수
를 제공해야 한다.val oddNumbersLessThan10 = generateSequence(1) { if (it < 10) it + 2 else null }
println(oddNumbersLessThan10.count()) // 6
인터페이스에 추상 메소드가 단 하나만 있을 경우
-> 함수형 인터페이스 or SAM 인터페이스라고 한다
SAM 은 단일 추상 메소드(single abstract method) 라는 뜻이다
코틀린은 함수형 인터페이스를 인자로 취하는 자바 메소드를 호출할 때 람다를 넘길 수 있게 해준다
코틀린 코드는 클래스 인스턴스를 정의하고 활용할 필요가 없다
SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수
-> 컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자를 사용할 수 있다
SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같다
SAM 생성자는 그 함수형 인터페이스의 유일한 추상 메소드의 본문에 사용할 람다만을 인자로 받아서 함수형 인터페이스를 구현하는 클래스의 인스턴스를 반환한다
람다로 생성한 함수형 인터페이스 인스턴스를 변수에 저장하는 경우에도 SAM 생성자를 만들 수 있다
어떤 객체의 이름을 반복하지 않고도 그 객체에 대해 다양한 연산을 수행할 수 있게 도와준다
-> with라는 라이브러리 함수를 통해 제공
with 함수
첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다의 수신 객체로 만든다
-> 인자로 받은 람다 본문에서는 this를 사용해 그 수신 객체에 접근할 수 있다
-> 일반적인 this와 마찬가지로 this와 .을 사용하지 않고 프로퍼티/메소드 이름만 사용해도 수신 객체의 멤버에 접근할 수 있다
with가 반환하는 값은 람다 코드를 실행한 결과며,
그 결과는 람다 식의 본문에 있는 마지막 식의 값이다
-> 람다의 결과 대신 수신 객체가 필요할 때 사용하는 것이
apply 라이브러리
메소드 이름 충돌
with에게 인자로 넘긴 객체의 클래스와 with를 사용하는 코드가 들어있는 클래스 안에 이름이 같은 메소드가 있으면 무슨 일이 생길까? 그런 경우 this 참조 앞에 레이블을 붙이면 호출하고 ㅣㅍ은 메소드를 명확하게 정할 수 있다alphabet 함수가 OuterClass의 메소드라고 하자
StringBuilder가 아닌 바깥쪽 클래스 (OuterClass)에 정의된 toString을 호출하고 싶다면 다음과 같은 구문을 사용this@OuterClass.toString()
// 알바펫 만들기
fun alphaber(): String {
val result = StringBuilder()
for(letter in 'A'..'Z') {
result.append(letter)
}
result.append("\nNow I Know the alphabet!")
return result.toString()
}
// with을 사용해 알파벳 만들기
fun alphabet(): String {
val stringBuilder = StringBuilder()
return with(stringBuilder) { <- 메소드를 호출하려는 수신 객체를 지정
for(letter in 'A'..'Z') {
this.append(letter) <- this를 명시해서 앞에서 지정한 수신 객체의 메소드를 호출
}
append("\nNow I Know the alphabet!") <- this를 생략하고 메소드를 호출
return this.toString() <- 람다에서 값을 반환
}
// with와 식을 본문으로 하는 함수를 활용해 알파벳 만들기
fun alphabet() = with(stringBuilder()) {
for(letter in 'A'..'Z') {
append(letter)
}
append("\nNow I Know the alphabet!")
toString()
with과 거의 동일하지만 유일한 차이는 항상 자신에게 전달된 객체를 반환한다는 점이다
객체의 인스턴스를 만들면서 즉시 프로퍼티 중 일부를 초기화해야 하는 경우에 유용하다
-> 함수의 본문에 간결한 식을 사용할 수 있기 때문