함수형 프로그래밍 with JS ②

:D ·2023년 4월 30일
0

해당 강의를 듣고 정리한 글입니다.... 🐿️
https://www.inflearn.com/course/functional-es6

섹션 3. map, filter, reduce

map

아래의 코드는 products 배열로 products의 price들만 있는 배열을 새로 만든다. 아래의 코드처럼 배열 각 요소에 대하여 주어진 함수를 수행한 결과를 모아 새로운 배열을 만들때 map을 사용한다.

const products = [
  { name: "반팔티", price: 15000 },
  { name: "긴팔티", price: 20000 },
  { name: "후드티", price: 40000 },
  { name: "바지", price: 30000 },
];

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

const names = [];
for (const p of products) {
  names.push(p.name);
}
console.log(names); // [ '반팔티', '긴팔티', '후드티', '바지' ]

위의 코드를 함수형으로,, map 함수로 작성해보았다.
함수형 프로그래밍에서는 외부세계에 영향을 미치지 않고, 인자와 리턴값으로 소통하는 것을 권장하기 때문에 위의 코드와는 return 부분이 다르다. 또한 어떠한 값을 수집할 것인지를 func 함수에게 위임하였다.

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

console.log(map((p) => p.price, products)); // [ 15000, 20000, 40000, 30000 ]
console.log(map((p) => p.name, products)); // [ '반팔티', '긴팔티', '후드티', '바지' ]

map 함수를 통해 중복을 제거할 수 있었고, 어떠한 이터러블 객체이건 원하는 요소를 뽑아내 새로운 배열을 만들 수 있었다.

map 함수는 이터러블 프로토콜을 따르고 있기 때문에 다형성이 굉장히 높다!

document.querySelectorAll('*').map(el ⇒ el.nodeName)); // error

document.querySelectorAll('*')가 NodeList(array 형태)로 반환 하지만 map을 사용하면 에러가 난다.
왜냐면은 .... 프로토타입에 map 함수가 구현이 안되어 있기 때문이다.
그렇지만 NodeList는 유사배열객체이지만 이터러블이기 때문에 아까 위에서 만든 map을 사용할 수 있다.

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

이런 것도 가능하다.

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

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

사실상 이터러블 프로토콜을 따르는 모든 것들을 map 할 수 있다!

filter

만약 특정 금액 이하를 걸러내어 배열을 반환하고 싶다면, 먼저 명령어 코드로 작성해보잡.

const under2000 = [];
for (const p of products) {
  if (p.price < 20000) under2000.push(p);
}
console.log(...under2000); // { name: '반팔티', price: 15000 }

map 처럼 함수형으로 filter 함수를 만들어보자면, 어떠한 조건이건 filter를 할수 있도록 func에 위임해주고, iter 인자를 받아 이터러블 프로토콜을 따르도록 해준다.

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

console.log(...filter((p) => p.price < 20000, products)); // { name: '반팔티', price: 15000 }

reduce

reduce는 값을 축약하는 함수이다.
이번에도 먼저 명령어 코드로 작성해보자.

const nums = [1, 2, 3, 4, 5];
let total = 0;
for (const n of nums) {
  total += n;
}
console.log(total); // 15

함수형으로 또 변경해보자구요... 내부적으로 reduce는 이렇게 동작한다.

const add = (a, b) => a + b;
console.log(add(add(add(add(add(0, 1)), 2), 3), 4), 5); // 15

저렇게 동작하도록 코드를 작성해보면....

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


const add = (a, b) => a + b;
console.log(reduce(add, 0, [1, 2, 3, 4, 5])); // 15

추가로.. 실제로 자바스크립트에서의 reduce는 acc값을 옵셔널하게 구현되어 있는데 수정해보자.
acc 값이 없으면 인자로 받은 iter의 첫 번째 값으로 acc의 기본값으로 변경해주도록 한다.

const reduce = (func, acc, iter) => {
  if (!iter) {
    iter = acc[Symbol.iterator]();
    acc = iter.next().value;
  }
  for (const a of iter) {
    acc = func(acc, a);
  }
  return acc;
};

const add = (a, b) => a + b;
console.log(reduce(add, [1, 2, 3, 4])); // 10

map + filter + reduce 중첩 사용과 함수형 사고

map, filter, reduce 함수를 통해 products를 다뤄보면서 조금 더 map, filter, reduce 응용에 대해 확인해보자

만약, 금액을 뽑아낼 때 30000원 미만의 상품만 뽑아내고 싶다면 어떻게 해야할까?

console.log(map((p) => p.price, filter((p) => p.price < 30000, products)));

또 여기서 그 상품들의 합친 금액을 알고 싶다면?

console.log(reduce(add, map((p) => p.price, filter((p) => p.price < 30000, products)))); // 35000

