Swift - 모나드


Swift의 함수형 프로그래밍 패러다임에서 중용한 모나드란 무엇인가?


함수형 프로그래밍이라는 것이 단순히 고차함수를 이용하는거나, 함수를 일급객체로 사용한다든가, 순환 함수를 사용한 로직을 구현하는 등에 국한 되는 것은 아니지만 모나드의 개념을 익혀두면 나중에 더 깊이 있게 함수형 프로그래밍을 이해할 수 있을 것이다. 스위프트에는 함수형 프로그래밍 패러다임에서 파생된 기능이나 개념이 종종 등장하는데, 이 개념을 이해하지 못하면 스위프트 기능의 절반 정도는 제대로 사용하지 못한다 할 수 있을 정도로 함수형 프로그래밍 패러다임은 스위프트에서 중요하다.

모나드라는 용어는 수학의 범주론에서 시작한다. 함수형 프로그래밍에서의 모나드는 순서가 있는 연산을 처리할 때 자주 활용하는 디자잉ㄴ 패턴이기도 하다. 프로그래밍에서 사용하는 모나드는 범주론의 모나드의 의미를 완벽히 구현하려고 하지 않기 때문에 범주론의 모나드 개념을 차용한 정도의 의미를 갖는다. 그래서 모나드의 성질을 완벽히 갖추지는 않았지만 대부분의 성질을 갖췄다고 해서 프로그래밍에서의 모나드를 Monadic이라고 표현한다. 혹은 모나드의 성질을 갖는 타입이나 함수를 모나딕 타입/모나딕 함수 등으로 표현한다.

프로그래밍에서 모나드가 갖춰야 하는 조건은 아래와 같다.

  • 타입을 인자로 받는 타입(특정 타입의 값을 포장)
  • 특정 타입의 값을 포장한 것을 반환하는 함수(메서드)가 존재
  • 포장된 값을 변환하여 같은 형태로 포장하는 함수(메서드)가 존재

즉 쉽게 설명하면 선물상자라고 생각하면 될 것 같다. 선물 상자는 선물을 포장하는 ‘상자’가 있고, 안에는 내용물(선물)을 담을 수 있다.

즉 타입을 인자롤 받는 타입 == 선물을 담을 수 있다(선물로는 장난감이 와도 되고, 게임기가 와도 되고 등등 어떤 타입의 선물이든 상자에 담을 수 있다.), 특정 타입의 값을 포장한 것을 반환하는 함수가 존재 == 선물을 포장하는 선물 상자를 반환하는 함수가 존재, 포장된 값을 변환하여 같은 형택 포장하는 함수가 존재 == 선물을 좀 변환하여 같은 상자로(형태)로 포장하는 함수가 존재

한다 정도롤 이해하면 될 것 같다. 이를 정석적으로 풀자면,

‘타입을 인자로 받는다’는 스위프트에서 제네릭이라는 기능을 통해 구현할 수 있다. 모나드를 이해하는 출발점은 값을 어딘가에 포장하는 개념을 이해하는 것에서 출발한다. 스위프트에서 모나드를 사용한 예 중에 하나가 바로 옵셔널로, 옵셔널은 값이 있을지 없을지 모르는 상태를 포장하는 것이다.

함수객체(Functor)와 모나드는 특정 기능이 아닌 디자인 패턴/ 자료구조라고 할 수 있다. 모나드를 이해하기 앞서 먼저 봐야 하는 개념이 있다.

컨텍스트

Context의 사전적 정의는 ‘맥락’, ‘전후 사정’이다. 컨텍스트는 contents를 담은 무언가를 말한다. 위에서 선물 상자를 예로 들었는데, 선물은 콘텐츠, 선물 상자는 컨텍스트가 되는 것이다.

옵셔널은 열거형으로 구현되엉있기 때문에 열거형 case의 연관 값을 통해 인스턴스 안에 연관 값(선물)을 갖고 있는 형태를 띈다. 옵셔널에 값이 없다면 열거형의 .none case로, 값이 있다면 .some(value) case로 값을 갖게 된다. 옵션ㄹ의 값을 추출한다는 것은 열거형 인스턴스 내부의 .some(value) case에서 value 연관 값을 꺼내는 것이 되겠다.

