컬렉션을 사용한 고차 함수

1. 소개

Kotlin에서 함수 유형 및 람다 표현식 사용 Codelab에서는 repeat()와 같은 다른 함수를 매개변수로 받거나 반환하는 함수인 고차 함수에 관해 알아봤습니다. 고차 함수는 더 적은 코드로 정렬이나 필터링과 같은 일반적인 작업을 실행하는 데 도움이 되므로 컬렉션과 특히 관련이 높습니다. 컬렉션에 관해 자세히 알아봤으므로 이제 고차 함수를 다시 살펴보겠습니다.

이 Codelab에서는 컬렉션 유형에 사용할 수 있는 다양한 함수(예: forEach(), map(), filter(), groupBy(), fold(), sortedBy())를 알아봅니다. 이 과정에서 람다 표현식을 사용하는 연습을 추가로 하게 됩니다.

기본 요건

  • 함수 유형 및 람다 표현식에 관한 지식
  • repeat() 함수와 같은 후행 람다 문법에 관한 지식
  • List와 같은 Kotlin의 다양한 컬렉션 유형에 관한 지식

학습할 내용

  • 람다 표현식을 문자열에 삽입하는 방법
  • List 컬렉션에서 forEach(), map(), filter(), groupBy(), fold(), sortedBy() 등 다양한 고차 함수를 사용하는 방법

필요한 항목

  • Kotlin 플레이그라운드에 액세스할 수 있는 웹브라우저

2. forEach() 및 람다를 사용한 문자열 템플릿

시작 코드

다음 예에서는 베이커리의 쿠키 메뉴를 나타내는 List를 사용하고, 고차 함수를 사용하여 다양한 방식으로 메뉴 형식을 지정합니다.

먼저 초기 코드를 설정합니다.

  1. Kotlin 플레이그라운드로 이동합니다.
  2. main() 함수 위에 Cookie 클래스를 추가합니다. Cookie의 각 인스턴스는 nameprice, 쿠키에 관한 기타 정보가 포함된 메뉴의 항목을 나타냅니다.
class Cookie(
    val name: String,
    val softBaked: Boolean,
    val hasFilling: Boolean,
    val price: Double
)

fun main() {

}
  1. Cookie 클래스 아래 main() 외부에서 다음과 같이 쿠키 목록을 만듭니다. 유형은 List<Cookie>로 추론됩니다.
class Cookie(
    val name: String,
    val softBaked: Boolean,
    val hasFilling: Boolean,
    val price: Double
)

val cookies = listOf(
    Cookie(
        name = "Chocolate Chip",
        softBaked = false,
        hasFilling = false,
        price = 1.69
    ),
    Cookie(
        name = "Banana Walnut",
        softBaked = true,
        hasFilling = false,
        price = 1.49
    ),
    Cookie(
        name = "Vanilla Creme",
        softBaked = false,
        hasFilling = true,
        price = 1.59
    ),
    Cookie(
        name = "Chocolate Peanut Butter",
        softBaked = false,
        hasFilling = true,
        price = 1.49
    ),
    Cookie(
        name = "Snickerdoodle",
        softBaked = true,
        hasFilling = false,
        price = 1.39
    ),
    Cookie(
        name = "Blueberry Tart",
        softBaked = true,
        hasFilling = true,
        price = 1.79
    ),
    Cookie(
        name = "Sugar and Sprinkles",
        softBaked = false,
        hasFilling = false,
        price = 1.39
    )
)

fun main() {

}

forEach()로 목록 반복

처음으로 알아볼 고차 함수는 forEach() 함수입니다. forEach() 함수는 매개변수로 전달된 함수를 컬렉션의 각 항목에 한 번 실행합니다. 이는 repeat() 함수 또는 for 루프와 비슷하게 작동합니다. 람다는 컬렉션의 각 요소에 실행될 때까지 첫 번째 요소에 실행된 후 두 번째 요소에 실행되는 방식으로 실행됩니다. 메서드 서명은 다음과 같습니다.

forEach(action: (T) -> Unit)

forEach()(T) -> Unit 유형의 함수인 단일 작업 매개변수를 사용합니다.

T는 컬렉션에 포함된 데이터 유형에 상응합니다. 람다가 단일 매개변수를 사용하므로 이름을 생략하고 it으로 매개변수를 참조할 수 있습니다.

