요즘 레츠스위프트를 구독하면서 여러가지 소식들을 접하고 있다. 여러 소식 중에 함수형 프로그래밍과 관련된 글을 그저께 읽다가 Functor와 Monad를 다시 접하게 되었다. 이 친구들은 기술 면접 준비하다가 몇번 스치다가 말았던 부분인데, 시간도 많으니! 이번 기회에 공부해보자라는 생각을 하게되었다.
Swift와 관련된 글을 찾고 싶어 구글링을 하니 2가지 좋은 글을 찾을 수 있었다.
Swift Monads, Functors and Applicatives with examples
Swift Functors, Applicatives, and Monads in Pictures
영문이기도 하고 개념이 좀 어려워서 여러번 읽었던 것 같다. 읽으면서 이해한 내용을 바탕으로 간단하게 글을 작성해보고자 한다.
먼저 Functor란 무엇일까? 처음에는 어떤 타입인건지, 어떤 함수를 말하는 건지, 어떤 함수에 쓰이는 파라미터인건지 분간이 안 갔다. 정답부터 말하자면,
map
함수를 구현하는 타입
이다.
여기서 map
은 다들 알고 있는 그 map
이다. 이제 예를 들어보자!
다음과 같은 enum
이 있다고 가정해보자. 참고로 Swift의 Optional
도 이처럼 정의되어있다.
enum Optional<T> {
case None
case Some(T)
}
그 다음 이 친구에게 map
함수를 다음과 같은 형태로 줘보자.
extension Optional {
func map<U>(f: (T) -> U) -> Optional<U> {
switch self {
case .Some(let x): return .Some(f(x))
case .None: return .None
}
}
}
이렇게 되면 Optional<T>
타입은 Functor가 되는 것이다. 간단명료하다! 여기서 짚고 넘어가야할 2가지 사항은 다음과 같다.
f
는 그저 Optional
로 래핑되어있던 내부 값을 변형하는 역할만 한다.f
의 결과를 다시 Optional
로 래핑하여 반환한다.그럼 Monad는 무엇일까? 이 역시 타입이라고 할 수 있다.
flatMap
함수를 구현하는 타입
map
을 사용했던 Functor와 다르게 Monad는 flatMap
을 구현해야 한다. 다음과 같이 말이다.
extension Optional {
func flatMap<U>(f: (T) -> Optional<U>) -> Optional<U> {
switch self {
case .Some(let x): return f(x)
case .None: return .None
}
}
}
이렇게 flatMap
도 구현하면, Optional<T>
타입은 Functor이자 Monad가 되는 것이다. map
구현과 비교해보면 다음과 같다.
f
는 Optional
로 래핑되어있던 내부 값을 변형하고, Optional
로 래핑한다.f
의 결과를 그대로 반환한다.이 차이는 그저 Optional
로 래핑하는 시기에만 차이가 있겠거니 생각할 수 있다. 나도 그랬다. 하지만 어떤 경우에는 완전히 다른 결과를 낼 수도 있다. 이는 뒤에서 설명하겠다.
나도 처음에는 꼭 저렇게 생겨야 하는건지 의구심이 들었다. 그래서 이 둘을 가진 타입들을 확인해봤다. 이는 아래와 같다.
// Optional
func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?
// Result
func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> Result<NewSuccess, Failure>
func flatMap<NewSuccess>(_ transform: (Success) -> Result<NewSuccess, Failure>) -> Result<NewSuccess, Failure>
Swift의 Optional
과 Result
는 대표적인 Functor이자 Monad이다. 이들이 가진 map
과 flatMap
의 정의를 보면 위에서 봤던 커스텀 map
과 flatMap
과 동일하게 생겼다는 것을 확인할 수 있다.
그리고 Array
역시 Functor이자 Monad이다. 하지만 흥미로운 건 정의가 조금 다르게 생겼다는 것이다.
// Array
func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
func flatMap<SegmentOfResult>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Element] where SegmentOfResult : Sequence
map
정의는 납득이 가지만, flatMap
의 경우 우리가 예상했던 것과 다르다. 나는 다음과 같이 되어있을거라고 예상했었다.
func flatMap<T>(_ transform: (Element) throws -> Array<T>) rethrows -> Array<T>
이 부분을 스택오버플로우에 올렸고 다음과 같은 답변을 받을 수 있었다. 처음으로 upvote를 받았다!ㅎㅎ
일단 where SegmentOfResult : Sequence
를 보면 Sequence
를 따르는 모든 타입은 이 flatMap
을 사용할 수 있다. 범용성을 위해 저런 식으로 정의해둔 것이라고 보면 된다.
그리고 사실 SegmentOfResult
는 Array<T>
이다. 또한, 반환되는 값인 [SegmentOfResult.Element]
역시 Array<T>
이다. 왜냐하면, SegmentOfResult
가 Array<T>
이라고 했으니, Array<T>.Element
는 T
이므로 최종적으로 [T]
가 된다. 그래서 Array<T>
가 될 수 있는 것이다. 이런 로직으로 공식문서 상에 있던 정의는 우리가 예상했던 정의와 동일해진다!
자 이제 마지막으로 차이점을 확인해보자. 다음과 같이 초기값이 있고, 래핑되어있는 10을 10 증가시키기 위해 함수 하나를 정의해두었다.
let value = Optional.Some(10)
func process<T>(_ value: T) -> Optional<Int> {
guard let intValue = value as? Int else {
return .None
}
return .Some(intValue + 10)
}
이 함수를 위에서 정의해둔 map
과 flatMap
을 통해 적용해 볼 것이다.
let mappedValue = value.map(f: process(_:))
let flatMappedValue = value.flatMap(f: process(_:))
문제는 여기서 발견된다. 타입을 확인해보면, mappedValue
는 Optional<Optional<Int>>
이고 flatMappedValue
는 Optional<Int>
이다. 완전히 다른 결과를 보여주는 것을 확인할 수 있다. 이유는 process(_:)
자체가 래핑된 값을 반환하는데, map
은 이를 다시 Optional
로 래핑하고, flatMap
은 이를 그대로 반환하기 때문이다.
또한, flatMap
이 왜 flat이라는 이름을 가지게 되었는지 알 수 있다. 중첩된 결과가 아닌 그대로의, 평평한 결과를 반환해줘서 flat이라는 이름을 가지게 된 것이다!
Functor와 Monad는 모두 타입이라는 것이다! 그리고 map
과 flatMap
의 결과는 때에 따라서 완전히 달라질 수 있다는 것을 알아두자!