[Kotlin] 5-1. 람다 프로그래밍

hansol_kim·2021년 9월 24일
0

kotlin 적응기

목록 보기
4/5
post-thumbnail
* 람다 식과 멤버 참조
* 함수형 스타일로 컬렉션 다루기
* 시퀀스: 지연 컬렉션 연산
* 자바 함수형 인터페이스를 코틀린에서 사용
* 수신 객체 지정 람다 사용

이벤트 발생시 핸들러 실행, 컬렉션에서 모든 원소에 특정 연산을 적용하자 와 같은 생각을 코드로 표현하기 위해 무명 내부 클래스를 사용하여 해결해왔다. 하지만 상당히 번거롭다.

이와 달리 람다 프로그래밍에서는 함수를 값처럼 다루는 접근 방법을 택함으로 이 문제를 해결한다.

button.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View view) {/* 클릭시 수행할 동작 */}	
});

위 코드를 보면 무명 내부클래스로 리스너의 동작을 구현하느라 코드가 번잡해졌다. 이를 람다식으로 바꿔보자.

button.setOnClickListener { /* 클릭 시 수행할 동작 */ }

람다와 컬렉션

컬렉션을 다룰 때 수행하는 대부분의 작업은 몇 가지 일반적인 패턴에 속한다. 해당 패턴은 컬렉션 라이브러리에서 제공해야 하지만 람다가 없다면 해당 패턴을 제공하기 어렵다. 코틀린에서는 람다를 이용하여 편리한 패턴을 제공한다. (물론 현재 자바 8에서 제공해주긴 한다)

람다를 사용해본 적이 없는 개발자에게 사람 리스트에서 가장 나이가 많은 사람을 검색하는 미션이 주어졌다.

fun findTheOldest(people: List<Person>) {
	var maxAge = 0
	var theOldest: Person? = null
	for (person in people) {
		if (person.age > maxAge) {
			maxAge = person.age
			theOldest = person
		}
	}
	println(theOldest)
}

순식간에 위와 같은 코드를 짜서 미션을 수행했다. 하지만, 코드가 길고 자칫하면 실수를 저지를 여지가 충분하다. 코틀린에서는 더 좋은 방법이 있다. 라이브러리 함수를 사용해보자.

println(people.maxBy { it.age })

대단하다! 중괄호에 둘ㄹ러싸인 코드 { it.age }maxBy함수에서 최댓값을 뽑는 기준이 무엇인지 인자를 가리킨다.

people.maxBy(Person::age)

이 코드도 위와 같은 역할을 한다.

컬렉션 함수형 API

필수적인 함수: filter와 map

filter함수는 컬렉션을 이터레이션하면서 주어진 람다에 각 원소를 넘겨서 람다가 true를 반환하는 원소만 모으는 함수이다.

val list = listOf(1, 2, 3, 4)
println(list.filter { it % 2 == 0 }
[2, 4]

위와 같이 filter함수는 컬렉션에서 조건에 맞지 않는 원소를 제거한다. 하지만 filter는 원소를 변환할 수는 없다. 이때 사용하는 것이 map함수이다. map함수는 주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아서 새 컬렉션을 만든다.

val list = listOf(1, 2, 3, 4)
println(list.map { it * it }
[1, 4, 9, 16]

User(이름과 나이를 포함)객체 리스트에서 User를 출력하는 것이 아닌 이름을 출력하고 싶다면 map으로 이름의 리스트로 변환하면 된다.

val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.map { it.name })
[Alice, Bob]

연쇄 호출하여 편리하게 코드를 작성할 수 있다. 예시로 30살 이상인 사람의 이름을 출력해보자.

people.filter { it.age >= 30 }.map(it.name)

filter와 map은 맵에서도 사용가능하다. → filterKeys, mapKeys, filterValues, mapValues

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

컬렉션의 모든 원소가 어떤 조건을 만족하는지 판단하는 연산이 있다. 바로 allany이다.

  • all : 모든 원소가 조건을 만족하는지 확인할 때
  • any : 조건을 만족하는 원소가 단 하나라도 있을 때
val people = listOf(Person("Alice", 29), Person("Bob", 31))

println(people.all { it.age > 30 }) -> 모든 원소가 만족 X 이므로 false
println(people.any { it.age > 30 }) -> 만족하는 원소가 존재하므로 true

조건을 만족하는 원소를 하나 찾고 싶으면 find 함수를 사용한다. find 함수는 조건을 만족하는 첫 번째 원소를 반환한다.(firstOrNull과 같은 역할을 하는 함수다.) 참고로 조건을 만족하는 마지막 원소를 반환하는 함수는 findLast이다.

조건을 만족하는 원소의 개수를 구하려면 count함수를 사용한다. (참고로 size를 사용하지 마라 - 중간 컬렉션이 한번 더 생기므로 성능에 좋지 않다.)

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

컬렉션을 여러 그룹으로 구분 지을 "키"를 선정하여 키 값에 따른 각 그룹이 "값"이 되도록 map을 만든다.

val people = listOf(
    Person("Alice", 28),
    Person("Bob", 31),
    Person("Brown", 32),
    Person("Kotlin", 28)
)
var map = people.groupBy { it.age }

위와 같이 groupBy로 묶은 경우 it.age가 키값이 되고 해당하는 값이 Person 객체가 된다. 그리고 값은 여러 개 있을 수 있으므로 기본적으로 List<T> 타입이 된다. 즉 여기서는 List<Person>이다.

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

flatMap은 먼저 인자로 주어진 람다를 컬렉션의 모든 객체에 적용하고 그 결과로 얻어지는 여러 리스트들을 한 리스트로 한 데 모은다.

flatMap을 이용하여 각 리스트에 있는 원소들을 평평하게 만들어 하나의 리스트로 만들 수 있다.

val corpList = listOf(
    Corp("Samsung", listOf("삼성전자", "삼성SDS", "삼성SDI", "삼성물산", "삼성화재")),
    Corp("LINE", listOf("라인플러스", "라인비즈플러스", "라인파이낸셜플러스"))
)

println(corpList.flatMap { it.sonCorpList })

// result
[삼성전자, 삼성SDS, 삼성SDI, 삼성물산, 삼성화재, 라인플러스, 라인비즈플러스, 라인파이낸셜플러스]

각 리스트에서 it.sonCorpList라는 리스트의 원소들을 하나의 리스트로 평평하게 만들었다.

flatten은 중첩된 리스트의 원소를 한 리스트로 모으고 싶을 때 사용한다. ???

flatMap하고 무슨차이지? 라고 반문할 수도 있겠다. 나 또한 역시 책에서 해당 글을 보고 차이를 이해하지 못했다.

flatten은 람다식이 없는 경우 사용한다. 즉, 위의 코드에서 적용하지 못한다. 이것이 가장 큰 차이일 수 있겠다. 위의 코드에서는 it.sonCorpList라는 람다식이 필요하다.

하지만, 람다식이 필요없을 때도 있다. 이 때 flatten을 사용하게 되는데 아래의 경우를 보자.

val deepArray = arrayOf(
    arrayOf(1),
    arrayOf(2, 3),
    arrayOf(4, 5, 6)
)

println(deepArray.flatten()) // [1, 2, 3, 4, 5, 6]

이 경우에는 순수한 리스트 of 리스트 형태이다 이때는 굳이 람다식이 필요가 없기 때문에 (flatMap으로도 가능하지만 굳이 그럴 필요없이)flatten함수로 평평하게 만든다.

profile
1주 1글 실천하자

0개의 댓글