JavaScript - go, pipe, currying

박정호·2022년 10월 25일
1

JS

목록 보기
19/24
post-thumbnail

⭐️ go 함수

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

👉 함수를 위에서부터 아래로, 왼쪽에서 오른쪽으로 평가하면서 연속적으로 함수를 실행하고 이전에 실행된 함수의 결과를 다음 함수에 전달하는 함수

👉 go함수로 표현하는 것은 함수형프로그래밍에서 코드를 값으로 다루는 아이디어라고 생각하면 된다.

👉 go함수를 통해서 어떠한 함수가 코드인 함수를 받아서 평가하는 시점을 원하는대로 다룰 수 있고 코드의 표현력을 높일 수 있다.

1️⃣ go 함수 기본동작 과정

  • go함수는 인자로 함수리스트를 받는다.
go ([
	0,
    a = > a + 1,  
    a = > a + 10,  
    a = > a + 100, 
    console.log 
])
  • spread연산자를 통해서 인자를 받아오는 것이 가능하고 리스트는 다음과 같이 존재한다.
const go = (...list) => { console.log(list)};

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

  • 첫번째 인자값을 초기값으로 하고, 두번째 인자부터 함수들의 리스트를 받아 반환 값을 다음 함수로 넘겨 함수를 리턴한다.
const go = (...args) => reduce((a, f)=>f(a),args);

go (
	0,
    a = > a + 1,  // a = 1
    a = > a + 10,  // a = 11
    a = > a + 100, // a = 111,
    console.log // 마지막 값인 111이 출력
)

2️⃣ map + filter + reduce 중첩

  1. filter를 통해 20000미만 가격의 상품을 필터링한다.
  2. map을 통해 필터링된 상품들의 가격을 뽑아낸다.
  3. reduce을 통해 가격들을 누적하여 총합을 구한다.

3️⃣ go함수를 통한 리팩토링

  • 가독성이 높아졌다.

⭐️ pipe 함수

go 함수와 달리 함수를 리턴하는 함수이다.
go함수는 인자값으로 들어온 함수에서 즉시실행하여 결과값을 반환해 다음 함수로 전달한다면
pipe함수는 함수자체를 반환하여 최종적으로 인자값으로 받은 함수리스트를 합성해서 합성된 합수를 가지고 로직을 수행한다.

1️⃣ pipe 함수 기본동작 과정

pipe 함수는 내부적으로 go함수를 사용한다.
pipe 함수의 첫 번째 인자를 값이 아닌 함수로 받을 수 있도록 하기 위해, 첫 번째 함수인 f와 나머지 함수인 ...fs를 따로 받는다. 그렇게 리턴된 합성된 함수는 인자(...as)를 받게 되면 go 함수를 의 인자로 함수를 넣어 차례로 함수를 실행시킬 때, 첫 번째 받은 함수에 해당 인자(...as)를 넣어 먼저 실행하게 하고, 그다음에 나머지 함수들을 차례로 실행시키도록 한다.

const pipe = (...fs) => (a) => go(a, ...fs);
const f = pipe(
    a=> a+1,
    a => a+10,
    a => a+100
); 
f(0)// 111

✏️ 위의 과정을 하나씩 분리

  1. pipe 함수는 인자로 함수들을 받는다.
pipe = (...funcs) => {};
  1. 함수를 리턴
pipe = (...funcs) => () => {};
  1. 여기서 arg는 pipe 함수가 실행되어 함축된 함수, 그 함수의 매개 변수이다.
pipe = (...funcs) => arg => {};
  1. 함수들을 함축해야 하므로 pipe의 인자로 들어온 함수들에 reduce를 사용한다. reduce의 시작으로 함축된 함수의 매개변수인 arg를 전달해준다. (arg: 값)
pipe = (...funcs) => arg => funcs.reduce(() => {}, arg);
  1. 이제 위와 같이 reduce의 첫번째 인자를 채워준다. 처음 reduce가 실행될 때는 a가 pipe 함수의 실행 결과인 함수의 인자 이 들어간다. 다음부터는 그 함수의 실행 결과 값이 a가 되어 누산되는 과정이 된다.
pipe = (...funcs) => arg => funcs.reduce((a, f) => f(a), arg);

2️⃣ pipe 함수에 기능 추가

현재 구현한 pipe함수는 최초 인자값으로 인자값을 하나만 받기 때문에 처음 인자값을 (0,1,2,...)으로 줄 수는 없다. 따라서 사용하려면 f(add(0,1))와 같이 사용해야 한다.

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

3️⃣ pipe함수를 통한 리팩토링

✏️ 다음은 중복되는 코드가 존재하는 두개의 함수가 있다.

//30,000 원이상의 제품의 총 가격을 구하는 함수
go(
    books,
    filter(p => p.price >= 30000),
    map(p => p.price),
    reduce(add),
    log
)
//30,000 원미만의 제품의 총 가격을 구하는 함수
go(
    books,
    filter(p => p.price < 30000),
    map(p => p.price),
    reduce(add),
    log
)

✏️ 기존 로직에서 map(p => p.price)과 reduce(add) 함수를 따로 빼서 total_price라는 파이파함수로 모듈화****

