https://dev-scratch.tistory.com/151
이제 위 게시물의 가장 상단 복잡했던 코드들을 아래와 같이 작성할 수 있다.
먼저 원본 코드는 아래와 같다.
const filter = (f, iter) => {
const 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 },
];
const add = (a, b) => a + b;
reduce(add, map(p => p.price, filter(p => p.price < 20000, products))));
이 함수를 go 함수를 사용해 아래와 같이 가독성 좋게 표현할 수 있다.
const go = (...args) => reduce((a, f) => f(a), args);
const add = (a, b) => a + b;
go(
products,
products => filter(p => p.price < 20000, products),
products => map(p => p.price, products),
products => reduce(add, prices),
console.log
);
위 함수는 위에부터 아래로 읽으면서 전체 흐름을 한 눈에 파악하기 좋다.
Curry
Curry 함수는 함수를 값으로 다루면서, 받아둔 함수를 내가 원하는 시점에서 평가하는 함수이다.
함수를 받아서 함수를 리턴하고, 인자를 받아서 원하는 인자 개수만큼 들어왔을 때 받아두었던 함수를 나중에 평가시킨다.
const curry = f =>
(a, ..._) => _.length
? f(a, ..._)
: (..._) => f(a, ..._);
위 함수는 첫 번째 매개변수와 나머지 함수 인자를 받아들인다. 이때, 총 인자가 2개 이상인 경우에는 인자와 함께 그 함수를 실행하고, 아니면 그 이후에 받은 인자들을 합쳐서 실행하는 함수이다.
const multiple = curry((a, b) => a * b);
multiple(1);
만약 이렇게 값을 받아서 사용하게 된다면, multiple 의 반환값은 아래와 같다. 위에서 거짓인 경우의 상황인 것이다.
(..._) => f(a, ..._)
이때, 한 번 더 값을 불러오면 원하는 값을 얻어낼 수 있다.
multiple(1)(2);
// 2
multiple(2)(3);
// 6
혹은 미리 변수에 첫 번째 함수를 반환하는 함수를 평가해 변수에 함수의 포인터를 전달하여 사용할 수도 있다.
const multiple3 = multiple(3);
multiple3(10);
// 30
multiple3(5);
// 15
multiple3(3);
// 9
그러면 이제 go와 curry를 혼합해 더 가독성이 좋은 코드를 만들어 볼 수 있다.
const map = curry((f, iter) => {
const res = [];
for (const a of iter) {
res.push(f(a))
}
return res;
});
const filter = curry((f, iter) => {
const 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;
});
이때, go 함수를 사용할 때는 아래와 같이 사용할 수 있다.
const add = (a, b) => a + b;
go(
products,
products => filter(p => p.price < 20000)(products),
products => map(p => p.price)(products),
products => reduce(add)(prices),
console.log
);
여기서 이 과정을 조금 더 축약할 수 있는데, 다음과 같다.
go(
products,
filter(p => p.price < 20000),
map(p => p.price),
reduce(add),
log
);
함수 조합으로 함수 생성
아래와 같이 거의 비슷한 함수가 있다고 가정해보자.
go(
products,
filter(p => p.price < 20000),
map(p => p.price),
reduce(add),
console.log
);
go(
products,
filter(p => p.price >= 20000),
map(p => p.price),
reduce(add),
console.log
);
여기서는 map과 reduce가 겹치기에 추가적으로 조합을 사용해 간단히 나타낼 수 있다.
const total_price = pipe(
map(p => p.price),
reduce(add)
);
go(
products,
filter(p => p.price < 20000),
total_price,
console.log
);
go(
products,
filter(p => p.price >= 20000),
total_price,
console.log
);
pipe를 사용해 함수들을 등록한 후, 향후에 트리거할 수 있도록 함수 포인터만 받아서 다시 go 함수에 전달한다.
그런데 여기서 filter와 total_price를 사용하는 것이 겹치므로 한 번 더 가공해 표현할 수 있다.
const total_price = pipe(
map(p => p.price),
reduce(add)
);
const base_total_price = predict => pipe(
filter(predict),
total_price
);
go(
products,
base_total_price(p => p.price < 20000),
console.log
);
go(
products,
base_total_price(p => p.price >= 20000),
console.log
);
출처: 인프런 함수형 프로그래밍과 JavaScript ES6+(https://www.inflearn.com/course/functional-es6/dashboard)
사실 curry에 대해서 완벽히 이해되지 않아서 조금 아쉽다. 그런데 매력이 느껴졌다. 복잡한 절차형 프로그래밍을 단순하게 함수형 프로그래밍으로 변경시킴으로써 함수를 값으로도 사용할 수 있기에 더욱 가독성 있는 코드, 유지보수하기 원활한 코드가 될 수 있다고 생각한다. curry의 의미를 조금 더 열심히 파악해야겠다.
'프론트엔드 > JS' 카테고리의 다른 글
[JavaScript] range와 느긋한 L.range (0) | 2023.04.30 |
---|---|
[JavaScript] 이터러블 객체와 순회 함수 응용 (0) | 2023.04.30 |
[JavaScript] go, pipe로 읽기 좋은 코드 만들기 (0) | 2023.04.27 |
[JavaScript] Map + Filter + Reduce 중첩 사용과 함수형 사고 (0) | 2023.04.27 |
[JavaScript] 이터러블 프로토콜과 map, filter, reduce (2) | 2023.04.25 |