콘텐츠로 건너뛰기
Home » Swift에서 Monad와 Curried Function 사용하기

Swift에서 Monad와 Curried Function 사용하기

Functional Programing in Swift

Swift는 함수형 프로그래밍은 아니지만 함수형 언어들의 특성을 많이 따르고 있다. 그 중에 모나드와 커리드함수(부분적용함수)를 만드는 방법에 대해 살펴보자.

모나드(Monads)

모나드는 특정한 상태로 값을 포장한 것을 말한다. 예를 들어 하스켈의 Maybe 타입이 이에 해당하는데 이는 값을 “있을지도 없을지도 모르는 상태”속에 포장한다음, 이를 일반함수에 연결하는 것이다. 쉽게 말하자면

  1. 특정한 상태에 포장된 값이 있다.
  2. 일반값을 받고 포장된 값을 리턴하는 함수가 있다.
  3. 1을 2에 대입하면 포장된 상태 내부의 값을 조작하는 효과를 얻을 수 있으며, 계속해서 포장된 상태를 유지할 수 있다.

이를 위해 하스켈에서는 >>=이라는 연산자를 정의하고 있다. 이는 좌변의 값을 우변의 함수에 주입한다. 이를 통해서 함수를 연쇄적으로 적용하는 표현식을 만들 수 있다. Swift 버전의 >>=를 정의해보도록 하자.

연산자 정의

infix operator >>= { associativity left }
func >>= <T, U>(lhs:T, rhs:(T)->U) -> U {
    return rhs(lhs)
}

연산자를 위와 같이 정의했다. 이 연산자는 왼쪽우선이다. 예를 들어 다음과 같은 함수가 있다고 하면

func addOne(i:Int) -> Int {
    return i + 1
}

다음과 같이 쓸 수 있다는 이야기다.

let two  = 1 >> = addOne
let four = 1 >> = addOne >> = addOne >> = addOne

모나드 타입

하스켈의 Maybe 타입은 그 자체로 모나드이다. (하스켈에서는 타입 Maybe가 Monad 타입 클래스를 따른다고 말한다. 하스켈에서 타입클래스는 일종의 프로토콜이라 할 수 있다.) 이와 유사한 개념이 Swift에도 있으니 (아마도 하스켈에서 따왔을 것이라 보는데) 바로 옵셔널이다. Swift의 타입들은 기본적으로 nil 값을 가질 수 없는데, nil일 수 있는 (nil은 objective-C에서는 포인터의 개념이었으나, Swift에서는 “값이 없는 상태”를 의미한다) 케이스는 얼마든지 있으므로 모든 타입에 대해 옵셔널타입을 만들 수 있다. 따라서 옵셔널 타입은 “값이 있을 수도 없을 수도”있는 상태(혹은 컨텍스트)에 값을 포장해 넣은 상태이고, 이는 개념적으로 모나드와 동일하다.
막대를 들고 줄을 타는 사람을 모델링해보자. 막대의 양 끝에는 새가 앉을 수 있다고 가정한다. 만약 왼쪽과 오른쪽의 새의 수가 4마리 이상 차이나면 균형을 잡기 힘들어서 떨어진다고 하자. (떨어지고나면 막대가 존재하지 않으므로 nil이다) 이것을 표현하는 데이터 타입을 클래스나 구조체가 아닌 단순 튜플로도 설명할 수 있다.

(left, right)

이는 정수 2개로 이루어진 튜플이 되는데, 이에 별칭(typealias)를 붙여보도록 하겠다.

typealias Pole = (Int, Int)

그리고 막대에 새가 앉는 것을 두 개의 함수 landLeft, landRight로 표현해보자. 고전적인 C형태의 함수로는 다음과 같이 표현할 수 있을 것이다.

func landLeft(birds:Int, pole:Pole?) -> Pole? {
    if let (x, y) = pole {
        if abs((x + birds) - y) < 4 {
            return (x+birds, y)
        }
    }
    return nil
}
func landRight(birds:Int, pole:Pole?) -> Pole? {
    if let (x, y) = pole {
        if abs(x - (y + birds)) < 4 {
            return (x, y+birds)
        }
    }
    return nil
}