forEach() 함수를 사용하여 cookies 목록의 항목을 출력합니다.

  1. main()에서 후행 람다 문법을 사용하여 cookies 목록에서 forEach()를 호출합니다. 후행 람다가 유일한 인수이므로 함수를 호출할 때 괄호를 생략할 수 있습니다.
fun main() {
    cookies.forEach {

    }
}
  1. 람다 본문에서 it을 출력하는 println() 문을 추가합니다.
fun main() {
    cookies.forEach {
        println("Menu item: $it")
    }
}
  1. 코드를 실행하고 출력을 살펴봅니다. 출력되는 것은 모두 유형의 이름(Cookie)과 객체의 고유 식별자이며 객체의 콘텐츠는 출력되지 않습니다.
Menu item: Cookie@5a10411
Menu item: Cookie@68de145
Menu item: Cookie@27fa135a
Menu item: Cookie@46f7f36a
Menu item: Cookie@421faab1
Menu item: Cookie@2b71fc7e
Menu item: Cookie@5ce65a89

문자열에 표현식 삽입

처음으로 문자열 템플릿을 알아볼 때 달러 기호($)를 변수 이름과 함께 사용하여 문자열에 삽입하는 방법을 확인했습니다. 하지만 이는 점 연산자(.)와 결합하여 속성에 액세스할 때는 예상대로 작동하지 않습니다.

  1. forEach() 호출에서 $it.name을 문자열에 삽입하도록 람다의 본문을 수정합니다.
cookies.forEach {
    println("Menu item: $it.name")
}
  1. 코드를 실행합니다. 클래스의 이름 Cookie와 객체의 고유 식별자, 그리고 .name이 삽입되는 것을 볼 수 있습니다. name 속성의 값은 액세스되지 않습니다.
Menu item: Cookie@5a10411.name
Menu item: Cookie@68de145.name
Menu item: Cookie@27fa135a.name
Menu item: Cookie@46f7f36a.name
Menu item: Cookie@421faab1.name
Menu item: Cookie@2b71fc7e.name
Menu item: Cookie@5ce65a89.name

속성에 액세스하여 이를 문자열에 삽입하려면 표현식이 필요합니다. 중괄호로 묶어 문자열 템플릿의 표현식 부분을 만들 수 있습니다.

2c008744cee548cc.png

람다 표현식은 여는 중괄호와 닫는 중괄호 사이에 배치됩니다. 속성에 액세스하고 수학 연산을 실행하고 함수를 호출할 수 있으며 람다의 반환 값은 문자열에 삽입됩니다.

이름이 문자열에 삽입되도록 코드를 수정해 보겠습니다.

  1. it.name을 중괄호로 묶어 람다 표현식으로 만듭니다.
cookies.forEach {
    println("Menu item: ${it.name}")
}
  1. 코드를 실행합니다. 출력에는 각 Cookiename이 포함됩니다.
Menu item: Chocolate Chip
Menu item: Banana Walnut
Menu item: Vanilla Creme
Menu item: Chocolate Peanut Butter
Menu item: Snickerdoodle
Menu item: Blueberry Tart
Menu item: Sugar and Sprinkles

3. map()

map() 함수를 사용하면 컬렉션을 동일한 수의 요소로 구성된 새 컬렉션으로 변환할 수 있습니다. 예를 들어 map() 함수에 각 Cookie 항목에서 String을 만드는 방법을 알려 준다면 map()List<Cookie>를 쿠키의 name만 포함된 List<String>으로 변환할 수 있습니다.

e0605b7b09f91717.png

베이커리의 대화형 메뉴를 표시하는 앱을 작성한다고 가정해 보겠습니다. 사용자가 쿠키 메뉴가 표시되는 화면으로 이동할 때 논리적인 방식(예: 이름 다음에 가격 표시)으로 데이터가 제공되는 것을 원할 수 있습니다. map() 함수를 사용하여 관련 데이터(이름, 가격)로 형식이 지정된 문자열 목록을 만들 수 있습니다.

  1. main()에서 이전 코드를 모두 삭제합니다. fullMenu라는 새 변수를 만들고 cookies 목록에서 map()을 호출한 결과와 동일하게 설정합니다.
