자바스크립트 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 중첩 사용과 함수형 사고

  1. map으로 가격을 뽑는다
  2. filter로 특정 금액(3만원) 이하의 상품만
  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+