개발자 도전기/[STUDY] JS || TS

JavaScript | FP && ES6+ | go, pipe, curry

답수 2022. 1. 27. 23:12
728x90
SMALL

 

 

함수형 프로그래밍은 함수를 값으로 다룰 수 있어서 코드를 가독성 좋게 작성할 수 있다. 어떻게? 중복되어 사용하는 함수들을 추상화하면서 함축하여 표현력을 더 좋게 하면 된다. 그래서 뭘로? 강의에서 알려준 go, pipe, curry를 사용해서!

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

이렇게 중첩되어 있는 코드는 가독성이 많이 떨어지기 때문!

 

 

go 함수

go는 함수와 인자를 전달해서 즉시 어떤 값을 평가하는 함수다. 

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

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

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

위의 예시로 더 구체적으로 설명하자면, go의 첫 번째 인자에는 시작하기 위한 값을 넣고 나머지 인자에는 함수들을 받아 값을 다음 함수로 넘기면서 차례대로 함수를 실행한다.

 

 

pipe

파이프는 함수를 리턴하는 함수로, 함수들이 나열되어 있는 합성된 함수를 만든다.

 

개념적으로 조금 더 이해할 필요가 있을 것 같다. 구글링해보면, pipe function에 대해 다음과 같은 정의가 있다.

pipeline consists of a chain of processing elements, arranged so that the output of each element is the input of the next.

파이프라인은 각 요소의 출력이 다음 요소의 입력이 되도록 배열된 처리 요소의 체인으로 구성된 것이라고 한다. 즉 파이프 함수는 함수들을 하나로 합성한 후 리턴하는 함수로, 리턴된 함수는 인자를 받아 함수들을 적용하며 값을 리턴한다.

 

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

우선 위에 pipe 함수 구현한 것을 보면, pipe 함수에 ...fs인자를 받고 함수를 리턴한다. 리턴된 함수는 이 인자를 받아 함수들을 적용하며 값을 리턴한다. 내부 함수로 go를 활용한다.

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

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

 

만약 첫 번째 함수의 인자가 2개 이상일 경우에는 아래처럼 첫 번째 함수 f와 나머지 함수인 ...fs를 분리시키고, 재귀적인 방법으로 모든 인자들을 받아서 리턴할 수 있도록 한다.

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

  log(f(0, 1));

 

go, pipe 활용 예시

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


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

 

products라는 객체가 있고, 이중 20000원 이하인 제품들의 가격의 합을 구하는 함수를 구현한다면 다음과 같다.

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

// expected output: 30000

시작하는 인자로 products를 넣고, 다음 실행하는 함수에 가격이 20000 이하인 요소를 선택, 그 요소들에서 가격 value만 리턴한 후, reduce, add로 합치기

 

이를 파이프 함수로 구현한다면 다음과 같이 할 수 있다.

const products20000 = pipe(
    filter(p => p.price < 20000),
    map(p => p.price),
    reduce(add),
    console.log);
    
products20000(products);

 

 

curry

curry는 여러 개의 인수를 각각 하나의 인수만 취하는 함수를 일련의 함수로 분해하는 것을 말한다. 커리는 currying이라는 키워드로 구글링하면 많은 정보를 얻을 수 있다.

 

더 찾아본 바로는, currying은 f(a,b,c)처럼 단일 호출로 처리하는 함수를 f(a)(b)(c)와 같이 각각의 인수가 호출 가능한 프로세스로 호출된 후 병합되도록 변환하는 것이다. 커링은 함수를 호출하지 않고, 단지 변환만 한다.

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

curry함수 구현을 보면, a 와 나머지 요소들을 인자로 받는다. _가 여러 개일 경우 length에서 true로 걸러진다. 즉, 인자가 두 개 이상이면 받아둔 함수를 즉시 실행하고, 두 개보다 작다면 함수를 다시 리턴 후에 그 이후에 받은 함수로 실행한다.

 

이전부터 구현했던 map, filter, reduce 함수들도 currying할 수 있다는 말

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

 

 

 

* ref

728x90
LIST