val fullMenu = cookies.map {

}
  1. 람다 본문에서 itnameprice를 포함하도록 형식이 지정된 문자열을 추가합니다.
val fullMenu = cookies.map {
    "${it.name} - $${it.price}"
}
  1. fullMenu의 콘텐츠를 출력합니다. forEach()를 사용하면 됩니다. map()에서 반환된 fullMenu 컬렉션에는 List<Cookie>가 아닌 List<String> 유형이 있습니다. cookies의 각 CookiefullMenuString에 상응합니다.
println("Full menu:")
fullMenu.forEach {
    println(it)
}
  1. 코드를 실행합니다. 출력이 fullMenu 목록의 콘텐츠와 일치합니다.
Full menu:
Chocolate Chip - $1.69
Banana Walnut - $1.49
Vanilla Creme - $1.59
Chocolate Peanut Butter - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79
Sugar and Sprinkles - $1.39

4. filter()

filter() 함수를 사용하면 컬렉션의 하위 집합을 만들 수 있습니다. 예를 들어 숫자 목록이 있다면 filter()를 사용하여 2로 나눌 수 있는 숫자만 포함된 새 목록을 만들 수 있습니다.

d4fd6be7bef37ab3.png

map() 함수의 결과는 항상 동일한 크기의 컬렉션을 생성하지만 filter()는 원래 컬렉션과 같거나 작은 크기의 컬렉션을 생성합니다. map()과 달리 결과 컬렉션도 동일한 데이터 유형을 가지므로 List<Cookie>를 필터링하면 또 다른 List<Cookie>가 생성됩니다.

map(), forEach()와 마찬가지로 filter()는 단일 람다 표현식을 매개변수로 사용합니다. 람다는 컬렉션의 각 항목을 나타내는 단일 매개변수를 가지며 Boolean 값을 반환합니다.

컬렉션의 각 항목의 경우 다음과 같습니다.

  • 람다 표현식의 결과가 true이면 항목은 새 컬렉션에 포함됩니다.
  • 결과가 false이면 항목은 새 컬렉션에 포함되지 않습니다.

이는 앱에서 데이터의 하위 집합을 가져오려는 경우 유용합니다. 예를 들어 베이커리에서 메뉴의 별도 섹션에 소프트 베이크 쿠키를 강조표시하려고 한다고 가정해 보겠습니다. 항목을 출력하기 전에 먼저 cookies 목록을 filter()할 수 있습니다.

  1. main()에서 softBakedMenu라는 새 변수를 만들고 cookies 목록에서 filter()를 호출한 결과로 설정합니다.
val softBakedMenu = cookies.filter {
}
  1. 람다 본문에서 불리언 표현식을 추가하여 쿠키의 softBaked 속성이 true와 같은지 확인합니다. softBakedBoolean 자체이므로 람다 본문에는 it.softBaked만 포함하면 됩니다.
val softBakedMenu = cookies.filter {
    it.softBaked
}
  1. forEach()를 사용하여 softBakedMenu의 콘텐츠를 출력합니다.
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. 코드를 실행합니다. 메뉴는 전과 동일하게 출력되지만 소프트 베이크 쿠키만 포함됩니다.
...
Soft cookies:
Banana Walnut - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79

5. groupBy()

groupBy() 함수를 사용하면 함수에 따라 목록을 맵으로 변환할 수 있습니다. 함수의 고유한 각 반환 값은 결과 맵의 키가 됩니다. 각 키의 값은 고유한 반환 값을 생성한 컬렉션의 모든 항목입니다.

54e190b34d9921c0.png

키의 데이터 유형은 groupBy()에 전달된 함수의 반환 유형과 동일합니다. 값의 데이터 유형은 원래 목록의 항목 목록입니다.

이는 개념화가 어려울 수 있으므로 간단한 예를 먼저 들어 보겠습니다. 전과 같은 숫자 목록이 있다고 할 때 홀수 또는 짝수로 그룹화합니다.

2로 나누고 나머지가 0인지 1인지 확인하여 숫자가 홀수인지 짝수인지 확인할 수 있습니다. 나머지가 0이면 짝수입니다. 나머지가 1이면 홀수입니다.

