JavaScript

[JavaScript] 함수형 프로그래밍 (map, filter, reduce)

swimKind 2022. 3. 31. 16:43

[JavaScript] 함수형 프로그래밍 (map, filter, reduce)

 

 

 

- 데이터

const products = [
  { name: '반팔티', price: 15000 },
  { name: '긴팔티', price: 20000 },
  { name: '핸드폰케이스', price: 15000 },
  { name: '후드티', price: 30000 },
  { name: '바지', price: 25000 }
];

 

 

# map

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

함수의 인자로 받은 iter를 통해 순회를 하고 새로운 배열의 형태를 만든다. 첫번째 인자 f는 이터러블의 어떤 값을 수집할 것인지 추상화한다. 그렇기때문에 받아온 함수(f)를 통해서 어떤 값을 수집할 것인지 f 함수에게 위임한다.

 

let names = [];
for (const a of products){
  names.push(p.name);
}

console.log(names); // ["반팔티", "긴팔티", "핸드폰케이스", "후드티", "바지"]
console.log(map(p => p.name, products)); // ["반팔티", "긴팔티", "핸드폰케이스", "후드티", "바지"]


let prices = [];
for (const a of products){
  prices.push(p.price);
}
console.log(prices); // [15000, 20000, 15000, 30000, 25000];
console.log(map(p => p.price, products)); // [15000, 20000, 15000, 30000, 25000];

함수형 프로그래밍에선 map이라는 함수의 보조함수를 통해서 이터러블 안에 있는 수집하고자 하는 어떠한 값을 수집할 수 있다. map 함수는 함수를 값으로 다루면서 원하는 시점에 인자를 적용하는 고차함수이다.

 

 

# 이터러블 프로토콜을 따른 map의 다형성

console.log([1, 2, 3].map(a => a + 1)); // [2, 3, 4]
console.log(document.querySelectorAll('*')); // undefined

[1, 2, 3]은 배열(Array)이고 이터러블이다. 하지만 querySelectorAll(NodeList)은 이터러블이긴 하지만 배열은 아니므로 Array.prototype을 상속하고 있지 않다. 따라서 map함수를 사용할 수 없다.

 

console.log(map(el => el.nodeName, document.querySelectorAll('*')));

const it = document.querySelectorAll('*')[Symbol.iterator]();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());

querySelectorAll은 이터러블이므로 [Symbol.iterator]() 메서드를 가지며 next()메서드로 값을 순회할 수 있다.

 

function* gen(){
  yield 2;
  if(false) yield 3;
  yield 4;
}

console.log(map(a => a * a, gen())); // [4, 16]

querySelectorAll 말고도 이터러블을 만족하는 것이라면 위에서 만들어놓은 map함수를 사용할 수 있다. gen()이라는 제너레이터 함수 또한 map으로 순회 가능하다. 이터러블 프로토콜을 따르는 이미 만들어져있는 이터러블에 map을 사용할 수 있지만 gen() 제너레이터 함수의 결과(문장)에 대해서도 map을 사용하여 순회할 수 있다. 그렇다는 것은 사실상 모든것들에 대해 map을 할 수 있다.

 

앞으로 만들어지는 WEB API 자바스크립트를 포함한 브라우저에서 사용되는 값들(querySelectorAll) 또한 ECMAScript의 이터러블 프로토콜을 따르고 있기때문에 계속해서 이러한 형태로 만들어질 것이다. 이터러블 프로토콜을 따르는 함수를 사용하는 것은 앞으로의 많은 다른 함수와의 조합성이 좋아진다. 훨씬 더 유연하고 다형성이 있는 기법을 사용할 수 있을것이다.

 

- Map

let m = new Map();
m.set('a', 10);
m.set('b', 20);

console.log(map(([k, a]) => [k, a * 2], m)); // [ ["a", 20], ["b", 40] ]
console.log(new Map(map(([k, a]) => [k, a * 2], m))); // { "a" => 20, "b" => 40 }

처음에 만든 Map객체와 동일하게 내부적으로 로직처리를 하고 난 후(*2)의 새로운 Map 객체를 map함수를 통해서 만들 수 있다.

 

 

