본문 바로가기

Web[웹]/ES6+ 함수형 언어

[JS: ES6+] 코드를 값으로 다루어 표현력 높이기 (go, pipe)

 

 

#1 go, pipe 함수를 통해 표현력을 높이기

 

go, pipe 함수란 무엇인가?

 

코드를 값으로 다룬다면 전체적인 코드의 표현력을 높일 수 있는데, go, pipe를 구현해서 표현력을 높여보자.

 

먼저 다음과 같은 코드가 있다고 가정하자.

 

a(b,c(d,e()));

 

위 코드는 함수가 연속해서 중첩되어 가독성이 떨어져서, 작성하기도 해석하기도 힘들다. 이를 간편하게 더 읽기 쉽게 코드를 짜기 위해서 위에서 언급한 go, pipe 함수를 활용해보자.

 

우선 go 함수를 보자.

 

go 함수는 인자를 받아 결과를 바로 산출해내는 함수로, 첫번째 인자는 시작되는 값을 받고, 나머지는 함수를 받아 첫번째 인자가 두번째 함수로 가서 결과를 만들고 그 결과가 또 세번째 함수로 가서 결과가 만들어지는 순으로 진행된다.  이러한 go 함수는 reduce로 쉽게 구현이 가능하다.

 

go 함수를 코드로 나타내보자.

 

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

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

 

다음은 pipe 함수이다.

 

pipe 함수는 함수를 리턴하는 함수로 인자로 함수들을 받아 그 함수들을 합성해 하나의 함수를 리턴한다.  go와 유사하다고 볼 수 있는데, go 함수는 인자로 받은 함수들을 모두 실행시켜 결과에 해당값을 리턴하지만, pipe 함수는 인자로 받은 함수를 모두 합쳐 합성된 함수를 리턴하는 점에서 다르다.

 

const pipe = (...funcs) => arg => funcs.reduce((a, f) => f(a), arg);

const p = pipe(
  a => a + 1,
  a => a + 10,
  a => a + 100
); 

console.log(p(0)); // 111

 

pipe 함수를 작성하는 과정을 하나씩 분리해서 알아보자.

 

pipe = (...funcs) => {};
// pipe 함수는 인자로 함수들을 받는다.

pipe = (...funcs) => () => {};
// 함수를 리턴하게 될 것이다.

pipe = (...funcs) => arg => {};
// 여기서 arg는 pipe 함수가 실행되어 함축된 함수, 그 함수의 매개 변수이다.

pipe = (...funcs) => arg => funcs.reduce(() => {}, arg);
// 함수들을 함축해야 하므로 pipe의 인자로 들어온 함수들에 reduce를 사용한다.
// reduce의 시작으로 함축된 함수의 매개변수인 arg를 전달해준다. (arg: 값)

pipe = (...funcs) => arg => funcs.reduce((a, f) => f(a), arg);
// 이제 위와 같이 reduce의 첫번째 인자를 채워준다.
// 처음 reduce가 실행될 때는 a가 pipe 함수의 실행 결과인 함수의 인자 **값**이 들어간다.
// 다음부터는 그 함수의 실행 결과 값이 a가 되어 누산되는 과정이 된다.

 

위 코드를 보면 마지막 reduce의 동작이 go와 거의 유사하기 때문에 아래와 같이 작성해도 된다.

 

pipe = (...funcs) => (arg) => go(arg, ...funcs);

 


 

go를 사용하여 읽기 편한 코드만들기

 

그럼 이제 이전 챕터에서 작성했던 코드를 가져와서 go 코드로 한번 바꿔보자.

 

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

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

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

 

위 코드는 아까 처음 언급했던 함수처럼, 코드를 짜기 힘들뿐만 아니라 해석하기도 힘들다.

 

이제 go를 이용해서 읽기 좋은 코드로 만들어보자.

 

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

 

코드줄은 더 길어졌지만, 위에서 부터 아래로 읽을 수 있게 되어서, 읽기 수월해졌다.

 


 

curry 를 적용시켜 더 읽기 편한 함수 만들기

 

 

위 코드를 더 간략하게 만들 수 있는데, curry를 filter, map, reduce에 적용시키면 된다.

 

먼저, curry 함수는 여러 개의 인자를 가진 함수를 호출 할때, 파라미터의 수보다 적은 수의 파라미터의 인자로 받으면, 누락된 파라미터를 인자로 받는 기법이다. 즉, 함수 하나가 n개의 인자를 받는 과정을 n개의 함수로 각각의 인자를 받도록 하는 것이다.

 

curry 함수가 동작하는 코드를 짜보자.

 

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

const mult = curry((a, b) => a * b);

console.log(mult()); /* 인자를 전달하지 않았을 경우 */
// 실행결과: (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);

console.log(mult(1)); /* 인자를 한개만 전달 했을 경우 */
// 실행결과: (..._) => f(a, ..._);

 

이제 본격적으로 앞에서 다루었던 예제의 filter, map, reduce 에 curry를 적용시켜보자.

filter, map, reduce 중첩 예제: https://opentogether.tistory.com/69

 

 

/*src="fx.js"*/

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

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

 

위에서 filter, map, reduce 에 curry를 적용했고, 이제 본격적으로 줄여보자.

 

/* 원래 코드 */
go(
   products,
   products => filter(p => p.price < 30000, products),
   products => map(p => p.price, products),
   prices => reduce(add, price),
   console.log);

/* curry를 적용한 코드 */
go(
   products,
   products => filter(p => p.price < 30000)(products),
   products => map(p => p.price)(products),
   prices => reduce(add)(price),
   console.log);  

 

위와 같이 표현한 것이 왜 더 코드가 간단하냐고 할 수도 있지만, 위 코드는 아래와 같이 변경이 가능하다.

products를 받아서 그대로 products로 전달했으므로, 다음과 같이 간략히 축약 가능하다.

 

/* curry를 적용한 코드 */
go(
   products,
   filter(p => p.price < 30000),
   map(p => p.price),
   reduce(add),
   console.log);

 

결론적으로, go함수와 함께 사용될 때, 리턴된 인자를 다음 함수의 두 번째 인자로 다시 넘겨줘야할 번거로움을 없앨 수 있다.

 


 

함수 조합으로 중복 줄이기

 

만약 2개의 go 함수가 다음과 있다고 가정해보자.

 

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

 

위 함수에 pipe를 적용시키면 중복을 줄일 수 있다.

 

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

const base_total_rice = predi => pipe(
   filter(predi),
   total_price,
)

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

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