이전 포스팅에서 iterable protocol을 따르는 함수를 추상화하며 사용하면서 가독성이 좋지 않다는 문제가 있었다.
console.log(
reduce(
(a, b) => a + b,
map(p => p.price,
filter(p => p.price > 20000, products))));
코드의 흐름이 읽는 순서와 반대이다.
중첩되는 구조로 코드를 읽기 어렵다.
그래서 이번에는 함수형 프로그래밍에서는 코드를 값으로 다룰 수 있기 때문에 코드를 값으로 다룰 수 있는 함수를 만듦으로써 읽기 좋고 표현력을 높이는 코드를 작성하고자 한다.
go 함수는 인자들을 받아 차례로 실행시켜 결과에 해당하는 값을 리턴하는 함수이다.
go(
0,
a => a + 1,
a => a + 10,
a => a + 100,
console.log);
인자들이 들어왔을 때, 가장 첫번째 인자를 다음 인자에게 전달하여 실행시키고 그 결과를 다음 인자에게 전달하는 하나의 로직이 필요하다는 점을 이용하여 이전 포스팅에 만든 reduce를 활용한다.
const reduce = (f, acc, iter) => {
if(!iter) { // 두번째 인자로 초깃값이 없다면 iter의 첫번째 값을 초기값으로
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for(const a of iter) {
acc = f(acc, a);
}
return acc;
}
const go = (...args) => reduce((a, f) => f(a), args);
이제 go 함수를 활용하면 이전에 비해 코드 양이 많아지고 간결하지는 않지만 코드의 흐름이 읽는 순서와 반대의 문제를 해결할 수 있다.
go(
products,
products => filter(p => p.price > 20000, products),
products => map(p => p.price, products),
prices => reduce((a, b) => a+b, prices),
console.log
)
pipe 함수는 함수들이 나열되어 있는 합성된 함수를 만드는 함수이다.
const pipe = (...fs) => (a) => go(a, ...fs);
다음과 같이 pipe 함수는 함수들을 인자로 받아 함수를 반환하는 함수로 이후에 인자가 들어왔을 때, 받은 인자를 이전에 받은 함수들에게 전달하는 구조이다.
const f = pipe(
a => a + 1,
a => a + 10,
a => a + 100);
f(0); // 111
여기서 조금 더 생각해서 첫번째 함수의 인자가 2개라고 한다면 먼저 평가를 해야하기 때문에 첫 번째 함수 f와 나머지 함수인 ...fs를 분리시켜 go함수에 인자로 전달하기 전에 첫 번째 함수 f를 먼저 평가해주는 방식으로 구현하면 된다.
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);
const f2 = pipe(
(a, b) => a + b,
a => a + 10,
a => a + 100);
f2(1, 5); // 116
그렇다면 pipe 함수의 합성된 함수를 만들어준다는 특성을 이용한다면 다음과 같이 필요한 부분만을 분리시켜 재사용성이 유리하게 할 수도 있을 것이다. (다만 여전히 코드가 비대, 이후에 나올 curry함수로 해결)
const totalPriceOver20000 = pipe(
products => filter(p => p.price > 20000, products),
products => map(p => p.price, products),
prices => reduce((a, b) => a+b, prices),
);
const totalPriceUnder20000 = pipe(
products => filter(p => p.price < 20000, products),
products => map(p => p.price, products),
prices => reduce((a, b) => a+b, prices),
);
go(
products,
totalPriceOver20000,
console.log
);
go(
products,
totalPriceUnder20000,
console.log
);
함수를 받아서 함수를 리턴하고 인자를 받아서 인자가 원하는 갯수만큼 들어왔을 때, 받아두었던 함수를 평가시키는 함수이다. 즉, 내가 원하는 시점에 평가시키는 함수이다.
// 처음 실행은 함수를 반환
// 이후에 실행할 때 인자가 두개이상이라면 즉시실행 아니면 또 다른 함수를 반환
const curry = f =>
(a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
(a, ...) 첫번째 인자와 나머지 인자를 받고 .length가 있으면, 즉 인자가 2개 이상(나머지 인자가 존재)이면 즉시 실행하고 2개 미만이라면 함수를 리턴 후 그 이후 또 다시 호출되었을 때 실행되는 구조이다.
curry는 f(a,b,c)처럼 단일 호출로 처리하는 함수를 f(a)(b)(c)와 같이 각각의 인수가 호출 가능한 프로세스로 호출된 후 병합되도록 변환되는 것이라고 생각하면 된다.
내가 원하는 시점에 평가시킬 수 있다면 다음과 같이 사용할 수 있다. (재사용이 뛰어남)
// 처음 실행은 함수를 반환
const mult = curry((a, b) => a * b);
const mult2 = mult(2);
// 엄청 긴 코드 생략...
// 엄청 긴 코드 생략...
// 엄청 긴 코드 생략...
// 원하는 시점에 평가가 가능
mult2(5); // 10
mult2(2); // 4
// ------- curry가 아니였다면 ---------
const mult = (a, b) => a * b;
mult(2, 5) // 10;
mult(2, 2) // 4;
그리고 이전에 만들어 두었던 map, filter, reduce 함수들을 curry와 함께 사용함으로써 오늘의 목표였던 가독성문제 또한 해결할 수 있다.
const map = curry((f, iter) => {...});
const filter = curry((f, iter) => {...});
const reduce = curry((f, acc, iter) => {...});
// 1차
go(
products,
products => filter(p => p.price > 20000)(products),
products => map(p => p.price)(products),
prices => reduce((a, b) => a+b)(prices),
console.log
);
// 2차
go(
products,
filter(p => p.price > 20000),
map(p => p.price),
reduce((a, b) => a+b),
console.log
);
이전에 비해 코드도 간결해지고 코드의 실행흐름이 읽는 순서와 동일해짐으로써 가독성이 좋아지고 결국 생산성도 좋아지고 유지보수 할 때도 유리해지게 되었다. (함수의 조합성 👍)