JavaScript

[JavaScript] 함수형 프로그래밍 (go, pipe, curry)

swimKind 2022. 4. 1. 15:16

[JavaScript] 함수형 프로그래밍 (go, pipe, curry)

 

 

 

- 데이터

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

 

 

 

# go

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

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

go() 함수는 함수들과 인자를 전달해서 즉시 어떤 값을 평가한다. go() 함수는 5가지의 인자를 함수를 받는다. go의 인자는 rest 파라미터를 이용하여 여러가지 인수를 배열로 받는다. rest 파라미터는 배열이고 이터러블 프로토콜을 만족한다. 차례대로 함수를 실행하여 값을 도출하기 위해 reduce() 함수를 사용하여 축약된 값을 반환하고 마지막은 console.log 함수를 통해 결과를 로그로 출력한다.

 

- go 함수 이용하기

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

기존의 map, filter, reduce 함수를 사용했을 때는 아래에서 위, 오른쪽에서 왼쪽으로 읽어야하는 느낌이 강했지만 go 함수를 사용하게되면 차례대로 위에서 아래, 왼쪽에서 오른쪽으로, 연속적으로 평가할 수 있다.

 

 

# pipe

const go = (...args) => reduce((a, f) => f(a), args);
const pipe = (...fs) => (a) => go(a, ...fs);

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

pipe 함수는 함수들이 나열된 합성된 함수를 리턴하는 함수이다. pipe 함수는 여러개의 함수들을 인자로 받아 연속적으로 실행하여 축약하는 하나의 함수를 만들어 리턴한다. pipe 함수의 인자로 여러가지 함수(fs)를 받고 함수를 시작하는데 쓰는 인자(a)를 나중에 받고, 받은 인자를 가지고 go()함수를 실행하면서 인자(a)와 함수(fs)를 전달한다.

 

- go, pipe 변형

const go = (...args) => reduce((a, f) => f(a), args);
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);

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

const f = pipe(
               (a, b) => a + b, 
               a => a + 10,
               a => a + 100
              );
  
console.log(f(0, 1)); // 111
// console.log(f(add(0, 1))); // pipe() 함수의 이점이 사라짐

만약 인자를 2가지 이상을 받아서 평가된 값으로 함수를 진행시킨다면 go() 함수의 경우는 2개 이상의 인자를 전달 받고 평가하는 함수를 지정하면 된다. 하지만 pipe() 함수는 f(add(0, 1))의 형식으로 인자를 전달해야 하는데 이 방식은 pipe() 함수로 사용하기에 다소 아쉽다. 그렇기 때문에 pipe() 함수에서 인자를 전달 받는 방식을 변경한다. pipe() 함수의 첫번째 인자로 들어갈 함수(f)는 따로 지정하고 리턴할 함수의 인자(a)를 여러개를 받아야하기 때문에 (...as)로 받는다. go() 함수의 인자로 전달될 부분도 마찬가지로 첫번째 인자와 나머지 전달될 인자를 구분한다.

 

 

# curry

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

const mult = curry((a, b) => a * b);
console.log(mult(3)); // (..._) => f(a, ..._);
console.log(mult(3)(2)); // 6

const mult3 = mult(3);
console.log(mult3(10)); // 30
console.log(mult3(5)); // 15
console.log(mult3(3)); // 9

curry는 함수를 값으로 다루면서 받아둔 함수를 원하는 시점에 평가시키는 함수이다. 먼저 함수를 받아서 함수를 리턴하고 인자를 받아서 인자가 원하는 갯수만큼 일때 받아두었던 함수를 평가시킨다. mult() 함수는 두개의 인자를 받고 곱하는 함수이다. 이 함수를 curry() 함수로 호출하면 두번째 인자까지 받았다는 조건(_.length ?)이 만족되면 실행(f(a, ..._)하고 첫번째 인자까지 받았다면 함수((..._) => f(a, ..._))를 반환한다.

 

- go와 curry의 조합

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

const map = curry((f, iter) => {
  let res = [];
  for (const a of iter) {
    names.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(
//    products,
//   products => filter(p => p.price < 20000)(products),
//    products => map(p => p.price)(products),
//    prices => reduce(add)(prices),
//    console.log
//   ); // 30000  
  
go(
   products,
   filter(p => p.price < 20000),
   map(p => p.price),
   reduce(add),
   console.log
  ); // 30000

기존에 만들었던 map, filter, reduce에도 curry를 적용했다. 하여 이 세 함수가 인자를 하나만 받으면 이후 인자를 더 받기로 기다리는 함수를 리턴하도록 되어있다. go() 함수를 살펴보면 기존에 함수(filter, map, reduce)를 첫번째 인자로 받고 두번째 인자에 데이터(products)를 받았는데, curry() 함수를 통해서 (map or filter or reduce)(products or price) 형식으로 첫번째 인자를 먼저 실행하면서 두번째 인자를 이후에 실행할 수 있다. 

또한, products를 받아서 함수(map or filter or reduce)에 그대로 products를 전달한다는 것은 해당하는 함수가 products를 받는다는 이야기고 products => (map or filter or reduce())(product) 함수가 products를 받는다는 얘기는 products를 제외하고 map or filter or reduce() 만으로 사용할 수 있다.

 

 

# 함수 조합으로 함수 만들기

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

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

go(
   product,
   base_total_price(p => p.price < 20000),
   console.log
  ); // 30000
  
go(
   product,
   base_total_price(p => p.price >= 20000),
   console.log
  ); // 75000

 

total_price라는 pipe 함수를 만들어서 가격의 축약된 값을 도출하는 함수를 만들어 중복을 제거한다. 또, base_total_price라는 predi 함수를 전달 받는 pipe 함수를 만든다. 이 함수의 경우는 predi함수로 전달받은 함수를 filter 함수의 인자로 pipe를 리턴하는 함수이다. 이러한 방식들로 고차함수들을 통해 함수의 조합으로 만들어가면서 함수를 잘게 나누면서 중복을 제거하며 더 많은 곳에서 사용할 수 있다.