커링과 부분 적용

커링(currying)이 어떤 의미이며 어디서 사용되는지 이해한 후 부분 적용(partial application) 이라는
또 다른 함수형 프로그래밍 개념을 살펴본다. 커링과 부분 적용 모두 함수 합성을 사용할 때 이해해야 한다.

용어 정리

단항 함수

함수 인자를 하나만 취하는 함수를 단항 함수(unary function) 라 한다.

1
const identity = x => x;

이항 함수

두 개의 인자를 취하는 함수를 이항 함수(binary function) 라 한다.

1
const add = (x,y) => x + y;

가변 인자 함수

가변 인자 함수란 다양한 개수의 인자를 취하는 함수다. 이전 자바스크립트 버전에서는
arguments 를 사용해 인자의 가변 개수를 알 수 있었다.

1
2
3
4
function variadic(a) {
console.log(a);
console.log(arguments);
}

arguments 를 사용해 추가적인 인자를 알아서 함수로 호출할 수 있다. 이 기술을 사용해 ES5 버전에서
가변 인자 함수를 사용할 수 있다. 하지만 ES6 를 사용하면 동일한 결과를 얻는데 전개 연산자(spread operator) 를
사용해야 한다.

1
2
3
4
const variadic = (a, ...variadic) => {
console.log(a);
console.log(variadic);
}

커링

커링을 간단하게 얘기하자면 n개 인자의 함수를 중첩된 단항 함수로 변화시키는 과정이다.

1
const add = (x,y) => x + y;

간단한 add 함수다. 이 함수를 add(1,1) 처럼 사용해 2라는 결과를 도출할 수 있다.
아래는 커링된 add 함수 형태이다.

1
const addCurried = x => y => x + y;

addCurried 함수는 add 의 커링 버전이다. addCurried 를 단일 인자로 호출하면 다음과 같다.

1
2
addCurried(4)
=> fn = y => 4 + y

클로저 개념으로 x 값을 가져와 함수를 반환한다.

원하는 결과를 얻으려면 다음과 같이 addCurried 함수를 호출할 수 있다.

1
2
addCurrried(4)(4);
// 8

일부러 add 함수를 변경해 addCurried 함수로 두 개의 인자를 받고 중첩된 단항 함수로 만들었다.
두 인자를 취하는 함수에서 하나의 인자를 취하는 함수로 바꾸는 과정을 커링이라고 한다.

1
2
3
4
5
6
7
const curry = binaryFn => {
return function (firstArg) {
return function (secondArg) {
return binaryFn(firstArg, secondArg);
}
}
}

이제 curry 함수를 사용해 add 함수를 다음과 같은 형태로 변환할 수 있다.

1
2
3
4
const add = (x, y) => x + y;
const autoCurriedAdd = curry(add);
autoCurriedAdd(2)(2);
// 4

커링은 n 개 인자의 함수를 중첩된 단항 함수로 변환하는 과정이다.

curry 함수 정의를 살펴보면 이항 함수를 중첩된 함수로 변환해 하나의 인자만 취하므로, 중첩된 단항 함수를 반환한다.

커링을 사용하는 경우

테이블을 만드는 함수를 생성한다고 해본다. 예를 들어 tableOf2, tableOf3, tableOf4 를 생성한다.

커링이 없는 테이블 함수
1
2
3
4
5
6
7
const tableOf2 = y => 2 * y;
const tableOf3 = y => 3 * y;
const tableOf4 = y => 4 * y;

tableOf2(4) // 8
tableOf3(4) // 12
tableOf4(4) // 16

이제 테이블의 개념을 다음과 같이 단일 함수로 일반화 할 수 있다.

1
const genericTable = (x,y) => x * y;

다음과 같이 tableOf2 를 얻고자 genericTable 을 사용할 수 있다.

1
2
3
genericTable(2, 2); // 4
genericTable(2, 3); // 6
genericTable(2, 4); // 8

tableOf3 과 tableOf4 도 동일하다. 패턴을 인식했으면 tableOf2 에는 첫 번째 인자로 2,
tableOf3 에는 3처럼 계속 채운다. 커링을 이용해 문제를 해결한다고 생각할 수 있다.

