Swift의 모나드가 갖춰야 하는 조건은 다음과 같다
- 타입을 인자로 받는 타입(특정 타입의 값을 포장)
- 특정 타입의 값을 포장한 것을 반환하는 함수(메소드)가 존재
- 포장된 값을 변환하여 같은 형태로 포장하는 함수(메소드)가 존재
무슨 말인지 하나도 이해가 안 갈 것이다.
모나드를 이해하는 출발점은 값을 어딘가에 포장하는 개념을 이해하는 것부터 출발한다.
스위프트에서 모나드를 사용한 예시 중 하나가 옵셔널이다.
따라서, 옵셔널을 파헤쳐보며 컨텍스트, 함수객체를 이해한 뒤 모나드를 다시 알아본다.
컨텍스트
컨텍스트(Context)의 사전적 정의는 '맥락', '문맥' 등이다. 이번 파트에서 컨텍스트는 '컨텐츠(Contents)를 담은 무언가'를 의미한다. 즉, 물컵에 물이 담겨있는 상황에서 물은 컨텐츠이고 물컵은 컨텍스트이다.
옵셔널을 되새겨보면, 옵셔널은 열거형으로 구현되어 있기 때문에 case의 연관 값을 통해 인스턴스 안에 연관 값을 갖는 형태이다. 옵셔널에 값이 없다면 열거형의 .none 케이스, 값이 있다면 .some(value) 케이스이다. 옵셔널의 값을 추출한다는 것은 열거형 인스턴스 내부의 .some(value) 케이스의 연관 값을 가져오는 것과 같다.
2라는 숫자를 옵셔널로 둘러싸면, 컨텍스트 안에 2라는 콘텐츠가 들어가있는 형태이다.
만약, 값이 없는 옵셔널 상태라면 '컨텍스트는 존재하지만 내부에 값이 없다' 라고 말할 수 있다.
옵셔널은 Wrapped 타입을 인자로 받는 제네릭 타입이다. 즉, 앞서 살펴본 모나드의 첫 번째 조건을 만족하는 타입이다.
그리고 옵셔널 타입은 Optional<Int>.init(2)와 같이 다른 타입(Int)의 값을 갖는 상태의 컨텍스트를 생성할 수 있으므로 모나드의 조건 중 두 번째 조건을 만족한다. 그럼 세 번째 조건을 만족하는지는 결론에서 알아보자.
※ 제네릭 타입은 C++에서의 템플릿과 유사한 개념이다.
아래는 Int 타입의 값을 전달받아 3을 더한 뒤 반환하는 함수이다.
func addThree(_ num: Int) -> Int {
return num + 3
}
addThree(_:) 함수의 전달인자로 컨텍스트에 들어가있지 않은 순수 값인 2를 전달하면 정상적으로 함수를 실행할 수 있다
하지만 addThree(Optional(2))와 같이 옵셔널을 전달인자로 사용하려면 옵셔널이라는 컨텍스트로 둘러싸여 있기 때문에 오류가 발생한다.
함수 객체
map은 컨테이너의 값을 변형시킬 수 있는 고차함수이다. 그리고 옵셔널은 컨테이너와 값을 갖기 때문에 map 함수를 사용할 수 있다. 따라서, 앞서 살펴 보았던 addThree(_:) 함수를 다음과 같이 사용할 수 있다.
Optional(2).map(addThree)
또한, 따로 함수가 없어도 아래와 같이 클로저를 사용할 수 있다.
var value: Int? = 2
value.map{ $0 + 3 } //Optional(5)
value = nil
value.map{ $0 + 3 } //nil(== Optional<Int>.none)
갑자기 맵을 언급하는 이유는 함수객체란 'map을 적용할 수 있는 컨테이너 타입'이라고 말할 수 있기 때문이다.
맵을 적용할 수 있는 Array, Dictionary, Set 등의 스위프트의 많은 컬렉션 타입이 함수객체이다.
모나드
함수객체 중에서 자신의 컨텍스트와 같은 컨텍스트의 형태로 Mapping할 수 있는 함수객체를 닫힌 함수객체라 하며, 모나드는 닫힌 함수객체이다.
함수객체는 포장된 값에 함수를 적용할 수 있었다. 따라서, 모나드도 Map을 적용할 수 있다. 이 Mapping의 결과가 함수객체와 같은 컨텍스트를 반환하는 함수객체를 모나드라고 할 수 있으며, 이런 맵핑을 수행하도록 플랫맵(flatMap)이라는 메소드를 활용한다.
플랫맵은 맵과 같이 함수를 매개변수로 받는다. 아래의 코드를 살펴보자.
func doubledEven(_ num: Int) -> Int? {
if num.isMultiple(of: 2) {
return num * 2
}
return nil
}
Optional(3).flatMap(doubledEven) // nil(== Optional<Int>.none)
짝수라면 2를 곱해서 반환하고 아니라면 nil을 반환하는 함수이다. Optional(3)의 플랫맵에 이 함수를 전달하면 다음과 같이 동작한다.
- 컨텍스트로부터 값 추출 (3)
- 추출한 값을 doubledEven 함수에 전달 (짝수가 아님)
- 빈 컨텍스트 반환 (nil)
그렇다면 map(_:) 메소드와는 무슨 차이일까?
플랫맵은 맵과 다르게 컨텍스트 내부의 컨텍스트를 모두 같은 위상으로 평평하게 펼쳐준다.
아래의 코드를 살펴보자.
let optionals: [Int?] = [1, 2, nil, 5]
let mapped: [Int?]: optionals.map{ $0 }
let compactMapped: [Int?]: optionals.compactMap { $0 }
print(mapped) // [Optional(1), Optional(2), nil, Optional(5)]
print(compactMapped) // [1, 2, 5]
우선, 컴팩트맵은 플랫맵과 같은 개념이다. Sequence 타입이 Optional타입의 원소를 포장한 경우에만 compactMap(_:) 메소드를 사용한다. 좀 더 분명한 뜻을 나타내기 위해 스위프트 4.1부터 이름이 변경된 것이니 헷갈리지 않아도 된다.
위의 코드에서 optionals 배열은 이중 컨테이너의 형태를 띄고 있다.
Array라는 컨테이너 내부에 Optional이라는 형태의 컨테이너가 여러개 들어있는 형태이다.
이러한 배열에 맵과 플랫맵을 각각 사용해본다면 다른 결과를 볼 수 있다.
맵 메소드를 사용한 경우는 Array 컨테이너 내부의 값 타입이나 형태에 관계없이 값이 있으면 그 값을 클로저 코드로 실행하고 다시 Array 컨테이너에 담는다. 하지만, 플랫맵 메소드를 사용한 경우 클로저를 실행하면 알아서 내부 컨테이너까지 값을 추출한다. 따라서, mapped 배열은 [Int?] 타입이 되고 compactMapped 배열은 [Int] 타입이 된다.
이해를 좀 더 돕기 위해 삼중 컨테이너에 중첩된 맵과 플랫맵을 비교해보자.
let multipleContainer = [[1, 2, Optional.none], [3, Optional.none], [4, 5, Optional.none]
let mappedMultipleContainer = multpleContainer.map{ $0.map{ $0 } }
let flatmappedMultipleContainer = multpleContainer.flatMap { $0.flatMap{ $0.flatMap{ $0 } }
print(mappedMultipleContainer)
//[[Optional(1), Optional(2), nil], [Optional(3), nil], [Optional(4), Optional(5), nil]
print(flatmappedMultipleContainer)
//[1, 2, 3, 4, 5]
굳이 설명하지 않아도 두 결과의 차이와 원인을 알 수 있다.
결론적으로, 모나드는 맨 처음에 말했던 조건들을 만족하는 함수객체의 일종이며,
그 중에서 자신의 컨텍스트와 같은 컨텍스트의 형태로 맵핑할 수 있는 함수객체이다.
혹시, 함수객체(Functor)와 모나드(Monad)의 개념이 혼동된다면 아래의 글을 추천한다.
https://stackoverflow.com/questions/45252709/what-is-the-difference-between-a-functor-and-a-monad
또한, 옵셔널(Optional)과 모나드(Monad)의 관계를 잘 설명한 아래의 글도 추천한다.
https://medium.com/97-things/optional-is-a-law-breaking-monad-but-a-good-type-7667eb821081
'앱 > iOS' 카테고리의 다른 글
Swift - ARC (0) | 2022.01.31 |
---|---|
Swift의 연산자 (0) | 2022.01.13 |
iOS - 객체 제어 (0) | 2021.11.07 |
iOS와 코코아 터치 프레임워크 (0) | 2021.11.06 |
iOS 앱의 구조 (0) | 2021.11.05 |