궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFT/Grammar

Swift: 기초문법 [모나드 - 컨텍스트, 함수객체, 모나드]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

본 게시글은 yagom님의 Swift 프로그래밍 3판을 참고하여 작성되었습니다.

 

모나드

Swift 에는 함수형 프로그래밍 패러다임에서 파생된 기능이나 개념이 종종 등장한다. 이 개념을 이해하지 못한다면 Swift 기능의 절반 정도를 사용하지 못하기 때문에 함수형 프로그래밍 패러다임을 이해하는 게 중요하다.

 

그 시작은 모나드(monad)이다. 

 

모나드는 한 문장으로 설명하기 어려운 개념이지만, 모나드라는 용어는 수학의 범주론에서 시작된다. 

  • 순서가 있는 연산 처리할 때 자주 활용하는 디자인 패턴
  • 사용하는 곳에 따라 수학의 범주론의 모나드인지 특정 디자인 패턴을 따르는 모나드인지 달라짐
  • 프로그래밍의 모나드는 수학 범주론의 모나드의 의미를 완벽히 구현하지 않고, 단지 개념만 차용한 정도의 의미를 가짐
  • 대부분의 성질을 갖추었다 하여 프로그래밍의 모나드를 모나딕(monadic) 이라고 표현함
  • 모나드의 성질을 갖는 타입이나 함수를 모나딕 타입 혹은 모나딕 함수 등으로 표현하기도 함
  • 모나드는 닫힘 함수객체임

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

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

 

타입을 인자로 받는다? 이것은 Swift에서 제네릭으로 구현 가능. 가장 기본적이면서 유용한 모나드는 바로 옵셔널이다.

 

일단 옵셔널을 활용해 모나드에 대해 알아볼까 한다.

 

모나드의 시작은 값을 어딘가에 포장한다는 개념을 이해해야 한다. 옵셔널은 값이 있거나 없을 수 있는 상태를 포장한다고 옵셔널 포스팅에서 작성한 적이 있다.

 

박스를 열었더니 30이라는 정수가 나올 수도, nil이 나올수 있는 그런 상태가 바로 옵셔널이다. 

 

함수 객체와 모나드는 특정 기능이 아닌 디자인 패턴 또는 자료구조라고 할 수 있다. 모나드에 대해 이해하려면 몇 가지 개념을 알아야 하는데 이 옵셔널을 순차적으로 들여다보면서 이해하도록 하려 한다.

컨텍스트  Context

컨텍스트의 사전적 의미는 '문맥', '맥락'이다. 컨텍스트는 '콘텐츠(contents)를 담은 그 무엇인가'를 뜻하는데, 예를 들어 컵에 물이 담겨있다고 가정하에 여기서 '물'은 콘텐츠이고, '컵'은 컨텍스트라고 할 수 있다.

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

 

만약 숫자 '2'라는 숫자를 옵셔널이라는 박스에 넣으면, 컨텍스트 안에 2라는 콘텐츠가 들어가게 된다. 그리고 '컨텍스트는 2라는 값을 가지고 있다'를 의미하는데, 만약 값이 없는 옵셔널 상태라면 '컨텍스트는 존재하지만 안에 값이 없다'를 의미한다.

 

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

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

addThree(_:) 함수의 전달 인자로 컨텍스트에 들어있지 않은 순수 값 (5)를 전달하면 addThree(_:) 함수는 매개변수로 일반 Int 타입의 값을 받기 때문에 정상적으로 함수를 실행할 수 있다.

addThree(5) //8

하지만 옵셔널을 전달인자로 사용하려 한다면 오류가 난다. 왜냐면 순수한 값이 아닌 옵셔널이라는 상자에 감싸 져서 전달되었기 때문이다.

addThree(Optional(5)) //error

함수객체

저번 포스팅에서 map에 대해 알아봤는데, 맵은 컨테이너의 값을 변형시킬 수 있는 고차 함수이다. 

TIP
 
 

컨테이너 Container
다른 타입의 값을 담을 수 있으므로 컨텍스트의 역할을 수행할 수 있다.

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

Optional(5).map(addThree) // Optional(8)

이렇게 맵을 사용 하면 컨테이너 안의 값을 처리할 수 있다.

그리고 아래처럼 따로 함수가 없어도 클로저를 사용할 수 있다.

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

TIP
 
 

함수객체란?
'맵을 적용할 수 있는 컨테이너 타입'이라고 말할 수 있다. 맵을 사용했었던 Array, Dictionary, Set 등 Swift의 많은 컬렉션 타입이 함수객체 이다!

맵이 컨테이너의 내부의 값을 갖고 addThree(:_) 함수를 실행시킬 수 있던 이유는?

맵을 사용 하여 컨테이너 내부의 값을 처리할 수 있는다는 걸 이제 알았는데, 함수객체와 맵 메서드의 동작 모식도를 보면 그 이유를 알 수 있다.

 

