본문 바로가기
프로그래밍 언어/Scala

Scala의 map, flatMap

by 조엘 2023. 11. 20.

안녕하세요 조엘입니다. 🙋🏻‍♂️
 
현재 Scala를 메인 언어로 사용하며 백엔드 개발을 하고 있는데요.
Java와는 또 다른 Scala의 매력을 알아가고 있습니다. 
함수형 프로그래밍으로써 Scala를 다룰 때 정말 자주 등장하는 map, flatMap을 살펴봅시다. 
 
피드백 환영입니다. 댓글 달아주세요 :)
 

List로 입문하기

Scala는 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어예요. 
 
오늘 알아볼 map, flatMap 모두 고차함수 인데요.
고차함수란 함수를 인자로 전달받거나, 함수를 결과로 반환하는 함수를 말해요.
 
map과 flatMap에 입문하기 위해 List를 통해 map과 flatMap이 각각 어떤 역할을 수행하는지 알아봅시다.
(Java8의 Stream과 매우 유사합니다)
 
map()

  • map()은 특정 함수를 파라미터로 받는다. 
  • map()은 파라미터로 받은 함수를 컬렉션의 모든 원소에 적용(apply)시킨다. 
  • map()은 기존의 컬렉션과 같은 타입의 새로운 컬렉션을 반환한다. 
val lists = List(1, 2, 3)
val double = (x: Int) => x * 2
val result = list.map(x => double(x))
println(result) // List(2, 4, 6)

 
flatten()

  • flatten()은 해당 컬렉션을 펼쳐준다. 
  • 다만, flatten()은 같은 타입의 컬렉션만을 펼쳐준다. 
val list = List(List(1, 2), List(3))
println(list.flatten) // List(1, 2, 3)

val invalidList = List(List(1, 2), 3)
println(invalidList.flatten) // No given instance of type Matchable => IterableOnce[B] was found for parameter toIterableOnce of method flatten in trait StrictOptimizedIterableOps

 
flatMap()

  • map()과 flatten() 메서드의 결합으로 볼 수 있음
  • map()을 통해 기존 컬렉션과 같은 타입을 리턴 받아 flatten을 적용
val list = List(List(1, 2), List(3, 4, 5))

val mapList = list.map(x => x.map(_ * 2))
println(mapList) // List(List(2, 4), List(6, 8, 10))

val flattenMapList = mapList.flatten
println(flattenMapList) // List(2, 4, 6, 8, 10)

val flatMapList = list.flatMap(x => x.map(_ * 2))
println(flatMapList) // List(2, 4, 6, 8, 10)

 


Functor & Monad

map과 flatMap은 List와 같은 컬렉션에서만 쓰이는 개념은 아닙니다. 
조금 더 깊게 알기 위해, Functor와 Monad의 개념을 간략히 소개할게요. 
어려운 개념이라 해당 글에서는 map, flatMap을 이해하기 위한 정도만 살펴보고 넘어갈게요. 
 
Functor와 Monad는 특정 타입을 감싼 타입에 대해, 그 감싼 타입을 다루는 고유의 기능을 제공합니다.  
이를 통해 함수형 프로그래밍에서 합성 가능하며, 단일 모듈로 동작하는 코드를 작성하기 용이해집니다.  
 
Functor

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

 
Functor는 A => B 로 변환하는 함수를 받아, F[A]를 F[B] 로 변환해주는 map 함수를 가지고 있어요. 
map() 함수로 F[A]의 내부 A에 직접 접근하지 않고, F[B]로 변경할 도구를 갖게 되는 것이에요. 
 
하지만 여기서 하나 문제가 발생합니다. 
F 역시 타입이다 보니, 반환될 B의 타입이 F[Int] 등의 타입으로 지정될 수 있어요. 
해당 경우 map 함수의 리턴 타입이 F[F[Int]] 가 되겠죠. 
 
이는 함수 합성과 체이닝의 일관성을 해치는 결과를 가져올 수 있어요. 
가령 위에서 살펴본 바와 같이 List를 다루는 여러 연산을 체이닝 할 때,
도중 뜬금없이 List[List[Int]] 타입이 있다면 예상과 다르게 동작할 수 있겠죠.
 
