JavaScript | FP && ES6+ | go, pipe, curry
함수형 프로그래밍은 함수를 값으로 다룰 수 있어서 코드를 가독성 좋게 작성할 수 있다. 어떻게? 중복되어 사용하는 함수들을 추상화하면서 함축하여 표현력을 더 좋게 하면 된다. 그래서 뭘로? 강의에서 알려준 go, pipe, curry를 사용해서!
console.log(
reduce(
add,
map(p => p.price,
filter(p => p.price < 20000, products))));
이렇게 중첩되어 있는 코드는 가독성이 많이 떨어지기 때문!
go 함수
go는 함수와 인자를 전달해서 즉시 어떤 값을 평가하는 함수다.
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 go = (...args) => reduce((a, f) => f(a), args);
go(
0,
a => a + 1,
a => a + 10,
a => a + 100,
console.log
); // 111
위의 예시로 더 구체적으로 설명하자면, go의 첫 번째 인자에는 시작하기 위한 값을 넣고 나머지 인자에는 함수들을 받아 값을 다음 함수로 넘기면서 차례대로 함수를 실행한다.
pipe
파이프는 함수를 리턴하는 함수로, 함수들이 나열되어 있는 합성된 함수를 만든다.
개념적으로 조금 더 이해할 필요가 있을 것 같다. 구글링해보면, pipe function에 대해 다음과 같은 정의가 있다.
pipeline consists of a chain of processing elements, arranged so that the output of each element is the input of the next.
파이프라인은 각 요소의 출력이 다음 요소의 입력이 되도록 배열된 처리 요소의 체인으로 구성된 것이라고 한다. 즉 파이프 함수는 함수들을 하나로 합성한 후 리턴하는 함수로, 리턴된 함수는 인자를 받아 함수들을 적용하며 값을 리턴한다.
const pipe = (...fs) => (a) => go(a, ...fs);
우선 위에 pipe 함수 구현한 것을 보면, pipe 함수에 ...fs인자를 받고 함수를 리턴한다. 리턴된 함수는 이 인자를 받아 함수들을 적용하며 값을 리턴한다. 내부 함수로 go를 활용한다.
const f = pipe(
a => a + 1,
a => a + 10,
a => a + 100
);
console.log(f(0)); /// 111
만약 첫 번째 함수의 인자가 2개 이상일 경우에는 아래처럼 첫 번째 함수 f와 나머지 함수인 ...fs를 분리시키고, 재귀적인 방법으로 모든 인자들을 받아서 리턴할 수 있도록 한다.
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);
const f = pipe(
(a, b) => a + b,
a => a + 10,
a => a + 100);
log(f(0, 1));
go, pipe 활용 예시
const map = (f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
};
const filter = (f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
};
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 products = [
{name: '반팔티', price: 15000},
{name: '긴팔티', price: 20000},
{name: '핸드폰케이스', price: 15000},
{name: '후드티', price: 30000},
{name: '바지', price: 25000}
];
products라는 객체가 있고, 이중 20000원 이하인 제품들의 가격의 합을 구하는 함수를 구현한다면 다음과 같다.
go (
products,
products => filter(p => p.price < 20000, products),
products => map(p => p.price, products),
prices => reduce(add, prices),
console.log
);
// expected output: 30000
시작하는 인자로 products를 넣고, 다음 실행하는 함수에 가격이 20000 이하인 요소를 선택, 그 요소들에서 가격 value만 리턴한 후, reduce, add로 합치기
이를 파이프 함수로 구현한다면 다음과 같이 할 수 있다.
const products20000 = pipe(
filter(p => p.price < 20000),
map(p => p.price),
reduce(add),
console.log);
products20000(products);
curry
curry는 여러 개의 인수를 각각 하나의 인수만 취하는 함수를 일련의 함수로 분해하는 것을 말한다. 커리는 currying이라는 키워드로 구글링하면 많은 정보를 얻을 수 있다.
더 찾아본 바로는, currying은 f(a,b,c)처럼 단일 호출로 처리하는 함수를 f(a)(b)(c)와 같이 각각의 인수가 호출 가능한 프로세스로 호출된 후 병합되도록 변환하는 것이다. 커링은 함수를 호출하지 않고, 단지 변환만 한다.
const curry = f =>
(a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
curry함수 구현을 보면, a 와 나머지 요소들을 인자로 받는다. _가 여러 개일 경우 length에서 true로 걸러진다. 즉, 인자가 두 개 이상이면 받아둔 함수를 즉시 실행하고, 두 개보다 작다면 함수를 다시 리턴 후에 그 이후에 받은 함수로 실행한다.
이전부터 구현했던 map, filter, reduce 함수들도 currying할 수 있다는 말
const map = curry((f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
});
const filter = curry((f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
});
const reduce = 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;
});
* ref