이제 빈 막대에서 다음과 같이 새가 앉은 후의 결과를 생각해보자.

  1. 왼쪽에 3마리
  2. 오른쪽에 2마리
  3. 오른쪽에 2마리
  4. 왼쪽에 1마리
  5. 왼쪽에 2마리

순서대로 앉음을 구현하면 다음과 같은 코드로 표현된다.

let poleA = landLeft(2, landLeft(1, landRight(2, landRight(2, landLeft(3, (0,0))))))

이건 너무 괄호 지옥이다. 이걸 앞서 만든 >>= 연산자를 사용해서 연쇄적으로 표기할 수 있을까? >>= 연산자를 사용하려면 (Pole?)->Pole? 타입의 함수가 필요하다. 그리고 이 함수는 결국 앞에서 정의한 두 함수를 호출해야 한다. 여기서 필요한 개념이 커링이 된다. 커링은 파라미터가 여럿있는 함수에 대해 파라미터를 일부만 적용한 중간단계의 함수를 만들 수 있는 것을 의미한다. 위 함수를 오버로딩해서 (Pole?)->Pole? 타입의 함수를 내놓는 부분적용 함수를 만들어 보자.

func landLeft(birds: Int) -> (Pole?) -> (Pole?) {
    let f:(Pole?)->(Pole?) = { pole in
        return landLeft(birds, pole)
    }
    return f
}
func landRight(birds: Int) -> (Pole?) -> (Pole?) {
    let f:(Pole?)->(Pole?) = { pole in
        return landRight(birds, pole)
    }
    return f
}

이 함수들은 첫번째 인자, 즉 새의 수를 받아서, 위에서 우리가 원하는 타입의 함수를 내놓는다. 그리고 같은 이름을 사용하기는 했지만, 시그니처 타입이 다르기 때문에 오버로딩되어 사용할 수 있다. 이제

let B = (0, 0) >>= landLeft(3) >>= landRight(2) >>= landRight(2) >>= landLeft(1) >>= landLeft(2)

이렇게 쓸 수 있다.
그런데 이런 식으로 함수를 체이닝하고 싶을 때마다 오버로딩해야 하는 불편할 수 있다. 공식문서에서는 아직 언급되지 않았지만 (이글의 작성일은 2014년 8월 22일) Swift는 각 파라미터들을 따로 괄호에 싸는 것으로 커링함수를 정의할 수 있다.
위 land… 함수들을 새로 정의해보자.

오버로딩이 아닌 완전히 새로 정의하는 것이다. 만약 playground에서 작성했다면 최초의 함수를 수정하고, 두 번째 함수를 삭제하라.

사실 함수의 본체는 똑같고 정의 부분만 “작은” 차이가 있다.

func landLeft(birds:Int)(pole:Pole?) -> Pole? {
//                     ~~~~~~~~~~~~
    if let (x, y) = pole {
        if abs((x + birds) - y) < 4 {
            return (x+birds, y)
        }
    }
    return nil
}
func landRight(birds:Int)(pole:Pole?) -> Pole? {
//                      ~~~~~~~~~~~~
    if let (x, y) = pole {
        if abs(x - (y + birds)) < 4 {
            return (x, y+birds)
        }
    }
    return nil
}

단지, 파라미터를 구분했을 뿐이다. 자, 이제 이 함수들은 이렇게 바뀐다.

  1. landLeft() 함수는 (Pole?)->(Pole?) 타입을 리턴하게된다.
  2. 이 때 생성되는 함수들은 반드시, 외부파라미터 이름을 사용해야 한다.
  3. 하지만 앞서 만든 >>= 연산자를 사용할 때는 외부 파라미터 이름을 쓰지 않아도 된다.

다음과 같이 테스트해보자.

