스칼라 함수형 프로그래밍 - 커링(Currying)이 불러온 착각. 언어가 기본 지원하는 제어 구조 처럼 보이는 제어 추상화

View: 189 0 0
작성자: 달빛제이크
카테고리: Scala Language
발행: 2024-06-28 수정 2024-06-29

안녕하세요. 달빛제이크입니다.

스칼라 코드를 보면 분명 함수이고 소괄호 '()' parentheses로 묶여 전달되어야 할 인수 부분에 중괄호 '{}' curly braces와 함께 긴 코드가 적혀있는 모습을 많이 볼 수 있습니다. 어떤 형태는 소괄호에 묶여 일부 인수가 전달되고, 그 뒤를 따라 중괄호에 마찬가지로 긴 코드가 작성되어 있습니다. 처음 스칼라로 코딩을 할 때 도대체 이런 형태가 어떻게 가능한지, 스칼라에서는 이렇게도 작성할 수 있는지 당황하게 되는데 커링(Currying)과 중괄호 사용 문법이 있어서 가능한 형태입니다.

1. 커링(Currying)

커링은 함수형 프로그래밍에서 사용하는 중요한 기법으로, 여러 개의 파라미터와 함께 선언된 함수를 한 개의 파라미터를 갖는 함수들의 연속으로 변환하는 과정을 의미합니다. 보통 여러 개의 파라미터를 소괄호 하나로 감싸는 형태에서 소괄호를 두 개 이상 사용해서 파라미터를 나누는 형태를 가지고 있습니다. 이 기법의 이름은 수학자 Haskell Curry의 이름을 따서 명명되었으며, 대표적인 함수형 프로그래밍 기법으로 널리 사용되고 있습니다. 수학적 표현으로 f(x, y, z) = f(x)(y)(z)로 나타낼 수 있습니다.

// 일반적인 함수 표현
def sum(x: Int, y: Int, z: Int): Int = x + y + z
val result = sum(1, 2, 3)    // 6

// 커링을 적용한 함수 표현
def curriedSum(x: Int)(y: Int)(z: Int): Int = x + y + z
val res = curriedSum(1)(2)(3)    // res = 6

// 커링 함수의 순차적 진행 과정
val a = curriedSum(1)    // a: (y: Int)(z: Int) => 1 + y + z
val b = a(2)     // b: (z: Int) => 1 + 2 + z
val c = b(3)    // 6

예제에서 커링을 적용했을 경우 각각의 파라미터를 나누어 순차적으로 인수를 전달하고 있습니다. 인수를 전달 받은 함수는 나머지 파라미터와 함께 함수 값을 반환하며 동일한 과정으로 나머지 인수를 처리합니다. 함수에서 선언한 모든 파라미터에 해당 인수들을 함께 전달하면 이와 같은 과정이 중간 과정 없이 한번에 진행되지만, 인수를 나누어 전달할 경우 부분 적용 함수(Partially applied function)와 유사하게 동작하는 것처럼 보입니다. 이것을 우리는 Eta Expansion(Eta 확장, Eta는 그리스 문자 η)으로 부르며, 다음과 같은 변환 과정을 의미합니다.

// 일반 함수
def add(x: Int, y: Int): Int = x + y

// Eta Expansion
(x: Int) => (y: Int) => add(x, y)

즉, Eta Expansion은 함수형 프로그래밍에서 함수의 형태를 변환하여 함수의 인수를 명시적으로 받는 함수로 만드는 과정이고, 부분 적용 함수는 함수의 일부 인수 만을 적용하고 나머지 인수를 나중에 적용할 수 있도록 새로운 함수를 생성하는 기법입니다. Eta Expansion은 Currying을 처리하는 과정에 적용된 개념이고 실제로 우리가 집중해야 하는 부분은 Currying의 사용 형태와 함수에 전달할 인수를 넘길 때 중괄호(Curly braces)를 사용할 수 있다는 점입니다.

