Swift Monad & Functor, 고차함수와의 관계

모나드(Monad)

모나드는 특정한 상태로 값을 포장 하는 것에서 부터 시작 된다. 스위프트에서는 이를 옵셔널 이라는 형태로 구현했는데 값이 있을지 없을지 모르는 상태 속에 포장하는 것이다.

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

컨텍스트 와 컨텐츠

컨텍스트와 컨텐츠를 주로 물과 물컵에 많이 비유한다. 안에 담기는 물은 컨텐츠 이고 그 컨텐츠를 담고 있는 물컴은 컨텍스트 이다. 이는 스위프트에서는 옵셔널과 연관이 있다.

옵셔널은 .none.some 이라는 두가지 컨텍스트를 가지고 있다. 옵셔널은 열거형으로 구현이 되어있고 값이 없으면 .none case 값이 있으면 .case som(value) 로 값을 지니게 된다. 옵셔널에서 값을 추출한다는 것은 .case som(value) 의 연관 값을 꺼내오는 것과 같다.

예를들어 아래의 addThree(_:) 함수에 각각 두개의 매개변수를 넘겨 보겠다.

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


addThree(2)		// 5
addThree(Optional(2)) // 에러 발생: !로 강제 추출하거나 함수의 매개변수가 옵셔널이 아니라고 에러가 발생

이는 addThree(_:) 함수에 전달하는 매개변수에 따라 출력이 다르게 된다. 위에서 옵셔널이 아닌 순수한 값 2를 넘겨주면 정상 작동을 한다. 하지만 Optional(2) 옵셔널 값을 주게 되면 에러가 발생하고 실행이 되지 않는다.

Optioanl(2) : 컨텍스트 안에 2라는 컨텐츠가 들어가있는 모양을 가지고 있다. 즉, “컨텍스트는 2라는 값을 가지고 있다.”

위 처럼 해석 할 수 있다. Optioanl(2) 라는 전달인자로 사용하게 되면 즉 옵셔널의 some 으로 둘러싸인 컨텍스트가 전달 되었기 떄문에 함수가 받아들일 수 없게 되고 실행이 안되는 것이다.

함수객체

고차함수의 일종인 map은 컨테이너 값을 변형시킬 수 있는 고차함수 이다.

옵셔널은 컨테이너(컨텍스트가 일종의 컨테이너 역할을 한다)와 값을 갖기 때문에 map 함수를 사용할 수 있다

위에서 Optional(2)의 값을 addThree(_:) 함수를 적용시키고 싶으면 map 함수를 사용하면된다. 옵셔널은 컨테이너를 갖고 이는 컨텍스트의 일종의 역할을 하기 때문에 가능한 것 이다. 맵을 사용하면 컨테이너 즉 컨텍스트(옵셔널)안의 값을 처리할 수 있다.

// 맵 고차함수를 사용하여 addThree(_:)에서도 옵셔널을 연산할 수 있다.
Optional(2).map(addThree)		// Optional(5)

클로저를 활용하여 간결하게 나타낼 수 있다.

var value: Int? = 2
value.map{ $0 + 3 } // Optional(5)
value = nil
value.map{ $0 + 3 } // nil == Optional.none

함수 객체란 맵을 적용할 수 있는 컨테이너 타입이다. Array, Dictionary, Set 등등 스위프트의 많은 컬렉션 타입이 함수 객체이다.

함수객체와 맵 메서드의 동작

image

위의 모식도를 코드로 구현

extension Optional {
    func optionalMap<U>(f: (Wrapped) -> U) -> U? {
        switch self {
        case .some(let x): return f(x)
        case .none: return .none
        }
    }
}

옵셔늘을 익스텐션하여 optioanlMap(_:) 메서드를 추가했다. 이 메서드를 호출하면 옵셔널 스스로 값이 있는지 없는지 switch 구문으로 우선 판단한다. 만약 값이 있다면 전달 받은 자신의 값을 적용한 결괏값을 다시 컨텍스트에 넣어 반환 하고 그렇지 않다면 함수를 실해앟지 않고 빈컨텍스트(nil) 을 반환한다.

Optional(2).optionalMap(addThree) 동작

image

내부에 값이 있다면 .some 을통해 위와 같은 작업이 진행되고 값이 만약 없다면 .none 을 통햎 빈 컨텍스트로 다시 반환한다.

모나드

모나드는 값이 있을 수도 있고 없을 수도 있는 컨텍스트를 갖는 함수 객체 타입이다.

위에서 함수객체는 포장된(옵셔널) 값에 함수(map)을 적용할 수 있었다. 그래서 모나드도 컨텍스트에 포장된 값을 처리하여 포장된 값을 컨텍스트에 다시 반환하는 함수를 적용할 수 있다.

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

doubledEven(_:) 함수와 플랫맵의 사용
func doubledEven(_ num: Int) -> Int? {
    if num % 2 == 0 {
        return num * 2
    }
    return nil
}

