[JS] 함수형 프로그래밍 입문 #2

본 강좌는 유인동님의 함수형 프로그래밍과 JavaScript ES6+ 강좌를 듣고 정리한 것입니다. 개인 학습용으로 유료 강좌 내용 전부가 아닌 유인동님의 깃허브에 공개된 자료를 바탕으로 코드를 요약 정리했습니다. 문제시 자삭하도록 하겠습니다.

함수형 프로그래밍 입문 #2

Map

기본 활용

const products = [
  { name : '반팔티', price : 10000 },
  { name : '후드', price : 20000 },
  { name : '바지', price : 50000 },
  { name : '손수건', price : 5000 },  
];

// 우리가 보통 이렇게 값을 처리하지

let prices = [];
for (const p of products) {
  names.push(p.price);
}
log(prices);

// map을 활용하면 이렇게 된다

/* 
우리가 기존에는 직접 p.price 등으로 어떤 값을 처리할 건지 지정해줬다면
map에서는 함수를 넘겨받아 어떤 값을 처리할 건지 함수한테 위임한다
*/

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

log(map(p => p.price, products));

point 1 : 원본 데이터를 넘겨주는 대신 새로운 값을 만들어 return해준다

위의 map 활용 예제에서 메서드 블록 내에서 log(prices) 를 하는 대신, map에서 새로운 값을 만든 후 return res를 통해 리턴값을 외부로 넘겨준다. 함수형이 지향하는 값의 불변성을 지키는 방법이다.

point 2 : 직접 행위를 지정하는 대신 다른 함수에 위임한 후, 그 함수를 값으로 넘겨받는다

기존 코드에서는 p.price 로 값을 직접 지정해줬다면, map에서는 함수를 넘겨받아 어떤 값을 처리할 건지 f라는 파라미터로 함수를 넘겨받는다. 불필요한 책임을 지는 것을 원치 않는다.

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

iterable protocol을 따르는 함수는 아래와 같이 다형성의 장점을 극대화한 함수형 메서드를 구현할 수 있다

// [1,2,3]은 Array가 iterator 이기 때문에 아래 코드는 결과값이 나옴

log([1,2,3].map(a => a + 1)); // [2,3,4]

// 근데 아래 document.querySelector()는 iterator가 아니기 때문에 undifined가 뜬다
// 실제로 내부를 들여다봐도 forEach는 있지만 map 함수는 없다. iterator가 아니니까.

log(document.querySelector('*').map(el => el.nodeName));

// 위에서 만든 map 함수로 만든 결과값은 신기하게도 나온다!
// 왜냐면 위의 map 함수를 통해 document.querySelector()가 iterator를 따르게 만들었기 때문. 

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

const it = document.querySelector('*')[Symbol.iterator]();
log(it.next()) // 값이 찍힌다!

제너레이터 함수의 값도 map을 활용할 수 있다.

=> 사실상 모든 값에 map을 적용할 수 있다

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

log(map(a => a * a, gen())); // [4, 9, 16];
let m = new Map();
m.set('a', 10);
m.set('b', 20);

const it = m[Symbol.iterator]();
it.next();	// { value : Array(2) , done : false };

// js의 Map 자료구조도 iterable protocol을 따르므로, 우리가 만든 map 함수를 구현할 수 있다
// 바뀐 Map 객체를 만드는 데도 이렇게 map을 조합할 수 있다
new Map(map(([k, a]) => [k, a * 2], m))));

filter

// 보통 이렇게 filter 하지만
let under20000 = [];
for (const p of products) {
	if (p.price >= 20000) under20000.push(p);
}
log(...under20000)

// 다형성을 가진 filter 함수를 구현한다면 이렇게 된다
const filter = (f, iter) => {
  let res = [];
  for (a of iter) {
    if (f(a)) res.push(a);
  }
  return a;
}

filter(a => a.price < 20000, products);

reduce

const nums = [1, 2, 3, 4, 5];

// 명령적 작성은
let total = 0;
for (const n of nums) {
  total = total + n;
}
log(total);

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

// 근데 문제가 하나 있다
// acc 값이 생략 될 경우에도 iter의 첫번째 값이 acc 초기값이 되도록 동작하게 해야댐
// iter를 iterable 객체로 바꿔주고, acc를 iter에서 next() 한 첫번째 value가 되도록 해준다

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로 누적하는 함수를 작성해보면

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

map, filter, reduce 중첩 사용

const products = [
  { name : '반팔티', price : 10000 },
  { name : '후드', price : 20000 },
  { name : '바지', price : 50000 },
  { name : '손수건', price : 5000 },  
];

// 가격을 뽑아보자 (map)
log(map(p => p.price, products));

// 특정 가격 이하의 가격을 뽑아보자 (map + filter)
// => map에 건내줄 가격을 filter를 통해 걸러준다
log(map(p => p.price, filter(p => p.price < 20000, products)));

// 그래서 그 특정 가격 이하의 뽑은 가격을 모두 합해보자
const add = (a, b) =>  a + b;
log(
  reduce(
  	add,
  	map(p => p.price,
      filter(p => p.price < 20000, products))));

// map과 filter의 순서를 바꿔도 결과는 동일하다
log(
  reduce(
  	add,
    filter(p => p.price < 20000,
  		map(p => p.price, products))));

읽는 방법

복잡해 보이지만 왼쪽에서 오른쪽으로 읽으면 해석이 된다

log(
  reduce(
  	add,
  	map(p => p.price,
      filter(p => p.price < 20000, products))));
  • products를 2만원 미만으로 필터를 하고,

  • 해당하는 프라이스를 맵을 통해 뽑아낸 다음에

  • 뽑아낸 모든 값을 add를 통해 더해준다

함수형 사고

사고의 흐름

어떤 배열이 있고, 특정한 값을 reduce로 add하고 싶다. 그러면 일단 이렇게 써보자.

log(
	reduce(
  	add,
    [10, 20, 30, 40]
  )
)

잘 동작하는 것을 알 수 있고, 그러면 저 임의의 배열이 있는 곳에 특정한 숫자 배열을 만들어 주면 된다 라고 생각이 든다

log(
	reduce(
  	add,
    map(p => p.price, products)
  )
)

특정한 값만을 추출해야 하므로, 저 products 자리에 추출한 값이 들어가야 한다 라고 생각이 든다

log(
	reduce(
  	add,
    map(p => p.price,
    filter(p => p.price < 20000, products)   
    )
  )
)
  • 코드를 잘 보면 무조건 예상 결과값, 기대값을 생각한 후 그에 맞춰 사고를 한다

  • 이걸 해서 이걸 해서 이걸 해서 이걸 해야지 라는 기존의 절차형 사고와는 달리,
    • 최종적으로 난 이게 필요하다
    • 이전에는 그럼 뭐가 있어야 하지? 라는 사고가 필요하다
  • topdown식 사고가 필요한 것 같다
 Date: 
 Tags:  JS