[JS] 함수형 프로그래밍 입문 #2
본 강좌는 유인동님의 함수형 프로그래밍과 JavaScript ES6+ 강좌를 듣고 정리한 것입니다. 개인 학습용으로 유료 강좌 내용 전부가 아닌 유인동님의 깃허브에 공개된 자료를 바탕으로 코드를 요약 정리했습니다. 문제시 자삭하도록 하겠습니다.
함수형 프로그래밍 입문 #2
Map
기본 활용
const products = [
{ name : '반팔티', price : 10000 },
{ name : '후드', price : 20000 },
{ name : '바지', price : 50000 },
{ name : '손수건', price : 5000 },
];
// 우리가 보통 이렇게 값을 처리하지
let prices = [];
for (const p of products) {
names.push(p.price);
}
log(prices);
// map을 활용하면 이렇게 된다
/*
우리가 기존에는 직접 p.price 등으로 어떤 값을 처리할 건지 지정해줬다면
map에서는 함수를 넘겨받아 어떤 값을 처리할 건지 함수한테 위임한다
*/
const map = (f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
}
log(map(p => p.price, products));
point 1 : 원본 데이터를 넘겨주는 대신 새로운 값을 만들어 return해준다
위의 map
활용 예제에서 메서드 블록 내에서 log(prices)
를 하는 대신, map
에서 새로운 값을 만든 후 return res
를 통해 리턴값을 외부로 넘겨준다. 함수형이 지향하는 값의 불변성을 지키는 방법이다.
point 2 : 직접 행위를 지정하는 대신 다른 함수에 위임한 후, 그 함수를 값으로 넘겨받는다
기존 코드에서는 p.price
로 값을 직접 지정해줬다면, map
에서는 함수를 넘겨받아 어떤 값을 처리할 건지 f
라는 파라미터로 함수를 넘겨받는다. 불필요한 책임을 지는 것을 원치 않는다.
이터러블 프로토콜에 따른 map의 다형성
iterable protocol
을 따르는 함수는 아래와 같이 다형성의 장점을 극대화한 함수형 메서드를 구현할 수 있다
// [1,2,3]은 Array가 iterator 이기 때문에 아래 코드는 결과값이 나옴
log([1,2,3].map(a => a + 1)); // [2,3,4]
// 근데 아래 document.querySelector()는 iterator가 아니기 때문에 undifined가 뜬다
// 실제로 내부를 들여다봐도 forEach는 있지만 map 함수는 없다. iterator가 아니니까.
log(document.querySelector('*').map(el => el.nodeName));
// 위에서 만든 map 함수로 만든 결과값은 신기하게도 나온다!
// 왜냐면 위의 map 함수를 통해 document.querySelector()가 iterator를 따르게 만들었기 때문.
log(map(el => el.nodeName, document.querySelector('*')));
const it = document.querySelector('*')[Symbol.iterator]();
log(it.next()) // 값이 찍힌다!
제너레이터 함수의 값도 map을 활용할 수 있다.
=> 사실상 모든 값에 map을 적용할 수 있다
function *gen() {
yield 2;
yield 3;
yield 4;
}
log(map(a => a * a, gen())); // [4, 9, 16];
let m = new Map();
m.set('a', 10);
m.set('b', 20);
const it = m[Symbol.iterator]();
it.next(); // { value : Array(2) , done : false };
// js의 Map 자료구조도 iterable protocol을 따르므로, 우리가 만든 map 함수를 구현할 수 있다
// 바뀐 Map 객체를 만드는 데도 이렇게 map을 조합할 수 있다
new Map(map(([k, a]) => [k, a * 2], m))));
filter
// 보통 이렇게 filter 하지만
let under20000 = [];
for (const p of products) {
if (p.price >= 20000) under20000.push(p);
}
log(...under20000)
// 다형성을 가진 filter 함수를 구현한다면 이렇게 된다
const filter = (f, iter) => {
let res = [];
for (a of iter) {
if (f(a)) res.push(a);
}
return a;
}
filter(a => a.price < 20000, products);
reduce
const nums = [1, 2, 3, 4, 5];
// 명령적 작성은
let total = 0;
for (const n of nums) {
total = total + n;
}
log(total);
// reduce 구현
const reduce = (f, acc, iter) => {
for (const a of iter) {
acc = f(acc, a);
}
return acc;
}
// 근데 문제가 하나 있다
// acc 값이 생략 될 경우에도 iter의 첫번째 값이 acc 초기값이 되도록 동작하게 해야댐
// iter를 iterable 객체로 바꿔주고, acc를 iter에서 next() 한 첫번째 value가 되도록 해준다
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;
}
// 그래서 값을 reduce로 누적하는 함수를 작성해보면
log(
reduce(
(total_price, product) => total_price + product.price,
0,
products));
map, filter, reduce 중첩 사용
const products = [
{ name : '반팔티', price : 10000 },
{ name : '후드', price : 20000 },
{ name : '바지', price : 50000 },
{ name : '손수건', price : 5000 },
];
// 가격을 뽑아보자 (map)
log(map(p => p.price, products));
// 특정 가격 이하의 가격을 뽑아보자 (map + filter)
// => map에 건내줄 가격을 filter를 통해 걸러준다
log(map(p => p.price, filter(p => p.price < 20000, products)));
// 그래서 그 특정 가격 이하의 뽑은 가격을 모두 합해보자
const add = (a, b) => a + b;
log(
reduce(
add,
map(p => p.price,
filter(p => p.price < 20000, products))));
// map과 filter의 순서를 바꿔도 결과는 동일하다
log(
reduce(
add,
filter(p => p.price < 20000,
map(p => p.price, products))));
읽는 방법
복잡해 보이지만 왼쪽에서 오른쪽으로 읽으면 해석이 된다
log(
reduce(
add,
map(p => p.price,
filter(p => p.price < 20000, products))));
-
products를 2만원 미만으로 필터를 하고,
-
해당하는 프라이스를 맵을 통해 뽑아낸 다음에
-
뽑아낸 모든 값을 add를 통해 더해준다
함수형 사고
사고의 흐름
어떤 배열이 있고, 특정한 값을 reduce로 add하고 싶다. 그러면 일단 이렇게 써보자.
log(
reduce(
add,
[10, 20, 30, 40]
)
)
잘 동작하는 것을 알 수 있고, 그러면 저 임의의 배열이 있는 곳에 특정한 숫자 배열을 만들어 주면 된다 라고 생각이 든다
log(
reduce(
add,
map(p => p.price, products)
)
)
특정한 값만을 추출해야 하므로, 저 products 자리에 추출한 값이 들어가야 한다 라고 생각이 든다
log(
reduce(
add,
map(p => p.price,
filter(p => p.price < 20000, products)
)
)
)
-
코드를 잘 보면 무조건 예상 결과값, 기대값을 생각한 후 그에 맞춰 사고를 한다
- 이걸 해서 이걸 해서 이걸 해서 이걸 해야지 라는 기존의 절차형 사고와는 달리,
- 최종적으로 난 이게 필요하다
- 이전에는 그럼 뭐가 있어야 하지? 라는 사고가 필요하다
- topdown식 사고가 필요한 것 같다