본 시리즈는 인프런 강의 함수형 프로그래밍과 JavaScript ES6+ ( 지식 공유자 : 유인동 님 ) 의 강의를 수강하면서 내용을 제 방식대로 포스팅하는 글입니다.
함수형 프로그래밍과 JavaScript ES6+ - 인프런 | 강의
ES6+와 함수형 프로그래밍을 배울 수 있는 강의입니다. 이 강좌에서는 ES6+의 이터러블/이터레이터/제너레이터 프로토콜을 상세히 다루고 응용합니다. 이터러블을 기반으로한 함수형 프로그래밍,
www.inflearn.com
이 내용은 이전 게시물과 이어집니다.
함수를 위에서부터 아래로, 왼쪽에서부터 오른쪽으로 함수를 평가하면서 연속적으로 함수를 실행하고, 이전에 실행된 함수의 결과를 다음 함수에게 전달하는 go 함수를 사용하여 코드 작성방식을 바꿀수 있다.
// 이전에 만들어 보았던 map filter reduce 함수의 중첩 사용을
// 좀더 보기 좋게 만들어보는 코드를 작성해보자
reduce(
add,
map(
(product) => product.price,
filter((product) => product.price < 20000, products)
)
);
// products 로 시작하고, 순서는 반대가 된다.
go(
products,
(products: ProductsType) => filter((p) => p.price < 20000, products),
(products: ProductsType) => map((p) => p.price, products),
(prices: number[]) => reduce(add, prices),
log
); // 30000
코드 량은 늘어났지만, 좀더 우리가 읽기 편한 코드가 되었다.
products 로 시작해서, filter 를 거치고 map 을 해서 가격만 뽑아내고, reduce 로 값을 축약하겠다는 표현이 되었다.
이번에는 curry 라는 함수를 만들어 보자.
curry 도 함수를 값으로 다루면서 받아둔 함수를 내가 원하는 시점에 평가시키는 함수다.
함수를 인자로 받아 함수를 리턴하고, 또다른 인자를 받아서 인자가 원하는 갯수만큼의 인자가 들어왔을때 받아두었던 함수를 나중에 평가하는 함수다.
설명이 복잡하니 코드로 이해해보자
function curry(fn: (a?: any, b?: any) => any) {
// 1. 먼저 함수를 리턴한다.
// 2. 리턴된 함수를 실행했을때 인자가 두개 이상이라면
// 3. 인자로 받았던 함수에 인자를 전달하여 즉시실행시킨다.
// 4. 아니라면 다시 그 이후에 받은 인자들을 합쳐서 실행하는 함수를 리턴한다.
return function (value: any, ...rest: Parameters<any>) {
return rest.length
? fn(value, ...rest)
: (...rest: Parameters<any>) => fn(value, ...rest);
};
}
const mult = curry((a: number, b: number) => a * b);
// curry 를 감싸서 함수를 만들면, 먼저 함수가 바로 리턴된다.
log(mult);
// function (value: any, ...rest) {
// return rest.length
// ? fn(value, ...rest)
// : (...rest) => fn(value, ...rest);
// 인자를 하나만 전달한 경우, 이 결과는 다시 함수가 리턴된다.
// 리턴된 함수는 나머지 인자를 마저 전달했을때 받아두었던 인자와
// 새로 받게되는 인자를 받도록 되어있다.
log(mult(1));
// (...rest) => fn(value, ...rest);
// 두 인자를 받았으므로 실행 결과가 나오게 된다.
log(mult(1, 2)); // 2
const mult3 = mult(3); // (3) => fn(value, 3)
log(mult3(10)); // 30
log(mult3(5)); // 15
log(mult3(3)); // 9
이러한 패턴으로 동작하는 함수가 curry 이다.
이제 이 curry 와 go 를 조합하여, 더 읽기 좋은 코드로 리팩터링 해보자.
기존에 만들었던 map, filter, reduce 함수에 curry 를 적용하여 바꿔보면,
const map = curry((fn: (elem: any) => any, iter: Iterable<any>) => {
let res = [];
for (const a of iter) {
res.push(fn(a));
}
return res;
});
const filter = curry((fn: (elem: any) => any, iter: Iterable<any>) => {
let res = [];
for (const a of iter) {
if (fn(a)) res.push(a);
}
return res;
});
const reduce = curry(
(
fn: (result: any, data: any) => any,
acc: number | Iterable<any>,
iter?: Iterable<any> | Iterator<any>
) => {
if (!iter) {
acc = acc as Iterable<any>;
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter as Iterable<any>) {
acc = fn(acc, a);
}
return acc;
}
);
이제 이 모든 함수가 다음 인자를 받을때까지 기다리게 된다.
이제 이 패턴대로 다시 리팩터링 해보면
go(
products,
(products: ProductsType[]) =>
filter((p: ProductsType) => p.price < 20000, products),
(products: ProductsType[]) => map((p: ProductsType) => p.price, products),
(prices: number[]) => reduce(add, prices),
log
); // 30000
go(
products,
(products: ProductsType[]) =>
// 앞부분이 함수고, 이후에 products 를 넘기도록 할수 있게된다.
// 하지만 영 이것만으로는 코드가 간단해져 보이지 않는다.
filter((p: ProductsType) => p.price < 20000)(products),
(products: ProductsType[]) => map((p: ProductsType) => p.price)(products),
(prices: number[]) => reduce(add)(prices),
log
); // 30000
// products 를 인자로 받아서 앞의 인자로 받은 함수에 그대로 다시 products 를 전달하는것은,
// procuts 를 미리 받아두고 다음 함수에 그대로 전달할수 있게 된다는 이야기 이다.
go(
products,
filter((p: ProductsType) => p.price < 20000),
map((p: ProductsType) => p.price),
reduce(add),
log
); // 30000
상당히 복잡햇던 코드를 go 를 통해 순서를 뒤집고, 함수를 부분적으로 실행하는 curry 를 통해
함수를 값으로 다루는 간결한 코드를 만들수 있게 되었다.
'Javascript' 카테고리의 다른 글
JS 에서 Set 객체의 사용 (0) | 2022.10.30 |
---|---|
JS에서 제너레이터란 ? (0) | 2022.10.29 |
JS 에서의 Symbol 의 사용과 응용 (0) | 2022.09.30 |
JS 에서 Symbol 이란? (0) | 2022.09.29 |
reduce 를 이용한 함수 pipe() 를 구현해보자 - JS ES6+ 함수형 프로그래밍 - 19 (0) | 2022.09.28 |