함수형 프로그래밍을 공부하다보면 모나드(Monad)라는 단어를 자주 접할 수 있었다.
사실 이 모나드라는 개념은 수학의 범주론에서 사용한 개념을 프로그래밍 상에 도입한 것이다.
수학이라는 말을 들으니 벌써부터 머리가 아프다;;
하지만 이번 포스트에서는 프로그래밍 입장에서 과연 모나드라는 것이 무엇이냐에 대하여 다루어 보려고 한다.
프로그래밍에서 모나드는 예를 들면 싱글톤 패턴과 같이 프로그래밍 디자인 패턴 혹은 모나드의 특징을 만족하는 어떤 것이라 할 수 있다.
그렇다면 프로그래밍에서 모나드는 무엇을 말하는 것일까?
모나드를 알아보기 전에 함수형 프로그래밍 패러다임에 대해 간단히 짚고 넘어가려고 한다.
먼저 함수형 프로그래밍 패러다임은 순수 함수 형태를 이용 하는데, 순수 함수란 매개변수로 들어온 전달인자와 함수의 지역 변수 만을 사용하여 외부의 다른 값에 영향을 미치지 않고 함수를 수행한 결과를 반환하는 방식이다.
함수형 프로그래밍은 순수 함수를 베이스로 다음과 같은 특징을 가진다.
함수는 하나의 일급 객체로서..
변수나 상수에 함수를 할당 가능하고 다른 함수의 전달인자로서 매개변수에 전달 가능하다.
다시 모나드로 돌아와서..
모나드를 이해하기 위한 첫 출발점은 바로 Context이다. Context는 무언가를 담을 수 있는 것이라고 할 수 있고, Context 안에 담겨 있는 것을 Contents라고 할 수 있다.
아래 그림을 보면 이해가 될 것이다.
위의 그림에서 상자가 있고 상자 안에 축구공이 담겨있다. 여기서 상자는 무언가를 담고 있는 Context라고 할 수 있고 Context인 상자에 담겨 있는 축구공은 Contents라고 할 수 있다.
그럼 프로그래밍을 할때 Context와 Contents의 개념을 어디서 확인할 수 있을까?
간단한 예시로 Optional
을 예로 들수 있다.
@frozen enum Optional<Wrapped>
Swift에서 Optional
의 정의는 열거형 제네릭 타입으로 값이 존재하면 case .some(value)
case로 값이 nil이라면 case .none
case가 된다.
let number: Int? = 42
let nothing: Int? = nil
그렇다면 위의 Optional<Int>
타입의 number
변수와 nothing
변수는 다음과 같이 표현할 수 있다.
number
변수는 Optional
이라는 Context 안에 42
라는 값이 Contents로 담겨 있고 nothing
변수는 Optional
이라는 Context 안에 아무 것도 없는 형태로 볼 수 있다.
정리하자면 Optional
이라는 형태처럼 어떤 값을 담는 형태가 일종의 모나드라는 것이다.
Container는 무언가를 담을 수 있는 것이라고 할 수 있고 프로그래밍에서는 Array
, Dictionary
, Set
등의 타입들은 특정 타입의 값을 담을 수 있으므로 Container라고 한다.
Container는 무언가를 담는 다는 측면에서 Context의 역할을 할 수 있기 때문에 Context로서 사용할 수 있다.
프로그래밍에서 Functor란 map
함수를 적용할 수 있는 컨테이너 타입이라고 할 수 있다.
map
함수를 적용할 수 있으며 어떤 값을 담을 수 있는 Optional
, Array
, Dictionary
, Set
과 같은 타입들이 모두 Functor에 해당한다. 따라서 앞서 언급한 타입 내부에는 모두 map
함수가 정의 되어 있다.
아래는 Swift의 Optional
타입에 정의 되어 있는 map
의 형태이다.
func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
map
함수는 U
타입을 반환하는 콜백 함수를 전달 받아 각 요소에 적용하고 결과적으로 Optional<U>
타입으로 맵핑한 반환 값을 반환한다.
let number: Int? = 42
let addedThree: Int? = number.map { (number: Int) -> Int in
return number + 3
}
print(addedThree) //Optional(45)
위의 예시는 Optional
Context에 담겨있는 숫자 42
를 map
method를 통해 3
을 더해주는 함수를 적용한 반환값을 다시 Optional
Context에 담아 반환하는 코드이다.
이것을 그림으로 이해하면 아래와 같다.
사실 동일한 Context에 담아서 반환하기만 하면 되기 때문에 내부에 담겨있는 값의 타입은 크게 중요하지 않다.
따라서 내부의 Int
타입의 값을 다른 타입으로 바꾸는 것도 가능하다.
let number: Int? = 42
let StringNumber: String? = number.map { (number: Int) -> String in
return String(number)
}
print(StringNumber) //Optional("42")
반대로 Context 내부에 값이 존재하지 않을 수도 있다.
그럼 map
은 콜백 함수를 실행하지 않고 빈 값을 Context로 포장하여 반환 한다. 다시 말하면 따로 nil
check를 구현하지 않아도 된다는 말과도 같다.
let nothing: Int? = nil
let addedThree = nothing.map { (number: Int) -> Int in
return number + 3
}
print(addedThree) //nil
만약 위의 코드를 기존의 옵셔널 바인딩을 사용하여 구현하면 다음과 같이 구현할 수 있다.
let nothing: Int? = nil
var addedThree: Int? = nil
if let nothing = nothing {
addedThree = nothing + 3
}
print(addedThree) //nil
비록 간단한 코드라 코드가 얼마 되진 않지만 코드의 양의 차이를 떠나서 map
함수를 사용한다면 nothing
변수에 값이 있든 없든 상관하지 않고도 동일한 로직으로 데이터 처리가 가능해진다.
map
함수를 단순히 내부 값에 접근하거나, 내부 요소를 순회하는 방법이라고 이해한다면 아래와 같은 문제에 직면하게 된다. 우리는 아래와 같이 String
을 Int
타입으로 캐스팅하는 상황에 대해 생각해볼 필요가 있다.
let stringNumber: String? = "45"
let result = stringNumber.map{ Int($0) }
Swift에서 Int
의 initializer는 다음과 같이 정의 되어 있다.
init?(_ description: String)
정의를 확인해 보면 초기화 실패 가능성에 대비해 반환 값이 Optional
형태인것을 확인할 수 있다. 그렇다면 위의 result
의 타입은 무엇이 될까?
다시 한번 map
함수의 정의를 살펴보면..
func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
❗️
map
함수는U
타입을 가지는 함수를 전달받아U?
를 반환하는 함수이다.
이미 Int
함수(생성자)의 결과가 Optional
이기 때문에 U
타입 자체가 이미 Optional
타입이며, map
함수의 반환 타입은 U?
타입이기 때문에 다음과 같은 결과가 나타난다.
print(type(of: result)) //Optional<Optional<Int>>
그저 String
타입을 Int
타입으로 바꾸면서 Optional<Int>
타입의 결과를 의도했을 수 있지만, 전혀 다른 결과를 도출해 낸다. 이러한 중첩적인 Context의 형태는 함수의 연속적인 합성을 저해하는 요인이 된다.
map
과 비슷하지만 위에서 언급한 map
의 단점을 보완하기 위한 method인 flatMap
method가 존재한다.
Swift에서 Optional
타입에 정의 되어 있는 flatMap
함수의 형태는 다음과 같다.
func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?
map
함수와의 차이라면, flatMap
은 U?
타입을 가지는 함수를 전달받아 최종적으로 U?
타입을 반환하는 함수이다. 따라서 flatMap
을 사용한다면 위에서 언급한 map
함수의 문제를 해결할 수 있다.
let stringNumber: String? = "45"
let result = stringNumber.flatMap{ Int($0) }
print(result) //Optional(45)
print(type(of: result)) //Optional<Int>
위의 예시에서 flatMap
은 Int?
을 반환하는 함수를 전달받아 결과로 Int?
타입의 값을 반환하고 있다.
추가적으로 Swift의 Sequence 타입에서 Optional
을 반환하는 함수를 flatMap
에 전달할때는 Swift 4.1버전부터 flatMap
대신 compactMap
이라는 함수를 사용한다.
Array
타입에 정의 되어 있는 compactMap
의 정의는 다음과 같다.
@inlinable public func compactMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]
compactMap
은 ElementOfResult?
타입의 값을 하나씩 가져와 [ElementOfResult]
을 반환하는 함수이다.
결국 Array
내부의 Optional
타입을 모두 같은 위상으로 풀어주기 때문에 아래의 예시와 같이 Array
내부에 nil
값이 존재하면 compactMap
의 적용 결과 nil
은 제외된다.
let optionalNumbers = [1, 2, Optional.none, 4, Optional.none, 6]
let numbers = optionalNumbers.compactMap { $0 }
print(numbers) //[1, 2, 4, 6]
정리하자면 모나드는 Functor의 한 종류이며 flatMap
method을 사용할 수 있는 것을 모나드라고 설명할 수 있다는 것이다.
Swift에서 값이 nil
인지 확인 하는 대표적인 방법 중 하나로 Optional Binding 방법이 있다.
아래의 코드는 Optional
타입의 상수에 값이 존재하는지 확인하고 값이 존재 한다면 다시 그 값을 Int
타입으로 타입 캐스팅 가능한지 확인하는 코드이다.
let StringNumber: String? = "45"
var result: Int?
if let string: String = StringNumber {
if let number: Int = Int(string) {
result = number + 3
} else {
result = nil
}
} else {
result = nil
}
print(result) //Optional(48)
모나드는 연속적인 체이닝 형태의 함수의 합성을 가능하게 하는 방향을 제시한다.
flatMap
을 사용할 수 있는 것을 모나드라고 할 수 있기 때문에 위의 코드를 flatMap
과 map
의 연속적인 체이닝 형태로 다시 구현한 형태는 아래와 같다.
let stringNumber: String? = "45"
var result: Int?
result = stringNumber.flatMap { (number: String) -> Int? in
return Int(number)
}.map { (number: Int) -> Int in
return number + 3
}
print(result) //Optional(48)
flatMap
의 경우도 마찬가지로 nil
을 만날 경우 콜백 함수를 실행하지 않고 아무것도 반환하지 않기 때문에 굳이 nil
check를 구현 하지 않아도 연속적인 함수의 합성을 가능하게 한다.
또한 flatMap
을 사용할 경우 더 적은 라인 수로 마무리가 되며, Swift에서 map
과 flatMap
은 타입 추론 등으로 생략이 가능하기 때문에 극단적으로 다음과 같이 한줄로도 마무리 할 수 있다.
let stringNumber: String? = "45"
var result: Int?
result = stringNumber.flatMap { Int($0) }.map { $0 + 3 }
print(result) //Optional(48)
Optional Binding 방법을 사용한 경우와 flatMap
을 사용한 방법을 비교해 보았을때 위의 예시는 간단한 경우 이지만 더 복잡하게 이중 혹은 삼중 Container 형태의 경우 코드 생산성 측면에서 더 극단적인 차이가 발생할 것이라고 생각한다.
모나드가 무엇인지 정리를 해서 작성해 보았지만 결국
모나드가 무엇이냐?
라는 질문을 받았을때 이것이 무엇이다라고 단 한 문장으로 정리하기 참 어려운 개념인거 같다.
다시 정리하자면 필자가 이해한 모나드의 정의는
정도로 정리할 수 있을거 같다.
Reference