Swift 정리 - 16. 모나드


모나드 개념에 대해 알아보자.


모나드는 함수형 프로그래밍 패러다임에서 등장하는 용어다. 함수형 프로그래밍에서, monad는 프로그램 요소 (function)들을 조합하고 그들의 리턴 값을 추가적인 연산을 통해 감싸는 구조의 소프트웨어 디자인 패턴이다. 무슨 말인지 이해하기 어려운데, 모나드를 이해하기 위해서는 값을 어딘가에 포장한다는 개념을 이해하는 것에서 출발한다. Swift에서 모나드를 사용한 예 하나는 옵셔널이다. 옵셔널은 값이 있을지, 없을지 모르는 상태를 포장한 것이다.

모나드는 순서가 있는 연산을 처리할 때 사용되는 디자인 패턴으로 부작용을 관리하기 위해 함수형 프로그래밍 언어에서 사용된다.

모나드를 깊게 판다면 끝도 없이 복잡해지고, 모나드를 설명하는 글들도 제각기 모나드가 무엇인지 설명하는 방식이 다 다르다. 이 글에서는 간단히 모나드를 이해하기 위해 필요한 개념들과 모나드에 대한 얕은 이해를 할 수 있는 정도로만 볼 것이다.

우선 모나드를 이해하기 위한 몇 가지 기본 개념들을 보자.

Context

Context는 문맥이다. 프로그래밍에서는 어떤 위치에 값이 있을 수 있는 맥락으로 생각해서, 콘텐츠를 담은 무언가를 뜻한다.

옵셔널을 예로 들면 옵셔널은 열거형으로 구현되어 있어 열거형 case의 연관값을 가지고 있다. 옵셔널에 값이 있다면 .some(value), nil이라면 .none 케이스일 것이다. 숫자 2를 옵셔널로 만들려면 옵셔널 열거형의 .some(value)의 value에 2를 넣어서 값을 감싸야 할 것이다. 즉 컨텍스트 안에 2라는 콘텐츠를 넣은 것이다. nil인 경우를 옵셔널로 만든다면 컨텍스트는 존재하지만, 내부에 값이 없다고 할 수 있다.

image

Functor

Functor(함수객체)는 map을 적용할 수 있는 컨테이너 타입이다. map은 컨테이너의 값을 변형해서 새로운 컨테이너를 리턴해주는 고차함수였다. 옵셔널도 컨테이너와 값을 갖고 있기 때문에 map 함수를 사용할 수 있다.

Optional(2).map{ $0 + 3 } // Optional(5)

위 코드는 map이 컨텍스트 내부 컨텐츠 값인 2를 꺼내, map body의 함수를 수행하고, 이를 담은 새로운 컨테이너(옵셔널)을 리턴했다. map을 적용할 수 있는 Array, Dictionay 등도 함수객체가 된다.

Monad

함수객체 중에서 자신의 컨텍스트와 같은 컨텍스트의 형태로 맵핑할 수 있는 함수객체를 모나드라고 한다. 위에서 본 예시에서 옵셔널은 자신의 컨텍스트(옵셔널)와 같은 컨텍스트(옵셔널)의 형태로 내부 컨텐츠를 맵핑할 수 있는 함수객체임을 확인했다. 따라서 옵셔널도 모나드다.

즉 모나드는 함수객체의 일종이다. 함수객체는 컨테이너 내부의 값에 함수를 적용할 수 있기 때문에(예시: map) 모나드도 컨텍스트 내부의 값에 함수를 적용해 이 값을 컨텍스트에 다시 담아 리턴하는 함수를 적용할 수 있다. 이 매핑의 결과가 함수객체와 같은 컨텍스트를 반환하는 함수객체를 모나드라고 하고, 매핑을 하는데 flatMap이라는 메서드를 활용한다.

flatMap은 map과 같이 함수를 매개변수로 받는다. 옵셔널은 모나드이기 때문에 flatMap을 사용할 수 있다.

func doubleEven(_ num: Int) -> Int? {
  if num % 2 == 0 {
    return num * 2
  }
  
  return nil
}

Optional(3).flatMap(doubleEven) // nil

위 코드에서는 다음의 과정이 이루어진다.

  1. Optional context에서 값을 추출한다(3이 나올 것이다.)
  2. 추출한 값 3을 doubleEven에 전달한다.
  3. 3은 짝수가 아니니 nil이 반환된다.
  4. 이 nil이라는 값이 없는 상태가 context에 담겨 반환된다. 즉 그냥 빈 컨텍스트가 반환된다.

만약 Optional.none.flatMap(doubleEven)을 수행하게 된다면?

  1. Optional context에서 값을 추출한다.(값이 없다!)
  2. flatMap은 아무것도 수행하지 않는다.
  3. 빈 컨텍스트 리턴

map과의 차이는 flatMap은 map과 다르게 context 내부의 context를 모두 같은 위상으로 평평하게 펼쳐준다는 것이다. 즉 박스 A와 박스 B가 있고 B안에 또 다른 박스가 있어도 박스 A 내부의 값과 박스 B안의 박스 안의 값을 같은 위상으로 펼친다는 것이다.

Sequence 타입이 옵셔널 타입의 Element를 포장한 경우 flatMap대신 compactMap이라는 이름으로 사용할 수 있다.

let optionals = [1, 2, nil, 3]

print(optionals.map{ $0 }) // [Optional(1), Optional(2), nil, Optional(3)]
print(optionals.compactMap{ $0 }) //[1, 2, 3]

optionals는 이중 컨테이너 형태를 띄고 있다.

image

map은 array 박스만 푼다. 그리고 내부의 옵셔널 박스들을 다시 배열에 담아 리턴한다. flatMap은 array 박스 내의 옵셔널 박스들도 푼다. flatMap은 내부의 값을 1차원적으로 펼쳐놓는 작업도 하기 때문에, 값을 꺼내 모두 동일한 위상으로 펼쳐놓는다고 한 것이다. 그래서 값들을 일자로 평평하게 펼친다해서 flatMap이라 하는 것이다.

  • 참조
  • https://overcurried.com/3%EB%B6%84%20%EB%AA%A8%EB%82%98%EB%93%9C/