Swift :: 1.2의 커리드함수와 함수 합성에 대해

커링

Swift 1.2부터는 커리드 함수를 만들 때 파라미터 이름을 생략할 수 있게 되었다.1 함수를 커리드함수로 정의할 때

func f(a:Int)(b:Int)(c:Int) -> Int {
    return a * 2 + b * 3 + c * 5
}

let a = f(1)
let b = a(b:2)
let c = b(c:3)

이런식으로 커리드된 함수에서는 파라미터 이름을 반드시 넣어야 했는데, 이렇게 부분적용함수의 나머지 인자에 대해 반드시 이름을 쓰는 것만 허용이 됐었다. 이 경우에는 부분적용함수를 실제로 호출하는 경우, 반드시 해당 파라미터 이름을 기술해야하므로, 원 함수의 시그니쳐를 알지못하면 쓰기가 어렵기 때문에 활용할만한 곳이 많이 않았다. 하지만 Swift 1.2에서부터 이런 제한이 없어지고, 아래와 같이 2차 이후 부터의 인자 외부 이름을 생략하는 것이 허용됐다.

func f(a:Int)(_ b:Int)(_ c:Int) -> Int {
    return a * 2 + b * 3 + c * 5
}

let r = f(2)(3)(4)

파라미터 이름을 생략할 수 있다는 것은 임의의 커스텀 연산자나 함수를 인자로 받는 함수에 이런 부분 적용함수를 쉽게 던져줄 수 있다는 것이다. 다음의 재밌는 예를 한가지 살펴보자. 여기서는 로그인 처리를 위한 일련의 코드를 쓴다.

함수 login은 이메일, 비밀번호를 부여받고, 성공 핸들러를 받아, 인증 후 결과를 피드백할 수 있는 형태의 함수이다.

func login(email:String, pw:String, success:(Bool) -> ()) {
    // 입력된 email, pw를 비교
    let auth = authenticate(email, pw)
    // 비교 결과에 따른 처리.
    success(auth)
}

그리고 이메일과 pw는 각각 사용자로부터 입력받는다. 이 때 유효성 검사라든지 중도 취소 등을 고려한다면, 이메일과 pw를 얻는 함수는 각각 String? 타입을 리턴한다고 볼 수 있다.

func getEmail() -> String? { ... }
func getPW() -> String? { ... }

이제 이메일과 PW를 각각 입력받아 로그인을 처리하는 전통적인 코드는 다음과 같은 식으로 쓰여질 것이다. 그리고 각각의 요소에 입력 실패하는 경우를 생각해보자.

if let email = getEmail() {
    if let pw = getPW() {
        login(email, pw){
            println($0? "success" : "fail")
        }
    } else {
        println("fail")
    }
    println("fail")
}

물론 Swift 1.2에서는 if 절 하나에서 두 개 이상의 옵셔널 바인딩을 지원하기 때문에 코드를 더 많이 줄일 수 있다.

if let email = getEmail(), pw = getPW() {
    login(email, pw) { println("success: \($0)")}
} else {
    println("fail")
}

Swift의 옵셔널은 모나드인 동시에 Applicative Functor의 특징과도 닮아있다. 그래서 Haskell에서 쓰이는 연산자 <*>를 똑같이 정의해 볼 수 있다. 이 연산자는 옵셔널인 함수와 옵셔널인 값을 연결하여, 두 피 연산자가 모두 nil이 아닌 유효한 값이 있는 경우 이를 처리하고, 어느 하나가 nil인 경우에는 nil을 리턴하게 한다.

infix operator <*> { associativity left precedence 170 }
func <*> <A, B> (lhs:((A) -> B)?, rhs: A?) -> B? {
    if let f = lhs, x = rhs {
        return f(x)
    }
    return nil
}

그리고 login 함수는 이제 커리드 함수로 바꿔서 정의한다.

func login(email:String)(_ pw:String)(_ success:(Bool) -> ()) {
    let auth = authenticate(email, pw)
    sucess(auth)
}

이제 다음과 같은 스타일로 쓰는 것이 가능하다. 언뜻보면 조금 이상해보일 수 있는데…

if f = login <*> getEmail() <*> getPW() {
    f { println($0? "success" : "fail") }
}

우선 첫번째 연결에서 login 과 getEmain()을 연결한다. 이 때 만약 getEmail()이 nil을 리턴한다면 유효하지 않은 연산으로 간주, 그 이후의 연산은 nil <*> getPW()가 되어 패스워드값 반환 여부에 무관하게 f는 nil이 되어 if 절 내부는 실행되지 않는다.

이는 위에서 언급한 if 절에서 2중으로 바인딩한 것과 비슷한 효과를 내지만 시각적으로 깔끔하고 읽기 쉬운 코드가 된다.