let land2BirdsAtLeft = landLeft(2)
let poleC = land2BirdsAtLeft(pole:(0, 0)) // -> (2, 0)
let land5BirdsAtRight = landRight(5)
let poleD = land5BirdsAtRight(pole:poleC) // -> (2, 5)

따라서 위에서 사용한 모나드 체인이 그대로 적용될 수 있다.

let E = (0, 0) >>= landLeft(3) >>= landRight(2) >>= landRight(2) >>= landLeft(1) >>= landLeft(2)

이 예시는 파라미터 2개짜리였지만, 그 이상의 파라미터를 받는 경우에도 가능하다.

func sumOfThreeInts(first: Int, second: Int, last: Int) -> Int {

대신에

func sumOfThreeInts(#first: Int)(second: Int)(last: Int) -> Int {
    return first + second + last
}

이렇게 정의하면

let add2 = sumOfThreeInts(first:2) // (Int)(Int) -> Int
let add2And5 = add2(second:5) // (Int) -> Int
let sum = add2And5(last: 9) // 16

이렇게 사용할 수 있는 것이다.

관련 예제 코드


/*
모나드는 특정한 타입을 감싸는 타입이며,
raw한 값을 감싸는 함수와
raw한 값을 모나드 값으로 바꾸는 어떤 함수에 바인딩된다.
이를 바탕으로 모나드 프로토콜을 정의하면 다음과 같다.
*/
protocol Monad {
typealias Element
static func _return_ (n:Self.Element) -> Self
func _bind_ (f:(Self.Element) -> Self) -> Self
}
/*
위 정의를 바탕으로 하스켈의 Maybe 타입을
흉내내어 본다.
이는 대수적으로는 Just A, Nothing 두 가지로 구분되므로
enum 타입이다.
*/
enum Maybe<T>: Monad, Printable {
typealias Element = T
case Just(Element)
case Nothing
// raw 한 값을 모나드값으로 만드는 함수 monad(_:)를
// 외부에서 별도 정의하는데, 이는 모나드 값을 만드는
// 아래 타입 메소드를 호출하게 된다.
static func _return_ (n:Maybe.Element) -> Maybe {
return .Just(n)
}
// 모나드 값을 함수에 바인딩하는 연산자 >>= 를 위한 함수
func _bind_ (f:(Element) -> Maybe) -> Maybe {
switch self {
case .Just(let a):
return f(a)
default:
return .Nothing
}
}
// println 문에 적용
var description: String {
switch self {
case .Just(let a):
return "Just \(a)"
default:
return "Nothing"
}
}
}
// 바인드 연산자
infix operator >>= { associativity left }
// 바인드 연산자는 모나드 타입 내 `_bind_` 메소드를 호출하고 끗.
func >>= <T, U:Monad where U.Element == T>(l:U, r:(T) -> U) -> U {
return l._bind_(r)
}
// raw 값을 모나드 값으로 바꾼다.
// 이 함수의 반환형은 좌변값의 타입에 의해 결정된다.
func monad <T, U:Monad where U.Element == T>(a:T) -> U {
return Maybe<T>._return_(a)
}
// 테스트용 함수 2개. 모나드에 바인딩될 수 있는 타입이다.
func increase(n:Int) -> Maybe<Int> {
return .Just(n + 1)
}
func fail(n:Int) -> Maybe<Int> {
return .Nothing
}
// 정수를 감싸는 모나드 타입 테스트
let a: Maybe<Int> = .Just(1)
let b = a >>= increase
let c = a >>= increase >>= increase
let d = a >>= increase >>= fail // .Nothing이 된다.
let e = a >>= increase >>= fail >>= increase
// ^~~~~ 여기서 .Nothing이 되었으므로 이후의
// 모든 바인딩에 대해서는 아무련 효력이 없다.
println([a, b, c, d, e])
// [Just 1, Just 2, Just 3, Nothing, Nothing]
typealias Pole = Maybe<(left:Int, right:Int)>
func leftLand(birds:Int)(pole:(Int, Int)) -> Pole {
if let (x, y) == pole {
if abs((x + birds) – y) < 4 {
return .Just((x+birds, y))
}
}
return .Nothing
}
/*:
모나드의 멋진점은 연속된 일련의 처리 중에 실패하는 경우(값이 없거나 유효하지 않게 되거나)에
이후의 흐름을 별도의 분기처리없이 함수를 연속적으로 체이닝하는 것으로 처리할 수 있다는 점이다.
이 문서의 이전 버전에서는 막대기를 들고 줄타기를 하는 중에 새가 막대 양쪽에 앉는 경우를 가정했었다.
이 부분을 커스텀 모나드로 적용해 보도록 하자.
** Swift의 커리드 함수는 반드시 인자명을 넣어야 해서 >>= 연산자의 적용을 할 수 없다. 따라서
클로저를 리턴하는 함수를 간접적으로 만들어야 한다.
*/
typealias Pole = Maybe<(Int, Int)>
func landLeft(n:Int) -> ((Int, Int)) -> Pole {
// ~~~~~~~~~~~~~~~~~~~~~
// raw한 값을 받아 모나드 값을 리턴하는 것이 포인트!
return {
(p:(Int, Int)) -> Pole in
let (x, y) = p
if x + n – y > 4 {
return .Nothing
}
let r = (x + n, y)
return .Just(r)
}
}
func landRight(n:Int) -> ((Int, Int)) -> Pole {
// ~~~~~~~~~~~~~~~~~~~~~
return {
(p:(Int, Int)) -> Pole in
let (x, y) = p
if y + n – x > 4 {
return .Nothing
}
let r = (x , y + n)
return .Just(r)
}
}
let pole:Pole = monad((0, 0))
let result = pole >>= landLeft(3) >>= landRight(5) >>= landRight(1) >>= landLeft(6)
println(result)
let result2 = pole >>= landLeft(3) >>= landRight(4) >>= landRight(5) >>= landLeft(7)
println(result2)
/*
Swift의 옵셔널타입도 모나드이다.
분명한 예로 옵셔널 체이닝이 있다.
someObject.somePropertyCanBeNil?.propertyMethod()
는 somePropertyCanBeNil이 nil 이 아니면 .propertyMethod()를 호출한 리턴값을
옵셔널로 돌려주고, 그렇지 않다면 nil을 돌려준다.
모나드를 이용한 이러한 체이닝의 장점은
모나드를 특정한 컨텍스트로 봤을 떄,
순수한 값을 컨텍스트로 감싼 후 바인딩을 연속적으로 해 나가면
그 횟수만큼 감싼 깊이가 반복되는 것이 아니라,
1차적인 컨텍스트만이 적용된다는 것이다.
"실패할 수 있는" 컨텍스트는 굳이 모나드가 아니더라도 감싸기만 하면
되는 것이기는 하다. 하지만 그것을 계산과정에서 일일이 언래핑하지 않는다면
그 결과가 계속 중첩될 수 있다는 것이다.
Optional<Optional<Optional<Optional<2>>>>
같은 값을 다시 언래핑하는 것은 여간 귀찮은 일이 아니지 않겠는가.
*/
struct Pole2: Printable {
var left: Int = 0
var right: Int = 0
func landLeft(n:Int) -> Pole2? {
if left + n – right > 4 {
return nil
}
return Pole2(left:left+n, right:right)
}
func landRight(n:Int) -> Pole2? {
if right + n – left > 4 {
return nil
}
return Pole2(left:left, right:right + n)
}
var description: String {
return "Pole(\(left), \(right))"
}
}
let ps = Pole2(left:0, right:0)
let result_success = ps.landLeft(3)?.landRight(2)?.landRight(4)?.landLeft(7)
let result_fail = ps.landLeft(3)?.landLeft(12)?.landRight(5).randRight(10)
// ^^^^^^^^^^^^^ 이시점에서 떨어져서 사망… nil이 된다.
println(result_success) // Optional(Pole(10, 6))
println(result_fail) // nil

view raw

Monads.swift

hosted with ❤ by GitHub