2. 중괄호(Curly braces)를 사용하여 인수 전달하기

커링이 함수 선언부에서 사용하는 기법이라면, 중괄호는 함수를 호출할 때 인수에 적용하는 문법입니다. 스칼라에서 제공하는 중괄호에 대한 문법은 매우 간단합니다.

  1. 함수에 인수를 넘길 때 소괄호 대신 중괄호를 사용할 수 있다.
  2. 중괄호를 사용할 때 인수는 한 개의 값 만 가질 수 있다.

예제를 통해 살펴 보겠습니다.

// 한 개의 파라미터를 갖는 함수
def saySomething(something: String) = println(something)
saySomething("World peace!!")    // 일반적인 함수 호출 정상 동작
saySomething{"World peace!!"}    // 중괄호 사용 정상 동작

// 두 개의 파라미터를 갖는 함수
def sum(x: Int, y: Int) = x + y
sum(1, 2)      // 3, 일반적인 함수 호출 정상 동작
sum{1, 2}    // error, 중괄호 내 인수는 한 개만 가능

// Currying 적용 함수
def sum(x: Int)(y: Int) = x + y
sum(1)(2)    // 3, 정상 동작
sum{1}(2)    // 3, 정상 동작
sum(1){2}    // 3, 정상 동작
sum{1}{2}    // 3, 정상 동작

// 첫 번째 파라미터에 익명 함수 적용
def operation( x: Int => Int)(y: Int) = x(y)
operation(x => x + 1)(2)    // 3, 정상 동작
operation { x =>
  x + 1 } (2)    // 3, 정상 동작
operation(x => x + 1){2}    // 3, 정상 동작
operation(x =>
  x + 1) {2}    // 3, 정상 동작

// 두 번째 파라미터에 익명 함수 적용
def oper(x: Int)(y: Int => Int) = y(x)
oper(3) { x => 
  x + 1 }    // 4, 정상 동작
oper{3} ( x =>
  x + 1 )    // 4, 정상 동작
oper{3}{ x=>
  x + 1 }    // 4, 정상 동작

중괄호에 한 개의 인수만 사용하면 순서에 상관없이 자유롭게 사용할 수 있습니다. 그런데, 소괄호와 별반 다를 게 없는 중괄호를 왜 사용할까요? 중괄호를 사용하면 다음과 같은 표현이 가능합니다.

def oper(x: Int)(y: Int => Int) = y(x)

oper(3) { x =>
  val a = 1
  val b = 2
  x + a + b }    // 6, 정상 동작

oper(3) ( x =>
  val a = 1
  val b = 2
  x + a + b )    // error

예제에서 확인할 수 있듯이, 중괄호를 사용하면 중괄호 내부에 함수를 직접 작성할 수 있고 다양한 표현이 가능합니다. 그래서 보통 스칼라에서는 함수 리터럴을 파라미터로 받는 함수를 호출 할 때 functionName{ } 또는 functionName(){ }의 형태로 많이 사용하고 간단한 함수 리터럴 뿐만 아니라 복잡한 형태의 함수 표현도 가능합니다. 이는 제어 추상화의 일반적인 모습으로 스칼라 프로그래밍에서 매우 많이 사용하는 코드의 형태입니다.

마무리

스칼라 언어에서는 커링을 통해 함수의 인수를 하나씩 적용할 수 있도록 변환하여 부분 적용, 고차 함수 사용, 함수 조합 등을 용이하게 하고, 함수 호출 시 중괄호를 사용하여 코드 블록을 인수로 전달할 수 있으며, 복잡한 표현식을 처리할 수 있습니다. 이는 커링과 중괄호를 통해 구현 가능한 제어 추상화는 불필요한 코드 사용을 줄이고 코드의 가독성과 재사용성, 유연성을 높이는 데 중요한 역할을 합니다.

다음 글에서는 제어 추상화의 마지막 주제인 By-name parameters에 대해 이야기 하겠습니다. 감사합니다.

comments 0