커링을 이용한 테이블 함수
1
2
3
const tableOf2 = curry(genericTable)(2);
const tableOf3 = curry(genericTable)(3);
const tableOf4 = curry(genericTable)(4);

커링 다시 살펴보기

앞에서 정의했던 curry 는 함수 하나에만 적용했다. 여러 개의 함수에 적용하는 방법은 간단하지만
curry 를 구현하는 데 있어 중요하다. 우선 아래의 코드처럼 첫 번째 규칙을 추가한다.

1
2
3
4
5
let curry = fn => {
if (typeof fn !== "function") {
throw Error("No function provided");
}
}

이 코드에서 2와 같은 정수를 가진 curry 함수를 호출하면 에러가 발생한다.
커링된 함수에 다음으로 필요한 것은 커링된 함수에 모든 인자를 제공받았다면
전달된 인자로 실제 함수를 호출하는 기능이다.

1
2
3
4
5
6
7
8
9
let curry = fn => {
if (typeof fn !== "function") {
throw Error("No function provided");
}

return function curriedFn(...args) {
return fn.apply(null, args);
}
}

이제 다음과 같은 multiply 라는 하무가 있다면 다음과 같이 새로운 curry 함수를 사용할 수 있다.

1
2
3
4
5
const multiply = (x,y,z) => x * y * z;
curry(multiply)(1,2,3);
// 6
curry(multiply)(1,2,0);
// 0

curry(multiply)(1,2,3) 에서 args 는 [1,2,3] 을 기라키며, fn 에서 apply를 호출하므로
결과는 다음 호출과 동등하다.

1
multiply(1,2,3);

이제 n개 인자 함수를 중첩된 단항 함수로 변환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let curry = fn => {
if (typeof fn !== "function") {
throw Error("No function provided");
}

return function curriedFn(...args) {
if (args.length < fn.length) {
return function(...curryArgs) {
return curriedFn.apply(null, args.concat(curryArgs));
};
}
return fn.apply(null, args);
}
}
1
2
3
4
5
6
7
if (args.length < fn.length) { // 1
return function(...curryArgs) {
return curriedFn.apply(
null, args.concat(curryArgs) // 3
); // 2
};
}

이 코드 어떻게 동작하는지 살펴본다.

  1. …args 를 통해 전달된 인자 길이가 함수 인자 리스트 length 보다 작은지 확인한다. 작다면 if 블록으로 진입하며
    그렇지 않다면 이전과 같이 전체 함수 호출로 돌아간다.
  2. if 블록에 들어가면 apply 함수를 사용해 curriedFn 을 재귀적으로 호출한다.
  3. concat 함수를 사용해 전달된 인자를 한 번에 연결시키고 curriedFn 을 재귀적으로 호출한다. 전달된 모든 인자가
    연결돼 있고 재귀적으로 호출되므로 1번 조건문에 도달하지 못한다.

이를 이해하면 curry 함수를 사용해 multiply 함수를 실행할 수 있다.

1
2
3
4
5
curry(multiply)(1)(2)(3);
// 6
let curriedMul3 = curry(multiply)(3);
let curriedMul2 = curriedMul3(2);
let curriedMul1 = curriedMul2(1);

주의할 부분은 curry 함수가 n개 인자의 함수를 예제에서 보듯이 단항 함수로 호출되는 함수로 변환한다는 점이다.

커링의 실제 사용

일상적으로 커링을 어떻게 사용하는지 알아본다.

배열 요소에서 숫자 검색

숫자를 요소로 갖는 배열을 검색해본다.

1
2
3
let match = curry(function(expr, str) {
return str.match(expr);
})

반환된 match 함수는 커링된 함수다. 첫 번째 인자 expr 을 요소가 숫자를 갖고 있는지 확인하는
정규 표현식인 /[0-9]+/ 로 나타낼 수 있다.

1
let hasNumber = match(/[0-9]+/);

다음으로 커링된 filter 함수를 생성한다.

1
2
3
let filter = curry(function(f, arr) {
return arr.filter(f);
});