Optional(3).flatMap(doubledEven)    // nil == Optional.none
map 과 flatMap의 차이점
let optionalArr: [Int?] = [1, 2, nil, 5]

let mappedArr: [Int?] = optionalArr.map{ $0 }
let flatmappedArr: [Int] = optionalArr.flatMap{ $0 }

print(mappedArr)        // [Optional(1), Optional(2), nil, Optional(5)]
print(flatmappedArr)    // [1, 2, 5]

위의 optionalArr 배열은 이중 컨테이너의 형태를 띠고 있다. Optional로 단일 컨테이너 형태일 거 같지만 배열의 옵셔널 인자 전체를 감싸는 Array라는 컨테이너가 존재한다. 맵 메서드의 결과는 해당 Array 컨테이너 내부의 값 타입이나 형태에 관계없이 Array 내부에 값이 있으면 그저 클로저의 코드에서만 실행하고 결과를 다시 Array 컨테이너에 담기만한다.

그러나 flatMap을 통해 클로저를 실행하면 알아서 내부 컨테이너 까지 값을 추출한다.

flatMap을 쓰면 nil을 걸러주는 이유가 여기에 있다. flatMap은 내부 컨테이너까지 값을 추출하기 때문

그렇기 때문에 mappedArr 배열은 [Int?] 타입이 되고 flatmappedArr 는 [Int] 타입이된다.

삼중컨테이너에 중첩

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

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

flatMap은 이름에서 알 수 있듯이 평평하게 펼친다(Flatten) 즉, 내부의 값을 모두 동일한 위상 으로 꺼내어 1차원 적으로 펼쳐 놓는다.

스위프트에서 옵셔널에 관련된 여러 컨테이너 값을 연달아 처리할 때, 바인딩을 통해 체인 형식으로 사용 할 수 있에 맵보다는 플랫 맵이 더욱 유용하게 쓰일 수 있다.

flatMap 활용

func stringToInt(str: String) -> Int? {
    return Int(str)
}

func intToString(integer: Int) -> String? {
    return "\(integer)"
}

var optionalString: String? = "2"

var result: Any = optionalString.flatMap(stringToInt).flatMap(intToString).flatMap(stringToInt)
print(result)   // Optional(2)
result = optionalString.map(stringToInt)    // 더 이상 체인 연결 불가
print(result)   // Optional(Optional(2))

플랫맵을 사용하여 체인을 연결했을 때 결과는 옵셔널 타입이다. 그러나 맵을 사용하여 체인을 연결하면 옵셔널의 옵셔널 형태로 반환한다. 그 이유는 플랫맵은 함수의 결괏값에 값이 있다면 추출해서 평평하게 만드는 과정을 내포(nil이 아닌 값을 추출 한다는 말), 맵은 그렇지 않기 떄문이다. 맵은 옵셔널 타입의 값을 옵셔널이라는 컨테이너 안에 다시 집어넣어 반환하고, 플랫맵은 추출 작업을 통해 옵셔널에서 꺼내온 값을 다시 옵셔널에 넣어주기 때문에 이 같은 연쇄 연산도 가능한 것이다.

Optional 의 map, flatMap 정의
public func map<U>(_ transform: (Wrapped) thorws -> U) rethrows -> U?
public func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?

맵에서 전달받은 함수 transform은 포장된 값(Wrapped)을 매개변수로 갖고 U 를 반환하는 함수이다. 예를 들어 stringToInt(:) 는 String 타입을 전달 받고 Int? 타입을 반환한다. 즉 U == Int? 가 된다. String 옵셔널의 맵에 stringToInt(:) 함수를 달 하면 최졸 반환 타입이 Int??가 된다.

반면에 플랫맵이 전달받은 transform은 포장된 값(Warrpped)를 매개변수로 갖고 U? 를 반환하는 함수이다. transform에 stringToInt(:) 를 대입해 생각해보면 U? == Int? 가 된다. 즉 U == INt 가 되기 때문에 플랫맵의 동작 결과는 최종적으로 Int? 타입을 반환하게 된다.

플랫맵은 체이닝 중간에, 연산에 실패하는 경우나 값이 없어지는 경우 (.none 이 된다거나 nil이 된다는 등) 이 외에는 별도의 예외 처리 없이 빈 컨테이너를 반환한다.

func intToNil(param: Int) -> String? {
    return nil
}

var optionalString: String? = "2"

var result: Any = optionalString.flatMap(stringToInt).flatMap(intToNil).flatMap(stringToInt)
print(result)   // nil

flatMap(intToNil 부분에서 nil, 즉 Optional.none 을 반환받기 때문에 이후에 호출되는 메서드는 무시한다. 옵셔널 체이닝과 완전히 같은 동작이다. 이는 옵셔널이 모나드이기 때문에 가능한 것이다.

스위프트의 가본 모나드 타입이 아니더라도 플랫맵 모양의 모나드 연산자를 구현하면 사용자정의 타입(흔히 클래스 또는 구조체 등)도 모나드로 사용할 수 있다.


Reference





© 2020. by Gaki

Powered by gaki