우연히 타입스크립트의 “튜플”에 관해 다루던 중 “Partial Appliacation”이란 개념이 등장하였고 모르고 넘어가면 섭섭하면서 동시에 찝찝하므로 다뤄보고자 한다.
해당 내용을 다루게 된 동기가 이상하긴 하지만 섭섭지 않게 배워보고자 한다.
여태껏 객체지향적인 프로그래밍과 사고속에서 코딩을 공부해와서 해당 “함수형 프로그래밍”이 굉장히 어색하게 느껴질테지만 천천히 알아보고자 한다.
커링(curry)은 프로그래밍 기법의 이름이다.
커링의 원리는 수학의 방정식에서 유래되었다.
자바스크립트로 만든 아래의 add
함수를 보자.
const add = (x,y) => x + y;
add
는 x
와 y
를 받아서 두 변수의 합을 반환한다. 이를 수학적으로 표기하면 다음과 같다.
f(x,y) = x + y // 흔히 우리가 중.고등 시절에 본 함수 식이다.
그리고 f(x,y)
역시 아래와 같이 표기할 수도 있다.
f(x,y) = h(x)(y) = x + y
여기서 h
는 f
를 다시 변환한 형태인데, 값을 하나 받아서 다른 값을 받는 함수를 반환하는 형태의 함수이다. 만약, x = 2
를 만족한다면 식은 아래와 같을 것이다.
f(2,y) = h(2)(y) = 2 + y
f(2,y)
는 물론 h(2)(y)
또한 우항 2 + y
와 동치이다. y
라는 변수를 받아 값을 반환하는 함수인 것이다.
이런 아주 간단한, 학창시절 수학시간에 배운 개념으로부터 “커링”의 이해를 시작할 수 있다.
다시 , 자바스크립트로 작성한 add
함수를 살펴보자.
const add = (x,y) => x + y;
만일 add
를 f(x,y)
형태로 표현할 수 있다면, add
는 h(x)(y)
의 형태로도 표현할 수 있어야 한다.
// Arrow function
const add = x => y => x + y;
function add(x) {
return function(y) {
return x + y;
}
}
자바스크립트는 함수가 일급 객체로 취급되기 때문에 함수가 함수를 리턴할 수 있다.
해당 내용은 고차함수라고도 부르는 HOF(Higher-Order-Function)라는 개념인데, 지금 해당 내용에 관해서 다루긴 방대하므로 따로 찾아보길 바란다. HOF 또한 함수형 프로그래밍에 쓰이는 개념이므로 알아둘 필요가 있다.
일단 위와 같이 표현된 add
함수를 보면 아래와 같이 사용될 수 있다.
function add(x) {
return function (y) {
return x + y;
};
}
const addFive = add(5);
addFive(7); //12
vsCode의 기능을 통해 정의를 살펴보면
add
함수를 호출한 변수 addFive
함수 또한 y
를 매개변수로 가지는 함수인 것을 바로 알 수 있다.
즉, 우리는 addFive(7)
을 통해 5 + 7
을 계산할 수 있는 것이다.
이 때, 꼭 알아야 할 특징은 함수를 호출하는 과정에 있어서 (물론 선언할 때도) 함수의 인자는 각 함수마다 오직 “하나”만 받을 수 있다.
결국, 커리라는 것은 하나의 인자를 받는 함수를 리턴하는 함수를 반환하는 함수를 반환하는 … (반복 가능) 함수이다. 매 턴 마다 받을 수 있는 인자는 오직 한 개 뿐이다. 그리고 한 개의 인자를 받는 해당 함수가 반환하는 함수 역시나 한 개의 인자를 받는다.
앞서 보았던 add
함수를 다시 살펴보자.
const add = (x,y) => x + y;
현재 add
함수는 x
와 y
이렇게 두 개의 인자를 받는다. 만약 세 개를 받는다면 어떨까?
const add = (x,y,z) => x + y + z
위의 구문을 “커리”로 표현한다면 어떨까?
//arrow
const add = (x) => (y) => (z) => x + y + z;
//normal
function add(x) {
return function (y) {
return function (z) {
return x + y + z;
};
};
}
const addFive = add(5);
const addSeven = addFive(7); //12
const addEight = addSeven(8); //20
console.log(addEight); //20
위와 같이 “커리” 구문으로 나타낼 수 있는데, 여기서 조금 더 단계를 세분화해 표현해보자.
const add = x => (y,z) => x + y + z
먼저 인자 하나를 받아야 하니까 x
를 받도록 하고, 나머지(여기선 y
, z
)는 리턴되는 함수가 받도록 처리한다. 그리고 해당 함수를 다시 풀어헤치는 것이다.
자, 방금 “Partial Application”을 설명했다. 위의 구문이 바로 파셜 애플리케이션 함수이다. 커리가 아니다.
갑자기 커리를 설명하다가 뜬금없이 이것이 “Partial Application”이라고 해서 굉장히 당황스러울 것이다. 하지만 분명히 저것은 커리가 아닌 “Partial Application”이다.
도대체 어떤 차이가 있는 것일까?
Currying : 하나의 인자를 받는 함수로 표현하며 그 함수는 다른 함수를 리턴하고, 리턴된 함수 역시 하나이 인자를 받는 규칙을 따른다.
Partial Application : 함수를 리턴하는 함수이지만 함수가 받는 인자는 꼭 하나일 필요는 없다. 여러 개를 받는 것이 가능하다.
// Currying function
const add = x => y => z => x + y + z;
// Partial Application function
const add = x => (y,z) => x + y + z;
const add = (x,y) => x + y;
새롭게 리턴되는 함수들은 아래의 꼴이다.const add = x => y => x + y;
(x,y)
두 인자를 받는 기존의 함수에 비해 커링과 파셜 애플리케이션은 x
, y
하나의 인자를 받는다. 즉, 적어도 1개 이상 인자를 덜 받는다고 할 수 있다.커링과 파셜 애플리케이션은 모두 게으른 평가 를 가능케 한다.
잠깐 “게으른 평가(Lazy Evaluation)”에 대해서 알아보자. 대부분의 언어에서 expression
을 만나면
“즉시 평가”하지만, 항상 효과적이진 않다. 예를 들어, 함수가 전달된 파라미터를 사용하지 않는 경우, 컴파일러가 이를 평가할 필요는 없다.
간단한 아래 코드를 보자.
function add(x,y) {
return x + 1;
}
add(2,3)
위 함수 add
는 파라미터로 x
, y
두 개를 가지지만 expression
에선 y
는 필요로 하지 않는다. 즉, 함수를 호출하는 구문 add(2,3)
에서 3
은 필요가 없다.
이때, 즉시 평가하지 않고, 필요한 것만 평가하는 방법을 “게으른 평가, 혹은 지연 평가(lazy evaluation)”라 하고, 즉시 평가하는 방법을 “strict evaluation”이라 한다.
게으른 평가(지연 평가)는 계산의 결과 값이 필요할 때 까지 계산을 늦추는 기법이다. 필요할 때 까지 계산을 늦추어 불필요한 계산을 줄임과 동시에 성능적인 면에서 효율성을 획득할 수 있다.
해당 “Lazy Evaluation”에 관한 자세한 내용은 따로 찾아보길 바란다.
그럼 아래 코드를 통해 실용적 사용 이유를 알아보자.
const getRGB = r => g => b => `rgb(${r},${g},${b})`; // use Curring
//`rgb(0,0,${b})`
const getBlue = getRGB(0)(0); // do not get red & green
const div = getElementById("myDiv");
div.addEventListener('click',()=>{
const randomColor = Math.floor(Math.random() * 255) + 0;
div.style.background = getBlue(randomColor);
});
html
에서 셀렉터로 지정한 #mydiv
를 클릭할 때 마다 파랑 계열의 색으로 배경 색이 바뀌는 기능을 수행하는 코드이다. 먼저 커리구문으로 getRGB
를 설정해놓고 원하는 색깔만 받기 위해 getBlue
, getRed
, getGreen
등을 설정할 수 있다. 커링으로 만들게 되면서 우린 DOM 엘리먼트가 클릭될 때 마다 모든 값(red , green , blue)의 평가를 할 필요가 없기 때문에 성능적인 측면에서 훨씬 효율적이다고 할 수 있다.
이처럼 우리는 커링과 파셜 애플리케이션을 통해 게으른 평가(혹은 지연 평가)에 대해 알아보았다. 물론 게으른 평가에 관해 깊게 알아보지는 않았지만 이 글을 통해 충분히 어떠한 원리인지는 이해가 갔을거라 생각한다. 커링과 파셜 애플리케이션의 핵심은 게으른 평가를 가능케 하고, 두 함수는 중첩되있는 함수(리턴되는 여러 개의 함수들)의 인자 갯수에 따라 구분을 할 수 있다는 점이지 않을까 싶다.