hasNumber 와 filter 를 사용해 findNumbersInArray 라는 새로운 함수를 만들 수 있다.

1
2
3
let findNumbersInArray = filter(hasNumber);
findNumbersInArray(["js", "number1"]);
// ["number1"]

배열 제곱

배열의 요소를 제곱하는 함수를 curry 를 사용해 만들어 본다.

1
2
3
4
5
6
7
let map = curry(function (f, arr) {
return arr.map(f);
});

let squareAll = map(x => x * x);
squareAll([1,2,3]);
// [1, 4, 9]

데이터 플로우

배열 같은 자료 구조를 다루면서 최종 인자가 squareAll, findNumbersInArray 와 같은 코드
전체에 사용할 수 있는 재사용 가능한 함수를 생성할 수 있게 해야한다.

부분 적용

함수 인자를 부분적으로 적용할 수 있는 partial 이라는 다른 함수를 살펴본다.

10ms 마다 일련의 연산을 처리한다고 가정해본다. 다음과 같이 setTimeout 함수를 사용한다.

1
2
setTimeout(() => console.log("Do X task"), 10);
setTimeout(() => console.log("Do Y task"), 10);

보다시피 setTimeout 함수가 호출될 때마다 10을 전달한다. curry 함수는 가장 왼쪽에서 가장 오른쪽의 리스트로
인자를 적용하기 때문에 이 문제에 curry 를 사용할 수 없다. 다른 해결 방법은 setTimeout 함수를 감싸 함수 인자의
오른쪽이 최우선으로 되게 하는것이다.

1
2
3
4
5
6
const setTimeoutWrapper = (time, fn) => {
setTimeout(fn, time);
}
const delayTenMs = curry(setTimeoutWrapper)(1000);
delayTenMs(() => console.log("Do X task"));
delayTenMs(() => console.log("Do Y task"));

이는 필요에 따라 적용된다. 하지만 이 문제는 setTimeoutWrapper 라는 래퍼를 한 단계 위에 작성해야 한다.
여기가 부분 적용 기술이 사용되는 곳이다.

부분 함수 구현

부분 적용 기술이 어떻게 작동되는지 완전히 이해하기 위해 partial 함수를 작성해본다.

1
2
3
4
5
6
7
8
9
10
11
12
const partial = function (fn, ...partialArgs) {
let args = partialArgs;
return function(...fullArgs) {
let arg = 0;
for (let i=0; i < args.length && arg < fullArgs.length; i++) {
if (args[i] === undefined) {
args[i] = fullArgs[arg++];
}
}
return fn.apply(null, args);
}
}

setTimeout 문제에 partial을 적용해본다.

1
2
let delayTenMs = partial(setTimeout, undefined, 10);
delayTenMs(() => console.log("Do Y task"));
1
2
3
4
5
partial(setTimeout, undefined, 10);

// 다음
let args = partialArgs
=> args = [undefined, 10];

args 값을 기억하는 클로저 함수를 반환한다. 반환된 함수는
fullArgs 라는 인자를 받고 이 인자를 전달해서 delayTenMs 와 같은 함수를 호출한다.

1
2
3
4
5
6
7
delayTenMs(() => console.log("Do Y task"));

// fullArgs 는 다음을 가리킨다.
// [() => console.log("Do Y task")]

// 클로저를 사용하는 args 는 다음을 가진다.
// args = [undefined, 10]

이제 for 루프 안에서는 함수에 필요한 인자 배열을 생성하고 반복한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (let i=0; i < args.length && arg < fullArgs.length; i++) {
if (args[i] === undefined) {
args[i] = fullArgs[arg++];
}
}

// args = [undefined, 10]
// fullArgs = [() => console.log("Do Y task")]
args[0] => undefined === undefined // true

// if 루프 내부
args[0] = fullArgs[0]
=> args[0] = () => console.log("Do Y task")
=> args = [() => console.log("Do Y task"), 10]

args 안에 필요한 인자가 있으면 fn.apply(null, args) 로 함수를 호출한다.
n개 인자의 모든 함수에 partial 을 적용할 수 있다.

참조: 함수형 자바스크립트 입문 2/e

댓글

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.