예를 들어 value를 2로 설정하면 컨텍스트 안에 2라는 컨텐츠가 들어가는 것이다. 만약 값이 없는(nil) 옵셔널 상태라면 컨텍스트는 존재하지만 내부에 값이 없다라고 할 수 있다.

옵셔널은 Wrapped 타입을 인자로 받는 제네릭 타입이다. 즉 모나드의 조건 중 첫 번째 조건을 만족하고, Optional.init(2) 처럼 값을 갖는 상태의 컨텍스트를 생성할 수 있으므로 조건 중 두번째를 만족한다. 세 번째 조건은 아래에서 알아보도록 하겠다.

아래 코드는 Int 타입의 값을 받아 3을 더해 반환하는 함수다.

func addThree(_ num: Int) -> Int {
  return num + 3
}

여기에 옵셔널 값을 전달하면 에러가 난다. 왜냐하면 옵셔널은 옵셔널이라는 컨텍스트로 순수한 값을 포장해 전달하는 것이기 때문이다. 선물상자로 다시 예를 들자면 위의 addThree 함수는 파라미터로 선물을 받아 선물에 특정 작업을 처리해서(장난감을 전달받아 장난감에 페인트 칠을 해주거나) 반환하는데, 옵셔널을 전달하면 선물이 들은(물론 안 들었을 수도 있다) 선물 상자를 전달한 것과 마찬가지다.

함수 객체

map 함수는 컨테이너(컨테이너는 다른 타입의 값을 담을 수 있으므로 컨텍스트의 역할을 수행할 수 있다)의 값을 변형할 수 있는 고차함수다. 그리고 옵셔널은 컨테이너와 값을 갖기 때문에 맵 함수를 사용할 수 있다.

Optional(2).map(addThree) //Optional(5)

따라서 맵 함수에 함수를 전달하지 않고 클로저를 전달해서 사용할 수도 있다.

var value: Int? = 2
value.map{ $0 + 3 } // Optional(5)
value = nil
value.map{ $0 + 3 } // nil(== Optional<Int>.none)

위 동작은 선물상자를 전달했을 때 내부의 선물에 가공처리를 해서 선물을 담은 선물 상자를 다시 반환 받은 모습으로 볼 수 있다. 위에서 보듯이, 함수 객체(functor)란 맵을 적용할 수 있는 컨테이너 타입이라고 할 수 있다.

모나드

함수 객체 중에서 자신의 컨텍스트와 같은 컨텍스트의 형태로 맵핑할 수 있는 함수객체를 닫힌 함수객체(Endofunctor)라고 한다. 모나드는 닫힌 함수객체다. 즉 옵셔널로 포장된 값을 다시 옵셔널로 포장해서 맵핑할 수 있고, 선물 상자라면 A 상자로 포장된 값을 A상자로 다시 포장해서 맵핑하고 B 상자로 포장된 값을 B 상자로 다시 포장해서 맵핑할 수 있는 객체를 말한다. 이런 맵핑을 ㅅ행하도록 플랫맵(flatMap)이라는 메서드를 활용한다.

플랫맵은 맵과 같이 함수를 매개변수로 받고, 옵셔널은 모나드이므로 플랫맵을 사용할 수 있다. 플랫맵도 맵과 비슷하게 먼저 컨텍스트로부터 값을 추출하고, 해당 값을 내부의 함수로 전달한 다음, 값에 따라서 컨텍스트를 반환한다.

플랫맵과 맵이 비슷해보이지만, 플랫맵은 맵과 다르게 컨텍스트 내부의 컨텍스트를 모두 같은 위상으로 평평하게 펼쳐준다는 차이가 있다. 즉 포장된 값 내부의 포장된 값의 포장을 풀어서 같은 위상으로 펼쳐준다는 것이다.

선물 상자 안에 선물 상자가 또 있는데 이를 이중으로 포장을 풀어 선물을 꺼낸다는 의미가 된다.

출처

  • Swift Programming - 야곰님 지음