컬렉션에 대한 함수형 API

유우선·2026년 2월 4일

Kotlin Study📚

목록 보기
13/32

개요

  • 함수형 프로그래밍 → 컬렉션 관련 작업에 여러 장점을 제공
    • 표준 라이브러리 제공 함수 사용
    • 람다를 통한 함수 동작 커스텀

원소 제거와 반환: filter와 map

filter, map → 컬렉션을 다루는 토대

  • 컬렉션의 원소를 걸러내기
  • 컬렉션의 각 원소를 다른 형태로 변환

filter 함수

  • 컬렉션을 순회하면서 주어진 람다가 true를 반환하는 원소를 저장한 새로운 컬렉션 반환
  • 반환된 컬렉션의 타입은 입력 컬렉션의 타입과 동일함
    fun main() {
        val people = listOf(Person("Alice", 29), Person("Bob", 31))
        println(people.filter { it.age >= 30 })
        // [Person(name=Bob, age=31)]
    }

map 함수

  • 입력 컬렉션의 원소를 변환
  • 주어진 함수를 컬렉션의 각 원소에 적용
  • 그 결과값들을 새 컬렉션으로 반환
  • 주어진 함수에 따라 반환하는 컬렉션 원소의 타입이 바뀜
    fun main() {
        val people = listOf(Person("Alice", 29), Person("Bob", 31))
        println(people.map { it.name })
        // [Alice, Bob]
    }
    
    // 맴버 참조를 사용한 코드 간소화
    poeple.map(Person::name)

filterIndexed, mapIndexed 함수

  • 익데스와 원소를 함께 사용
    fun main() {
        val numbers = listOf(1, 2, 3, 4, 5, 6, 7)
        val filtered = numbers.filterIndexed { index, element ->
            index % 2 == 0 && element > 3
        }
        println(filtered)
        //[5, 7]
        val mapped = numbers.mapIndexed { index, element ->
            index + element
        }
        println(mapped)
        //[1, 3, 5, 7, 9, 11, 13]
    }

map 컬렉션에 filter와 map 함수 적용

data class Person (val name: String, val age: Int)

fun main() {
    val numbers = mapOf(0 to "zero", 1 to "one")
    println(numbers.mapValues { it.value.uppercase() })
    //{0=ZERO, 1=ONE}
}
  • 맵의 경우 키와 값을 걸러내는 함수가 따로 존재함
    • key → filterKeys, mapKeys
    • value → filterValue, mapValue

컬랙션 값 누적: reduce와 fold

  • 원소로 이뤄진 컬렉션을 받아서 한 값을 반환
  • 누적기를 통해 점진적으로 만들어짐

reduce

  • 컬렉션의 첫 번째 원소부터 연산 시작
fun main() {
    val list = listOf(1, 2, 3, 4)
    println(list.reduce { acc, element ->
        acc + element
    })
		// 10
    println(list.reduce { acc, element ->
        acc * element
    })
    // 24
}

fold

  • 임의의 시작 값을 지정할 수 있음
fun main() {
    val people = listOf(
        Person("Alex", 29),
        Person("Natalia", 28)
    )
    val folded = people.fold("") { acc, person ->
        acc + person.name
    }
    println(folded)
    // AlexNatalia
}

연산 중간 단계의 값 뽑아내기: runningReduce, runningFold

  • 모든 중간 누적 값이 저장된 리스트를 반환함
fun main() {
    val list = listOf(1, 2, 3, 4)
    val summed = list.runningReduce { acc, element ->
        acc + element
    }
    println(summed)
		// [1, 3, 6, 10]
    val multiplied = list.runningReduce { acc, element ->
        acc * element
    }
    println(multiplied)
		// [1, 2, 6, 24]
    val people = listOf(
        Person("Alex", 29),
        Person("Natalia", 28)
    )
    println(people.runningFold("") { acc, person ->
        acc + person.name
    })
    // [, Alex, AlexNatalia]
}

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

  • all, any, none → 컬렉션의 모든 원소가 어떤 조건을 만족하는지 판단
  • count → 조건을 만족하는 함수의 개수를 반환
  • find → 조건을 만족하는 첫 번째 원소를 반환

all 함수

  • 모든 원소가 술어를 만족하는지 판단
    val canBeInClub27 = { p: Person -> p.age <= 27 }
    
    fun main( ){
        val people = listOf(Person("Alice", 27), Person("Bob",31))
        println(people.all(canBeInClub27))
        // false
    }

any 함수

  • 모든 원소 중 술어를 술어를 만족하는 원소가 하나라도 있는지
    val canBeInClub27 = { p: Person -> p.age <= 27 }
    
    fun main( ){
        val people = listOf(Person("Alice", 27), Person("Bob",31))
        println(people.any(canBeInClub27))
        // true
    }

