Week2. go, pipe, curry... 짤로 표현하면 대강 이런 느낌
함수형 프로그래밍에서 map, filter, reduce를 활용해 중첩 함수로 표현할 수 있지만 이는 코드의 가독성을 떨어트린다. go
, pipe
함수를 통해 함수를 값으로 다뤄 코드의 표현력을 높일 수 있다.
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);
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;
}
accumulator
를 args[Symbol.iterator]()
가 반환한 iterator
로 바꾸고, accumulator
는 next().value
, 즉 초깃값 0이 된다.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)
}
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와 같이 함수들이 중첩되어 있어서 그 원리를 파악하기 너무 힘들었는데 추상화 덕분에 내부 로직을 사실 몰라도(?) 이전 평가 값이 다음 함수의 인자로 사용된다는 것만 알면 되니까 편하긴 하다 ㅋㅋㅋ.
go, pipe와 마찬가지로 함수를 값으로 다루며 받아둔 함수를 원하는 시점에 평가시키는 함수이다.
😉 간단 로직
- 함수를 받아 함수를 리턴.
const curry = func => () => {};
- 리턴되는 함수는 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이라면 함수를 리턴했다가 그 함수의 인자로 값을 전달하면 결과값을 전달한다.
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, reduce
에 curry
를 적용하면 인자가 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
)
따라서 위 함수를 아래와 같이 간단히 표현할 수 있다.
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
를 받는다.
조건만 다를 뿐 같은 기능을 하는 두 개의 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 들어가면서 함수가 함수를 인자로 받고 또 리턴된 함수 값을 인자로 받고 막 섞고 하다보니 헷갈려서 어렵다... 객체지향이나 절차지향의 경우 평소 생각하던 로직대로 코드를 짜면 되지만 함수형 프로그래밍은 마치 시간을 넘나드는 인터스텔라 영화같은 코드다. 오늘껄 내것으로 만들기 전에 내일의 강의들이 또 나를 기다리고 있어서 걱정이 된다. 하지만 사람은 닥치면 어떻게든 해낸다고...! 힘내보자 화이팅! !