코드를 값으로 다루어 표현력 높이기

이효범·2022년 4월 28일
0
post-thumbnail

Go

함수형 프로그래밍에서는 코드를 값으로 다루는 아이디어를 많이 사용하게 된다.
코드를 값으로 다룰 수 있기 때문에, 어떤 함수를 받아서 평가하는 시점을 원하는 대로 다룰 수가 있기 때문에 코드의 표현력을 높인다던지 식의 좋은 아이디어들을 구현하기 편하다.

밑의 코드를 보자.

const log = console.log;

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

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

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

위의 코드들을 코드를 값으로 다루는 함수를 만들어서 표현력을 더 좋게 하고 가독성이 좋게하는 아이디어들을 확인해 보도록 하자.

위의 코드는 중첩이 되어있다보니 코드의 가독성이 떨어지는 경향이 있다.
이 코드를 좀 더 읽기 편하게 go 라는 함수를 만들며 리팩토링을 해보도록 한다.

const go = () => {};

// go 함수는 다음처럼 사용된다.
go(
	0,
  	a => a + 1;
  	a => a + 10,
    a => a + 100,
  	log);
// 111
# go

const go = (....args) =>   reduce((a, f) => f(a), args);
// 인자들을 축약해 나간다 => reduce를 사용하자
// 들어온 인자들을 활용하여, 첫 번째 인자를 그 다음 인자인 함수에 전달을 하고 
// 그 함수의 결과를 또 다음 함수의 인자를 전달하는 식으로 계속해서 연속적으로
// 하나의 일을 해야 한다. 그렇다는 것은 이러한 로직은 reduce 라는 것이다.
// 즉, args 를 특정 함수로 축약해서 하나의 특정한 값으로 만들어 나간다. 

이러한 코드 예시처럼,
reduce 라는 함수를 사용하면 특정 리스트를 축약해나가는 코드를 작성할 때
굉장히 재밌고 쉽게 만들어 나갈 수 있다.

pipe

한가지 함수를 더 만들어 볼텐데 pipe 라는 함수이다.
pipe 함수는 go 함수와 다르게 함수를 리턴하는 함수인데,
go 함수를 인자들을 전달해서 즉시 어떠한 값을 평가하는데 사용한다면
pipe 함수는 함수들이 나열되어 있는 합성된 함수를 만들어 리턴하는 함수이다.

const pipe = (...fs) => (a) => go(a, ...fs); // 함수를 리턴하는 함수 
// 내부에서 go 함수를 사용한다.


// pipe 함수는 다음처럼 사용된다.
const f = pipe(
	a => a + 1;
  	a => a + 10,
    a => a + 100);
// 세 개의 함수를 연속적으로 실행하면서 
// 축약하는 하나의 함수를 만들어서 리턴한다.

log(f(0)); // 111

pipe 함수에 기능 추가하기

const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs); // 여러 개의 인자를 받을 수 있도록 수정


// pipe 함수는 다음처럼 사용된다.
const f = pipe(
	(a, b) => a + b;  // 첫 번째 인자는 두 개 이상 받을 수 있도록 pipe에 기능을 추가
  	a => a + 10,
    a => a + 100);
// 세 개의 함수를 연속적으로 실행하면서 
// 축약하는 하나의 함수를 만들어서 리턴한다.

log(f(0, 1)); // 111

위의 pipe 함수의 구현은 다음 go 함수와 같다.

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

go를 사용하여 읽기 좋은 코드로 만들기

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

이제 함수를 위에서부터 아래로, 왼쪽에서부터 오른쪽으로 평가하면서 연속적으로 함수를 실행하고, 이전의 실행된 함수의 결과를 다음 함수에게 전달하는 go 라는 함수를 만들었기 때문에 위 코드의 표현을 조금 바꿀 수가 있다.

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

기존의 코드보다 양은 조금 많아 지고 간결하진 않지만,
위에서부터 아래로 평가하는 방식으로 코드를 작성했기 때문에
코드를 읽을 때 조금 더 편해졌다.

go+curry를 사용하여 더 읽기 좋은 코드로 만들기

curry 라는 함수를 만들어 보자.
이 함수 역시 코드를 값으로 다루면서 받아둔 함수를 원하는 시점에 평가시키는 함수이다.

curry 함수는 우선 함수를 받아서 함수를 리턴하고 원하는 개수만큼의 인자가 들어왔을 때 받아두었던 함수를 나중에 평가시킨다.

코드를 보면 더욱 직관적으로 이해가 될 것이다. 밑의 코드를 보도록 한다.

## curry
const curry = f => 
	(a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._) ;  // 인자가 두 개 이상 전달되었다면 받아둔 함수를 즉시 실행하고 
// 만약 인자가 두 개보다 작다면 함수를 다시 리턴한 후에 그 이후에 받은 인자들을 합쳐서 
// 함수를 실행한다.

const mult = curry((a, b) => a * b);
console.log(mult); // (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._)
console.log(mult()); // (..._) => f(a, ..._)
console.log(mult(5)); // (..._) => f(a, ..._)
console.log(mult(5)(10)); //  50
console.log(mult(5, 10));  // 50

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

이러한 함수가 있다는 이야기는 위에서 만든 코드를 좀 더 간결하게 표현할 수 있다는 이야기이기도 하다. 이를테면 다음과 같다.

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

// map, filter, reduce를 모두 curry 함수에 적용

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

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


// map, filter, reduce에 모두 curry를 적용시킨 모습
// 이제 위 함수들은 인자를 하나만 받으면 일단 이후 인자들을 더 받기로 기다리는
// 함수를 리턴하도록 되어있다. 

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

이러한 코드는 다음과 같이 더욱 간결하게 표현할 수 있다.

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

결국 기존의 코드에서부터 현재까지의 리팩토링 된 코드는 다음과 같다.

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

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

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

함수를 값으로 다루는 여러 함수들을 이용하여 더 표현력이 높고 깔끔한 코드를 얻을 수 있었다.


함수 조합으로 함수 만들기

밑에 같은 데이터를 가지고 두 가지의 일을 하고 있는 코드가 있다.

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

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

현재 위의 코드는 중복이 있는데, 이렇게 파이프라인으로 만들어진 코드를 굉장히 쉽게 조합하여서 중복을 제거할 수 있다.

다음 사례를 살펴보자.

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

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


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

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

이런 식으로 함수형 프로그래밍에서는 이러한 고차 함수들을 함수의 조합으로 만들어가면서 함수들을 계속해서 잘게 나누어,
중복을 제거하고 또한 높은 재사용성을 가질 수 있도록 사용한다.

profile
I'm on Wave, I'm on the Vibe.

0개의 댓글