TIL | go, pipe, curry 함수로 코드 표현력 높이기

noopy·2021년 8월 10일
0

TIL

목록 보기
5/21
post-thumbnail

Week2. go, pipe, curry... 짤로 표현하면 대강 이런 느낌

함수형 프로그래밍에서 map, filter, reduce를 활용해 중첩 함수로 표현할 수 있지만 이는 코드의 가독성을 떨어트린다. go, pipe 함수를 통해 함수를 값으로 다뤄 코드의 표현력을 높일 수 있다.

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

🍡 go

go 함수는 인자로 함수리스트를 받아 첫 번째 인자값을 초기값으로, 두번째 인자부터 함수 리스트를 받아 반환 값을 다음 함수로 넘겨 최종적인 축약 값을 리턴한다. 마치 reduce와 로직이 같으므로 reduce를 활용해 구현해보자.

// 이전에 구현한 reduce
const reduce = (func, accumulator, iterator) => {
  if (!iterator) { // acculator 자리가 iterator로 당겨졌기 때문에 iterator가 없는거임.
    iterator = accumulator[Symbol.iterator]();
    accumulator = iterator.next().value;
  }
  
  for (const value of iterator) {
	accumulator = func(accumulator, value);
  }
  return accumulator;
}


// go 함수
const go = (...args) => reduce((acc, func) => func(acc), args);

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

👇🏻 코드 설명

1.go의 인자로 들어온 args를 스프레드 연산자로 배열화한다.
...args = [0, f, f, f]

const go = (...args) => reduce((acc, func) => func(acc), args);
  1. go 함수의 결과값으로 두 개의 인자를 받는 reduce 함수를 사용한다.
  • func: (acc, func) => func(acc)
  • accumulator: args
const reduce = (func, accumulator, iterator) => {
  if (!iterator) { // acculator 자리가 iterator로 당겨졌기 때문에 iterator가 없는거임.
    iterator = accumulator[Symbol.iterator]();
    accumulator = iterator.next().value;
  }
  • 세번째 인자가 없으므로 accumulatorargs[Symbol.iterator]()가 반환한 iterator로 바꾸고, accumulatornext().value, 즉 초깃값 0이 된다.
  1. for of 문을 돌면서 iterator = args를 하나씩 순회한다. 이미 next()가 한번 진행됐기 때문에 accumulator = 0인 상태를 한번 더 기억하자.
for (const value of iterator) {
	accumulator = func(accumulator, value);
  }

reduce의 첫번째 인자 func: (acc, func) => func(acc)accumulator = 0과 value = a => a + 1을 인자로 받는다.
즉, func는 (0, a => a + 1): 0을 인자로 받는a => a + 1 함수이므로 1을 리턴해 accumulator에 할당한다.

이후 아래 과정을 거쳐 console에 111이 찍힌다.

(1, a => a + 10) = acc(10) = 11;
value = a => a + 100
accumulator = 11

(11, a => a + 100) => acc(100) = 111;
value = console.log
accumulator = 111

(111, console.log) => console.log(111)
  }

go를 활용해 이전 값 재사용하기

const products = [
  {name: '반팔티', price: 15000},
  {name: '긴팔티', price: 20000},
  {name: '원피스', price: 25000},
  {name: '후드티', price: 30000}
];

console.log(reduce(
		add, 
		map(value => value.price,
		filter(value => value.price < 30000, products))));
// 60000;

앞의 TIL에서 map, filter, reduce를 활용해 30000 미만인 가격들을 합해보았다. 하지만 코드가 충첩해 가독성이 좋지 않기 때문에 이번엔 go 함수를 활용해 똑같은 값을 출력해보자.

go(
  products, // 초기 accumulator 값
  products => filter(value => value.price < 30000, products),
  products => map(value => value.price, products), // [15000, 20000, 25000];
  prices => reduce(add, prices), // 15000, [20000, 25000] 형태
  console.log
)

위 코드보다 간결하진 않지만, 중첩이 줄어들어 읽기 편해진 느낌이다 🌬. go 내부적으로 reduce와 같이 함수들이 중첩되어 있어서 그 원리를 파악하기 너무 힘들었는데 추상화 덕분에 내부 로직을 사실 몰라도(?) 이전 평가 값이 다음 함수의 인자로 사용된다는 것만 알면 되니까 편하긴 하다 ㅋㅋㅋ.

🍡 curry

go, pipe와 마찬가지로 함수를 값으로 다루며 받아둔 함수를 원하는 시점에 평가시키는 함수이다.

😉 간단 로직

  1. 함수를 받아 함수를 리턴.
const curry = func => () => {};
  1. 리턴되는 함수는 func에서 사용될 인자를 미리 인터셉트.
  • 만약 인터셉트한 인자가 두개 이상일 경우,
    즉 초깃값 떼어내고 나머지 인자들이 0이 아닐경우, func 함수를 즉시 실행한다.
  • 만약 인자가 두개 미만일 경우, 초깃값과 이후 들어올 값들을 모두 모아 인자로 전달한다.
const curry = func => 
		(acc, ...rest) => 
		rest.length ? // 만약 인자가 2개 이상일 경우
            	func(acc, ...rest) : // func 함수 즉시 실행
        	(...rest) => // 인자가 2개 보다 작다면
        	func(acc, ...rest);
		// 미리 킵해놨던 초깃값과 ...이후 들어올 값을 전달.