익스텐션제네릭에 대해 읽고 나서 다시 확인해보는 것을 추천합니다.

// 옵셔널의 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 구문으로 판단 ➜ 값이 있다면 전달받은 함수에 자신의 값을 적용 ➜ 결괏값을 다시 컨텍스트에 넣어 반환 ➜ 만약 값이 없었다면 함수 실행하지 않고 빈 컨텍스트 반환

모나드

모나드(닫힌 함수객체)란? 함수객체 중 자신의 컨텍스트와 같은 형태로 맵핑할 수 있는 객체
  • 자신의 컨텍스트와 같은 형태로 맵핑할 수 있음
  • 포장된 값을 컨텍스트에 다시 반환하는 함수(map)를 적용할 수 있음
  • 이 맵핑의 결과가 햄수객체와 같은 컨텍스트를 반환하는 함수객체를 모나드 라 칭함
  • 이 맵핑을 수행하도록 플랫맵(flatMap) 메서드를 활용함

플랫맵은 맵과 동일하게 함수를 매개변수로 받고, 옵셔널은 모나드 이기 때문에 플랫맵을 사용할 수 있다.

 

짝수면 2를 곱한 후 반환하고 홀수 면 nil를 반환하는 함수 doubledEven(_:)가 있고, Optional(3)의 플랫맵에 이 함수를  전달한 결과를 아래와 같이 확인할 수 있다.

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

Optional(3).flatMap(doubledEven)
//nil(Optional<Int>.none

컨텍스트로부터 3이라는 값을 추출  ➜ 추출한 값을 doubledEven 함수에 전달 ➜ 짝수가 아닌것을 확인함 ➜ 빈 컨텍스트 nil 반환

 

만약 Optional.none.flatMap(doubledEven) 처럼 빈 컨텍스트에서 플랫맵을 사용하면 이렇게 된다.

빈 컨텍스트 ➜ 플랫맵은 아무것도 수행하지 않음 ➜ 결국에 다시 빈 컨텍스트를 반환

flatMap(_:)과 map(_:)의 차이

흠.. map(_:) 메서드와 별 차이가 없어 보이는데, 이 차이를 제대로 알지 못하면 맵 메서드를 사용해야 하는 경우에 잘못해서 flatMap을 사용할 수가 있다. 

 

flatMap - map과 다르게 컨텍스트 내부의 컨텍스트를 모두 같은 위상으로 Flat(평평) 하게 펼처줌. 쉽게 말해 포장된 값 내부의 포장을 풀어서 같은 위상으로 펼쳐줌!

 

바로 위에서 사용한 코드 블록을 보면 옵셔널 타입에 사용했던 flatMap(_:) 메서드를 시퀀스(sequence)[각주:1] 타입이 옵셔널 타입의 Element를 포장한 경우 compactMap(_:) 이라는 이름으로 사용한다.

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는 이중 컨테이너 형태이다.

optional 배열 모식도

optionalsArray라는 컨테이너 내부에 optional이라는 컨테이너들이 여러 개가 있는 형태이다. 이 배열의 맵과 플랫맵 메서드를 호출한다면 다른 결과를 볼 수 있다. 

 

맵 메서드Array 컨테이너 내부의 값 타입이 무엇이든 Array내부에 값만 있다면 그 값을 클로저의 코드에만 실행하고 결과를 다시 Array 컨테이너에 담기만 한다.

 

하지만 플랫맵 메서드를 실행하면 알아서 내부 컨테이너까지 값을 추출한다. 그렇기에 mapped는 다시 [Int?] 타입이 되고, compactMapped[Int] 타입이 되는 것이다.

 

다시 한번 말하지만 플랫맵은 map과 다르게 컨테이너 내부의 포장을 풀어서 같은 위상으로 펼쳐준다고 생각하면 이해하기 쉽다.

 

삼중 컨테이너에 중첩된 맵과 플랫맵을 사용할 수 도 있다.

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 } }
// error: 'flatMap'은 더 이상 사용되지 않습니다. 클로저가 선택적 값을 반환하는 경우에는 compactMap(_:)을 사용하세요.
let flatmappedMultipleContainer = multipleContainer.compactMap{ $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]

multipleContainer 모식도

Swift에서 옵셔널에 관련해 여러 개의 컨테이너 값을 연달아 처리할 때는, 바인딩을 통해 체인 형식으로 사용 가능하기 때문에 맵보다는 플랫맵이 더 유용하게 사용된다.

 

 

읽어주셔서 감사합니다 🤟

 

  1. 시퀀스(Sequence)는 직역하면 연속열이 될 수 있으며, 문자 그대로 개개의 원소들을 순서대로 하나씩 순회할 수 있는 타입을 의미한다. 간단하게 말해 Array나 Dictionary 등이 있다. [본문으로]

잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.