[Swift] Functor and Monad in Swift
Swift의 Monad를 소개하기 위해 작성된 포스팅으로 Monad를 단독으로 다루는 것보다 Functor와 함께 이야기하는 것이 이해에 도움이 된다고 생각하여 Functor와 Monad를 이해하기 위한 몇몇 개념과 함께 이들을 알아보도록 하겠습니다.
Context
컨텍스트(Context)와 컨텐트(Content)의 관계는 다음과 같습니다.
컨텍스트는 컨텐트를 담고있는 형태로 옵셔널(Optional)을 예로 들면 Optional(2)
에서 Optional
은 컨텍스트가 2
는 컨텐트가 됩니다. 만일 옵셔널안에 값이 존재하지 않는다면 컨텍스트만 존재하는 꼴이 되겠죠.
이렇게 컨텍스트는 무언가를 담고있는 것을 의미합니다. 이렇게 무엇을 담고있다는 의미에서 컨테이너(Container) 역시 컨텍스트의 일종으로 볼 수 있습니다. 그럼 그 컨테이너를 살펴보도록 하겠습니다.
Container
우리가 흔히 사용하는 자료구조인 Array
, Set
그리고 Dictionary
와 같은 자료구조들도 일종의 컨테이너라고 할 수 있습니다. 이들은 한 개 이상의 원소를 가질 수 있는 컨테이너들로 쉽게 컨테이너라는 개념이 연상될 것입니다. 하지만 이들만이 컨테이너라고 할 수 있을까요?
위에서 언급했던 옵셔널도 컨텍스트의 일종이라고 하였습니다. 그렇다면 옵셔널은 컨테이너이기도 할까요? 그렇습니다. 옵셔널도 컨테이너의 한 종류로 그 컨테이너 안에는 값이 존재할 수도 존재하지 않을 수도 있습니다. 즉 두 가지의 경우를 담고 있는 컨테이너라고 이해할 수 있습니다.
Map
우리는 컨테이너에 매핑(Mapping)이라는 연산을 수행할 수 있습니다. 매핑을 굳이 직역하자면 사상이라 할 수 있습니다.
컨테이너의 원소 값 각각에 특정한 변형을 적용하는 것을 변형을 사상한다고 표현할 수 있습니다.
다음은 이러한 매핑 연산의 예시입니다.
[1, 2, 3, 4] --- [2,4,6,8]
["1", "2", "3"] --- [1, 2, 3]
["Harry", "Joe", "David"] --- ["My name is Harry", "My name is Joe", "My name is David"]
이런 매핑은 내부 원소의 값의 변형만 일어납니다. 그 원소를 담고있는 컨테이너의 변형은 일어나지 않습니다. 그말인즉슨 Array
에 매핑 연산을 진행하였다고 Array
가 Dictionary
가 되는 컨테이너의 변형은 일어나지 않는다는 것입니다.
이렇게 값에 변형을 매핑할 수 있는 모든 것들을 Functor라고 합니다. 그렇다면 Functor는 컨테이너와 같은 의미일까요? 이에 대해서는 같다는 것이 지배적인 의견인 것 같으나 완전히 같지는 않다는 의견도 존재합니다.
Functor
Functor는 위에서 언급했듯이 변형을 사상할 수 있는 모든 것들을 말합니다. 이렇게 Functor에서 매핑 연산이 일어나는 과정을 다이어그램으로 표현하자면 다음과 같습니다.
여기서 주목해야 할 점은 매핑으로 변형된 값은 다시 Functor로 감싼 후 반환된다는 것입니다. 그리고 Swift에서 매핑의 대표적인 메소드가 바로 map
입니다.
그리고 저는 Functor로써의 옵셔널에 주목해보려 합니다. 옵셔널 역시 내장 메소드로 map
을 갖고 있습니다. 옵셔널에서 map
메소드를 지원하지 않는다고 가정해보도록 하겠습니다. 그리고 서버로부터 데이터를 받아와 이를 UILabel
에 출력해주고자 할 때 우리는 다음과 비슷한 코드로 이를 구현할 것입니다.
단순히 이름을 출력해주는 것인데 if-let
이나 guard-let
과 같은 코드 때문에 코드가 길어지게 되죠. 하지만 옵셔널도 Functor로써 map
메소드를 지원하기 때문에 우리는 다음과 같이 코드를 길이를 줄일 수 있습니다.
그럼 대체 map
메소드가 어떻게 구현되어 있기에 이러한 코드가 가능한 것일까요? 이를 이해하기 위해선 옵셔널이 어떻게 동작하는지 알고 있어야 합니다. 옵셔널은 다음과 같이 enum
을 사용하여 구현되어 있습니다.
값이 있는 경우와 없는 경우로 나뉘어 있죠. 옵셔널의 map
메소드도 역시 enum
을 활용하였습니다. 코드로 살펴보겠습니다.
옵셔널의 map
메소드는 Wrapped
타입을 파라미터로 받고 U
타입을 반환하는 함수 transform
을 파라미터로 받습니다. 그리고 map
메소드 자체는 U?
타입을 반환합니다.
그리고 switch
문에서 self
, 즉 map
메소드를 호출한 옵셔널 타입의 값을 검사하므로 map
메소드를 호출한 옵셔널 컨텍스트 안에 값이 존재한다면 해당 값을 컨텍스트로부터 추출하여 파라미터로 받아온 transform
함수에 넘겨 값을 처리합니다. transform
메소드의 반환 결과는 U
타입이지만 옵셔널 map
의 반환 타입이 U?
이므로 컴파일러가 자동으로 U?
로 감싸 반환합니다. 반면에 map
메소드를 호출한 옵셔널 타입의 변수에 값이 존재하지 않는다면 nil
값을 반환하는 것입니다.
그렇기 때문에 위의 예제에서 userName
이 map
메소드를 호출하게 되면 다음과 같은 과정을 거치게 됩니다.
userName
에 값이 존재하는지 switch
문을 통해 검사
- 값이 존재하므로 이를 추출하여 파리미터로 넘어온 함수
greeting(_:)
함수에 파라미터로 추출한 값을 넣는다.
- 함수의 실행 결과로
"안녕하세요 \(name)님"
을 반환하고 map
메소드는 이 결과를 Optional("안녕하세요 \(name)님")
으로 반환
- 결과가
label.text
에 담김. (label.text
의 타입은 String?
)
그럼 위의 과정과 Functor에서 매핑이 진행되는 과정을 다시 한번 살펴보고 다음 코드를 봐주시기 바랍니다.
number
의 타입은 무엇일까요?
.
.
.
.
.
.
Int?
라고 예상하셨나요? 틀렸습니다!
정답은 바로 Int??
로 Optional<Optional<Int>>
입니다. 이러한 과정이 나오게 된 과정을 살펴보기 전 명심해야할 것은 위에서 언급했던 "여기서 주목해야 할 점은 매핑으로 변형된 값은 다시 Functor로 감싼 후 반환된다는 것입니다." 문장입니다.
예제에서 Functor는 옵셔널입니다. 그리고 Int($0)
의 반환 결과 역시 옵셔널 타입입니다. 즉 위의 다이어그램에서 Apply Function의 결과로 옵셔널 타입의 값이 반환되고 그 값을 옵셔널 Functor안에 넣어 반환하기 때문에 최종 결과가 Optional<Optional<Int>>
가 되는 것입니다.
이를 코드로 옵셔널 map
의 코드와 함께 살펴보면 Wrapped
타입은 String
이 됩니다. 그리고 Int(_:)
의 반환 타입은 Optional<Int>
타입이죠. 그리고 코드의 transform
반환 타입이 U
이므로 U == Optional<Int>
관계를 갖는 것입니다. 그리고 map
메소드의 최종 반환 타입이 U?
이기 때문에 U? == Optional<Optional<Int>>
이므로 최종 반환 결과 값의 타입은 Optional<Optional<Int>>
가 되는 것입니다.
그렇다면 모나드(Monad)란 무엇일까요?
Monad
모나드는 Functor의 일종입니다. 모나드를 쉽게 설명하자면 flatMap
연산이 가능한 모든 것들을 우리는 모나드라고 부를 수 있습니다.
모나드를 값이 있을 수도 있고 없을 수도 있는 상태의 컨텍스트를 갖는 Functor의 일종이라고 설명되곤 하는데 이 부분에 대해서는 아직 저 스스로 이해가 조금 더 필요한 것 같습니다.
하지만 단순하게 생각해서 값이 있을 수도 없을 수도 있는 컨텍스트라하면 옵셔널은 단번에 따오릅니다. 맞습니다! 옵셔널도 모나드에 속합니다. 그리고 모나드를 다음과 같이 설명하기도 합니다.
" Monads apply a function that returns a wrapped value to a wrapped value. "
저는 이를 " 모나드는 포장된 값에 포장된 값을 반환하는 함수를 적용시킨다. " 라 이해를 하였습니다.
그렇다면 먼저 옵셔널에서의 flatMap
과 map
의 코드를 함께 살펴보도록 하겠습니다.
map in Optional
flatMap in Optional
둘의 차이점이 보이시나요? 바로 transform
메소드의 반환 타입입니다.
그렇다면 다음의 코드에서 mapResult
와 flatMapResult
의 타입은 어떻게 될까요?
.
.
.
.
.
.
결과는 다음과 같습니다.
Optional<Optional<Int>>
Optional<Int>
mapResult
의 타입이 Optional<Optional<Int>>
인 이유는 위에서 살펴보았습니다. 그렇다면 flatMapResult
의 타입은 왜 Optional<Int>
일까요? map
과 flatMap
의 차이점은 transform
메소드의 반환 타입이 다르다고 말씀드렸습니다.
예제의 Int(_:)
의 반환 타입은 Optional<Int>
입니다.그리고 transform
의 반환 타입은 U?
이기 때문에 U? == Optional<Int>
가 되는 것입니다. 마지막으로 flatMap
의 최종 결과 반환 타입은 U?
이기 때문에 Optional<Int>
가 되는 것입니다. 이렇게 " 모나드는 포장된 값에 포장된 값을 반환하는 함수를 적용시킨다. " 라는 뜻을 코드로써 어떻게 구현되어 있는지를 확인할 수 있었습니다.
한 번 더 예를 들어 다음과 같이 짝수라면 반으로 나누는 메소드와 이를 연속으로 호출한다면 그 모습은 다음과 같을 것입니다.
Swift 4.1부터 Array
과 같이 Sequence한 원소를 갖는 컨테이너에서 flatMap
은 deprecated 되었고 compactMap
으로 대체되었습니다.
옵셔널이 아닌 모나드, 즉 Array
과 같이 여러 원소를 갖는 컨테이너에서 compactMap
은 내부 컨텍스트의 값도 추출할 수 있습니다. 이를 위해 Array
에서 적용되는 compactMap
에 대해 알아보도록 하겠습니다.
결과를 보시면 내부 컨텍스트의 값을 추출한다는 것을 단번에 이해하실 수 있으실 겁니다.
이를 이해하기 위해서는 Array
에서의 compactMap
이 내부적으로 어떤 원리로 작동하는지 이해하고 있어야 합니다.
여러 원소를 갖는 모나드에서 map
은 단순히 클로저를 수행하고 결과를 그대로 반환하지만 위의 코드에서와 같이 compactMap
은 내부 컨텍스트 (예제에선 옵셔널)의 값을 추출하여 담아 반환합니다.
마무리
사실 한 기업의 면접에서 "모나드는 무엇인가요?" 라는 질문을 받았었습니다. 하지만 스위프트를 처음 공부할 때 이해하기 힘들어 덮어두고 넘어갔던 내용이었던지라 식은땀을 한 바가지 흘렸었죠. 물론 면접의 결과는 물 보듯 뻔했습니다.
그래서 "제대로 이해하보자" 라고 생각하고 이렇게 공부를 하고 기록을 해보았습니다. 사실 지금도 100% 이해했다고 하기는 힘들 것 같습니다. 하지만 어렴풋이 감이 생겼고 이를 지속적으로 발전시켜가도록 노력해야겠습니다. 감사합니다.
참고자료
- Swift Functors, Applicatives, and Monads in Pictures
- 모나드(Monad)와 함수 객체
- Functor의 개념과 Swift내의 functor
- Higher-order functions, Functor and Monad in Swift
- Elements of Functional Programming - Youtube
- Monads in Swift