본문 바로가기
개발/코틀린(kotlin)

[Kotlin] 코틀린 람다 #2 Collection API - filter, map, groupby, flatmap, sequence, find, any, all, count

by darksilber 2020. 1. 29.
반응형

출처 - https://tourspace.tistory.com/111?category=797357

 

람다는 collection에서 사용할때 막강한 힘을 보여 줍니다.

코틀린에서 제공하는 collection 관련 API는 새롭게 추가되었다기 보단 기존 java, c#, 그루비, 스칼라등에서 사용하는것들과 동일 합니다.

 

5.2.1 filter, map

filter는 predicate형태의 람다식을 인자로 받아 조건에 따라 collection의 원소를 filtering 하는 기능을 합니다.

fun main(args: Array) { 
	val list = listOf(1, 2, 3, 4) 
    println(list.filter { it % 2 == 0 }) 
}

list에서 짝수만 뽑아내는 코드 입니다.

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

fun main(args: Array) { 
	val people = listOf(Person("Alice", 29), Person("Bob", 31))
    println(people.filter { it.age > 30 })
}

또한 나이가 30 초과인 사람만 뽑아내는 코드 입니다.

 

map은 원소를 원하는 형태로 변환하는 기능을 하며 이들의 반환값은 list 입니다.

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

fun main(args: Array) { 
	val people = listOf(Person("Alice", 29), Person("Bob", 31)) 
    println(people.map { it.name }) 
}

이때 앞서서 나왔던 method reference를 이용하여 하기와 같이 표현해도 상관 없습니다.

people.map {Person::name}

만약 가장 나이가 많은 사람의 이름을 구해야 한다면 다음 두가지로 코드를 작성해 볼 수 있습니다.

// filter의 내부에서 maxBy 이용
people.filter { it.age == people.maxBy{Person::age}!!.age} 

// filter의 외부에서 maxBy 이용 
val maxAge = people.maxBy{Person::age}!!.age 
people.filter { it.age == maxAge}

상단이 코드는 매우 비효율적인 코드 입니다. filter를 돌면서 내부적으로 max를 구하기 위해 내부적으로 매번 for문을 돌겠죠?

따라서 무작정 코드량을 줄이기 보단 반복이 중첩되지 않는지 내부적인 동작에 대해서 고려한 후에 코드를 작성하는게 좋습니다.

 

 

맵의 경우 filterKeys, mapKeys란 keyword를 제공하고, 값을 위한 filterValues, mapValues란 키워드도 제공합니다.

fun main(args: Array) { 
	val numbers = mapOf(0 to "zero", 1 to "one")
    println(numbers.mapValues { it.value.toUpperCase() }) 
}

5.2.2 all, any, count, find

 API Description  Return type nullable 
all  collection 전체가 주어진 predicate를 만족하는지를 판단합니다. Boolean  X 
any collection의 원수중에 하나라도 주어진 predicate를 만족하는지를 판단합니다. Boolean X
count 주어진 predicate를 만족하는 원소의 개수를 반환합니다. Int X
find 주어진 predicate에 만족하는 첫번째 원소를 반환합니다. <T>

 

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

val canBeInClub27 = { p: Person -> p.age <= 27 } 

fun main(args: Array) { 
	val people = listOf(Person("Alice", 27), Person("Bob", 31)) 
    println(people.all(canBeInClub27)) 
}
fun main(args: Array) { 
	val list = listOf(1, 2, 3) 
    println(!list.all { it == 3 }) 
    println(list.any { it != 3 }) 
}

 

all은 전체 원소에 대해 만족해야 하므로 반환값은 false 입니다.

!all 이나 !any를 사용할 수 있습니다. 다만 !all = any, !any = all 과 같으므로 굳이 ! 연산자를 붙여서 사용하지 않는것이 가독성면에서 좋습니다.

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

val canBeInClub27 = { p: Person -> p.age <= 27 }

fun main(args: Array) { 
	val people = listOf(Person("Alice", 27), Person("Bob", 31)) 
    println(people.find(canBeInClub27))
}

find의 경우 perdicate를 만족하는 첫번째 원소를 반환하므로 만족하는 값이 없다면 null이 반환될 수 있습니다.

따라서 명시적으로 fisrtOrNull을 사용해도 됩니다.

// size 이용 
val count = people.filter(canBeInClub27).size

// count 이용 
val count = people.count(canBeInClub27)

 

collection의 size를 이용해도 값은 동일하지만, size의 경우 filter의 결과인 새로운 list가 만들어지고 size를 측정하므로 위 경우에는 count를 이용하는게 더 효율적입니다. (count는 중간 결과(list)를 만들지 않습니다.)

 

5.2.3 groupBy

sql에서나 제공하던 group by를 collection에서도 제공합니다.

물론 기능은 sql보다야 훨씬 제한적입니다.^^a

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

fun main(args: Array) {
	val people = listOf(Person("Alice", 31), 
    	Person("Bob", 29), Person("Carol", 31))
    println(people.groupBy { it.age })
}

매번 보던 Person class 입니다.

두개를 만들어 list에 넣고 list의 groupBy를 수행하면 나이에 따라 grouping이 됩니다.

따라서 위 코드의 return값은 Map<Int, List<Person>> 이 됩니다.

-> key: age, value: 해당되는 person 객체를 담는 리스트

fun main(args: Array) { 
	val list = listOf("a", "ab", "b") 
    println(list.groupBy(String::first)) 
}