Monad

trait Monad[M[_]] extends Functor[M] {
  def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B]
}

 
조금 더 발전해 봅시다. Monad는 모든 Functor를 포함하는 더 큰 개념이에요. 
Monad에는 Functor에는 존재하지 않았던 기능을 제공하는데요. 
A => M[B]로 변환하는 함수를 받아 M[A]를 M[B]로 변환해주는 flatMap 함수를 가지고 있어요. 
 
Monad는 앞서 Functor에서 마주했던 문제를 해결할 수 있어요. 
flatMap을 통해 기존의 타입과 동일하게 메서드를 체이닝 할 수 있는 것이지요. 
 
개념만으로는 어렵기 때문에, 앞선 List로 입문한 예시와 거의 비슷하게 해당 문제 상황을 만나봅시다. 
 
Scala에서 List는 Monad이자 Functor인데요.
오로지 Functor였다면 발생할 수 있는 체이닝의 문제점을 어떻게 flatMap으로 해결할 수 있는지 봅시다. 

val list = List(1, 2, 3)
val double = (x: Int) => x * 2
val makeAsListSize3 = (x: Int) => List(x, x, x)

val listSum = list.sum
println(listSum)				// 6


val doubleListSum = list.map(double(_)).sum
println(doubleListSum)				// 12

val errorList = list.map(makeAsListSize3(_)) 	// 여기서 List[List[Int]] 타입이 튀어나옴
                    .sum 			// List[List[Int]] 타입에 대한 sum을 할 수 없음 (functor의 문제점)

val makeAsListSize3Sum = list.flatMap(makeAsListSize3(_)).sum
println(makeAsListSize3Sum)			// 18 (flatMap으로 극복)


val addAll = list.map(double(_))		// List(2, 4, 6)
                .map(double(_)) 		// List(4, 8, 12)
                .flatMap(makeAsListSize3(_))	// List(4, 4, 4, 8, 8, 8, 12, 12, 12)
                .map(double(_))			// List(8, 8, 8, 16, 16, 16, 24, 24, 24)
                .sum				// 144

println(addAll)					// 144

 


언제 쓰면 좋은가?

앞선 문단에서 Functor와 Monad는 특정 타입을 감싼 타입에 대해, 감싼 타입을 다루는 고유의 기능을 제공한다고 했는데요.
 
애초에 특정 타입을 감싼다는 것은, 타입을 다룸에 있어 언어 차원에서 제한된 기능만을 제공한다고 볼 수 있어요. 
값을 의도한 바대로 다루기 위해 Java에서도 원시값포장, 일급컬렉션 등의 이름으로 Wrapper 클래스를 만들곤 했는데요. 
Functor/Monad 역시 개발자가 Scala 언어가 의도한 대로 코드를 쓸 수 있도록 힌트를 제공해 주는 것이죠. 
 
Scala는 함수형 프로그래밍 언어를 표방해요.
함수의 합성을 통해 어떻게 수행할 것인지가 아닌, 무엇을 수행할 것인지에 집중할 수 있도록 코드를 작성할 수 있어요.
 
스칼라에서 Monad의 개념을 따르는 타입은 살펴본 List 외에도 Option, Future, Try 등이 있어요. 
 
map과 flatMap은 특히 Future를 활용한 비동기 처리에서 보다 더 깔끔한 코드를 작성할 수 있도록 도와주는데요. 
다음 포스팅에서 Scala의 Future와 함께 map/flatMap의 사용 예시를 더 알아볼게요. 🙌🏻
 
 
긴 포스팅 읽어주셔서 감사합니다!
오늘도 좋은 하루 보내세요 :)
 
 
참고
https://gist.github.com/jooyunghan/e14f426839454063d98454581b204452
https://tech.kakao.com/2016/03/03/monad-programming-with-scala-future/
https://tech.kakao.com/2017/09/02/parallel-programming-and-applicative-in-scala/
https://hamait.tistory.com/606
https://www.geeksforgeeks.org/scala-map-method/
https://knight76.tistory.com/entry/scala-map-flatten-flatmap-%EC%98%88%EC%8B%9C
 

반응형

댓글