똑같은 삽질은 2번 하지 말자

Javascript ES6+ 에서 함수형 프로그래밍(go, pipe,curry) 본문

Javascript

Javascript ES6+ 에서 함수형 프로그래밍(go, pipe,curry)

곽빵 2021. 8. 26. 23:46

좋은 코드란 ?

누구나 이해하기 쉽고, 직관적인 코드들이다.

그런 직관적이 코드들을 구현하기위한 과정을 적어보자

이하의 map, filter, reduce (표준 라이브러리들과 기능들은 같은데 인자로 iterable을 받는다는 점이 틀리다)

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

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

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

이런 function들을 연결시켜주는 함수를 만들어 읽기 좋은 코드를 생산해보자

 

코드를 값으로 다루어보기

go(0, a=>a+1, a=>a+2, a=>a+3, (a) => console.log(a))

위의 첫번째로 받은인자로 부터 0+1+2+3 = 6 이 출력되도록 하는 go함수를 구현하자

const go = (...args) => {
  reduce((a, f) => f(a), args); // reduce의 첫번째 인자로 함수, 두번째 인자로 함수들이 들어온다.
}                               // reduce 내에서 3번째 인자가 안들어옴으로써 args에있는 첫번째 요소를 들고온다.
                                // 그리고 reduce의 보조함수로써 받은 (첫번째 요소, 함수) => 함수(첫번째 요소)
                                // 를 하면 args에 있는 나머지 함수들을 iter = Array<function>
                                // for(const a of iter) (누적결과, a) => a(누적결과)
                                // 를 함으로써 연속적으로 함수가 실행 결국 6이 출력될 것이다.

 

함수들이 나열되어있는 합성된 함수를 만드는 pipe를 구현해 보자

const f = pipe(a=>a+1, a=>a+10, a=>a+100) // 함수들을 합성해서 f 함수에 넘겨주는 pipe
const pipe = (...fs) => (a) => go(a, ...fs);

console.log(f(0)) // 111

 

여기서 f 함수에 인자로 두개가 들어올 경우엔? 

const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs)
// f => 첫번째 함수는 인자가 두개가 들어올껄 예상 따로 빼준다.
// ...as => 두개 이상의 인자들은 펼쳐진다.
// f(...as)를 함으로써 실행한 값을 넘겨준다 3이 넘어감
// 나머지 처리들은 위에서 설명했던 go의 동작과 같이 진행됨
// 113 출력

const f = pipe((a,b)=>a+b, a=>a+10, a=>a+100)

conosole.log(f(1,2));

 

위에서 만든 go를 이용해 좋은 코드를 만들어보자

// need: price 가 15000 이상인 상품들의 가격합
go(products,
   products => filter(p=>p.price >= 15000, products),
   products => map(p => p.price, products),
   prices => reduce(add, prices)
   console.log(totalPrice)
)

 

여기서 좀더 추상적인 개념이 들어가서 더 좋은 코드를 만들어볼수 있는데,

함수를 내가 원하는 시점에 평가하는 관점을 가진 함수를 반환하는 함수를 만들어보자.

const curry = f => 
  (a, ..._) => _.length ? f(a, ...) : (..._) => f(a, ..._);
  // _.length가 있다는 말은 인자가 2개이상 들어온 경우
  // 인자가 2개이상 들어온 경우에는 즉시실행 
  // 2개 미만인 경우에는 나중에 받은 인자와 함께 실행
  
const add = curry((a,b) => a + b)

console.log(add(2)(3)) // 5
console.log(add(3)(10)) // 13

 

// 즉시 실행하지 않고 이후 인자를 기다리도록 curry를 감싸준다
const map = curry((f, iter) => {
  let res = [];
  for (const a of iter) {
    res.push(f(a));
  }
  return res;
});

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

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

// go로 만든 좋은코드를 더 좋은코드로
go(
  products,
  filter(p => p.price < 20000), // (products) => filter(p => p.price < 20000)(products)
  map(p => p.price), // (products) => map(p => p.price)(products)
  reduce(add), // (prices) => reduce(add)(prices)
  log);
Comments