이번 포스트에서는 함수형 프로그래밍에서 중요한 개념 중 하나인 커링(Currying)에 대해서 알아보고 이를 swift로 구현을 해보도록 하겠습니다.

  • Currying이란?
    Currying은 다인자 함수를 단인자 함수를 여러개 연결한 형태로 변환하는 것입니다. 이러한 변환은 다음과 같은 유용성을 가질 수 있습니다.

    1. 인자 수를 자유롭게 조절함으로써 함수를 인자로 받는 프레임워크 등에서 요구하는 형태로 사용자의 함수를 손쉽게 변환할 수 있습니다. 커링이 없다면 해당 인자를 받지 않는 형태의 별도 함수를 정의해야 합니다.

    2. 특정 인자가 반복적으로 사용될 경우, 해당 인자를 반복적으로 넘기지 않고도 재활용할 수 있습니다. 이는 코드의 중복을 줄여줍니다.

    3. 외부상태를 커링으로 주입하면(함수 내에서 상태를 변경하지 않는 경우 한정), 비순수함수가 가지는 장점인 매개변수가 적다는 장점(매개변수 대신 외부 상태를 직접 참조하므로)을 누릴 수 있습니다.

  • 커링의 구현 in Swift
    Swift의 함수는 클로저 타입으로 나타낼 수 있습니다. 예를 들어 두개의 Int 인자를 받아서 Int를 반환하는 함수를 커링하는 함수를 다음과 같이 표현할 수 있습니다.

      func curry(_ closure: (Int, Int) -> (Int), _ element: Int) -> (Int) -> (Int) {
          let result = { a in 
              closure(element, a)
          }
          return result
      }
    

    하지만 이대로는 커링을 제대로 사용하기가 어렵습니다. 연쇄가 불가능하기 때문입니다. Swift의 클로저는 멤버변수나 인스턴스 메소드 같은 것을 가지지 않기 때문에 이를 포장할 필요가 있습니다.

      struct Function2 { // Int 인자 2개를 받아 Int를 반환하는 함수를 객체화 시킨 것
          let closure: (Int, Int) -> (Int)
    
          // 커링하는 함수, Function1에도 같은 패턴의 함수가 존재한다.
          func curry(_ a: Int) -> Function1 { // Function1: Int 인자 하나를 받아서 Int를 반환하는 함수를 객체화 시킨 것
              let result = { b in
                  self.closure(a, b)
              }
    
              return Function1(closure: result)
          }
    
          func trigger(_ a: Int, _ b: Int) -> Int { // Function 객체를 함수처럼 실행하기 위한 함수
              return closure(a, b)
          }
      }
    

    하지만 이렇게 하면 여러 형태의 함수들에 적용하기가 어렵습니다. 따라서 좀 더 일반화하기 위해 제네릭을 적용해보겠습니다.

      struct Function2<A, B, R> {
          let closure: (A, B) -> R
    
          func curry(_ a: A) -> Function1<B, R> {
              let result = { b in
                  self.closure(a, b)
              }
    
              return Function1(closure: result)
          }
    
          func trigger(_ a: A, _ b: B) -> R {
              return closure(a, b)
          }
      }
    

    여기에 일반 클로저를 우리가 정의한 함수 객체로 변경하기 위한 글로벌 함수를 만들겠습니다.

      func function<A, B, R>(_ function: @escaping (A, B) -> R) -> Function2<A, B, R> {
          return Function2(closure: function)
      }
    

    그렇게 되면 이제 커링을 할 준비가 되었습니다. 예제로 Swift의 max 함수를 커링해보도록 하겠습니다.

      let maxWith10 = function(Swift.max).curry(10) // 10과 대소를 비교해서 큰 수를 반환하는 함수
    
      print(maxWith10.trigger(5)) // 10
    

    위와 같은 패턴으로 다른 수의 인자를 받는 함수 객체와 글로벌 함수를 만들면 연속적으로 커링을 적용할 수 있게 됩니다. 이를 구현하고, 연산자 재정의를 통해 좀 더 래핑한 소스코드는 여기에서 확인할 수 있습니다. 커링에 관심있는 분들의 많은 사용과 피드백 환영합니다.


참고자료 Wikipedia - Currying