검색결과 리스트
모나드에 해당되는 글 1건
- 2015.10.25 [swift2] Functor 와 Monad
글
[swift2] Functor 와 Monad
Swift
2015. 10. 25. 02:58
이 포스트는 아래 원문을 참고하여, Functor 와 Monad 에 대해 설명합니다.
1. Functor 란?
Functor 란 map 함수를 지원하는 컨테이너 타입입니다.
- Array 타입은 map 함수를 지원하기 때문에 Functor 입니다.
- Array 엘리먼트들을 2 배로 곱하는 예제를 살펴보겠습니다.
var doubledImperative:[Int] = [1, 2, 3]
for number in numbers {
doubledImperative.append(number * 2)
}
print(doubledImperative) // 2, 4, 6
- 단순히 루프문을 사용하여 배열 엘리먼트를 2배하여 새 배열에 담습니다.
Map
- map 함수를 사용하면 다음과 같이 처리할 수 있습니다.
let numbers = [1, 2, 3]
let doubledNumbers = numbers.map { $0*2 }
print(doubledNumbers) // 2, 4, 6
- 예제에서처럼, map 을 사용하면 Array 엘리먼트를 변환하는 작업의도를 더 명확하게 표현할 수 있습니다.
- 즉, 어떻게가 아닌 무엇을 달성하려는지 를 더 잘 표현합니다.
- 함수형 프로그래밍의 장점 중 하나라고도 할수 있습니다.
Optional도 map 적용이 가능한 컨테이너 타입입니다.
- map은 Array 뿐만 아니라 어떤 컨테이너 타입에도 구현할수 있는 고차원 함수입니다.
- 값이 있거나 또는 없음을 포장하는 Optional 타입도 해당합니다.
고차원 함수: 파라미터 또는 반환값으로 함수를 전달할수 있는 함수.
let number:Int? = 815 // Optional(815)과 동일함
let transformedNumber = number.map{ $0*2 }.map{ $0%2 == 0 }
print(nilNumber.map{ $0*2 }.map{ $0%2 == 0 }) // Optional(true)
Optional.map 은 우리를 대신에 nil 을 처리해줍니다.
- Optional 타입에 map을 사용했을 때 장점은 우리를 대신해 nil 값을 처리준다는 것입니다.
- 원래 값이 nil 이었다면 map을 적용해도 nil을 반환합니다.
- 이로인해 연산의 중간과정에서 중첩된 if let을 사용해야하는 번거로움을 피할수 있습니다.
// 원래값이 nil인 경우
let nilNumber:Int? = nil
let transformedNilNumber = nilNumber.map{ $0*2 }.map{ $0%2 == 0 }
print(transformedNilNumber) // nil
- map을 사용하지 않았다면 다음과 같이 처리했을 것입니다.
let nilNumber:Int? = nil
var result = false
if let number = nilNumber {
result = ((number*2) % 2) == 0
}
print(result)
컨테이너 타입에 따라 map은 조금 다르게 동작합니다.
- map은 컨테이너 타입에 따라, 의미적으로 조금 다르게 동작한다는 것을 알 수 있습니다.
- T 타입의 값을 포함하는 컨테이너 타입을 구현할 때, map 메서드의 일반적인 시그니처는 다음과 같습니다.
func map<U>(transformFunction: T -> U) -> Container<U>
- T 는 현재 컨테이너가 포함하는 엘리먼트 타입.
- U 는 반환될 컨테이너의 엘리먼트 타입.
커스텀 컨테이너 Container 구조체에 map을 구현해보겠습니다.
- 예제로, Container 라는 커스텀 컨테이너 타입을 구현해보겠습니다.
struct Container<T> {
var value:T
func map<U>(transform: T -> U) -> Container<U> {
let newContainer = Container<U>(value: transform(self.value))
return newContainer
}
}
- 우리는 T 타입을 파라미터로 받아, U 타입을 반환하는 함수(transform)를 map 에 제공합니다.
- map은 내부값에 변환함수를 적용하고 교체된 값을 갖는 다른 컨테이너 인스턴스를 생성하여 반환합니다.
- 아래와 같이 사용할 수 있습니다.
var container = Container<Int>(value: 5)
var resultContainer = container.map { "\($0 * 10)" }
print(resultContainer.value) // "50"
또다른 커스텀 컨테이너 Result 타입을 구현해봅시다.
- Result enum 은 요즘 오픈소스 swift 코드에서 많이 볼수 있는 패턴입니다.
enum Result<T> {
case Value(T)
case Error(NSError)
}
- 몇몇 프로그래밍 언어에서 Either 라고 알려진 타입의 구현입니다.
- case Error 연관값은 제네릭 대신 NSError 타입으로 연산의 결과를 보고하는데 사용합니다.
- 개념적으로 Result는 값이 있을 수도 있고, 없을 수도 있는 임의의 타입의 값을 포장하는 Optional과 유사합니다.
- 이 경우에는 추가적으로 왜 그 값이 없는지도(NSError) 알려줍니다.
- 아래와 같이 파일에서 컨텐츠를 읽고 Result 객체로 반환하는 함수를 구현해보겠습니다.
func dataWithContentsOfFile(file:String, encoding:NSStringEncoding) -> Result<NSData> {
do {
let data = try NSData(contentsOfFile: NSBundle.mainBundle().pathForResource(file, ofType: nil)!, options: NSDataReadingOptions.DataReadingMapped)
return Result.Value(data)
} catch {
return Result.Error(error as NSError)
}
}
- 이 함수는 NSData 값을 담은 Result.Value를 반환하거나, (파일을 읽을 수 없는 경우에는) NSError를 갖는 Result.Error를 반환합니다.
텍스트 파일을 읽어 대문자로 변환하는 작업을 구현해봅시다.
var stringContents:String?
switch data {
case let .Value(value):
stringContents = NSString(data: value, encoding: NSUTF8StringEncoding) as? String
case let .Error(error):
break
}
let uppercasedContents:String? = stringContents?.uppercaseString
- 매 단계마다 값이 있는지 검색해야 하기 때문에, 중첩된 if let 와 switch 문을 사용하게 됩니다.
map을 사용하면 이렇게 처리할 수 있습니다.
- NSData 를 받아서, String 으로 변환하고, 그리고 다시 대문자로(String) 변경하는 과정을 수행합니다.
NSData -> String -> String
- 일련의 map 변환을 적용하여 아래와 같이 처리할 수 있습니다. (map의 구현에 대해서는 아래에서 다룹니다.)
let data:Result<NSData> = dataWithContentsOfFile("test.txt", encoding: NSUTF8StringEncoding)
let uppercaseContents:Result<String> = data.map { NSString(data: $0, encoding: NSUTF8StringEncoding)! }.map { $0.uppercaseString }
- Array 에 map을 적용했던 이전 예제와 유사하게 이 코드는 훨씬더 의도가 명확합니다.
어떻게 Result.map 을 구현했을까요?
extension Result {
func map<U>(f: T -> U) -> Result<U> {
switch self {
case let .Value(value):
return Result<U>.Value(f(value))
case let .Error(error):
return Result<U>.Error(error)
}
}
}
- 변환함수 f는 타입 T (NSData)의 값을 받고, 타입 U (String)을 반환합니다.
- 값이 있으면 단순히 value를 파라미터로 f를 호출하고, 값이 없으면 동일한 error를 갖는 다른 Result.Error를 반환합니다.
- Result라는 껍질(컨테이너)이 포장하고 있는 알맹이에 f함수를 젹용한다고 생각하면 쉽게 이해할수 있을 것입니다.
요약하면
- Optional, Array 와 같은 컨테이너 타입에 구현된 map 함수가 무엇을 하는지 알아보았습니다.
- map 은 변환함수에 의해 변경된 값을 갖는 새 컨테이너를 반환합니다. (알맹이만 변경합니다.)
- Functor는 map 을 구현하는 타입입니다.
- Dictionary, 클로져와 같은 타입도 Functor 라고 할 수 있고, map 함수가 어떻게 동작하는지 대략 예측해볼 수 있습니다.
2. Optional 은 Monad 입니다.
- Monad는 값이 있을 수도 있고 없을 수도 있는 상태를 포장하는 타입입니다. (swift 에서는 Optional)
- Monad 는 Functor 의 한 유형입니다.
map 의 변환함수의 반환타입이 Result 라면 어떤일이 발생할까요?
- 위 예에서 변환함수를 다른 값으로 변환하는데 사용했습니다.
- 만일 변환함수가 새 Result 객체를 반환하게 되면 어떻게 될까요?
- 마찬가지로 변환함수가 새 Error 에러를 반환하면 어떻게 될까요?
- 먼저 map 함수 시그니처를 통해 알아봅시다.
func map<U>(f: T -> U) -> Result<U>
- T는 NSData 이고 U는 Result
입니다. 반환타입 Result를 시그니처에 대입해보면
func map(f: NSData -> Result<String>) -> Result<Result<String>>
- 반환 값에 Result 가 중첩 되었다는 것에 주목하세요. 우리가 원했던 결과는 아닙니다.
중첩된 Result를 받아 단순 Result로 flatten 하는 함수를 구현해봅시다.
extension Result {
static func flatten<T>(result:Result<Result<T>>) -> Result<T> {
switch result {
case let .Value(innerResult):
return innerResult
case let .Error(error):
return Result<T>.Error(error)
}
}
}
- flatten 함수는 중첩된 내부타입 T를 갖는 Result를 받아, 단순히 Value와 Error의 연관값을 추출해서 단일 Result
를 반환합니다. flatten 함수는 다른 문맥에서도 찾을수 있는데, 그중 하나로 배열의 배열을 일차원 배열로 flatten 하는 예입니다.
map, flatten 을 결합해서 우리의 Result
-> Result 변환에 적용해봅시다.
let stringResult = Result<String>.flatten(data.map { (data:NSData) -> (Result<String>) in
if let string = NSString(data: data, encoding: NSUTF8StringEncoding) as? String {
return Result<String>.Value(string)
} else {
return Result<String>.Error(NSError(domain: "error", code: -1, userInfo: nil))
}
})
일반적으로 map 함수에 flatten 과정을 적용해 flapMap 또는 flattenMap이라 불립니다.
extension Result {
func flapMap<U>(f:T -> Result<U>) -> Result<U> {
return Result.flatten(map(f))
}
}
Monad 타입은 flapMap 함수와 찰떡궁합 입니다.
- map이 동작하는 타입은 여기서 보았던 것과 유사한 시그니처를 갖는 flatMap 함수를 구현합니다.
- 다음에 Functor와 Monad라는 단어를 들으면 두려워하지 맙시다.
- Functor와 Monad는 단순히 컨테이너 타입에 적용할수 있는 공통 연산을 기술하기 위한 디자인 패턴입니다.
'Swift' 카테고리의 다른 글
| [swift2] Core Image Filter 예제 (0) | 2015.11.10 |
|---|---|
| [swift2.1] Xcode7.1 Playground 변경사항 (0) | 2015.10.27 |
| [swift2] Functor 와 Monad (0) | 2015.10.25 |
| [swift2] 타입변환 연산자 (is, as, as?, as!) (0) | 2015.10.18 |
| [swift] Swift 코딩환경 Playground (0) | 2015.10.05 |
| [swift2] Swift Guard (0) | 2015.09.11 |