[Kotlin in Action] 5장 람다로 프로그래밍

Sdoubleu·2023년 3월 19일
0

Kotlin in Action

목록 보기
4/9
post-thumbnail

5장에서 다루는 내용

  1. 람다 식과 멤버 참조
  2. 함수형 스타일로 컬렉션 다루기
  3. 시퀀스: 지연 컬렉션 연산
  4. 자바 함수형 인터페이스를 코틀린에서 사용
  5. 수신 객체 지정 람다 사용
  • 람다 식/람다
    기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각
    -> 공통 코드 구조를 라이브러리 함수로 뽑아낼 수 있다

  • 수신 객체 지정 람다
    -> 람다 선언을 둘러싸고 있는 환경과는 다른 상황에서 람다 본문을 실행할 수 있다


5.1 람다 식과 멤버 참조

5.1.1 람다 소개: 코드 블록을 함수 인자로 넘기기

  • 무명 내부 클래스를 사용하면 코드를 함수에 넘기거나 변수에 저장할 수 있지만 번거로움
    -> 함수형 프로그래밍에서는 함수를 값으로 다루는 접근 방법을 택하면 해결

  • 람다 식을 사용하면 함수를 선언할 필요가 없고 코드 블록을 직접 함수의 인자로 전달 가능

// 람다로 리스너 구현
button.setOnClickListener { } 

5.1.2 람다와 컬렉션

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) <- 위와 동일한 결과 도출

5.1.3 람다 식의 문법

  • 람다는 값처럼 여기저기 전달할 수 있는 동작의 모음
    -> 람다를 따로 선언해서 변수에 저장할 수도 있다
val sum = { x: Int, y: Int -> x + y }
              ↪ 파라미터        ↪ 본문
println(sum(1, 2)) <- 변수에 저장된 람다를 호출한다
3
  • 코드의 일부분을 블록으로 둘러싸 실행할 필요가 있다면
    run을 사용
    -> 인자로 받은 람다를 실행해주는 라이브러리
>>> 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"은 자동 생성된 파라미터 이름
  • 람다를 변수에 저장할 때는 파라미터의 타입을 추론할 문맥이 존재 X
    -> 파라미터 타입을 명시해야 한다
val getAge = { p:Person -> p.age }
people.maxBy(getAge)

5.1.4 현재 영역에 있는 변수에 접근

  • 람다를 함수 안에서 정의하면 함수의 파라미터뿐 아니라 람다 정의의 앞에 선언된 로컬 변수까지 람다에서 모두 사용할 수 있다

  • 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다
    -> 람다 안에서 바깥의 변수를 변경해도 된다

  • 람다에서 람다 밖 함수에 있는 파이널이 아닌 변수에 접근할 수 있다
    -> 변수를 변경할 수도 있다

5.1.5 멤버 참조

  • 넘기려는 코드가 이미 함수로 선언된 경우는 어떻게 해야 할까?
    -> 함수를 직접 넘긴다
    -> 함수를 값으로 바꿀 수 있는데, 이중 콜론(::)을 사용한다

  • :: 를 사용하는 식을 멤버 참조라고 부른다

  • 멤버 참조
    프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어준다

// 둘은 같은 역할
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

5.2 컬렉션 함수형 API

5.2.1 필수적인 함수: filter와 map

  • filter 함수
    컬렉션을 이터레이션하면서 주어진 람다에 각 원소를 넘겨서 람다가 true를 반환하는 원소만 모은다
    -> 결과는 입력 컬렉션의 원소중에서 주어진 술어를 만족하는 원소만 이뤄진 새로운 컬렉션이다
val list = listOf(1, 2, 3, 4)
println(list.filter { it % 2 == 0 }
[2, 4]
  • 컬렉션에서 원치 않는 원소를 제거한다
    원소를 변환 ❌
  • map 함수
    주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아서 새 컬렉션을 만든다
    -> 원본 리스트와 원소의 개수는 같지만, 각 원소는 주어진 함수에 따라 변환된 새로운 컬렉션이다
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 }
  • 맵의 경우 키와 값을 처리하는 함수는 따로 존재
  1. filterKeys / filterValues
  2. mapKeys / mapValues

5.2.2 all, any, count, find: 컬렉션에 술어 적용

  • 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가 훨씬 효율적)

  • 조건을 만족하는 원소가 없으면 null이 나온다는 사실을 더 명확히 하고 싶다면 firstOrNull을 쓸 수 있다
    -> find는 firstOrNull과 같다