none 함수

  • 술어를 만족하는 원소가 하나도 없는지
    data class Person (val name: String, val age: Int)
    
    val canBeInClub27 = { p: Person -> p.age <= 27 }
    
    fun main( ){
        val people = listOf(Person("Alice", 27), Person("Bob",31))
        println(people.none(canBeInClub27))
        // false
    }

빈 컬렉션과 술어

  1. any → 컬렉션이 비어있으면 술어 람다를 만족하는 원소도 없음
  2. none → true를 반환함
  3. all → 술어 람다가 무엇이든 관계없이 true를 반환

count 함수

  • 술어를 만족하는 원소의 개수 반환
    val canBeInClub27 = { p: Person -> p.age <= 27 }
    
    fun main( ){
        val people = listOf(Person("Alice", 27), Person("Bob",31))
        println(people.count(canBeInClub27))
        // 1
    }

find 함수

  • 술어를 만족하는 원소를 하나 찾아서 반환해줌
  • 조건을 만족하는 첫 번째 원소 반환
  • 빈 컬렉션에 대해선 null 반환
  • firstOrNull 함수와 같음
    val canBeInClub27 = { p: Person -> p.age <= 27 }
    
    fun main( ){
        val people = listOf(Person("Alice", 27), Person("Bob",31))
        println(people.find(canBeInClub27))
        // Person(name=Alice, age=27)
    }

