본문 바로가기
Swift

모나드에 관하여

by iOS 개린이 2023. 3. 10.

Swift는 함수형 프로그래밍 패러다임에서 파생된 기능이나 개념이 종종 등장하기 때문에 이런 개념들을 익히지 못하면 Swift 를 제대로 다루지 못한다.

단순히 고차함수의 사용, 함수의 일급객체 등의 개념 뿐만 아니라 모나드에 대해서 알고 있으면 함수형 프로그래밍을 좀 더 깊이 이해할 수 있다.

 

모나드

-모나드는 특정 기능이 아닌 디자인 패턴 혹은 자료구조라고 할 수 있다.

-함수형 프로그래밍에서 모나드는 순서가 있는 연산을 처리할 때 자주 활용하는 디자인 패턴이다.

-모나드의 성질을 갖는 타입이나 함수를 모나딕 타입 혹은 모나딕 함수 등으로 표현한다.

 

 

모나드의 조건

-모나드가 갖춰야 하는 조건은 다음과 같다.

1. 타입을 인자로 받는 타입(특정 타입의 값을 포장)

2. 특정 타입의 값을 포장한 것을 반환하는 함수(메서드)가 존재

3. 포장된 값을 변환하여 같은 형태로 포장하는 함수(메서드)가 존재

 

 

 

모나드를 이해하기 위해서 선행으로 이해해야 할 개념이 두 가지 있다.

바로 컨텍스트와 함수 객체인데 하나씩 알아보자.

 

 

컨텍스트

-컨텍스트의 사전적 정의는 "맥락", "전후 사정" 등이다. 

여기서 우리가 말하는 컨텍스트는 "어떤 위치에 값이 존재할 수 있는 맥락" 이라고 볼 수 있다.

예를 들어 컨텍스트는 컨텐츠를 담은 상자와 같다. 즉, 물컵에 물이 담겨있으면 물은 컨텐츠며, 물컵은 컨텍스트라고 할 수 있다.

 

-앞으로의 설명에는 옵셔널이 계속해서 들어가는데, 이유는 Swift에서 모나드를 사용한 것이 옵셔널이기 때문이다.

따라서 먼저 옵셔널의 설명을 해보면

옵셔널은 열거형으로 구현되어 있어서 옵셔널에 값이 없으면 .none case로, 값이 있으면 .some(value) case로 값을 지니게 된다.

그래서 옵셔널 값을 추출해준다는 것은 .some case의 연관값을 꺼내오는 것과 같다.

 

 

다시 본론으로 돌아가서

컨텍스트와 컨텐츠와의 관계를 이해하기 위해서 다음과 같은 사진을 보자.

https://zeddios.tistory.com/449

 

숫자 30(컨텐츠)을 옵셔널로 포장해줄 때, 컨텍스트 안에 숫자 30이 들어가는 것과 같다.

그럼 컨텍스트는 30이라는 값을 가지고 있다고 말할 수 있고, 만약 값이 없는 옵셔널이라면 컨텍스트는 존재하지만, 내부에 값이 없다고 할 수 있다.

이것이 컨텍스트와 컨텐츠의 관계이다.

 

 

모나드의 조건과 옵셔널

-위에서 모나드가 갖춰야 할 조건에는 3가지가 있다고 했는데, 옵셔널은 과연 이 3가지 조건에 모두 적합할까?

 

1) 옵셔널은 wrapped 타입을 인자로 받는 타입(제네릭)이다.

따라서 "타입을 인자로 받는 타입(특정 타입의 값을 포장)" 조건을 만족한다.

 

2) 옵셔널 타입은 Optional<Int>.init(2) 처럼 특정 타입인 Int의 값(2)을 갖는 상태의 컨텍스트를 생성해줄 수 있다.

따라서 "특정 타입의 값을 포장한 것을 반환하는 함수가 존재" 조건을 만족한다.

 

3) 마지막 조건에 적합한지는 예제를 통해 알아보자.

 

addThree 함수는 매개변수로 Int 타입을 받고 있다. 따라서 컨텍스트에 들어가 있지 않은 순수 Int값을 넣어줄 때 정상적으로 실행이 가능하다.

 

여기에 컨텍스트로 포장되어 있는 Optional을 전달인자로 사용한다면?

 

