[functional JS ES6+] map, filter, reduce

작성:    

업데이트:

카테고리:

태그: , ,

본 포스트는 인프런의 함수형 프로그래밍과 JavaScript ES6+ 강의(링크)를 듣고 정리한 내용입니다.


map

map 함수?

불필요한 for문을 제거하면서 동일한 기능을 하게 한다

products = [
  {name: '바지', price: 25000},
  {name: '반팔티', price: 15000},
  {name: '긴팔티', price: 20000},
]

// 기존 for 문
let names = [];
for (const p of products) {
  names.push(p.name);
}
log(names); // ["바지", "반팔티", "긴팔티"]


// map 문
const map = (f, iter) => {
  let res = [];
  for (const a of iter) {
    res.push(f(a));
  }
  return res;
}

log(map(p => p.name, products)); // ["바지", "반팔티", "긴팔티"]
  • 인자로 iterable을 받고, 함수도 받을 수 있다.
  • 함수로 iterable의 각 요소를 처리한 값들을 return에 반환
  • map 함수는 함수를 인자로 다루어 함수 내부에서 사용하기 때문에 고차함수
  • iterable protocol을 따르므로 높은 다형성을 가진다.


map 함수의 높은 다형성

log([1, 2, 3].map(a => a+1)); // [2, 3, 4]
log(map(el => el.nodeName, document.querySelectorAll('*'))); // ["HTML", "HEAD", "SCRIPT", ...]

document.querySelectorAll('*').map(el => el.nodeName); // TypeError
  • iterable이라면 map함수를 사용할 수 있다.
  • 하지만 iterable처럼 생긴 객체들에 대해 주의해야 한다.
  • 예를 들면 document.querySelectorAll을 통해 반환되는 NodeList는 array처럼 생겼으나, array를 상속받은 객체가 아니어서, map함수를 내장하고 있지 않아, 특별한 정의 없이 사용할 수 없다.
  • 결론 : iterable인 것이 반드시 map함수를 내장하고 있다는 것은 아니다!


filter

T/F 여부에 따라 걸러내는 함수

// 기존 명령형
const under20000 = [];
for (const p of products) {
  if (p.price < 20000) under20000.push(p);
}
log(under20000); // [{name: '반팔티', price: 15000}]


// filter 함수 사용
const filter = (f, iter) => {
  let res = [];
  for (a of iter) {
    if (f(a)) res.push(a);
  }
  return res;
}

log(filter(p => p.price < 20000, products)); // [{name: '반팔티', price: 15000}]
log(filter(n => n % 2, [1, 2, 3, 4])); // [1, 3]
log(filter(n => n % 2, function *() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
})) // [1, 3, 5]
  • filter 함수 역시 iterable protocol을 따르는 iterable을 조작 가능


reduce

iterable을 하나의 값으로 축약하는 함수

// nums를 모두 더하는 경우
const nums = [1, 2, 3, 4, 5];

// 기존 방식
let total = 0;
for (const n of nums) {
  total += n;
}
log(total); // 15


// reduce 방식
const reduce = (f, init, iter) => {
  acc = init;
  for (const a of iter) {
    acc = f(acc, a);
  }
  return acc;
}
const add = (a, b) => a + b;
log(reduce(add, 0, nums));
  • iterable을 모두 돌면서 인자로 주어지는 함수 f를 누적값과의 연산으로 처리
  • 최종적으로 모두 iterable을 돌면서 마무리되는 acc 값을 return


  • JS 내장 reduce는 init(초기값)을 생략하는 경우 iter의 첫번째 요소를 init으로 사용하도록 자동 설정
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 질문 내용

이 과정에서 이해가 되지 않아 질문을 올렸다가 이해해버려서 정정한 질문을 남긴다


안녕하세요! 초기값 acc가 없는 경우 iter에서 첫 값을 acc로 지정하고 next() 메서드를 통해 두번째 값부터 f(acc, a)를 누적하는 로직에서 궁금한 점이 있어 질문 남깁니다.

인자에 acc가 부재한 경우 if(!iter) 조건이 아니라 if(!acc) 조건으로 iter의 첫 값을 acc에 지정해주는 게 맞지 않나 싶었습니다. 해당 부분 시작 전에도 JS 내장 reduce 방식처럼 acc가 없는 경우 사용하는 방식이라고 소개하셔서 코드 부분에서 더 괴리가 있는 것 같습니다.

또한 acc[Symbol.iterator](); 의 경우 acc는 초기값이고, iterator 프로토콜을 따르는 배열은 인자로 주어진 iter니까 iter[Symbol.iterator]()가 맞지 않나요?

const reduce = (f, acc, iter) => {
  if (!iter) {
    iter = acc[Symbol.iterator]();
    acc = iter.next().value;
  }
  ...
}

강의에서는 위와 같이 작성해주셨는데, 제가 생각했을 때 이해가 되는 코드는 아래와 같습니다.

const reduce = (f, iter, acc) => {
  if (!acc) {
    iter = iter[Symbol.iterator]();
    acc = iter.next().value;
  }
  ...
}

acc가 없는 경우 인자가 2개가 들어온다고 생각하면, acc를 3번째 인자로 두어야 acc가 undefined라 !acc가 true 처리되어서 if문을 돌 것 같아서 순서를 바꿔보았습니다.

제가 배움이 얕아 잘못 생각하고 있는 것이라면 어떤 부분에서 잘못 생각하고 있는건지 여쭙고 싶습니다. 감사합니다.

__

라고 생각했는데, 아예 iter랑 acc를 재지정해주셨다는 걸 알게 되었습니다. 다른 수강생들에게도 도움이 되기 위해 정리해보는데, 혹시 오류가 있다면 짚어주시면 감사하겠습니다.

초기값(acc)이 함수로 전달되지 않는 경우 인자가 당겨져서 if 문 이전까지는 acc가 iterable(배열)이고, iter가 부재해undefined가 됩니다. 그러면 초기값이 없다는 if 문의 판별 조건은 (!iter)가 됩니다.

if문 내부에서는 acc가 iterable이니 이를 iter로 다시 지정하는 것이고, acc는 단어 뜻 그대로 초기값으로 설정하기 위해 iter.next().value로 iterable의 첫 값으로 지정합니다.

next()를 해서 iterable은 두 번째 값부터 for문에서 iteration을 진행하게 됩니다.

iter와 acc 단어 자체의 의미에 집중하다보니 이해가 되지 않았는데, 당겨진다고 생각하니까 바로 납득이 되네요. 오류가 있다면 지적 부탁드립니다. 감사합니다!

댓글남기기