리스트를 분해해 리스트의 쌍으로 만들기: partition

  • 술어를 만족하는 그룹과 만족하지 못하는 그룹으로 분할
  • filter와 filterNot 함수를 사용해 분할 할 수 있음
    val canBeInClub27 = { p: Person -> p.age <= 27 }
    
    fun main( ){
        val people = listOf(Person("Alice", 26),
            Person("Bob",29),
            Person("Carol", 31)
        )
        val comeIn = people.filter(canBeInClub27)
        val stayOut = people.filterNot(canBeInClub27)
        println(comeIn)
        // [Person(name=Alice, age=26)]
        println(stayOut)
        // [Person(name=Bob, age=29), Person(name=Carol, age=31)]
    }
  • partition 함수를 사용하면 두 리스트를 한번에 초기화할 수 있음
    val canBeInClub27 = { p: Person -> p.age <= 27 }
    
    fun main( ){
        val people = listOf(Person("Alice", 26),
            Person("Bob",29),
            Person("Carol", 31)
        )
        val (comeIn, stayOut) = people.partition(canBeInClub27)
        println(comeIn)
        // [Person(name=Alice, age=26)]
        println(stayOut)
        // [Person(name=Bob, age=29), Person(name=Carol, age=31)]

리스트를 여러 그룹으로 이뤄진 맵으로 바꾸기: groupBy

  • 컬렉션을 어떤 특성에 따라 여러 그룹으로 나눌 수 있음
  • 결과를 map 컬렉션으로 반환함
    fun main( ){
        val people = listOf(
            Person("Alice", 31),
            Person("Bob", 29),
            Person("Carol", 31)
        )
        println(people.groupBy { it.age })
        //Map<Int, List<Person>>
        // {31=[Person(name=Alice, age=31), Person(name=Carol, age=31)], 
        // 29=[Person(name=Bob, age=29)]}
    }
  • 반환된 Map을 mapKeys나 mapValues로 변경할 수 있음

문자열 컬렉션의 원소를 첫 글자로 분류

fun main( ){
    val people = listOf("apple", "apricot", "banana", "cantaloupe")
    println(people.groupBy(String::first))
    // {a=[apple, apricot], b=[banana], c=[cantaloupe]}
}
  • first → String의 확장 함수

컬렉션을 맵으로 변환: associate, associateWith, associateBy

associate 함수

  • 원소를 그룹화 하지 않으면서 컬렉션으로 부터 맵을 만들어냄
  • 입력 컬렉션의 원소로부터 키/값 쌍을 만들어내는 람다를 입력
    fun main() {
        val people = listOf(Person("Joe", 22), Person("Mary", 31))
        val nameToAge = people.associate { it.name to it.age }
        println(nameToAge)
        // {Joe=22, Mary=31}
        println(nameToAge["Joe"])
        // 22
    }

associateWith

  • 컬렉션의 원래 원소를 맵의 key로 사용
  • 입력한 람다로 key에 대응하는 value를 생성

associateBy

  • 컬렉션의 원래 원소를 맵의 value로 사용
  • 입력한 람다로 value의 key를 생성
fun main() {
    val people = listOf(
        Person("Joe", 22),
        Person("Mary", 31),
        Person("Jamie", 22)
    )
    val personToAge = people.associateWith { it.age }
    println(personToAge)
    // {Person(name=Joe, age=22)=22, Person(name=Mary, age=31)=31, Person(name=Jamie, age=22)=22}
    val ageToPerson = people.associateBy { it.age }
    println(ageToPerson)
    //{22=Person(name=Jamie, age=22), 31=Person(name=Mary, age=31)}
}

맵의 키 → 유일해야함

  • 변환 함수가 키가 같은 값을 여러 번 추가하게 되면 마지막 결과가 그 이전에 들어간 결과를 덮어씀

가변 컬렉션의 원소 변경: replaceAll, fill

replaceAll

  • 지정한 람다로 얻은 결과로 컬렉션의 모든 원소를 변경

fill

  • 모든 원소를 똑같은 값으로 변환
fun main() {
    val names = mutableListOf("Martin", "Samuel")
    println(names)
    // [Martin, Samuel]
    names.replaceAll { it.uppercase() }
    println(names)
    // [MARTIN, SAMUEL]
    names.fill("(redacted)")
    println(names)
    // [(redacted), (redacted)]
}

컬렉션의 틀별한 경우 처리: ifEmpty

  • 컬렉션에 아무 원소도 없을 때 기본값을 생성하는 람다를 제공할 수 있음
    fun main() {
        val empty = emptyList<String>()
        val full = listOf("apple", "orange", "banana")
        println(empty.ifEmpty { listOf("no", "values", "here") })
        // [no, values, here]
        println(full.ifEmpty { listOf("no", "values", "here") })
        // [apple, orange, banana]
    }

ifBlank: 문자열에서 isEmpty의 형제

  • “비어있다”라는 조건을 “공백만 있다”로 바꿀 수 있음
    fun main() {
        val blankName = " "
        val name = "J. Doe"
        println(blankName.ifEmpty { "(unnamed)" })
        //
        println(blankName.ifBlank{ "(unnamed)" })
        // (unnamed)
        println(name.ifBlank { "(unnamed)" })
        // J. Doe
    }

컬렉션 나누기: chunked와 windowed

windowed 함수

  • 리스트의 원소를 윈도우로 묶어서 연산 후 하나의 값으로 반환
  • 연산 순서에 따라 인덱스 부여
  • 1회 연산 후 윈도우가 이동하기 때문에 “슬라이딩 윈도우” 라고 함
fun main() {
    val temperatures = listOf(27.7, 29.8, 22.0, 35.5, 19.1)
    println(temperatures.windowed(3))
    // [[27.7, 29.8, 22.0], [29.8, 22.0, 35.5], [22.0, 35.5, 19.1]]
    println(temperatures.windowed(3) { it.sum() / it.size})
    // [26.5, 29.099999999999998, 25.53333333333333]
}

chunked 함수

  • 컬렉션을 주어진 크기의 서로 겹치지 않는 부분으로 나눔
fun main() {
    val temperatures = listOf(27.7, 29.8, 22.0, 35.5, 19.1)
    println(temperatures.chunked(2))
    // [[27.7, 29.8], [22.0, 35.5], [19.1]]
    println(temperatures.chunked(2) { it.sum() })
    // [57.5, 57.5, 19.1]
}

컬렉션 합치기: zip

  • 두 컬렉션에서 같은 인덱스에 있는 원소들의 쌍으로 이뤄진 리스트를 만들 수 있음
  • 람다를 전달하면 출력 양식을 변경할 수 있음
  • 결과 컬렉션의 길이 == 두 입력 컬렉션 중 더 짧은 컬렉션의 길이
fun main() {
    val names = listOf("Joe", "Mary", "Jamie")
    val ages = listOf(22, 31, 31, 44, 0)
    println(names.zip(ages))
    // [(Joe, 22), (Mary, 31), (Jamie, 31)]
    println(names.zip(ages) { name, age -> Person(name, age) })
    // [Person(name=Joe, age=22), Person(name=Mary, age=31), Person(name=Jamie, age=31)]
}
  • zip도 to 처럼 중위 표현식을 사용할 수 있음
    • println(names zip ages)
  • zip을 연쇄적으로 호출할 수 있음
    val countries = listOf("DE", "NL", "US")
    println(name zip ages zip countries)
    // [((Joe, 22), DE), ((Mary, 31), NL), ((Jamie, 31), US)]

내포된 컬렉션의 원소 처리: flatMap, flatten

flatMap 함수

  • 컬렉션의 각 원소를 파라미터로 주어진 함수를 사용해 변환
  • 변환한 결과를 하나의 리스트로 반환
    class Book(val title: String, val authors: List<String>)
    
    val library = listOf(
        Book("Kotiln in Action", listOf("Isakova", "Elizarov", "Aigner", "Jemerov")),
        Book("Atomic Kotlin", listOf("Eckel", "Isakova")),
        Book("The Three-Body Problem", listOf("Liu"))
    )
    
    fun main() {
        val authors = library.flatMap { it.authors }
        println(authors) // 라이브러리의 모든 저자의 리스트
        // [Isakova, Elizarov, Aigner, Jemerov, Eckel, Isakova, Liu]
        println(authors.toSet()) // 라이브러리의 모든 저자의 집합 (중복 없음)
        // [Isakova, Elizarov, Aigner, Jemerov, Eckel, Liu]
    }

flatten 함수

  • 다차원 컬렉션을 1 차원 컬렉션으로 변환

0개의 댓글