함수형프로그래밍 - 읽기 좋은 코드 만들기

ZeroJun·2022년 5월 4일
0

함수형프로그래밍

목록 보기
2/5

이 포스트는 유인동님의 https://www.inflearn.com/course/functional-es6/dashboard 강의를 참조하여 작성되었습니다.

함수형프로그래밍은 코드를 값으로 다루는 경우가 많다.
그래서 어떤 함수가 코드인 함수를 받아서 평가하는 시점을 원하는대로 다룰 수 있다.이는 코드의 표현력(가독성)을 높이는 등 다양한 시도를 해볼 수 있게한다.

go

let log = console.log;

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

이전에 만들었던 reduce함수는 위처럼 어떤 이터러블 받아 그 이터러블을 순회하면서 아규먼트로 받은 함수의 리턴 값을 계속 축적해 나가는 함수다. 이를 이용해 go라는 함수를 만들 수 있다.


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

go(
  0, // 초기 값 0이 다음 함수의 인자가 된다.
  a => a + 1, // 함수의 리턴 값이 다음함수의 인자가 된다.
  a => a + 10, // 함수의 리턴 값이 다음함수의 인자가 된다.
  a => a + 100, // 함수의 리턴 값이 다음함수의 인자가 된다.
  log // 최종 값을 log로 보여준다.
  // 111
); // 결국 이것은 하나의 값으로 만들어가는 reduce동작이다.

코드를 보면 go라는 함수는 여러가지 아규먼트들을 받아 reduce를 실행하게 되는데, 이는 곧 아규먼트들로 입력한 값 혹은 함수들이 입력한 순서대로 실행되면서 최종적으로 log함수에 값이 넘어가는 것을 알 수 있다.

이렇게 go함수는 어떠한 값을 즉시 평가할 때 사용한다.

pipe

go와 비슷하면서 함수를 리턴하는 함수를 즉, 합성된 함수를 만드는 것을 pipe라고 한다.

const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);
// 함수들을 인자로 받아 go를 리턴하는 형태
// 첫번째 인자를 두 개 이상의 인자를 받아 실행하는 함수로 넣어준다. 

const f = pipe( // go로 합성된 함수인 pipe를 f에 담는다.
  (a, b) => a + b,
  a => a + 10,
  a => a + 100,
  // 입력받은 함수를 합성한다.
)
log(f(0, 1)); // pipe를 할당한 f에 인자를 주면 비로소 값이 리턴된다.

이처럼 pipe는 함수를 반환하므로 f에 할당한 후 f에 인자를 넣어 호출 할 때, 값이 리턴된다.

go를 통해 읽기 좋은 코드 만들기

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

// 기존 코드
log(
  reduce(
    add,
    map(p => p.price,
      filter(p => p.price < 20000, products))));
// 1. products의 price를 20000미만으로 필터한다.
// 2. 1을 통해 걸러진 값을 map을 통해 price만 뽑아낸다.
// 3. 뽑아낸 price를 add로 출력을 한다.

// go를 이용한 가독성 높은 코드 작성
go(
  products,
  // products array를 받는다.
  products => filter(p => p.price < 20000, products),
  // products array를 인자로 받아 filter를 실행한다.
  products => map(p => p.price, products),
  // filter된 products를 인자로 받아 map을 실행하여 price로 mapping한다.
  price => reduce(add, price),
  // price로 mapping된 인자를 받아 reduce를 실행한다.
  log
  // 최종 값을 log에 담아 실행한다.
)

go를 이용하면 이처럼 위에서부터 아래로 순차적으로 읽기 좋게 코드를 작성할 수 있다.

curry

커리는 함수를 값으로 다루면서 받아둔 함수를 원하는 시점에 평가시키는 함수이다.
커리는 함수를 받아서 함수를 리턴하고, 인자를 받아서 인자가 원하는 개수만큼의 인자가 들어왔을 때 받아두었던 함수를 나중에 평가시키는 함수다.