파라미터 명을 신경 쓸 필요가 없는 커리드 함수는 함수 합성과 관련지어 생각해도 재밌게 활용할 수 있다.


infix operator ° { associativity right precedence 200 } func ° <A, B, C> (lhs: (B) -> C, rhs: (A) -> B) -> (A) -> C { return { (x:A) -> C in lhs(rhs(x)) } } infix operator ⬅ { associativity right precedence 190 } func ⬅ <A, B> (lhs: (A) -> B, rhs: A) -> B { return lhs(rhs) } infix operator ➡ { associativity left precedence 190 } func ➡ <A, B> (lhs: A, rhs: (A) -> B) -> B { return rhs(lhs) }

위 코드는 세 개의 연산자를 새로 정의하는데, 첫번째는 중학교 수학시간에 배우는 함수 합성 연산자이고, 다른 두 개는 함수의 왼쪽과 오른쪽에서 인자를 함수로 넣을 수 있는 연산자이다.

이번에는 fmap 함수를 하나 적용해보자. 이는 특정한 컨텍스트에 변형함수를 사상(map)하는 함수이다. 시퀀스타입들의 map은 이 fmap의 특별한 형태라 할 수 있다.

func fmap <A, B> (transform:(A) -> B)(_ value:[A]) -> [B] {
    return value.map(transform)
}

func fmap <A, B> (transform:(A) -> B)(_ value:A?) -> B? {
    if let v = value {
        return transform(v)
    }
    return nil
}

fmap 함수는 리스트의 map 메소드와 비슷한데, 배열이나 옵셔널에 대해서 함수를 적용하는 것이다.

이제 위에서 만들어 놓은 몇 가지 연산자들을 조합해서 사용해보자.

let f = { (a:Int) -> Int in a + 2 }
let g = { (a:Int) -> Int in a * 2 }

//  합성함수 만들기 
let h =  f ° g ° f
let i = g ° f ° g

//  적용하기 
let j = f ° g ° f ⬅  120
print(j)
let k = 120 ➡  f ° g ° f 
print(k)

//  합성함수를 fmap 을 통해서 사상하기 
let z = fmap (f ° g) ⬅ [4, 16]
print(z)

보다 수학 노트처럼 보이는 함수 적용 코드가 만들어진다. 이 때 fmap 은 “함수”를 하나 받으면 부분적용 함수를 만들게 되기 때문에 함수를 여러 번 매핑할 필요 없이 합성함수를 만들 수 있게 된다. 또 fmap 그자체로 함수 하나를 받으면 역시 함수를 리턴하기 때문에 다른 함수와 합성하는 것도 가능하다. 아래의 예는 좀 어거지 같지만 그것을 보여준다. 리스트를 받을 수 있는 fmap 은 리스트를 리턴할 수 있기 때문에 두 fmap 부분적용함수는 합성이 가능하다.

let y = fmap {8 * $0} ° fmap (f ° g) ⬅ [1,2,4]
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
print(y)

이를 원래 map 메소드를 이용한다면, 아래와 같이 쓰인다.

let y = [1, 2, 4].map{ f(g($0))}.map{ 8 * $0 }

이 된다. 후자의 경우, jQuery와 비슷한 문법이 되는데, 내 경우에는 어지간해서는 읽기가 너무 불편하여 위쪽의 코드가 더 마음에 드는 편이다.

또 이 합성 코드의 강력한 점은, 동일한 코드를 사용하면서 피 연산자를 리스트가 아닌 옵셔널을 던져줄 때, 그대로 적용이 가능하다는 것이다. (왜냐면 fmap 함수가 옵셔널에 대해서도 정의되어 있으니) 이는 각각의 변형 함수를 리스트용, 옵셔널용을 따로 정하지 않아도, 계산 컨텍스트 처리가 fmap 에 위임되기 때문에 가능한 것이다.

let u: Int? = 4
let y2 = fmap {8 * $0} ° fmap (f ° g) ⬅ u
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
print(y2)
//Optional(80)

Swift 자체가 완전한 함수형 언어는 아니지만, 자체적으로 지원하는 기능들을 이용하면 함수형 언어들이 제공하는 깔끔한 문법을 쉽게 흉내낼 수 있다. 이러한 코드들은 그 저변에 함수형 패러다임의 의식 흐름을 전제하고 있기 때문에 연습해보는 가치가 충분히 있다고 생각된다.


  1. 이전에는 나뉘어진 튜플을 이용해서 함수를 선언하는 경우, 두 번째 이후부터는 호출시에도 반드시 파라미터 명을 넣어주어야 했고, “_”를 이용해서 외부 이름을 강제로 생략하는 것이 불가능했다.