# filter

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

map과 형태는 동일하다. 하지만 첫번째 인자로 받는 함수(f)를 가지고 어떤 조건을 가지고 로직을 처리할 것인지에 대해 체크하는 부분을 받아온 함수에 위임한다.

 

let under20000 = [];
for (const a of products){
  if(p.price < 20000) under20000.push(p);
}
console.log(...under20000); // { name: "반팔티", price: 15000 } { name: "핸드폰케이스", price: 15000 }

console.log(...filter(p => p.price < 20000, products)); // { name: "반팔티", price: 15000 } { name: "핸드폰케이스", price: 15000 }

let over20000 = [];
for(const a of products){
 if(p.price >= 20000) over20000.push(p);
}
console.log(...over20000); // { name: "긴팔티", price: 20000 } { name: "후드티", price: 20000 } { name: "바지", price: 25000 }

console.log(...filter(p => p.price >= 20000, products); // { name: "긴팔티", price: 20000 } { name: "후드티", price: 20000 } { name: "바지", price: 25000 }

내부에 있는 값에대한 다형성은 첫번째 인자로 받은 보조함수를 통해서 지원을 해주고 외부의 경우는 두번 째 인자로 받은 이터러블 프로토콜을 따르는 인자를 통해서 다형성을 지원해줄 수 있다.

 

console.log(filter(n => n % 2, [1, 2, 3, 4])); // [1, 3]
console.log(fitler(n => n % 2, function* () {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
}())); // [1, 3, 5]

filter함수 역시 이터러블 프로토콜을 따르고 함수형적인 코딩을 통해서 중복을 제거하고 구현할 수 있다.

 

 

# reduce

const reduce = (f, acc, iter) => {  
  for(const a of iter){
    acc = f(acc, a)
  }
  return acc;
}

reduce는 값을 축약하는 함수이다. 특정한 값들을 순회하면서 하나의 값으로 누적할 때 사용하는 함수이다. 

 

const add = (a, b) => a + b;

console.log(reduce(add, 0, [1, 2, 3, 4, 5]); // 15

// console.log(add(add(add(add(add(0, 1), 2), 3), 4), 5)); 형태로 실행된다.

reduce 함수는 첫번째 인자로 로직을 처리할 함수, 두번째 인자로 초기값, 세번째 인자로 이터러블을 받는다.

 

console.log(reduce(acc, [1, 2, 3, 4, 5]); // 15

초기 값을 전달하지 않아도 reduce 함수는 똑같은 값을 도출하는데 그 이유는

 

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;
}

위의 if문의 로직과 같이 reduce 함수의 파라미터의 초기값을 누락하고 호출하게되면 reduce 내부의 로직에서 acc에 들어온 이터러블의 [Symbol.iterator] 메서드를 실행하고 초기 값에 iter 이터러블의 next()를 실행한 값을 넘겨주기때문에 초기값이 없더라도 같은 값으로 실행된다.

 

console.log(reduce(
  (total_price, product) => total_price + product.price, 
  0, 
  products)); // 105000

reduce는 보조함수를 통해서 축약하는 것을 완전히 위임하기 때문에 숫자나 숫자 배열 뿐만아니라 객체 등의 복잡한 값들을 특정한 값으로 축약할 수 있다. reduce도 역시 보조함수를 통해서 안쪽에 있는 값의 다형성을 지원해주며 이터러블을 통해서 외부값의 다형성도 지원한다.

 

 

# map, filter, reduce

const add = (a, b) => a + b;

console.log(
  reduce(
    add, 
    map(p => p.price, 
      filter(p => p.price < 20000, products))));
      
console.log(
  reduce(
    add, 
    filter(n => n >= 20000, 
      map(p => p.price, products))));

map과 filter, reduce 함수가 중첩되어있는 함수이다. 이렇게 작성된 함수들은 뒤부터 읽으면 쉽게 읽을 수 있다. 함수를 작성할 때 내부에서 사용하려는 함수와 외부에서 평가될 이터러블의 값을 유추하면서 작성한다.