const curry = f =>
  (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
// 1. 함수를 받아서 (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
// 함수를 리턴한다.
// 2. 리턴된 함수가 실행되었을 때, 받은 인자가 2개 이상 
// (_.length가 존재한다는 것은 받은 인자가 총 2개 이상이라는 것이다.)
// 일 경우 받아둔 함수를 즉시 실행하고, 인자가 2개 이하라면 함수를 다시 리턴한다.
// 함수를 리턴한 후 그 이후에 받은 인자를 합해서 함수를 실행한다.

const mult = curry((a, b) => a * b);
log(mult); // (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._)
log(mult(1)); // (..._) => f(a, ..._)
log(mult(1)(2)); // 2;

const mult3 = mult(3); // 인자가 2개 필요한 mult에 미리 인자 1개를 담아둔다.
log(mult3(10)); // mult(3, 10);이 되어 30을 리턴한다.
log(mult3(5)); // mult(3, 10);이 되어 30을 리턴한다.
log(mult3(3)); // mult(3, 10);이 되어 30을 리턴한다.

이렇게 받아야하는 인자를 받을 때까지 함수로 대기하다가 인자가 채워지는 순간 값이 리턴되는 형태다.

map, reduce, filter를 curry로 감싸기

const map = curry((f, iter) => {
  // f : 함수 파라미터, iter : 이터러블 프로토콜을 따르는 파라미터
  let result = [];
  for (const a of iter) {
    result.push(f(a)); // 어떤 값을 수집할 것인지는 f에게 위임한다.
  }
  return result;
});

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

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

map, reduce, filter를 curry로 감싸주게될 경우 인자를 하나만 받으면 이후 인자를 더 받을 수 있는 함수를 리턴하게 된다.

curry로 감싼 go의 활용법

// 위에서 적은 go
go(
  products,
  // products array를 받는다.
  products => filter(p => p.price < 20000, products),
  // products array를 인자로 받아 filter를 실행한다.
  products => map(p => p.price, products),
  // filter된 products를 인자로 받아 map을 실행하여 price로 mapping한다.
  price => reduce(add, price),
  // price로 mapping된 인자를 받아 reduce를 실행한다.
  log
  // 최종 값을 log에 담아 실행한다.
)

// curry로 감싸진 go의 활용1
go(
  products,
  // products array를 받는다.
  products => filter(p => p.price < 20000)(products),
  // 1. products array를 인자로 받아 추가 인자를 받을 수 있는 함수를 리턴한다.
  // 2. 그 이후 인자를 products를 받아 실행시킬 수 있다.
  products => map(p => p.price)(products),
  // 위와 동일하다.
  price => reduce(add)(price),
  // 위와 동일하다.
  log
  // 최종 값을 log에 담아 실행한다.
)

// curry로 감싸진 go의 활용2
go(
  products,
  filter(p => p.price < 20000),
  // curry를 통해 filter의 인자로 products를 받기 때문에 이런 형태가 가능하다.
  map(p => p.price),
  // 위와 동일하다.
  reduce(add),
  // 위와 동일하다.
  log
  // 최종 값을 log에 담아 실행한다.
)

함수 조합으로 함수 만들기

// 서로 다른 일을 하지만 중복되는 요소가 많은 경우.
go(
  products,
  filter(p => p.price < 20000),
  map(p => p.price),
  reduce(add),
  log
);

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

// pipe를 통해 합성함수를 total_price에 할당하여 활용한다.
const total_price = pipe(
  map(p => p.price),
  reduce(add),
);

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

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

// 함수를 하나 받아서 pipe를 반환하는 함수다.
const base_total_price = predi => pipe(
  filter(predi),
  total_price
);

go(
  products,
  base_total_price(p => p.price < 20000),
  // pipe에 함수를 할당했으니 합성된 함수인 filter와 total_price가 도출된다.
  log
);

go(
  products,
  base_total_price(p => p.price >= 20000),
  log
);

함수형프로그래밍 적용 예시

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

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

// products를 입력하면 총 수량을 반환하는 함수.
// 추상화1
const total_quantity_go = products => go(products,
  map(p => p.quantity),
  // quantity만 추출
  reduce(add),
  // 순차적으로 1+2 -> 3+3 -> 6+4 -> 10+5 = 15
);
log(total_quantity_go(products)); // 15

// 추상화2 : 이 함수는 products에 의존적인 문제가 있다.
// pipe를 통해 합성된 함수들을 total_quantity_pipe에 할당.
const total_quantity_pipe = pipe(
  map(p => p.quantity),
  // quantity만 추출
  reduce(add),
  // 순차적으로 1+2 -> 3+3 -> 6+4 -> 10+5 = 15
);
log(total_quantity_pipe(products)); // 15

// 전체 상품의 가격을 반환하는 함수
const total_price = pipe(
  map(p => p.price * p.quantity),
  reduce(add)
);
log(total_price(products)); // 34500

// 추상화3 : 이를 통해 products에 대한 의존성을 제거했다.
const sum = curry((f, iter) => go(
  iter,
  map(f),
  reduce(add)
));
log(sum(p => p.quantity, products));
log(sum(p => p.price * p.quantity, products));

const total_quantity_pipe_추상화3 = products =>
  sum(p => p.quantity, products);

log(total_quantity_pipe_추상화3(products)); // 15

const total_price_추상화3 = products =>
sum(p => p.price * p.quantity, products); 
  log(total_price_추상화3(products)); // 34500

// 이렇게 추상화 레벨이 높은 sum을 다음과 같이 활용할 수 있다.
log(sum(user => user.age, [ // 60
  { age: 30 },
  { age: 20 },
  { age: 10 }
]));

0개의 댓글