이 작업은 모듈로 연산자(%)를 사용하면 됩니다. 모듈로 연산자는 표현식의 왼쪽에 있는 피제수를 오른쪽의 제수로 나눕니다.

4c3333da9e5ee352.png

나누기 연산자(/)와 같이 나누기 결과를 반환하는 대신 모듈로 연산자는 나머지를 반환합니다. 이는 숫자가 짝수인지 홀수인지 확인하는 데 유용합니다.

4219eacdaca33f1d.png

groupBy() 함수는 { it % 2 } 람다 표현식과 함께 호출됩니다.

결과 맵에는 01이라는 두 개의 키가 있습니다. 각 키의 값은 List<Int> 유형입니다. 키 0의 목록에는 모든 짝수가 포함되어 있으며 키 1의 목록에는 모든 홀수가 포함되어 있습니다.

사진을 찍은 대상이나 위치별로 사진을 그룹화하는 사진 앱이 실제 사용 사례가 될 수 있습니다. 베이커리 메뉴에서는 쿠키의 소프트 베이크 여부에 따라 메뉴를 그룹화해 보겠습니다.

groupBy()를 사용하여 softBaked 속성을 기반으로 메뉴를 그룹화합니다.

  1. 이전 단계에서 filter() 호출을 삭제합니다.

삭제 코드

val softBakedMenu = cookies.filter {
    it.softBaked
}
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. cookies 목록에서 groupBy()를 호출하여 결과를 groupedMenu라는 변수에 저장합니다.
val groupedMenu = cookies.groupBy {}
  1. it.softBaked를 반환하는 람다 표현식을 전달합니다. 반환 유형은 Map<Boolean, List<Cookie>>입니다.
val groupedMenu = cookies.groupBy { it.softBaked }
  1. groupedMenu[true] 값이 포함된 softBakedMenu 변수와 groupedMenu[false] 값이 포함된 crunchyMenu 변수를 만듭니다. Map의 첨자 지정 결과가 null을 허용하므로 Elvis 연산자(?:)를 사용하여 빈 목록을 반환할 수 있습니다.
val softBakedMenu = groupedMenu[true] ?: listOf()
val crunchyMenu = groupedMenu[false] ?: listOf()
  1. 코드를 추가하여 소프트 쿠키 메뉴를 출력한 다음 크런치 쿠키 메뉴를 출력합니다.
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
println("Crunchy cookies:")
crunchyMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. 코드를 실행합니다. groupBy() 함수를 사용하면 속성 중 하나의 값에 따라 목록을 두 개로 분할할 수 있습니다.
...
Soft cookies:
Banana Walnut - $1.49
Snickerdoodle - $1.39
Blueberry Tart - $1.79
Crunchy cookies:
Chocolate Chip - $1.69
Vanilla Creme - $1.59
Chocolate Peanut Butter - $1.49
Sugar and Sprinkles - $1.39

6. fold()

fold() 함수는 컬렉션에서 단일 값을 생성하는 데 사용됩니다. 주로 총가격을 계산하거나 목록의 모든 요소를 합산하여 평균을 구하는 등의 작업에 가장 많이 사용됩니다.

a9e11a1aad05cb2f.png

fold() 함수는 다음 두 매개변수를 사용합니다.

  • 초깃값. 데이터 유형은 함수를 호출할 때 추론됩니다. 즉, 초깃값 0Int로 추론됩니다.
  • 초깃값과 동일한 유형의 값을 반환하는 람다 표현식

람다 표현식에는 추가 매개변수 두 개가 있습니다.

  • 첫 번째는 누산기라고 합니다. 데이터 유형이 초깃값과 동일합니다. 이를 누계라고 생각하면 됩니다. 람다 표현식이 호출될 때마다 누산기는 람다가 이전에 호출된 때의 반환 값과 동일합니다.
  • 두 번째는 컬렉션의 각 요소와 동일한 유형입니다.

앞서 본 다른 함수와 마찬가지로 람다 표현식은 컬렉션의 각 요소에 호출되므로 fold()를 모든 요소를 합산하는 간결한 방법으로 사용할 수 있습니다.

fold()를 사용하여 모든 쿠키의 총가격을 계산해 보겠습니다.

  1. main()에서 totalPrice라는 새 변수를 만들고 cookies 목록에서 fold()를 호출한 결과와 같게 설정합니다. 초깃값으로 0.0을 전달합니다. 유형은 Double로 추론됩니다.
