모나드는 여러 영역에서 다양한 뜻을 가지기도 하고 한 문장으로 설명하기는 어려운 개념이다. 모나드라는 용어는 수학의 범주론에서부터 시작한다. 함수형 프로그래밍에서의 모나드는 순서가 있는 연산을 처리할 때 자주 활용하는 디자인 패턴이다. 사용하는 곳에 따라 수학의 범주론에서 말하는 모나드인지 특정 디자인 패턴을 따르는 모나드인지가 다르다.
프로그래밍에서 사용하는 모나드는 범주론의 모나드의 의미를 완벽히 구현하려고 하지 않기 때문에 범주론의 모나드 개념을 차용한 정도의 의미를 갖는다. 그래서 모나드의 성질을 완벽히 갖추지 못했지만 대부분의 성질을 갖추었다고 하여 프로그래밍에서의 모나드를 모나딕(Monadic)이라고 표현한다. 혹은 모나드의 성질을 갖는 타입이나 함수를 모나딕 타입 혹은 모나딕 함수 등으로 표현하기도 한다.
프로그래밍에서 모나드가 갖춰야 하는 조건은 다음과 같다.
- 타입을 인자로 받는 타입(특정 타입의 값을 포장)
- 특정 타입의 값을 포장한 것을 반환하는 함수(메서드)가 존재
- 포장된 값을 변환하여 같은 형태로 포장하는 함수(메서드)가 존재
'타입을 인자로 받는다'는 표현은 스위프트에서 제네릭이라는 기능을 통해 구현할 수 있다.
모나드를 이해하는 출발점은 값을 어딘가에 포장하는 개념을 이해하는 것에서 출발한다. 스위프트에서 모나드를 사용한 예 중에 하나가 바로 옵셔널이다. 옵셔널은 값이 있을지 없을지 모르는 상태를 포장하는 것이다.
함수객체(Functor)와 모나드는 특정 기능이 아닌 디자인 패턴 혹은 자료구조라고 할 수 있다. 모나드를 이해하기에 앞서 이해해야 할 몇 가지 개념이 있다. 옵셔널을 하나하나 파헤쳐 보면서 순서에 따라 조금씩 알아보자.
16.1 컨텍스트
이번 파트에서의 컨텍스트는 '콘텐츠를 담은 그 무엇인가'를 뜻한다. 즉, 물컵에 물이 담겨있으면 물은 콘텐츠고 물컵은 컨텍스트라고 볼 수 있다.
옵셔널은 열거형으로 구현되어 있어서 열거형 case의 연관 값(4.5.3절)을 통해 인스턴스 안에 연관 값을 갖는 형태이다. 옵셔널에 값이 없다면 열거형의 .none case로, 값이 있다면 열거형의 .some(value) case로 값을 지니게 된다. 옵셔널의 값을 추출한다는 것은 열거형 인스턴스 내부의 .some(value) case의 연관 값을 꺼내오는 것과 같다.
2라는 숫자를 옵셔널로 둘러싸면, 컨텍스트 안에 2라는 콘텐츠가 들어가는 모양새다. 그리고 '컨텍스트는 2라는 값을 가지고 있다'고 말할 수 있다. 만약 값이 없는 옵셔널 상태라면 '컨텍스트는 존재하지만 내부에 값이 없다'고 할 수 있다. 이처럼 값(콘텐츠)과 컨텍스트의 관계를 이해하는 것이 이 장의 출발점이다.
Optional은 Wrapped 타입을 인자로 받는 (제네릭)타입이다. 즉, 앞서 살펴본 모나드의 조건 중 첫 번째 조건을 만족하는 타입이다. 그리고 Optional 타입은 Optional<Int>.init(2) 처럼 다른 타입(Int)의 값을 갖는 상태의 컨텍스트를 생성할 수 있으므로 모나드의 조건 중 두 번째 조건을 만족한다. 그렇다면 세 번째 조건은? 이제 알아보자.
[코드 16-1]에 Int 타입의 값을 전달받아 3을 더하여 반환하는 함수를 선언하였다.
/*
코드 16-1 addThree 함수
*/
func addThree(_ num: Int) -> Int {
return num + 3
}
[코드 16-1]의 addThree(_:) 함수의 전달인자로 컨텍스트에 들어있지 않은 순수 값인 2를 전달하면 정상적으로 함수를 실행할 수 있다. addThree(_:) 함수는 매개변수로 일반 Int 타입의 값을 받기 때문이다.
그러나 [코드 16-3]처럼 옵셔널을 전달인자로 사용하려고 한다면 오류가 발생한다. 순수한 값이 아닌 옵셔널이라는 컨텍스트로 둘러싸여 전달되었기 때문이다.
/*
코드 16-3. 옵셔널은 연산할 수 없는 addThree 함수
*/
addThree(Optional(2))
16.2 함수객체
우리는 앞서 맵(15.1절)에 대해 알아봤다. 맵은 컨테이너(컨테이너는 다른 타입의 값을 담을 수 있으므로 컨텍스트의 역할을 수행할 수 있다)의 값을 변형시킬 수 있는 고차함수다. 그리고 옵셔널은 컨테이너와 값을 갖기 때문에 맵 함수를 사용할 수 있다. [코드 16-4]처럼 맵을 사용하면 컨테이너 안의 값을 처리할 수 있다.
/*
코드 16-4. 맵 메서드를 사용하여 옵셔널을 연산할 수 있는 addThree(_:) 함수
*/
Optional(2).map(addThree) // Optional(5)
따라서 [코드 16-5]처럼 따로 함수가 없어도 클로저를 사용할 수도 있다.
/*
코드 16-5. 옵셔널에 맵 메서드와 클로저의 사용
*/
var value: Int? = 2
value.map { $0 + 3 } // Optional(5)
value = nil
value.map { $0 + 3 } // nil(=Optional<Int>.none)
맵을 언급한 이유는 '함수객체란 맵을 적용할 수 있는 컨테이너 타입'이라고 말할 수 있기 때문이다. 우리가 앞서 맵을 사용해보았던 Array, Dictionary, Set 등등 스위프트의 많은 컬렉션 타입이 함수객체이다.
옵셔널의 맵 메서드는 다음과 같이 표현할 수 있다.
/*
코드 16-6. 옵셔널의 map 메서드 구현
*/
extension Optional {
func map<U>(f: (wrapped) -> U) -> U? {
switch self {
case .some(let x): return f(x)
case .none: return .none
}
}
}
옵셔널의 map(_:) 메서드를 호출하면 옵셔널 스스로 값이 있는 지없는지 switch 구문으로 판단한다. 값이 있다면 전달받은 함수에 자신의 값을 적용한 결괏값을 다시 컨텍스트에 넣어 반환하고, 그렇지 않다면 함수를 실행하지 않고 빈 컨텍스트를 반환한다.
16.3 모나드
함수객체 중에서 자신의 컨텍스트와 같은 컨텍스트의 형태로 맵핑할 수 있는 함수객체를 닫힌 함수객체(Endofunctor)라고 한다. 모나드는 닫힌 함수객체이다.
함수객체는 포장된 값에 함수를 적용할 수 있었다. 그래서 모나드도 컨텍스트에 포장된 값을 처리하여 포장된 값을 컨텍스트에 다시 반환하는 함수(맵)를 적용할 수 있다. 이 매핑의 결과가 함수객체와 같은 컨텍스트를 반환하는 함수객체를 모나드라고 할 수 있으며, 이런 맵핑을 수행하도록 플랫맵(flatMap)이라는 메서드를 활용한다.
플랫맵은 맵과 같이 함수를 매개변수로 받고, 옵셔널은 모나드이므로 플랫맵을 사용할 수 있다.
/*
코드 16-7. doubledEven(_:) 함수와 플랫맵의 사용
*/
func doubledEven(_ num: Int) -> Int? {
if num.isMultiple(of: 2) {
return num * 2
}
return nil
}
Optional(3).flatMap(doubledEven)
// nil ( ==Optional<Int>.none)
맵과 플랫맵의 차이는 무엇일까? 플랫맵은 맵과 다르게 컨텍스트 내부의 컨텍스트를 모두 같은 위상으로 평평(flat)하게 펼쳐준다는 차이가 있다. 즉, 포장된 값 내부의 포장된 값의 포장을 풀어서 같은 위상으로 펼쳐준다는 뜻이다.
[코드 16-7]에서 Optional 타입에 사용하였던 flatMap(_:) 메서드를 Sequence 타입이 Optional 타입의 Element를 포장한 경우에 compactMap(_:) 이라는 이름으로 사용한다. 이 경우를 제외한 다른 경우에는 그대로 flatMap(_:)이라는 이름을 사용한다. compactMap의 사용 방법은 flatMap과 같다.
/*
코드 16-8. 맵과 컴팩트의 차이
*/
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]
맵 메서드를 사용한 결과는 Array 컨테이너 내부의 값 타입이나 형태가 어찌 되었든, Array 내부에 값이 있으면 그 값을 그저 클로저의 코드에서만 실행하고 결과를 다시 Array 컨테이너에 담기만 한다. 그러나 플랫맵을 통해 클로저를 실행하면 알아서 내부 컨테이너까지 값을 추출한다. 그렇기 때문에 mapped는 다시 [Int?] 타입이 되며, compactMapped는 [Int] 타입이 된다.
/*
코드 16-9. 중첩된 컨테이너에서 맵과 플랫맵(콤팩트맵)의 차이
*/
let multipleContainer = [[1, 2, Optional.none], [3, Optional.none]
, [4, 5, Optional.none]]
let mappedMultipleContainer = multipleContainer.map{ $0.map{ $0 } }
let flatmappedMultipleContainer = multipleContainer.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]
플랫맵은 내부의 값을 1차원적으로 펼쳐놓는 작업도 하기 때문에, 값을 꺼내어 모두 동일한 위상으로 펼쳐놓는 모양새를 갖출 수 있다. 그래서 값을 일자로 평평하게 펼친다고해서 플랫맵으로 불린다.
스위프트에서 옵셔널에 관련된 여러 컨테이너의 값을 연달아 처리할 때, 바인딩을 통해 체인 형식으로 사용할 수 있기에 맵포다는 플랫맵이 더욱 유용하게 쓰일 수 있다.
Int 타입을 String 타입으로, 그리고 String 타입을 Int 타입으로 변환하는 과정을 체인 형식으로 구현해보자.
/*
코드 16-10. 플랫맵의 활용
*/
func stringToInteger(_ string: String) -> Int? {
return Int(string)
}
func integerToString(_ integer: Int) -> String? {
return "\(integer)"
}
var optionalString: String? = "2"
let flattenResult = optionalString.flatMap(stringToInteger)
.flatMap(integerToString)
.flatMap(stringToInteger)
print(flattenResult) // Optional(2)
let mappedResult = optionalString.map(stringToInteger) // 더 이상 체인 연결 불가
print(mappedResult) // Optional(Optional(2))
플랫맵을 사용하여 체인을 연결했을 때 결과는 옵셔널 타입이다. 그러나 맵을 사용하여 체인을 연결하면 옵셔널의 옵셔널 형태로 반환된다. 그 이유는 플랫맵은 함수의 결괏값에 값이 있다면 추출해서 평평하게 만드는 과정을 내포하고, 맵은 그렇지 않기 때문이다. 즉, 플랫맵은 항상 같은 컨텍스트를 유지할 수 있으므로 이같은 연쇄 연산도 가능한 것이다.
플랫맵은 체이닝 중간에, 연산에 실패하는 경우나 값이 없어지는 경우(.none이 된다거나 nil이 된다는 등)에는 별도의 예외 처리없이 빈 컨테이너를 반환한다.
/*
코드 16-13. 플랫맵 체이닝 중 빈 컨텍스트를 만났을 때의 결과
*/
func integerToNil(param: Int) -> String? {
return nil
}
var optionalString: String? = "2"
var result: Int? = optionalString.flatMap(stringToInteger)
.flatMap(integerToNil)
.flatMap(stringToInteger)
print(result) // nil
flatMap(intToNil) 부분에서 nil, 즉 Optiional.none을 반환받기 때문에 이후에 호출되는 메서드는 무시한다. 이는 앞서 우리가 알아본 옵셔널 체이닝과 완전히 같은 동작이다. 바로 옵셔널이 모나드이기 때문에 가능한 것이다.
스위프트의 기본 모나드 타입이 아니더라도 플랫맵 모양의 모나드 연산자를 구현하면 사용자 정의 타입(흔히 클래스 또는 구조체 등)도 모나드로 사용할 수 있다.
'iOS > SWIFT' 카테고리의 다른 글
[Swift] Chapter 18. 상속 (0) | 2021.10.10 |
---|---|
[Swift] Chapter 17. 서브스크립트 (0) | 2021.10.10 |
[Swift] Chapter 15. 맵, 필터, 리듀스 (0) | 2021.10.10 |
[Swift] Chapter 14. 옵셔널 체이닝과 빠른 종료 (0) | 2021.10.09 |
[Swift] Chapter 13. 클로저 (0) | 2021.10.09 |
댓글