[swift2] Functor 와 Monad

Swift 2015. 10. 25. 02:58

이 포스트는 아래 원문을 참고하여, Functor 와 Monad 에 대해 설명합니다.

출처: Functor and Monad in Swift


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

[swift] Map, Filter, Reduce

Swift 2015. 6. 22. 23:09



함수를 인자로 취하는 함수를 고차원 함수(higher-order function)라고 합니다.
Swift 표준라이브러리에서 array의 메서드로 제공하는 고차원 함수인 map, filter, reduce에 대해서 알아보겠습니다.

1. Map

    func map<U>(transform: (T) -> U) -> Array<U>

Map은 배열 각 요소 x에 변환함수 transform을 적용하고 그 결과값으로 구성된 배열을 반환하는 함수입니다.
배열요소들을 다른 값으로 맵핑하는 함수이지요.

2. Filter

func filter(includeElement: (T) -> Bool) -> Array<T>

Filter는 조건식을 인자로 받아, 조건식이 true를 만족하는 요소들로만 구성된 배열을 반환하는 함수입니다.
쉽게 말하면, 배열요소들을 필터링하는 함수입니다.

3. Reduce

func reduce<U>(initial: U, combine: @noescape (U, T) -> U) -> U

Reduce는 U를 초기값으로 하여,각 배열요소들과 순차적으로 결합연산을 하여 누적된 단일값을 반환하는 함수입니다.


4. 사용예

이제 사용법에 대해서 알아볼까요? 다음과 같은 정수형 배열이 있습니다.

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  1. 1. 정수형 배열의 각 요소에 1을 더한 배열을 얻고 싶다.
  2. 2. 정수형 배열의 각 요소에 2를 곱한 배열을 얻고 싶다.
  3. 3. 정수형 배열에서 짝수로만 구성된 배열을 얻고 싶다.
  4. 4. 정수형 배열 값들의 총합을 얻고 싶다.

map 함수를 사용하여 1,2번 문제의 답을 쉽게 얻을 수 있습니다.

// 1. 정수형 배열의 각 요소에 2를 곱한 배열을 얻고 싶다.  
arr.map { (x) -> Int in
    return x+1
}

// 2. 정수형 배열의 각 요소에 2를 곱한 배열을 얻고 싶다.
arr.map { (x) -> Int in
    return x*2
}

filter를 사용하여 3번을 해결해보겠습니다.

// 3. 정수형 배열에서 짝수로만 구성된 배열을 얻고 싶다. 
arr.filter { (x) -> Bool in
    return x%2 == 0
}  

당연히 4번은 reduce의 몫입니다.

// 4. 정수형 배열 값들의 총합을 얻고 싶다.
arr.reduce(0, combine: { (result, x) -> Int in
   return result + x
})

Trailing Closure 를 사용하여 다소 낯선 형태지만 코드가 너무 짧아 놀라셨죠?   

(Trailing Closure는 함수의 마지막 인자가 클로져(함수)일 경우, 클로져를 함수 호출부의 꼬리(trailing)에 표기하는 방식입니다. )

이러한 고차원 함수들을 복합적으로 사용하여 프로그래밍하는 방식을 함수형 프로그래밍이라고 합니다. 

함수형 프로그래밍은 짧고 명료한 코드를 작성할 수 있게하고, 버그의 가능성 또한 훨씬 줄인다고 합니다.

낯선 함수형 프로그래밍 방식에 익숙해지려면 다소 시간이 걸리겠지요? ^^


5. Map 함수 구현하기

학습한 내용을 좀더 잘 이해하기 위해 직접 map 함수를 만들어 보겠습니다.

일반적인 방법으로 1,2번 문제를 구현하는 코드를 작성했습니다.

var arr = [1, 2, 3, 4, 5]

func pluseOne(arr:[Int]) -> [Int] {
    var result:[Int] = []
    for x in arr {
        result.append(x + 1)
    }
    return result
}

func multiplyTwo(arr:[Int]) -> [Int] {
    var result:[Int] = []
    for x in arr {
        result.append(x * 2)
    }
    return result
}  

두 함수는 연산하는 행위만 제외하고 거의 비슷합니다. 연산하는 행위를 함수로 묶어내면 일반화가 가능해보이네요.

func map(arr:[Int], transform:(Int->Int)) -> [Int] {
    var result:[Int] = []
    for x in arr {
        result.append(transform(x))
    }
    return result
}  

이제 제네릭을 사용하여 범용타입으로 대치하면

func map<T, U>(arr:[T], transform:(T->U)) -> [U] {
    var result:[U] = []
    for x in arr {
        result.append(transform(x))
    }
    return result
}

map 함수가 완성되었습니다. 우리는 글로벌 함수로 구현했지만 표준라이브러리에서는 array의 메소드로 제공한다는 점이 유일한 차이입니다.


5. Filter 함수 구현하기

Map 함수를 구현해보아서 Filter 함수 구현은 좀더 간단합니다.

// 일반적인 방법으로 구현하고
func filter(arr:[Int]) -> [Int] {
    var result:[Int] = []
    for x in arr {
        if x%2 == 0 {
            result.append(x)
        }
    }
    return result
}

// 함수로 공통 행위를 묶어낸 다음
func filter(arr:[Int], includeElement:(Int->Bool)) -> [Int] {
    var result:[Int] = []
    for x in arr {
        if includeElement(x) {
            result.append(x)
        }
    }
    return result
}


// 제네릭 적용
func filter<T>(arr:[T], includeElement:(T->Bool)) -> [T] {
    var result:[T] = []
    for x in arr {
        if includeElement(x) {
            result.append(x)
        }
    }
    return result
}


6. Reduce 함수 구현하기

단계적으로 구현해보니 고차원 함수 구현이 어렵지 않죠? 이제 reduce를 구현해 볼까요?

// 일반적으로 구현하고  
func reduce(arr:[Int]) -> Int {
    var result:Int = 0
    for x in arr {
        result = result + x
    }
    return result
}

// 초기값 할당과 누적하는 행위를 파라미터로 분리하고 
func reduce(arr:[Int], initial:Int, combine:(Int,Int)->Int) -> Int {
    var result:Int = initial
    for x in arr {
        result = combine(result, x)
    }
    return result
}

// 제네릭 적용 
func reduce<T, U>(arr:[T], initial:U, combine:(U, T)->U) -> U {
    var result:U = initial
    for x in arr {
        result = combine(result, x)
    }
    return result
}  

지금까지 Swift 표준라이브러리의 Map, Filter, Reduce에 대해 알아보았습니다.




'Swift' 카테고리의 다른 글

[swift2] Xcode7 beta2 Swift 언어 변경사항  (0) 2015.06.24
[swift] 문자열 Indexing과 Slicing  (0) 2015.06.23
[swift] Map, Filter, Reduce  (0) 2015.06.22
[swift2] New in Swift2.0  (0) 2015.06.19
[swift] Apple Swift Blog  (0) 2015.06.10
[swift] Swift 기초 - 4  (0) 2014.10.29