[functional-js] 함수의 중첩, go, pipe, curry

younoah·2021년 2월 6일
2

[functional-js]

목록 보기
5/16

이 글은 유인동님의 함수형 프로그래밍 강의내용을 정리한 글입니다.

서론

함수가 중첩되어있는 경우 가독성이 좋지않다.

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

함수형 프로그래밍에서는 함수를 값으로 활용한다.

함수를 값으로 다룬다면 함수의 표현력을 높일 수 있다.

함수들을 연속적으로 사용하는 함수, 함수들을 함축하는 함수를 구현하여

중첩된 함수의 가독성을 높여보자!

go

첫번째 인자에는 시작되는 값을 넣고 나머지 인자에는 함수들을 받아 값을 다음 함수로 넘기면서 차례대로 함수를 실행한다.

구현

// reduce 함수
const reduce = (f, acc, iter) => {
    // acc는 옵셔널 acc가 생략됬을 경우 예외처리
    if(!iter) {
        iter = acc[Symbol.iterator]();
        acc = iter.next().value;
    }
    for (const value of iter) {
        acc = f(acc, value);
    }
    return acc;
};

// go 함수
const go = (...args) => reduce((value, f) => f(value), args);

go함수는 함수들을 차례로 실행하면서 하나의 결과로 축약하는 과정이기 때문에 reduce 함수를 활용하여 구현한다.

활용

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

go(
	add(0, 1), // 2개 이상의 인자를 처리해 하나의 값을 반환하는 함수는 첫 번째 인자로!
    a => a + 1,
    a => a + 10,
    a => a + 100,
    console.log
);
// 111

값 0을 시작으로 다음 함수로 넘기고 다시 그 결과를 다음 함수로 넘기면서 하나의 값으로 축약된다.

pipe

여러 함수들을 차례대로 합쳐서 하나의 함수를 리턴한다.

리턴 받은 함수를 활용하여 여러 함수를 차례대로 실행한다.

구현

// const pipe = (...fs) => (a) => go(a, ...fs); // 인자가 1개일 때만 동작
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs); // 첫번째 함수의 인자가 2개 이상일 때를 위한 버전

pipe함수는 함수들을 하나의 함수로 합처서 리턴한다.
리턴된 함수는 인자를 받아 함수들을 적용하며 값을 리턴한다.
이는 go함수와 같은 동작을 하기 때문에 내부적으로 go함수를 활용한다.

활용

const f = pipe(
    (a, b) => a + b, // 첫번째 함수만 인자가 2개
    a => a + 10,
    a => a + 100
);

console.log(f(0, 1));

pipe 함수의 리턴 함수에서 여러개의 인자를 받기 위해서는 pipe 함수의 첫번째 인자의 함수에서 하나의 값으로 축약한다.

pipe 함수 구현에서 첫 번째 함수 f 와 나머지 함수인 `...fs 를 따로 받는다. 그렇게 리턴된 합성함수는 여러개의 인자 ...as 를 받아 f 함수에 넣고 결과를 나머지 함수들이 받으면서 차례대로 실행된다.

go함수를 사용하여 코드 개선하기

  • 이전 코드
// map
const map = (f, iter) => {
  const res = [];
  for (const el of iter) {
    res.push(f(el));
  }
  return res;
};

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

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

// add 함수
const add = (a, b) => a + b;

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

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

// 2만원  이하의 제품들의 가격의 총합
console.log(
  reduce(
    add,
    map(
      p => p.price,
      filter(p => p.price < 20000, products)
    )
  )
);
// 30000
  • go함수를 사용하여 개선된 코드
go(
	products,
    products => map(p => p.price, products),
    prices => filter(price => price < 20000, prices),
    prices => reduece(add, prices),
    console.log
);
// 30000

curry

인자가 원하는 갯수만큼 들어왔을 때 받아온 함수를 실행한다.

인자가 원하는 갯수 이하일 때는 받아온 함수를 대기 시킨다.

즉, 어떤 함수가 원하는 갯수가 될때까지 대시키도록 만드는 함수이다.

구현

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

위의 구현 내용은 반드시 2개의 인자가 들어왔을 때 값을 리턴한다.

활용

// const multi = (a, b) => a * b;
const multi = curry((a, b) => a * b);

console.log(multi);
console.log(multi(3));
console.log(multi(3)(2));

const multi3 = multi(3);
console.log(multi3(1));
console.log(multi3(2));
console.log(multi3(3));
console.log(multi3(4));

go함수 + curry함수

커링을 이용하면(curry 함수를 사용하면) 함수를 부분적으로 사용할 수 있게된다.

기존에 구현한 map, filter, reduce 함수에 curry 함수를 적용하면

go 함수에서 map, filter, reduce함수를 좀 더 축약해서 직관적으로 go 함수를 사용할 수 있다.

map, filter, reduce 함수들은 인자가 최소 2개 여야 동작하기 때문에 curry를 감싸면 부족한 인자가 올 때까지 대기하는 함수가 되기 때문이다.

  • go함수 + curry함수를 사용하여 개선된 코드
// curry
const curry = f => (a, ..._) => (_.length ? f(a, ..._) : (..._) => f(a, ..._));

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

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

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

// add 함수
const add = (a, b) => a + b;

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

// 예제
const products = [
  { name: '반팔티', price: 15000 },
  { name: '긴팔티', price: 20000 },
  { name: '핸드폰케이스', price: 15000 },
  { name: '후드티', price: 30000 },
  { name: '바지', price: 25000 },
];
  • 최종 개선 과정과 결과
// 함수의 중첩사용
console.log(
  reduce(
    add,
    map(
      p => p.price,
      filter(p => p.price < 20000, products)
    )
  )
);

// go함수를 활용하여 가독성 업! 로직의 순서대로 함수실행!
go(
  products,
  products => filter(p => p.price < 20000, products),
  products => map(p => p.price, products),
  prices => reduce(add, prices),
  console.log
);

// curry를 감싼 함수들 (중간단계)
go(
  products,
  products => filter(p => p.price < 20000)(products),
  products => map(p => p.price)(products),
  prices => reduce(add)(prices),
  console.log
);

// curry를 감싼 함수들 활용 (최종단계)
go(
  products,
  filter(p => p.price < 20000),
  map(p => p.price),
  reduce(add),
  console.log
);

함수 조합으로 함수 만들기 (pipe)

pipe를 활용하면 함수들을 의미적으로 하나의 함수로 합처서 사용할 수 있다.

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

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

같은 데이터를 가지고 두개의 함수가 있다.

이 두 함수는

  reduce(add),
  console.log

라는 부분이 중복이다.

pipe를 이용하면 중복을 의미적으로 하나의 함수로 합칠 수 있다.

  • pipe를 활용하여 가독성 업!
const getTotalPrice = pipe(
  map(p => p.price),
  reduce(add)
);

go(
  products,
  filter(p => p.price < 20000),
  getTotalPrice,
  console.log
);

go(
  products,
  filter(p => p.price >= 20000),
  getTotalPrice,
  console.log
);

pipe 함수를 사용하면 여러곳에서 발견되는 중복된 코드를 하나의 의미있는 함수로 합처서 사용이 가능하다.

profile
console.log(noah(🍕 , 🍺)); // true

0개의 댓글