개발하는 뚝딱이

[swift] 모나드 본문

swift

[swift] 모나드

개발자뚝딱이 2020. 6. 23. 12:49

 

책 <스위프트 프로그래밍 3판 ;야곰 지음> 을 정리한 글입니다.

 


 

 

모나드

- 함수형 프로그래밍에서 모나드는 순서가 있는 연산을 처리할 때 자주 활용하는 디자인 패턴 (수학의 모나드와 다른 의미)

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

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

 

1. 컨텍스트

- Context ; 콘텐츠를 담을 수 있는 컨테이너 역할 (물컵에 물이 담겨져 있을 때 ; 물컵 - 컨텍스트, 물 - 콘텐츠)

 

- 옵셔널은 열거형으로 구현되어 있어 case의 연관 값을 통해 인스턴스 안에 연관 값을 갖는 형태

- 옵셔널에 값이 없으면 열거형의 .none case로, 값이 있으면 열거형의 .some(value) case로 값을 갖게 된다

  → 옵셔널의 값을 추출하는 것은 열거형 인스턴스 내부의 .some(value) case의 연관 값을 꺼내오는 것 과 같음

 

- Optional은 Wrapped 타입을 인자로 받는 (제네릭) 타입 → 모나드의 첫 번째 조건 만족

- Optional 타입은 Optional<Int>.init(2)처럼 다른 타입(Int)의 값을 갖는 상태의 컨텍스트를 생성할 수 있음

   → 모나드의 두 번째 조건 만족

func addThree(_ num: Int) -> Int {
    return num + 3
}

 - addThree(_:) 의 함수의 전달인자로 컨텍스트에 들어있지 않은 순수 값인 2를 전달하면 정상적으로 함수 실행 가능

- 그러나 addThree(Optional(2)) 실행 시 오류 발생, 옵셔널이란 컨텍스트로 둘러싸여 전달되었기 때문

 

 

2. 함수 객체

- 맵은 컨테이너의 값을 변형시킬 수 있는 고차함수

- 옵셔널은 컨테이너와 값을 갖기 때문에 맵 함수를 사용할 수 있음

Optional(2).map(addThree) // 맵 메서드를 사용하여 옵셔널 연산 가능

// 옵셔널에 메서드와 클로저의 사용
var value: Int? = 2
value.map({ $0 + 3 }) // Optional(5)
value = nil
value.map({ $0 + 3 }) // nil
// Optional의 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 구문으로 판단

- 값이 있으면 전달받은 함수에 자신의 값을 적용한 결괏값을 다시 컨텍스트에 넣어 반환, 값이 없으면 함수를 실행하지 않고 빈 컨텍스트 반환

 

 

3. 모나드

- 함수객체 중 자신의 컨텍스트와 같은 컨텍스트의 형태로 맵핑할 수 있는 함수객체를 '닫힌 함수객체' Endofunctor 라고 함

- 모나드는 닫힌 함수객체

 

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

- 모나드도 컨텍스트에 포장된 값을 처리하여 포장된 값을 컨텍스트에 다시 반환하는 함수(맵)을 적용 할 수 있음

- 매핑의 결과가 함수 객체와 같은 컨텍스를 반환하는 함수객체를 모나드라고 할 수 있으며, 이런 매핑을 수행하도록 플랫맵 (flatMap) 이라는 메서드를 활용

 

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

// doubleEven(_:) 함수와 플랫맵의 사용

func doubleEven(_ num: Int) -> Int? {
    if num.isMultiple(of: 2) {
        return num * 2
    }
    return nil
}

Optional(3).flatMap(doubleEven) // nil
Optional.none.flatMap(doubleEven) // nil

- 플랫맵은 맵과 다르게 컨텍스트 내부의 컨텍스트를 모두 같은 위상으로 평평하게 펼쳐준다는 차이가 있음

- 내부의 값을 1차원적으로 펼쳐놓는 작업을 함

 

- Optional 타입에 사용하였던 flatMap(_:) 메서드를 Sequence 타입이 Optional 타입의 Element를 포장한 경우 compactMap(_:)이라는 이름으로 사용

- 이 경우를 제외한 다른 경우 그대로 flatMap(_:)이라는 이름을 사용

- compactMap(_:)과 flatMap(_:)의 사용 방법은 같으나 좀 더 분명한 뜻을 나타내기 위해 구분

// 맵과 컴펙트맵(플랫맵)의 차이

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]

optionals 배열의 모식도

 

// 중첩된 컨테이너에서 맵과 플랫맵(콤팩트맵)의 차이

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.compactMap{$0}}

print(mappedMultipleContainer) // [[Optional(1), Optional(2), nil], [Optional(3), nil], [Optional(4), Optional(5), nil]]
print(flatMappedMultipleContainer) // [1, 2, 3, 4, 5]

 

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 mappedReusult = optionalString.map(stringToInteger) // 더 이상 체인 연결불가
print(mappedReusult) // Optional(Optional(2))

- 플랫맵은 함수의 결괏값에 값이 있다면 추출해서 평평하게 만드는 과정을 내포함

- 따라서 항상 같은 컨텍스트를 유지할 수 있으므로 연쇄 연산도 가능

 

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

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

- 맵에서 전달받은 trnasform은 포장된 값을 매개변수로 갖고 U를 반환하는 함수

- stringToInt(_:)는 String 타입을 전달받고 Int? 타입을 반환, U == Int?가 됨

- String 옵셔널의 맵에 stringToInt(_:) 함수를 전달하면 최종 반환 타입이 Int??가 됨

 

- 플랫맵이 전달받은 transform은 포장된 값을 매개변수로 갖고 U?를 반환하는 함수

- stringToInt(_:)를 대입하면 U? == Int?가 됨

- U == Int가 되므로 최종적으로 Int? 타입 반환

 

// 옵셔널 바인딩을 통한 연산

var result: Int?
if let string: String = optionalString,
    let number: Int = stringToInteger(string),
    let finalString: String = integerToString(number),
    let finalNumber: Int = stringToInteger(finalString) {
    result = Optional(finalNumber)
}

print(result) // Optional(2)
// 플랫맵 체이닝 중 빈 컨텍스트를 만났을 때의 결과
func integerToNil(param: Int) -> String? {
    return nil
}

optionalString = "2"

result = optionalString.flatMap(stringToInteger)
    .flatMap(integerToNil)
    .flatMap(stringToInteger)

print(result) // nil

- 플랫맵에서 Optional.none 즉, nil을 반환하면 그 이후에 호출되는 메서드는 무시

- 옵셔널이 모나드이기 때문에 가능

 

 

'swift' 카테고리의 다른 글

[swift] 상속  (0) 2020.09.05
[swift] 서브스크립트  (0) 2020.06.25
[swift] 타입캐스팅  (0) 2020.05.17
[swift] 맵, 필터, 리듀스  (0) 2020.05.13
[swift] 옵셔널 체이닝과 빠른 종료  (0) 2020.05.08