// 사용 예시
const multiple = (a, b) => a * b; // a와 b를 곱하는 함수
const multiple = curry((a, b) => a * b);

console.log(multiple(3, 4)); // 12

console.log(multiple(3)) // 함수 리턴
console.log(multiple(3)(4)) // 12

a와 b를 곱해주는 multiple 함수가 있다. 이 함수를 curry 함수의 인자로 전달하게 된다면 어떻게 될까?
인자가 2개 이상이기 때문에 (acc, ...rest) => multiple(acc, ...rest) 함수를 리턴한다.
만약 인자가 2개 미만일 경우, (...rest) => multiple(acc, ...rest) 함수를 리턴한다. 만약 인자가 1개이고 초깃값이 3이라면 함수를 리턴했다가 그 함수의 인자로 값을 전달하면 결과값을 전달한다.

🍡 go-curry

const products = [
  {name: '반팔티', price: 15000},
  {name: '긴팔티', price: 20000},
  {name: '원피스', price: 25000},
  {name: '후드티', price: 30000}
];

go(
  products, // 초기 accumulator 값
  products => filter(value => value.price < 30000, products),
  products => map(value => value.price, products), // [15000, 20000, 25000];
  prices => reduce(add, prices), // 15000, [20000, 25000] 형태
  console.log
)

go 예시로 들었던 함수들(filter, map, reduce) 원본 위치에 curry를 적용하면 좀 더 효율적이다. filter, map, reducecurry를 적용하면 인자가 2개 미만일 경우, 일단 이 함수들을 킵해뒀다가 인자가 추가되면 이전 인자에 합쳐서 계산해준다.

go(
  products, // 초기 accumulator 값
  products => filter(value => value.price < 30000)(products),
  products => map(value => value.price)(products), // [15000, 20000, 25000];
  prices => reduce(add)(prices), // 15000, [20000, 25000] 형태
  console.log
)
// filter 부분만 살펴보면 curry를 적용한 경우,
// filter 함수가 인자를 하나 받기 때문에 (products)로 이후 인자를 추가해주는 형식으로 바꿀 수 있음. 그래도 결과는 같음

// 코드를 더 간단히 하기 전에 설명!
// products를 인자로 받아 `filter(value => value.price < 30000)` 함수에 그대로 products를 전달한다는 뜻은 둘다 products를 받기 때문에 생략 가능.

go(
  products, // 초기 accumulator 값
  filter(value => value.price < 30000),
  map(value => value.price)(products), // [15000, 20000, 25000];
  reduce(add), // 15000, [20000, 25000] 형태
  console.log
)

따라서 위 함수를 아래와 같이 간단히 표현할 수 있다.

🍡 pipe

go 함수는 함수와 인자들을 전달해 즉시 값을 평가하고 다음 함수로 전달하지만, pipe 함수는 함수들이 나열되어 있는 합성된 함수를 리턴하는 함수이다.

const pipe = (...funcs) => (acc) => go(acc, ...funcs);
const pipe2 = (...funcs) => (acc) => (...args) => reduce((acc, func) => func(acc), args) // go를 풀어쓴 버전

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

console.log(pipeFunc(0)) // 여기가 초기 accumulator 값

pipe 함수는 내부적으로 go 함수를 사용한다. go 함수는 초깃값으로 사용할 0과 함수 리스트가 필요하기 때문에 인자로 acc...funcs를 받는다.

  1. pipeFunc(0) => pipe를 통해 반환된 함수에 인자로 0을 전달해, 첫번째 함수에 인자로 전달한다.
  2. 첫 번째 함수의 결과값 1 (0 + 1)을 다음 함수의 인자로 전달한다.
  3. 두 번째 함수의 결과값 11 (1 + 10)을 다음 함수의 인자로 전달한다.
  4. 세 번째 함수의 결과값 111 (11 + 100)을 console.log를 통해 출력한다.

함수 조합으로 함수 만들기

🍡 코드 중복 없애기

조건만 다를 뿐 같은 기능을 하는 두 개의 go함수가 있다.

const totalPrice = pipe(
  map(value => value.price),
  reduce(add)
)

const products = [
  {name: '반팔티', price: 15000},
  {name: '긴팔티', price: 20000},
  {name: '원피스', price: 25000},
  {name: '후드티', price: 30000}
];

go(
  products,
  filter(value => value.price < 30000),
  totalPrice, // 중복 제거
  console.log
)

go(
  products,
  filter(value => value.price >= 30000),
  totalPrice, // 중복 제거
  console.log
)

pipe, go, curry 등을 사용해 기존 함수들을 리팩토링하고 잘게 쪼개어 조합성을 높인다.

느낀점

go,pipe 들어가면서 함수가 함수를 인자로 받고 또 리턴된 함수 값을 인자로 받고 막 섞고 하다보니 헷갈려서 어렵다... 객체지향이나 절차지향의 경우 평소 생각하던 로직대로 코드를 짜면 되지만 함수형 프로그래밍은 마치 시간을 넘나드는 인터스텔라 영화같은 코드다. 오늘껄 내것으로 만들기 전에 내일의 강의들이 또 나를 기다리고 있어서 걱정이 된다. 하지만 사람은 닥치면 어떻게든 해낸다고...! 힘내보자 화이팅! !

profile
💪🏻 아는 걸 설명할 줄 아는 개발자 되기

0개의 댓글