읽기 좋은 코드를 만들자!라는 목표로 몇가지 개념을 더 알아보자.
함수형 프로그래밍에서는 코드를 '값'으로 사용하는 아이디어를 많이 사용한다.
전 섹션에서 작성했던 코드를 go함수와 pipe함수를 사용해 발전시켜보자.
go함수
우선 go함수는 다음과 같이 실행된다.
0이 a로 들어가고 0+1=1이 또 a로 들어가고...이런식으로 연속적으로 값이 들어가며 실행된다.
go(
0,
(a) => a + 1,
(a) => a + 10,
(a) => a + 100,
log
); //111
인자들이 리스트로 들어왔을 때, 하나의 값으로 축약된다!는 개념으로 보자면
go는 앞서 배웠던 reduce함수로 구현할 수 있다.
[JavaScript] #3. map, filter, reduce (함수형 프로그래밍과 JavaScript ES6+)
이번엔 map(), filter(), reduce() 세가지 함수에 대해 알아보자. map 함수 다음과 같이 5개의 상품이 있다고 해보자. const products = [ { name: "반팔티", price: 15000 }, { name: "긴팔티", price: 20000 }, { name: "핸드폰
r-ing.tistory.com
go함수가 연속적으로 값이 들어가며 작동한다는 것을 확인했는데, 이를 reduce에 적용해보자.
함수f에 a를 적용해주면 f(a)가 다음번째 a가 되고, 그 a가 다시 f에 들어가며 반복되다가 하나의 값으로 축약된다.
const go = (...args) => reduce((a, f) => f(a), args);
go(
0,
(a) => a + 1,
(a) => a + 10,
(a) => a + 100,
log
);
이렇게 reduce함수를 활용하면 특정 리스트를 하나로 축약할 때 유용하다.
pipe함수
go함수가 '값'을 return한다면 pipe함수는 '함수'를 return한다.
const f = pipe(
(a) => a + 1,
(a) => a + 10,
(a) => a + 100
);
log(f(0));
2~4라인 코드를 pipe로 합쳐서 f에게 전달하려면 pipe함수는 어떻게 구현해야할까?
(7라인 코드의 결과는 위에서 봤던 go함수 예제와 같은 값이 나와야한다.)
우선 함수들을 fs로 전달받고, 시작하는 인자(a)를 받는다. (위 예시에서는 0)
그리고 go 함수를 활용해서 첫번째 인자(a)를 주고, 함수들(...fs)을 주면 pipe함수가 완성된다.
//시작하는 인자가 a 하나 일 때
const pipe = (...fs) => (a) => go(a, ...fs);
이걸, 시작하는 인자가 두개 일 때도 처리할 수 있게 조금 더 발전시켜보자.
//시작하는 인자가 여러 개일 때도 구현
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);
이처럼 첫번째 함수 f만 빼주고, 나머지 함수는 ...fs에 넣고
받은 여러 개의 인자 ...as를 첫번째 함수의 인자로 넣어주면된다.
자, 이제 go함수를 배웠으니 지난 세션3에서 썼던 코드를 더 읽기 쉽게 바꿔보겠다.
log(
reduce(
add,
map(
(p) => p.price,
filter((p) => p.price < 20000, products)
)
)
);
이 코드였는데, 저번에는 오른쪽에서부터 왼쪽으로 읽으며 판단했던걸 go함수로 더 알아보기 쉽게 바꿀 수 있다.
go(
products,
(products) => filter((p) => p.price < 20000, products),
(products) => map((p) => p.price, products),
(prices) => reduce(add, prices),
log
);
이 코드와 전 코드를 비교해보면 코드가 훨씬 이해하기 쉽게 작성된걸 확인할 수 있다.
curry(currying 커링)
f(a, b, c)와 같은 함수를 f(a)f(b)f(c)와 같이 부분적으로 실행
//curry함수 구현
const curry = (f) => (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
curry는 함수 f를 받아 함수를 리턴한다.
삼항연산자를 사용해 구현하는데,
(length가 있을 때 = 받은 인자가 두 개 이상일 때) 받아둔 함수를 즉시 실행하고,
(그게 아니라면) 그 이후에 받은 인자들을 합쳐서 실행한다.
예시를 보면 어떤 상황에 사용하는지 쉽게 이해할 수 있다.
const mult = curry((a, b) => a * b);
log(mult(1)); //(..._) => f(a, ..._)
log(mult(1)(2)); //2
const mult3 = mult(3);
log(mult3(10)); //30
log(mult3(5)); //15
log(mult3(3)); //9
2번째 라인을 보면 알 수 있듯이 인자를 하나만 받으면 이후 인자를 받기 위해 기다리는 함수를 return한다.
이제, 앞서 go함수로 수정했던 코드를 currying(커링)시켜 더 간결하게 바꿔보자.
우선 js파일에 curry를 추가하고 map, filter, reduce함수를 curry함수로 감싸주었다.
const log = console.log;
const curry =
(f) =>
(a, ..._) =>
_.length ? f(a, ..._) : (..._) => f(a, ..._);
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;
});
go(
products,
(products) => filter((p) => p.price < 20000, products),
(products) => map((p) => p.price, products),
(prices) => reduce(add, prices),
log
);
위 코드가 함수의 순서를 바꾸는 go 함수를 배우고 수정했던 코드이다.
go(
products,
(products) => filter((p) => p.price < 20000)(products),
(products) => map((p) => p.price)(products),
(prices) => reduce(add)(prices),
log
);
map과 filter함수는 products를 받아서 products를 전달하고 있고,
reduce함수는 prices를 받아서 prices를 전달하고 있으며
js파일의 map, filter, reduce함수를 curry로 묶어줬으므로 다음과 같이 수정할 수 있다.
go(
products,
filter((p) => p.price < 20000),
map((p) => p.price),
reduce(add),
log
);
go와 currying을 통해 코드가 훨씬 간결해졌다.
마지막으로 새로운 함수를 만들어 함수의 조합으로 코드를 다듬어 보겠다.
const total_price = pipe(
map((p) => p.price),
reduce(add)
);
const base_total_price = (predi) => pipe(filter(predi), total_price);
go(
products,
base_total_price((p) => p.price < 20000),
log
);
모든 가격을 더하는 total_price함수를 만들고 그 함수와 filter함수로 조건에 맞는 가격을 거르도록 수정했다.
사실 이번 세션 개념이 바로바로 이해되진 않아 강의를 몇번 돌려보고 구글링도 해보며 공부했다.
하지만 어려운 것과는 별개로 길고 복잡한 코드를 간단하게 줄이는 과정이 재밌었다.