바로 오류가 발생한다.

이유는 옵셔널이라는 컨텍스트로 둘러싸여 있기에 다른 타입이 전달된 것과 마찬가지다.

 

따라서 옵셔널은 모나드의 마지막 조건인 "포장된 값을 변환하여 같은 형태로 포장하는 함수가 존재" 까지 만족한다.

 

 

 

함수 객체(Functor)

-함수객체는 "고차함수인 map을 적용할 수 있는 컨테이너 타입"을 말한다.

그리고 옵셔널은 컨테이터와 값을 가지고 있기 때문에 맵 함수를 적용할 수 있다. == 옵셔널은 함수객체

 

 

위에서 보았듯이 value는 옵셔널 타입으로 순수 Int인 3과는 다른 타입이기 때문에 에러가 발생한다고 했죠?

value는 컨텍스트 안에 들어있고, 컨텍스트 안에 값이 있을지, 없을지 모르는데 3을 더해줄 수는 없기 때문에

이 값을 꺼내주는 작업이 필요하다. 그 작업이 바로 map이다.

 

 

map은 컨테이너 안에 요소를 하나씩 보면서, 값이 존재하는지 확인하고 값이 존재한다면 해당 요소를 꺼내준다.

여기서 해당 요소를 꺼냈다는 것은 값을 꺼내왔다는 것과 동일하기 때문에 3과 연산이 가능해진 것이다.

결괏값으로 옵셔널 5가 나온 이유는 map이 값을 컨텍스트 안에 넣어서 돌려주기 때문이다.

 

함수 객체의 정의는 "맵을 적용할 수 있는 컨테이너 타입"이라고 했다.

따라서 방금 적용한 옵셔널은 함수 객체가 되는 것이다.

또한 옵셔널 외에도 Array, Dictionary, Set 등 Swift의 컬렉션 타입에서 맵을 사용할 수 있는데,

이런 컬렉션 타입도 모두 함수 객체다.

 

 

 

모나드(Monad)

-함수객체의 일종이면서 컨텍스트 개념이 더해진 것이 모나드다.

즉, 값이 있을 수도, 없을 수도 있는 컨텍스트를 가지고, map을 적용할 수 있는 타입이 모나드인 것이다.

 

-함수객체는 포장된 값에 함수를 적용할 수 있었다.

그래서 모나드도 컨텍스트에 포장된 값을 처리하여, 그 값을 컨텍스트에 다시 반환하는 함수를 적용할 수 있는데, 이와 같은 기능을 수행하는 플랫맵이라는 메서드가 있다.

 

 

flatMap(플랫맵)

-flatten(평평하게 하다) + map이 합쳐진 의미로 map 함수의 기능에서 flatten 속성이 더해진 것이라 볼 수 있다. (flat이 포장을 풀어 평평하게 만들어주는 것.)

 

-플랫맵은 포장된 값을 받아서 값이 있으면 포장을 풀어 연산을 한 후에 포장된 값을 반환하고,

값이 없으면 없는대로 다시 포장해서 반환한다.

 

-플랫맵은 3가지 기능이 있다.

1. non-nill인 결과들을 가지는 배열을 리턴

2. 주어진 Sequence 내의 요소들을 하나의 배열로 리턴

3. 주어진 Optional이 not-nil인지 판단 후 unwrapping하여 closure 파라미터로 전달

 

 

맵과 플랫맵의 사용

 

optional 타입의 배열에 각각 map과 flatMap을 사용해주었다.

 

결과는?

 

결과를 보면 플랫맵은 컨테이너 안에 있는 옵셔널 요소들을 모두 포장 해제하고 반환해준 것을 알 수 있다.

플랫이 포장을 풀어 평평하게 해준다고 했는데, 그 뜻이 바로 예제와 같은 것이다.

 

 

compactMap(컴팩트맵)

-플랫맵의 예제에서 flatMap 메서드를 사용하면 다음과 같은 에러를 볼 수 있다.

 

내용은 closure가 옵셔널 값을 반환하는 경우에 flatMap은 deprecated 되었기 때문에 compactMap을 사용하라는 것이다.

