study record

[Swift] 모나드 본문

Swift/스위프트 프로그래밍

[Swift] 모나드

asong 2021. 12. 6. 23:15

프로그래밍에서 모나드가 갖춰야 하는 조건

  • 타입을 인자로 받는 타입(특정 타입의 값을 포장)
  • 특정 타입의 값을 포장한 것을 반환하는 함수가 존재
  • 포장된 값을 변환하여 같은 형태로 포장하는 함수가 존재

모나드를 이해하는 출발점은 값을 어딘가에 포장하는 개념을 이해한 것에서 출발한다. 스위프트에서 모나드를 사용한 예 중에 하나가 바로 옵셔널이다. 옵셔널은 값이 있을지 없을지 모르는 상태를 포장하는 것이다.

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

컨텍스트

이번 파트에서 컨텍스트는 '콘텐츠를 담은 그 무엇인가'를 뜻한다. 물컵에 물이 담겨있으면 물은 콘텐츠고 물컵은 컨텍스트라고 볼 수 있다.

옵셔널은 열거형으로 구현되어 있어서 열거형 case의 연관값을 통해 인스턴스 안에 연관 값을 갖는 형태이다. 옵셔널에 값이 없다면 열거형의 .none case로, 값이 있다면 열거형의 .some(value) case로 값을 지니게 된다. 옵셔널의 값을 추출한다는 것은 열거형 인스턴스 내부의 .some(value) case의 연관 값을 꺼내오는 것과 같다.

2라는 숫자를 옵셔널로 둘러싸면, 컨텍스트 안에 2라는 콘첸츠가 들어가는 모양새이다. 만약 값이 없는 옵셔널 상태라면 '컨텍스트는 존재하지만 내부에 값이 없다'고 할 수 있다.

Optional은 Wrapped 타입을 인자로 받는 (제네릭)타입이다. 모나드의 조건 중 첫 번째 조건을 만족하는 타입이다. 그리고 Optional 타입은 Optional<Int>, init(2) 처럼 다른 타입의 값을 갖는 상태의 컨텍스트를 생성할 수 있으므로 모나드의 조건 중 두 번째 조건을 만족한다.

 

함수객체

맵은 컨테이너의 값을 변형시킬 수 있는 고차함수이다. 그리고 옵셔널은 컨테이너와 값을 갖기 때문에 맵함수를 사용할 수 있다.

// 맵 메서드를 사용하여 옵셔널을 연산할 수 있는 addThree() 함수
Optional(2).map(addThree) //Optional(5)

 

맵을 언급한 이유는 '함수객체란 맵을 적용할 수 있는 컨테이너 타입'이라고 말할 수 있기 때문이다. 맵을 사용해보았던 Array, Dictionary, Set 등등 스위프트의 많은 컬렉션 타입이 함수객체이다.

 

먼저 맵이 함수를 인자로 받으면 함수객체에 맵이 전달받은 함수를 적용하고, 새로운 함수객체를 반환한다.

// 옵셔널의 map 메서드 구현
func map<U>(f: (Wrapped) -> U) -> U? {
	switch self{
		case .some(let x): return f(x)
		case .none: return .none
	}
}

옵셔널의 map() 메서드를 호출하면 옵셔널 스스로 값이 있는지 없는지 switch 구문으로 판단한다. 값이 있다면 전달받은 함수에 자신의 값을 적용한 결괏값을 다시 컨텍스트에 넣어 반환하고, 그렇지 않다면 함수를 실행하지 않고 빈 컨텍스트를 반환한다.

 

모나드

함수객체 중에서 자신의 컨텍스트와 같은 컨텍스트의 형태로 맵핑할 수 있는 함수객체를 닫힌 함수객체라고 한다. 모나드는 닫힌 함수객체이다.

함수객체는 포장된 값에 함수를 적용할 수 있었다. 그래서 모나드도 컨텍스트에 포장된 값을 처리하여 포장된 값을 컨텍스트에 다시 반환하는 함수(맵)을 적용할 수 있다. 이 매핑의 결과가 함수객체와 같은 컨텍스트를 반환하는 함수객체를 모나드라고 할 수 있으며, 이런 맵핑을 수행하도록 플랫맵이라는 메서드를 활용한다.

플랫맵은 맵과 같이 함수를 매개변수로 받고, 옵셔널은 모나드이므로 플랫맵을 사용할 수 있다.

플랫맵은 맵과 다르게 컨텍스트 내부의 컨텍스트를 모두 같은 위상으로 평평하게 펼쳐준다는 차이가 있다. 즉, 포장된 값 내부의 포장된 값의 포장을 풀어서 같은 위상으로 펼쳐준다는 뜻이다.

맵 메서드를 사용하면 Array 컨테이너 내부의 값 타입이나 형태가 어찌 되었든 Array 내부에 값이 있으면 그 값을 그저 클로저의 코드에서만 실행하고 결과를 다시 Array 컨테이너에 담기만 한다. 그러나 플랫맵을 통해 클로저를 실행하면 알아서 내부 컨테이너까지 값을 추출한다.

// 중첩된 컨테이너에서 맵과 플랫맵(콤팩트맵)의 차이
let multipleContainer = [[1, 2, Option.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]]