자바스크립트 ES6에서 배열은 프로토타입 메서드 map, filter, reduce를 가진다. 배열이 아니더라도 이터러블이라면 사용할 수 있는 함수형 map, filter, reduce 함수를 구현해본다.
map
수집할 값 정보를 파라미터 함수 f
에게 완전히 위임한 고차함수 map을 구현해보자.
이터러블을 순회하며 함수 f
를 실행한 값을 배열에 넣어 리턴하도록 하면 된다.
const map = (f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
}
map(p => p.name, products)
map(p => p.price, products)
이터러블 프로토콜을 따른 map의 다형성
document.querySelectorAll('*').map(el => el.nodeName);
위 코드에서 querySelectorAll
의 리턴값인 NodeList
는 배열을 상속받지 않았기 때문에
map 함수가 없다고 뜨고, 에러가 뜬다. 그럴 땐 위에서 정의한 map 함수를 쓰면 된다.
map(el => el.nodeName, document.querySelectorAll('*'));
제너레이터가 반환하는 이터레이터를 순회하는 것도 가능하다.
function *gen() {
yield 2;
if (false) yield 3;
yield 4;
}
map(a => a*a, gen());
최근 ECMAScript의 헬퍼 함수들이 이터러블 프로토콜을 기반으로 만들어지고 있어 앞으로 호환성도 좋아질 예정이다.
자료구조 Map 써보기
기존의 Map
자료구조를 사용해 새로운 map을 만들 수도 있다.
let m = new Map();
m.set('a', 10);
m.set('b', 20);
new Map(map(([k,v] => [k, v*2]), m));
filter
이터러블에서 조건에 맞는 원소만 걸러내는 함수형 filter
를 만들어본다.
전체적인 구현 방식은 map
과 비슷한데, f(a)
가 true
인 값만 걸러내면 된다.
const filter = (f, iter) => {
let result = [];
for (const a of iter) {
if(f(a)) result.push(a)
}
return result;
}
filter(p => p.price > 3000, products);
reduce
reduce는 값을 순회하면서 하나의 값으로 누적해 축약하는 함수이다. reduce는 영어로 '졸이다'는 뜻도 있는데, 여러개의 값을 졸여서(누적해서) 하나의 결과값을 생성한다고 생각하면 이해가 편하다. 이 함수는 보조함수(1번째 파라미터)에 어떻게 축약할지를 완전히 위임한다.
배열의 모든 원소를 더하고 싶을 때 reduce
를 쓰지 않고 구현한다면 아래와 같다.
const nums = [1,2,3,4,5]
let total = 0;
for(const n of nums) {
total += n;
}
이제 reduce
를 사용해 리팩토링해보자.
자바스크립트 내장 reduce
에서는 acc
를 생략할 수도 있으니 만약 acc
가 생략되었다면 자동으로 첫번째 요소를
두번째 파라미터를 기본값으로 변환하도록 한다.
reduce(add, 0, [1,2,3,4,5]); // 15
// =(add(add(add([0,1]), 2), 3).... 을 반복하는 것임
const add = (a, b) => a+b;
reduce(add, [1,2,3,4,5]); // 이렇게 온다면 자동으로
reduce(add, 1, [2,3,4,5]); // 첫번째 요소를 기본값으로 변환해 계산한다.
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
함수는 가장 먼저 3번째 파라미터(이터레이터)가 있는지 확인하고, 없을 경우
acc
에서 자기 자신 이터레이터를 꺼내오고, 그 이터레이터.next()
의 값을 acc
로 지정한다.
reduce로 products의 모든 가격을 더한다면
reduce((total, curr) => total + curr.price, 0, products)
map+filter+reduce 중첩 사용과 함수형 사고
map
으로 가격을 뽑는다filter
로 특정 금액(3만원) 이하의 상품만reduce
로 모든 가격의 합을 구한다
이 때 filter
, map
의 순서를 바꿔도 결과는 동일하다.
const add = (a, b) => a+b;
reduce(
add,
map(p => p.price,
filter(p => p < 30000, products)
)
)
좀 더 함수형으로 생각하려면 add
다음 파라미터(curr
)나 map
의 두번째 파라미터(타겟 arr)가 숫자가 있는 배열로 평가되도록 코드를 작성하면 된다.
reduce(
add,
// 숫자 배열로 평가되도록
)
참고: 인프런 함수형 프로그래밍과 Javascript ES6+