[FP/JS] prototype이 Arrary가 아닌 iterable 객체 map, filter, reduce 구현하기
2023년 03월 27일
1. 함수형 프로그래밍에서 따로 map, filter, reduce를 만드는 이유?
함수형 프로그래밍에서는 불변성과 순수성을 유지하는 것이 중요합니다. 이를 위해 입력값을 변경하지 않고, 부작용(side effect)을 일으키지 않는 순수 함수를 사용합니다. 이 때, 이터러블 객체를 다루는 함수들도 순수 함수로 만들기 위해서는, 이터러블 객체에 대한 순회 방식과 결과값을 생성하는 방식에 대한 제어권을 개발자가 가지는 것이 중요합니다.
따라서, 함수형 프로그래밍에서는 이터러블 객체에 대한 map, reduce, filter 등의 함수를 따로 만들어서 사용합니다. 이러한 함수들은 이터러블 객체를 순회하면서, 순수 함수를 적용하고 새로운 이터러블 객체를 생성하는 방식으로 동작합니다. 이를 통해 입력값의 불변성과 부작용의 방지를 보장하면서도, 새로운 이터러블 객체를 생성하는 효율적인 방법을 제공합니다.
그렇다면 기존의 map, filter, reduce는 불변성과 순수성이 제대로 유지되고 있지 않을까요?
맞습니다. Array
객체의 map, filter, reduce 함수들은 함수형 프로그래밍에서 이터러블 객체를 다루는 함수들과 유사한 기능을 제공하지만, 기본적으로 부작용을 일으키기 때문에, 순수 함수라고 할 수는 없습니다.
이 함수들은 원본 배열을 변경하지 않고, 새로운 배열을 반환하기 때문에, 불변성을 유지하는 것은 맞습니다. 하지만, 이 함수들 내부에서 다른 부작용을 일으키는 경우가 있습니다. 예를 들어, map 함수 내부에서 콜백 함수의 결과값을 저장하기 위해 새로운 배열을 생성하거나, reduce 함수 내부에서 콜백 함수를 호출하기 전에 초기값을 설정하는 등의 작업을 수행합니다. 이러한 작업들은 순수 함수의 조건을 만족하지 않습니다.
따라서, 함수형 프로그래밍을 엄밀하게 따르는 경우, Array 객체의 map, filter, reduce 함수들은 함수형 프로그래밍에서 이터러블 객체를 다루는 함수들보다는 불순 함수라고 할 수 있습니다.
또한 이터러블 객체이지만 Array가 prototype이 아닌 경우에는 map, filter, reduce같은 메소드를 사용하지 못합니다. 예시로
const nodes = document.querySelectorAll("*");
nodes는 이터러블 객체임에도 Array
를 상속받지않고 NodeList
라는 객체를 상속받고 있기 때문에 map같은 메소드를 사용하지 못합니다. 그러니 한번 만들어봅시다!
2. map, filter, reduce 구현
2-1. map
const arr = [1, 2, 3];
const nodes = document.querySelectorAll("*");
const map = (f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
};
console.log(map((e) => e * 2, arr)); // [2, 4, 6,]
console.log(map((e) => e.nodeName, nodes)); // ["HTML", "HEAD", "META", "META", "META", "TITLE", "BODY"]
기존의 map을 사용하는 것과 같은 기능을 수행하는 map 함수입니다. 첫 번쨰 인자로는 내부에서 실행할 함수를 받고 두 번쨰 인자로는 iterable한 객체를 받고 있습니다.
내부에서 for of... 문으로 iter을 순회하면서 res라는 새로운 배열에 삽입 후 배열을 리턴합니다. 간단하쥬?
2-2. filter
const arr = [1, 2, 3];
const nodes = document.querySelectorAll("*");
const filter = (f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) {
res.push(a);
}
}
return res;
};
console.log(filter((e) => e === 2, arr)); // [2]
console.log(filter((e) => e.nodeName === "SCRIPT", nodes)); // [script, script, script .....]
마찬가지로 nodeName이 script인 노드들만 가져와서 새로운 배열을 리턴하는 filter 함수를 구현했습니다.
2-3. reduce
const arr = [1, 2, 3, 4, 5];
const add = (a, b) => a + b;
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;
};
console.log(reduce(add, 0, arr)); // 15
console.log(reduce(add, arr)); // 15
첫 번쨰 인자로 숫자를 더해주는 함수를 삽입했고 두번쨰 인자는 초기 값 세 번쨰 인자는 이터러블 객체를 삽입합니다. 여기서 포인트는 세 번쨰 인자를 삽입하지 않았고 두 번째 인자값이 이터러블일 경우입니다.
이럴 경우에는 위 코드로 설명하자면 arr의 [Symbol.iterator]
을 뽑아서(이터러블) iter을 생성합니다. 그 후 iter의 next메소드를 실행하면 value, done 값이 존재합니다. value는 배열의 값이고 done은 마지막 값 여부입니다. 따라서 iter.next().value
는 arr의 첫 번째 값인 1입니다.
즉 iter객체(arr와 똑같은 형태)를 생성하며 acc의 값을 생성해서 초기값을 개발자가 설정을 마음대로 조절할 수 있다는 점입니다.
여기까지 보신다면 그래서 왜 이렇게 까지 하는지 이해가 안되실 수도 있을 겁니다. 그 부분은 이제부터 제가 포스팅하는 함수형 프로그래밍에 꼭 필요하기 때문에 지금 이해하실 필요는 없습니다. ^^
참고 문서
인프런 강의 : 함수형 프로그래밍과 JavaScript ES6+