val totalPrice = cookies.fold(0.0) {
}
  1. 람다 표현식에 두 매개변수를 모두 지정해야 합니다. 누산기에 total을 사용하고 컬렉션 요소에 cookie를 사용합니다. 매개변수 목록 뒤에 화살표(->)를 사용합니다.
val totalPrice = cookies.fold(0.0) {total, cookie ->
}
  1. 람다 본문에서 totalcookie.price의 합계를 계산합니다. 이 값은 반환 값으로 추론되고 다음에 람다가 호출될 때 total에 전달됩니다.
val totalPrice = cookies.fold(0.0) {total, cookie ->
    total + cookie.price
}
  1. 가독성을 위해 문자열로 형식이 지정된 totalPrice 값을 출력합니다.
println("Total price: $${totalPrice}")
  1. 코드를 실행합니다. 결과는 cookies 목록의 가격 합계와 같아야 합니다.
...
Total price: $10.83

7. sortedBy()

컬렉션에 관해 처음 알아볼 때 sort() 함수는 요소를 정렬하는 데 사용할 수 있다고 배웠습니다. 그러나 이는 Cookie 객체 컬렉션에서는 작동하지 않습니다. Cookie 클래스에는 여러 속성이 있으며 Kotlin은 개발자가 어떤 속성(name, price 등)으로 정렬하려는지 알 수 없습니다.

이러한 경우 Kotlin 컬렉션은 sortedBy() 함수를 제공합니다. sortedBy()를 사용하면 정렬 기준으로 사용하려는 속성을 반환하는 람다를 지정할 수 있습니다. 예를 들어 price를 기준으로 정렬하려면 람다는 it.price를 반환합니다. 값의 데이터 유형에 자연스러운 정렬 순서가 있는 경우(문자열은 알파벳순으로 정렬되고 숫자 값은 오름차순으로 정렬됨) 해당 유형의 컬렉션과 마찬가지로 정렬됩니다.

5fce4a067d372880.png

sortedBy()를 사용하여 쿠키 목록을 알파벳순으로 정렬합니다.

  1. main()에서 기존 코드 뒤에 alphabeticalMenu라는 새 변수를 추가하고 cookies 목록에서 sortedBy()를 호출하는 것과 동일하게 설정합니다.
val alphabeticalMenu = cookies.sortedBy {
}
  1. 람다 표현식에서 it.name을 반환합니다. 결과 목록은 계속 List<Cookie> 유형이지만 name을 기준으로 정렬됩니다.
val alphabeticalMenu = cookies.sortedBy {
    it.name
}
  1. alphabeticalMenu의 쿠키 이름을 출력합니다. forEach()를 사용하여 각 이름을 새 줄에 출력할 수 있습니다.
println("Alphabetical menu:")
alphabeticalMenu.forEach {
    println(it.name)
}
  1. 코드를 실행합니다. 쿠키 이름은 알파벳순으로 출력됩니다.
...
Alphabetical menu:
Banana Walnut
Blueberry Tart
Chocolate Chip
Chocolate Peanut Butter
Snickerdoodle
Sugar and Sprinkles
Vanilla Creme

8. 결론

축하합니다. 컬렉션에서 고차 함수를 사용할 수 있는 방법을 보여주는 몇 가지 예를 살펴봤습니다. 정렬 및 필터링과 같은 일반적인 작업은 코드 한 줄로 실행할 수 있어 프로그램이 더 간결해지고 표현력도 풍부해집니다.

요약

  • forEach()를 사용하여 컬렉션의 각 요소를 반복할 수 있습니다.
  • 표현식을 문자열에 삽입할 수 있습니다.
  • map()은 종종 다른 데이터 유형의 컬렉션으로 컬렉션의 항목 형식을 지정하는 데 사용됩니다.
  • filter()는 컬렉션의 하위 집합을 생성할 수 있습니다.
  • groupBy()는 함수 반환 값을 기준으로 컬렉션을 분할합니다.
  • fold()는 컬렉션을 단일 값으로 변환합니다.
  • sortedBy()는 지정된 속성별로 컬렉션을 정렬하는 데 사용됩니다.

9. 자세히 알아보기