실제로 공식문서를 보면 flatMap이 deprecated 되어있고, 같은 기능을 하는 compactMap이 나와있다. (Swift 4.1부터 변경되었음.)

 

따라서 flatMap을 통해 옵셔널 타입의 요소들을 가지고 단순히 not-nil 결괏값들을 얻고 싶으면 compactMap을 사용하고, 이 경우를 제외한 다른 경우에는 flatMap을 사용하면 된다.

 

 

compactMap(flatMap)과 map의 차이

 

플랫맵 예제에서 플랫맵을 컴팩트맵으로만 변경하고 실행한 결과이다.

 

맵과 컴팩트 맵의 차이를 좀 더 구체적으로 설명해보면

 

위 그림과 같이 optionalArray는 Array라는 컨테이너 내부에 옵셔널이라는 형태의 컨테이너들이 여러개 들어가 있는 형태이다.

이 배열의 맵 메서드와 컴팩트맵 메서드를 각각 호출해보면 다른 결과가 나온다.

 

먼저 맵 메서드를 호출한 결과는 Array 컨테이너 내부의 값 타입이나 형태가 어떤지는 상관없이 Array 내부에 값이 있다면 그 값을 클로저의 코드에서 실행하고, 결과를 다시 Array 컨테이너에 담아준다.

하지만 컴팩트맵은 클로저를 실행하면 자동으로 내부 컨테이너까지 값을 추출해준다.

따라서 맵의 결과는 [Int?] 타입이 반환되고, 컴팩트맵은  [Int] 타입을 반환해준다.

 

 

중첩된 컨테이너 구조에서 맵과 플랫맵(컴팩트맵)은 더욱 큰 차이를 보여준다.

 

플랫맵과 맵의 결과를 보면 확연한 차이를 볼 수 있다.

플랫맵은 내부의 값을 1차원적으로 펼쳐놓는 작업까지 하기 때문에 값을 꺼내고, 모두 동일하게 만들어 반환해주는 것을 볼 수 있다.

평평하게 펼쳐준다는 플랫맵의 정의와 같으면서 위에서 보았던 플랫맵의 기능 2번에 해당한다! 

 

 

옵셔널의 맵과 플랫맵의 정의

-옵셔널의 맵과 플랫맵의 정의를 보면 왜 둘의 차이가 있는지 이해할 수 있다.

func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?

Wrapped 과정에서 flatMap은 옵셔널안에 값이 있다면 추출해서 진행하고, map은 flatMap과 다르게 바로 값을 반환한다.

이 차이로 인해 플랫맵은 옵셔널에서 Chanining이 가능하고, 맵은 불가능한 결과를 가져오는 것이다.

 

 

 

마지막으로 모나드 개념이 필요한 이유가 무엇일까?

옵셔널을 보면 값을 옵셔널이라는 컨텍스트에 넣고, 이를 묶음으로 처리할 수 있게 해주었죠?

이렇게 모나드는 어떤 타입에 대한 추가적인 컨텍스트를 제공하고, 이를 한 묶음으로 처리할 수 있게 해준다.

(모나드의 개념과 유용성을 한 가지 이유만으로 설명하기에는 한참 모자라다.)

 

 

 

이렇게 모나드를 이해하기 위해 선행으로 컨텍스트, 함수객체에 대해서 알아보았고,

모나드는 무엇이고, 모나드가 사용할 수 있는 맵 삼형제인 맵, 플랫맵과 컴팩트맵에 대해서 알아보았다.

함수형 프로그래밍을 조금이나마 이해한 것일까..? 계속해서 정진하자.

 

 

 

Reference:

-야곰님의 Swift 문법 개정 3판

-[iOS] Swift 모나드 : 네이버 블로그 (naver.com)

-[Swift] 29. 모나드(Monad), 함수객체(Functor), 컨텍스트(Context) (tistory.com)

-flatMap() 정리하기. map(), compactMap() 과의 차이 (tistory.com)

-https://zeddios.tistory.com/449

 

'Swift' 카테고리의 다른 글

Nested Types(중첩 타입)에 관하여  (0) 2023.03.13
타입 캐스팅에 관하여  (0) 2023.03.13
고차함수에 관하여  (0) 2023.03.06
Closer에 관하여2  (0) 2023.03.02
Closer에 관하여  (0) 2023.03.01