조금 복잡하지만 오른쪽에서 부터 읽어내면 된다. filter -> map -> reduce 순서로!

섹션 4. 코드를 값으로 다루어 표현력 높이기

위의 예시에서 중첩이 되어있다보니, 코드 가독성이 떨어지는데 그 코드를 조금 더 읽기 편하게 변경해보자.

go

go 함수로는 함수의 결과를 다음 함수의 인자로 전달하여 결과를 만드는 함수를 만들려고 한다.
즉, 인자들을 하나의 값으로 축약해나가는 방식이다.

// 함수의 결과를 다음 함수의 인자로 전달하는 방식
const go = (...args) => {
  reduce((a, f) => f(a), args);
};

go(
  0,
  (a) => a + 1,
  (a) => a + 10,
  (a) => a + 100,
  console.log
); // 111

pipe

go는 즉시 값을 평가하는데 사용된다면, pipe 는 함수들이 나열되어있는 합성된 함수들을 만드는 함수이다.
먼저 합성된 함수들을 (fs)받고, 나중에 인자를 받는다(a). 받은 인자로 go 함수를 실행하면 pipe 함수가 만들어진다.

const pipe = (...fs) => (a) => go(a, ...fs);
const f = pipe(
  (a) => a + 1,
  (a) => a + 10,
  (a) => a + 100,
  console.log
);

f(20); // 131

인자를 두개를 받는 함수도 사용할 수 있도록 수정해보자.

const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);
const f = pipe(
  (a, b) => a + b,
  (a) => a + 10,
  (a) => a + 100,
  console.log
);

f(20, 10); // 140
console.log(reduce(add, map((p) => p.price, filter((p) => p.price < 30000, products)))); 

아까 이 코드를 go로 리팩터링 해보자구요..

go(
  products,
  (products) => filter((p) => p.price < 30000, products),
  (products) => map((p) => p.price, products),
  (prices) => reduce(add, prices),
  console.log
);

간결해지지는 않았지만 코드 가독성은 더 좋아졌다.

curry

curry 함수를 값으로 다루면서 받아둔 함수를 내가 원하는 시점에 평가시키는 함수이다.


// 함수를 받아서 함수를 리턴하고, 리턴된 함수가 인자가 2개 이상이면 즉시 실행, 아니라면 다시 함수를 리턴
const curry = (f) => (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);

const mult = curry((a, b) => console.log(a * b));
const mult3 = mult(3);
mult3(10); // 30
mult3(5); // 15

curry를 map, filter, reduce를 적용하면 map, filter, reduce 함수들은 인자를 하나만 받으면 이후 인자들을 받도록 기다리는 함수를 리턴한다.

const curry =
  (f) =>
  (a, ..._) =>
    _.length ? f(a, ..._) : (..._) => f(a, ..._);

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

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

const reduce = curry((func, acc, iter) => {
  if (!iter) {
    iter = acc[Symbol.iterator]();
    acc = iter.next().value;
  }
  for (const a of iter) {
    acc = func(acc, a);
  }
  return acc;
});

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

const go = (...args) => {
  reduce((a, f) => f(a), args);
};

그러면 products를 이후에 주도록 아래와 같이 변경이 가능하다.

go(
  products,
  (products) => filter((p) => p.price < 30000)(products),
  (products) => map((p) => p.price)(products),
  (prices) => reduce(add)(prices),
  console.log
);

또 최종적으로는 이렇게 표현이 가능하다. 이 부분이 아직 이해가 안되어서,, 다시 들어봐야겠다!

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

이렇게 map((p) => p.price), reduce(add)중복이 되는 코드가 있다면

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

go(
  products,
  filter((p) => p.price >= 30000),
  map((p) => p.price),
  reduce(add),
  console.log
);

이렇게 수정이 가능하다.

const total_price = pipe(
  map((p) => p.price),
  reduce(add));

const base_total_price = predi => pipe(
  filter(predi),
  total_price);

go(
  products,
  base_total_price((p) => p.price < 30000),
  console.log
);

go(
  products,
  base_total_price((p) => p.price >= 30000),
  console.log
);

함수형 프로그래밍에서는 잘게 나누어진 고차함수들을 함수의 조합으로 만들어가면서 중복을 제거하는 식으로 사용할 수 있다.

회고 💫

넘모🔥 어려워서 2번 보고 정리했다. 이해한 것 같지만 다시 보면 또 이해가 안될 것 같아서 계속 코드를 보면서 익숙해지도록 공부해야겠다...
실제 프로젝트에 적용해보면서 익숙해지고 싶은데, 강의를 다 들으면 간단하게 적용해볼 수 있는 예제를 찾아서 해봐야겠당.

profile
강지영입니...🐿️

0개의 댓글