De-Note sparkafka's dev blog

[Scala] 커링(Currying)에 대한 고찰

들어가며

예전에 잠시 Scala를 공부했을 때에도 Currying에 대해 배웠다. 그 때는 단순히 함수의 입력값이 정해져 있을 때 커링을 사용하면 코드를 줄일 수 있다 정도로 알고 넘어갔다. 솔직히 이게 중요한 내용인 지도 잘 납득이 되지 않았다. 그런데 이번에 Coursera 강의를 들으며 커링에 대해 이론적인 내용이 나왔는데 흥미로워서 정리를 하고자 한다.

Coursera의 Functional Programming Principles in Scala 및 다른 곳의 내용을 참조하였다.


Currying

  • 일급 객체(first-class object)와 고차 함수(higher order function)

함수형 언어들은 함수를 일급 객체로 취급한다. 일급 객체란, 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다(위키백과). 이것은 함수를 다른 value들과 똑같이 취급한다는 뜻이다. 함수가 함수의 인자로 전달이 될 수도 있고, 결과값으로 반환이 될 수도 있다.

다른 함수를 인자로 받거나, 함수를 반환하는 함수를 고차 함수라 한다.

// 고차 함수의 예
def sum(f: Int => Int, a: Int, b: Int) = {
  def loop(a: Int, acc: Int): Int =
    if (a>b) acc
    else loop(a+1, f(a)+acc)
  loop(a, 0)
}

위 함수는 a에서 b까지의 정수를 f 연산을 해준 뒤 모두 더한 값을 반환하는 함수이다. 이 함수를 활용해보자.

// 1부터 10까지의 정수 합
sum(x => x, 1, 10)
// 1부터 4까지 정수의 제곱의 합
sum(x => x * x, 1, 4)
val res0: Int = 55
val res1: Int = 30

위와 같이 인자로 들어가는 함수에 따라 다른 함수를 다양하게 활용할 수 있는 것이 고차 함수를 쓰는 이유이다.
sum 함수를 이용하여 함수를 인자로 받지 않는 새 함수를 만들어보자.

def sumInts(a: Int, b: Int) = sum(x => x, a, b)
def sumCubes(a: Int, b: Int) = sum(x => x * x * x, a, b)

sumInts(1, 10)
sumCubes(1, 3)
val res2: Int = 55
val res3: Int = 36

이런 식으로 함수 인자를 미리 넣은 함수를 만들 수 있다.

  • Currying

이제 sum 함수를 함수를 반환하는 함수로 다시 써보자.

// 함수 반환하는 함수
def sum(f: Int=> Int): (Int, Int) => Int ={
  def sumF(a: Int, b: Int): Int =
    if (a>b) 0
    else f(a) + sumF(a+1, b)
  sumF
}

위 함수는 정수 하나를 인자로 받고 정수를 반환하는는 함수 f를 인자로 받아서 정수 2개를 인자로 받아 정수를 반환하는 함수이다. 함수의 입력, 출력을 간단히 표현하면 다음과 같다.

(Int => Int) => ((Int, Int) => Int)

이 함수로 함수를 만들어보자.

def sumInts = sum(x=>x)
def sumCubes = sum(x=>x*x*x)

sumCubes(1, 10)
sumInts(10, 20)
val res0: Int = 3025
val res1: Int = 165

그런데 sumCubes, sumInts와 같은 함수의 선언 없이 바로 써먹을 수 있을까? 할 수 있다.

def cube(x: Int): Int = x * x * x
sum(cube) (1, 10)
val res2: Int = 3025

Scala에서는 왼쪽에서 오른쪽으로 함수를 적용시킨다. sum(cube)에서 sumcube에 적용되고 sum of cubes 함수를 반환한다. 이 함수가 결국 sumCubes 함수와 동일하다. 그리고 이 함수가 (1,10) 인자에 적용된다. 결국

sum(cube)(1,10) == (sum(cube))(1,10)

이다.

이것을 일반화해보자.

def f(a1)(a2)...(an) = Expr

def f(a1)(a2)...(an-1) = {
  def g(an) = {
    Expr
  }
  g
}

와 동일하다. 또

def f(a1)(a2)...(an) = Expr

def f = (a1 => (a2 => ... => an => E)...))

와 동일하다.


마치며

Currying의 이론적인 부분에 대해 알아보았다. Scala에서 왼쪽에서 오른쪽으로 함수가 적용된다는 것이 중요하다. Currying을 사용해 더 유연하고 깔끔한 프로그래밍이 가능하다.