curry 라는 함수를 만들어봅니다.
curry는 함수를 값으로 다루며 받아둔 함수를 원하는 시점에 평가 시키는 함수입니다.
우선 함수를 받은 후 함수를 리턴하고, 인자를 받아서 인자가 원하는 갯수만큼의 인자가 들어왔을때 받아두었던 함수를 나중에 평가 시키는 함수 입니다.
코드로 확인해 봅니다.
const curry = f => (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
1. 함수를 받아(f) 함수를 리턴합니다.. () => {};
2. 리턴하는 함수에서는 ( f )함수에서 사용할 첫번째 인자( a )와 나머지 인자( ..._ )를 대신해서 받습니다. ( a, ..._ )
3. 만약 함수의 인자가 2개 이상 전달 되었을 경우( _.length ) 받아둔 함수를 즉시 실행을 합니다.( f(a, ..._) )
4. 만약 2개 이상이 아니라면 다시한번 함수를 리턴합니다 ( () => )
5. 그 이후 들어올 값들을 받은 후 ( ..._ ), 미리 받아 두었던 인자 ( a ) 와 새로받은 ( ..._ ) 인자를 전달하여 함수 ( f )를 실행합니다.
const add = (a, b) => a + b;
const add2 = curry(add);
// (1)
console.log(add2(10, 5));
// > 15
// (2)
console.log(add2(10)(5));
// > 15
위와 같이 add 함수를 curry 로 감싸서 실행한 경우입니다.
(1)
1. add 를 curry 로 감싼 add2 라는 함수에 10 과 5의 인자를 선언합니다.
2. curry 함수가 실행됩니다.
3. add2(10, 5) 의 인자가 2개 이상이므로, f(a, ..._) 와 같이 실행됩니다.
-> f = add, ( a, ..._ ) = ( 10, 5 ) --> add2(10, 5) = add(10, 5) -> 15
(2)
1. add 를 curry 로 감싼 add2 라는 함수에 10이라는 인자를 선언한 후, 해당 결과로 나오는 함수에 5라는 인자는 선언합니다.
2. curry 함수가 실행됩니다.
3. add2(10) 의 인자가 1개이므로, (..._) => f(a, ..._) 와 같이 함수를 리턴하여 실행됩니다.
4. 해당 함수를 함수로 구현하여 나중에 선언받는 인자를 선언하는 함수를 실행합니다.
-> f = add, ( ..._ ) = ( 5 ), a = ( 10 ) --> add2(10)(5) = ( 5 ) => add( 10, 5 )
(2) 번과 같이 함수를 값으로 리턴하여 기본인자를 각각의 값으로 가지는 함수로 변환하여 병합시킬수 있도록 처리가 가능합니다.
이와같은 특성을 활용하여 기존 product의 go, map, filter, reduce 를 활용했던 코드를 curry 를 활용하여 조금더 가독성 좋게 구현해봅니다.
우선 map, filter, reduce 를 curry 함수로 감싸줍니다.
const curry = f => (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
const filter2 = curry((f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
});
const map2 = curry((f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
});
const reduce2 = curry((f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
});
이후 이전과 동일한 go 함수를 curry 가 적용된 함수를 사용하여 구현해 봅니다.
// (1)
go(
products,
products => filter(p => p.price < 20000, products),
products => map(p => p.price, products),
prices => reduce(add, prices),
console.log
);
// > 30000
// (2)
go(
products,
filter2(p => p.price < 20000),
map2(p => p.price),
reduce2(add2),
console.log
);
// > 30000
(1) 은 기존의 go를 이용하여 구현한 코드이며, (2) 는 curry 가 적용된 함수를 활용하여 작성된 코드입니다.
기존 products => filter(p => p.price < 20000, products) 에서 curry 를 적용하여 filter(p => p.price < 20000)(products) 로 사용이 가능하게됨에 따라, (2)와 같이 구현이 가능하게 되며 훨씬 더 깔끔하게 작성하여 가독성을 높일 수 있습니다.
curry의 경우 한번 작성하면서 조금 이해가 되지 않는 부분이 있어, 여러번 순차적으로 작성된 함수를 따라가보며 이해도를 높여 나갔습니다.
전체 코드 예시입니다.
/**
* curry
*/
const products = [
{name: '반팔티', price: 15000, quantity: 1},
{name: '긴팔티', price: 20000, quantity: 2},
{name: '핸드폰케이스', price: 15000, quantity: 3},
{name: '후드티', price: 30000, quantity: 4},
{name: '바지', price: 25000, quantity: 5}
];
const filter = (f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
};
const map = (f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
};
const go = (...args) => reduce((a, f) => f(a), args);
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};
const curry = f => (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
const filter2 = curry((f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
});
const map2 = curry((f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
});
const reduce2 = curry((f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
});
const add = (a, b) => a + b;
const add2 = curry(add);
// (1)
console.log(add2(10, 5));
// > 15
// (2)
console.log(add2(10)(5));
// > 15
go(
products,
filter2(p => p.price < 20000),
map2(p => p.price),
reduce2(add2),
console.log
);
// > 30000
출처: 프로그래머스 - 강사 유인동/함수형 프로그래밍과 ES6