const total_price = pipe(
    map(p => p.price),
    reduce(add)
);
go(
    books,
    filter(p => p.price < 30000),
    total_price,
    log
)

✏️ 1차 모듈화후에도 filter와 total_price라는 두개의 함수가 인자값을제외하면 중복되기에 모듈화가 가능. (base_total_price)

const total_price = pipe(
    map(p => p.price),
    reduce(add)
);
const base_total_price = predi => pipe(
    filter(predi),
    total_price
);
go(
    books,
    base_total_price(b=>b.price >=30000),
    log
)

go(
    books,
    base_total_price(b=>b.price <30000),
    log
)

💡 잠깐)
: 봐도봐도 이해가 명확히 되지 않는다면 꼭꼭 읽어보자.
-> 함수형 프로그래밍 pipe

⭐️ currying

여러 개의 인자를 가진 함수를 호출 하는 경우, 파라미터의 수보다 적은 수의 파라미터를 인자로 받으면 누락된 파라미터를 인자로 받는 기법.

즉, 커링은 함수 하나가 n개의 인자를 받는 과정을 n개의 함수로 각각의 인자를 받도록 하는 것이다. 부분적으로 적용된 함수를 체인으로 계속 생성해서 결과적으로 값을 처리하도록 하는것이 본질이다.

앞서 go함수를 이용해서 중첩코드를 리팩토링하였으나 currying 통해 한번 더 리팩토링이 가능하다.

1️⃣ curry함수 기본동작 과정

  • curry함수는 인자값으로 함수(f)를 받아서 함수를 리턴.
    ((a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);)

  • 반환된 함수가 실행되었을 때 인자값이 2개 이상일 경우(.length?) 즉시 함수를 실행
    (f(a,..._)))

  • 인자값이 2개 미만일 경우 함수를 다시 반환 후 ( (..._) => f(a, ..._)) 가지고 있다가 나중에 인자값을 받으면 (..._) 받아놨던 인자와 합쳐 실행.

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

예시

const mult = curry((a, b) => a * b);
log(mult);//(a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._)
log(mult(1)); //(..._) => f(a, ..._)
log(mult(2,3)); // 6
log(mult(2)(3)); //6
  • 인자값 없이 mult 호출 시 currry함수만 호출되는걸 볼 수 있다. 출력된 함수내용 중 f부분에 mult에서 인자값으로 넘겨 준 함수가 들어가는 것.

  • 인자값을 하나만 줬을 경우에는 .length가 false가 되어 ((...) ⇒ f(a,..._)가 반환.

  • 인자값을 2개(2,3)를 넘겨주면 .length가 true로 f(a, ...)함수가 즉시 실행되어 6을 반환.

  • 인자값을 순차적으로 2개(mult(2)(3))를 넘겨주면 첫 인자값을 넘겨주며 함수가 반환되고((..._) ⇒ f(2,..._)) 그 다음 인자값인 (3)을 넘겨주며 함수가 실행되어 6이 반환.

2️⃣ currying을 통한 리팩토링

✏️ map, reduce, filter함수에 curry함수로 wrapping해주기

const curry = f => (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
const map = curry((f, iter) => {
    let res = [];
    for (const i of iter) {
        res.push(f(i));
    }
    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(
    books,
    books => filter(p => p.price < 30000)(books),
    books => map(p => p.price)(books),
    prices => reduce(add)(prices),
    log
)

✏️ 리팩토링

/* 기존 코드 */
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로 전달했으므로, 다음과 같이 간략히 축약 가능

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

⭐️ 실전예제

✅ Products를 함수의 인자로 넘기면 수량과 가격을 출력해주는 pipe함수가 존재한다.

이때 수량과 가격에 대한 로직 중 다른 점인 map()내에 구현된 계산을 제외하고는 같은 틀을 갖고 있는 함수들이다.

즉, total_quantity, total_price는 products라는 객체에 의존하는 것이다.(products에 의해서만 동작하는 기능들이라는 뜻.)

따라서, 추상화 레벨을 높혀서 중복을 방지하고, 수량,가격 뿐만 아니라 다른 상품관련속성들에 대해서도 사용될 수 있게 만들어보자.


sum이라는 go함수내에 f 즉, 코드(함수)를 값으로 받아서 map함수의 인자로 전달하였다.
그러면 total_quantity, total_price와 같이 sum의 인자로 수량과 상품에 대한 정보를 전달하는 동작을 구현할 수 있다. 만약 다른 상품리스트의 상품의 가격을 알고 싶어도 이제 sum(다른상품리스트 수량, 다른상품리스트)처럼 구현하면 된다.

✅ 마지막으로 위의 코드에서 더 간결하게 코드를 작성하는 방법은 currying을 사용하는 것이다.

sum함수에 curry를 작성하면, sum을 호출할때는 다음과 같이 실행할 수 있다는 뜻이다.

즉, products를 받아서 sum이 return한 함수의 products를 전달만 하고 있는 것이기 때문에 다음과 같이 작성하여도 무방하다.

최종 코드

🌈 HTML로 출력하기

참조 및 참고하기 좋은 사이트

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글