5.2.3 groupBy: 리스트를 여러 그룹으로 이뤄진 맵으로 변경

  • 컬렉션의 모든 원소를 어떤 특성에 따라 여러 그룹으로 나누고 싶을 때
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 등을 사용해 변경할 수 있다

5.2.4 flatMap과 flatten: 중첩된 컬렉션 안의 원소 처리

  • flatMap 함수
    먼저 인자로 주어진 람다를 컬렉션의 모든 객체에 적용하고 람다를 적요한 결과 얻어지는 여러 리스트를 한 리스트로 한데 모은다
val strings = listOf("abc","def")
println(strings.flatMap { it.toList() })
[a, b, c, d, e, f]

↪ toList 함수를 문자열에 적용하면 그 문자열에 속한 모든 문자로 이뤄진 리스트가 만들어진다
↪ map과 toList를 함께 적용하면 문자로 이뤄진 리스트로 이뤄진 리스트가 생긴다
flatMap 함수는 리스트의 리스트에 들어있던 모든 원소로 이뤄진 단일 리스트를 반환한다

  • flatten
    flatMap과 달리 특별히 변환해야 할 내용이 없다면 리스트의 리스트를 평평하게 펼칠 수 있는 함수

5.3 지연 계산(lazy) 컬렉션 연산

  • map이나 filter 같은 컬렉션은 즉시 생성한다
    -> 컬렉션 함수를 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다

  • 시퀀스
    중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있다
    people.map(Person::name).filter { it.startsWith("A") }
    -> filter와 map이 리스트를 반환하므로 연쇄 호출이 리스트를 2개 만든다

people.asSequence() <- 원본 컬렉션을 시퀀스로 변환한다
	.map(Person::name)
    .filter { it.startsWith("A") }
    .toList() <- 시퀀스 결과를 다시 리스트로 변환한다
  • sequence 인터페이스의 강점
    인터페이스 위에 구현된 연산이 계산을 필요할 때 비로소 계산된다

⚡5.3.1 시퀀스 연산 실행: 중간 연산과 최종 연산

  • 시퀀스에 대한 연산은 중간 연산 과 최종 연산으로 나뉜다

  • 중간 연산
    다른 시퀀스를 반환
    -> 항상 지연 계산된다

  • 최종 연산
    결과를 반환
    -> 연기됐던 모든 계산이 수행된다

// 중간 연산이 없는 시퀀스 연산
>>> 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를 어떤 순서로 수행해도 되지만,
변환의 전체 횟수는 달라진다

⚡5.3.2 시퀀스 만들기

  • 시퀀스를 만드는 다른 방법으로 generateSequence 함수를 사용할 수 있다
    -> 이전의 원소를 인자로 받아 다음 원소를 계산한다

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

5.4 자바 함수형 인터페이스 활용

인터페이스에 추상 메소드가 단 하나만 있을 경우
-> 함수형 인터페이스 or SAM 인터페이스라고 한다

  • SAM 은 단일 추상 메소드(single abstract method) 라는 뜻이다

  • 코틀린은 함수형 인터페이스를 인자로 취하는 자바 메소드를 호출할 때 람다를 넘길 수 있게 해준다

  • 코틀린 코드는 클래스 인스턴스를 정의하고 활용할 필요가 없다

5.4.1 자바 메소드에 람다를 인자로 전달

  • 함수형 인터페이스를 인자로 원하는 자바 메소드에 코틀린 람다를 전달할 수 있다

5.4.2 SAM 생성자: 람다를 함수형 인터페이스로 명시적으로 변경

  • SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수
    -> 컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자를 사용할 수 있다

  • SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같다

  • SAM 생성자는 그 함수형 인터페이스의 유일한 추상 메소드의 본문에 사용할 람다만을 인자로 받아서 함수형 인터페이스를 구현하는 클래스의 인스턴스를 반환한다

  • 람다로 생성한 함수형 인터페이스 인스턴스를 변수에 저장하는 경우에도 SAM 생성자를 만들 수 있다


5.5 수신 객체 지정 람다: with와 apply

  • 수신 객체 지정 람다
    수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메소드를 호출할 수 있게 하는 것

5.5.1 with 함수

  • 어떤 객체의 이름을 반복하지 않고도 그 객체에 대해 다양한 연산을 수행할 수 있게 도와준다
    -> 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()

5.5.2 apply 함수

  • with과 거의 동일하지만 유일한 차이는 항상 자신에게 전달된 객체를 반환한다는 점이다

  • 객체의 인스턴스를 만들면서 즉시 프로퍼티 중 일부를 초기화해야 하는 경우에 유용하다
    -> 함수의 본문에 간결한 식을 사용할 수 있기 때문

profile
개발자희망자

0개의 댓글