위 코드의 경우 String:first로 묶었으므로 return값은 Map<String, List<String>>이 됩니다.

 

5.2.4 flatMap와 flatten

flatMap은 주어진 람다로 Map을 만들고 이를 다시 flat하게 만드는 함수 입니다.

 map을 처리하고 난 다음의 결과가 list인경우 이 list의 원소를 다시 펼쳐서 하나의 list로 만듭니다.

헷깔릴 수도 있지만 "list의 list를 처리할때 쓴다"를 기억하고 있으면 쉬울것 같습니다.

 

fun main(args: Array) { 
	val strings = listOf("abc", "def") 
    println(strings.flatMap { it.toList() }) 
}

위 코드를 분석해 보면 아래 두 단계를 거칩니다.

  1. it.toList()를 이용하여 원소를 map 처리한다. 결과 => list("abc"), list("def")
  2. list의 원소를 flat 하게 만든다. 결과 => list("a","b","c","d","e","f")
class Book(val title: String, val authors: List) 

fun main(args: Array) {
	val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")), 
    				   Book("Mort", listOf("Terry Pratchett")),
               		   Book("Good Omens", listOf("Terry Pratchett", "Neil Gaiman")))
    println(books.flatMap { it.authors }.toSet()) 
}

한 책에 저자가 여러명일수 있습니다.

따라서 위 코드에서는 람다식{it.authors}의 map 결과가 list입니다.

flatMap이기 때문에 list로 나온 결과를 전부 flat 하게 펼칩니다.

그리고 나서 toSet()으로 묶어주면 중복이 제거되어 unique한 author로 구성된 set이 반환됩니다.



5.3  Collection의 lazy execution

앞단에서 수행한 filter나 map은 호출시 바로 수행되어 수행될대마다 새로운 list를 반환합니다.

filter와 map을 연속해서 chaining 해서 쓴다면 하나의 구문이 끝날때 마다 중간연산 결과로 새로운 list가 계속 만들어 집니다.

 

단! 자바8에서 stream은 그렇지가 않습니다.

중간 연산결과 없이 한번에 쭈~~욱 연산되어 결과가 반환됩니다.

 

코틀린에서는 중간 연산결과 없이 자바8의 stream처럼 연산을 하려면 asSequence() 를 이용해야 합니다.

listOf(1, 2, 3, 4).asSequence() 
					.map { print("map($it) "); it * it }
                    .filter { print("filter($it) "); it % 2 == 0 }

먼저 list를 sequence()로 만듭니다.

그런후 제곱을 하고 짝수만 filter로 걸러냅니다.

 

이 연산은 해당 코드 호출시에는 수행되지 않습니다.

일단 list가 sequnce로 변환되면, 각 단계별 중간 연산결과를 만들어 내지 않기 때문에 최종 결과를 요청하는 시점에 모든 계산이 수행 됩니다.

최종 연산을 요청하는 방법은 마지막에 toList(), toSet()을 붙여 결과를 요청하는 api를 붙이면 됩니다.

fun main(args: Array) { 
	listOf(1, 2, 3, 4).asSequence() 
    				  .map { print("map($it) "); it * it }
                      .filter { print("filter($it) "); it % 2 == 0 } 
                      .toList() 
}


이 계산의 순서는 아래와 같습니다.

1 -> 제곱:1 -> 짝수? -> 버림

2 -> 제곱:4 -> 짝수? -> list에 추가

3 -> 제곱:9 -> 짝수? -> 버림

4 -> 제곱: 16 -> list에 추가.

 

일반 map, filter와는 다른 형태 입니다.

일반 map, filter 였다면, 원소에 제곱을 한 전체 리스트를 만들고, 그 리스트에서 다시 전체를 대상으로 짝수인지 판별합니다.

 

즉 sequence는 원소가 하나씩 전체 flow를 거치고, 그다음 원소, 그다음..형식으로 처리 됩니다.

따라서 위 코드처럼 sequence로 만들어 처리하는 경우 위 코드의 filter와 map을 순서를 바꿨다면 속도는 훨씬 빨라지겠죠?

 

sequence를 사용하면 중간 연산를 실행하지 않는 장점이 있기 때문에, 원소의 수가 많을때 사용하면 속도나 메모리면에서 훨씬 좋은 성능을 만들수 있습니다.

 

5.3.2 시퀀스 만들기

fun main(args: Array) { 
	val naturalNumbers = generateSequence(0) { it + 1 } 
    val numbersTo100 = naturalNumbers.takeWhile { it <= 100 } 
    println(numbersTo100.sum())
}

generateSequence()를 이용하면 sequence를 생성할 수 있습니다.

이 함수는 이전값을 토대로 다음값을 만듭니다.

(java에도 range(), rangeClosed() 라는 stream 생성함수가 있습니다.)

 

변수 naturalNumbers와 numbersTo!00은 모두 sequence 입니다.

따라서 최종 연산인 sum()을 호출하기 전까지는 수행되지 않습니다.

 

과연 이런 sequence 함수는 어디다 쓸까요?

아래 코드는 특정 파일이 hidden 폴더 안에 있는지를 확인하는 예제 입니다.

fun File.isInsideHiddenDirectory() = 
	generateSequence(this) { it.parentFile }.any { it.isHidden } 
    
fun main(args: Array) { 
	val file = File("/Users/svtk/.HiddenDir/a.txt") 
    println(file.isInsideHiddenDirectory())
}

여기서 any를 find로 바꾸면 원하는 폴더를 반환하도록 할 수 도 있습니다.

반응형

댓글