Functor와 Monad

InTaek Cho·2021년 1월 29일
1
post-custom-banner

요즘 레츠스위프트를 구독하면서 여러가지 소식들을 접하고 있다. 여러 소식 중에 함수형 프로그래밍과 관련된 글을 그저께 읽다가 Functor와 Monad를 다시 접하게 되었다. 이 친구들은 기술 면접 준비하다가 몇번 스치다가 말았던 부분인데, 시간도 많으니! 이번 기회에 공부해보자라는 생각을 하게되었다.

Swift와 관련된 글을 찾고 싶어 구글링을 하니 2가지 좋은 글을 찾을 수 있었다.

Swift Monads, Functors and Applicatives with examples

Swift Functors, Applicatives, and Monads in Pictures

영문이기도 하고 개념이 좀 어려워서 여러번 읽었던 것 같다. 읽으면서 이해한 내용을 바탕으로 간단하게 글을 작성해보고자 한다.

Functor

먼저 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가지 사항은 다음과 같다.

  1. f는 그저 Optional로 래핑되어있던 내부 값을 변형하는 역할만 한다.
  2. f의 결과를 다시 Optional로 래핑하여 반환한다.

Monad

그럼 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 구현과 비교해보면 다음과 같다.

  1. fOptional로 래핑되어있던 내부 값을 변형하고, Optional로 래핑한다.
  2. f의 결과를 그대로 반환한다.

이 차이는 그저 Optional로 래핑하는 시기에만 차이가 있겠거니 생각할 수 있다. 나도 그랬다. 하지만 어떤 경우에는 완전히 다른 결과를 낼 수도 있다. 이는 뒤에서 설명하겠다.

근데 왜 map과 flatMap은 저렇게 생겼어?

나도 처음에는 꼭 저렇게 생겨야 하는건지 의구심이 들었다. 그래서 이 둘을 가진 타입들을 확인해봤다. 이는 아래와 같다.

// 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의 OptionalResult는 대표적인 Functor이자 Monad이다. 이들이 가진 mapflatMap의 정의를 보면 위에서 봤던 커스텀 mapflatMap과 동일하게 생겼다는 것을 확인할 수 있다.

그리고 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을 사용할 수 있다. 범용성을 위해 저런 식으로 정의해둔 것이라고 보면 된다.

그리고 사실 SegmentOfResultArray<T>이다. 또한, 반환되는 값인 [SegmentOfResult.Element] 역시 Array<T>이다. 왜냐하면, SegmentOfResultArray<T>이라고 했으니, Array<T>.ElementT이므로 최종적으로 [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)
}

이 함수를 위에서 정의해둔 mapflatMap을 통해 적용해 볼 것이다.

let mappedValue = value.map(f: process(_:))
let flatMappedValue = value.flatMap(f: process(_:))

문제는 여기서 발견된다. 타입을 확인해보면, mappedValueOptional<Optional<Int>>이고 flatMappedValueOptional<Int>이다. 완전히 다른 결과를 보여주는 것을 확인할 수 있다. 이유는 process(_:) 자체가 래핑된 값을 반환하는데, map은 이를 다시 Optional로 래핑하고, flatMap은 이를 그대로 반환하기 때문이다.

또한, flatMap이 왜 flat이라는 이름을 가지게 되었는지 알 수 있다. 중첩된 결과가 아닌 그대로의, 평평한 결과를 반환해줘서 flat이라는 이름을 가지게 된 것이다!

결론

Functor와 Monad는 모두 타입이라는 것이다! 그리고 mapflatMap의 결과는 때에 따라서 완전히 달라질 수 있다는 것을 알아두자!

post-custom-banner

0개의 댓글