JS_코드를 값으로 다루는 go, pipp, curry 함수
자바스크립트에서는 코드를 값으로 다루는 go, pipe, curry라는 함수들 있다.
go 함수
함수를 위에서부터 아래로, 왼쪽에서부터 오른쪽으로부터 평가하면서
연속적으로 함수를 실행하고 이전에 실행된 함수의 결과를 다음 함수에 전달하는 함수
인자들을 받는데, 인자들을 통해서 하나의 값으로 축약을 해나가는 로직으로 구현되어 있다.
이 인자들을 이용해서 인자의 1번째 인자를 그 다음 인자(함수) 에게 전달을 하고 그 함수의 결과를 또 다음 함수의 인자로 전달을 해야한다.
즉, 해당 로직은 reduce이다.
args를 축약해서 하나의 값으로 만드는 과정이다.
앞서 알아보았듯 초기값을 전달하지 않고 args를 그대로 전달해도 첫번째 값이 뽑혀 초깃값으로 사용이 될 것이다.
const go = (...args) => reduce((a, f)=>f(a),args);
go (
0,
a = > a + 1, // a = 1
a = > a + 10, // a = 11
a = > a + 100, // a = 111,
console.log
)
0과 a + 1 의 결과은 a = 1은 그다음 a로 전달되고
그다음 번째인 a=> a + 10 가 전닫받은 a = 11은 그 다음 a로 전달되고
그다음 번째인 a => a + 100 이 전달받은 a =111은 console.log와 만나 출력되게 된다.
go 함수를 이용해 코드를 읽기 쉬운 코드로 표현해보자
const products = [
{name : '반팔', price:1000},
{name : '긴팔', price:3000},
{name : '7부', price:6000},
{name : '10부', price:9000},
];
console.log(
reduce(
add,
map(a => a.price,
filter(a => a.price < 5000, products)
)
)
);
go(
products,
products => filter(a => a.price < 5000, products),
products => map(a => a.price, products),
prices => reduce(add, prices),
console.log
);
코드가 훨씬 읽기 쉬워졌다.
products로 시작해서,
그 값을 필터를하고 필터의 결과를 이용해 map을 하고
그 결과를 이용해 reduce로 하나의 값으로 축약하는 후 출력하겠다는 과정이다!
이는 한눈에 보기에도 코드의 흐름을 파악하는데 매우 용이한것 같다!!!
pipe 함수
go 함수와 다르게 함수를 리턴하는 함수이다.
go 함수는 즉시 함수들과 인자를 전달하여 즉시 어떤 값을 평가하는데 사용한다면
pipe 함수는 함수들이 나열되어 있는 합성된 함수를 만드는 함수이다.
// pipe 함수는 내부적으로 go 함수를 사용하는 함수이다.
// 함수들을 받고 받은 인자를 go함수에 인자들을 전달해주면 된다.
const pipe = (...fs) => (a)=> go(a, ...fs);
go(
0,
a => a + 1,
a => a + 10,
a => a + 100
);
// 아래 3개의 함수를 연속적으로 실행하면서 축약하는 하나의 함수를 만들어
// func1에 함수를 리턴한다!
const func1 = pipe(
a => a + 1,
a => a + 10,
a => a + 100
);
console.log(func1(0));
// 111
여기서 pipe 함수에 기능을 추가해보자
만약 초기값이 2개가 주어져야 하는 상황이다.
go 함수의 경우 시작하는 인자가 2개여야하는 상황은 아래와 같이 사용하면 된다.
구현이 아닌 사용할때 인자로 add 함수를 콜백함수로 넣어주면 된다.
go(
add(0, 1),
a => a + 10,
a => a + 100
);
----------------------------
위 코드는 아래 코드와 같은 의미이다.
go(
1,
a => a + 10,
a => a + 100
);
하지만 pipe 함수의 경우 위와 같은 방법과는 다르게 함수를 호출할때 직접 add 함수를 넣어줘야한다.
const func1 = pipe(
a => a + 1,
a => a + 10,
a => a + 100
);
console.log(func1(0));
// 111
const func1 = pipe(
a => a + 1,
a => a + 10,
a => a + 100
);
console.log(func1(add(0, 1));
// pipe 함수에서 a => a + 1 함수를 제거하지 않음.
// 111 + 1 ==> 112
----------------------------------
const func1 = pipe(
(a,b) => a + b,
a => a + 10,
a => a + 100
);
console.log(func1(0, 1));
// NaN
따라서, 위 부분을 마치 아래 코드와 같이 동작하도록 구현해주면 된다.
(함수형 프로그래밍에서의 사고방식이다!!!)
go(
add(0, 1),
a => a + 1,
a => a + 10,
a => a + 100
);
1. func1이 받을 인자가 Line 1 의 a 인데 ...as로 여러개의 인자들을 받게 수정하며
2. go 함수를 시작하는 부분은 a, b 인자를 펼쳐서 전달을 한다.
3. 그 다음 함수들이 실행되도록 , 첫번째 함수만 꺼내고 나머지 함수들을 전달하도록 변경해준다.
const go = (...args) => reduce((a, f) => f(a), args);
go(
add(0, 1),
a => a + 1,
a => a + 10,
a => a + 100
);
const pipe = (...fs) => (a)=> go(a, ...fs);
const func1 = pipe(
(a,b) => a + b,
a => a + 10,
a => a + 100
);
--------------------------------------------------------------
1. const pipe = (...fs) => (...as)=> go(a, ...fs);
2. const pipe = (...fs) => (...as)=> go(...as, ...fs);
3. const pipe = (f, ...fs) => (...as)=> go(f(...as), ...fs);
3번의 pipe 함수는 아래와 동일한 코드이다.
go(
add(0, 1),
a => a + 1,
a => a + 10,
a => a + 100
);
const pipe = (f, ...fs) => (...as)=> go(f(...as), ...fs);
pipe 함수는 아래와 동일한 코드이다.
go(
add(0, 1),
a => a + 1,
a => a + 10,
a => a + 100
);
curry 함수
curry 라는 함수 역시 함수를 값으로 다루며 받아둔 함수를 내가 원하는 시점에 평가시키는 함수이다.
함수를 받아 함수를 리턴하고 인자를 받아서 원하는 갯수의 인자가 들어왔을때
받아온 함수를 나중에 평가시키는 함수이다!!
첫번째 인자와 나머지 인자를 받게된다.
일단 함수(a)를 받아 함수(b)를 리턴한다. f => () =>
b 함수에서는 a 함수에서 사용할 인자를 대신 받는다. 첫번째 인자와 나머지 인자를 받는다.(a, ..._) =>
만약 b 함수에 인자가 2개 이상 전달되었다면(특정 조건) _.length ?
만약 length가 있다면? 받아둔 함수를 즉시 실행하고 f(a, ..._)
아니라면 다시 한번 함수를 리턴한다. : () =>
일단 이후 인자들을 더 받기로 기다리는 함수를 생성한다. : (..._) => f()
받아놓았던 a와 새로 받은 인자들을 받아서 함수를 실행한다. f(a, ..._);
함수를 받아 함수를 리턴하고
리턴된 함수가 실행되었을때 들어온 인자가 2개 이상이라면 받아둔 함수를 즉시 실행을 하고
만약에 인자가 2개 이하라면 함수를 리턴한 후에 그 이후에 받은 인자들을 합쳐서 실행하는 함수이다.
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(1));
// (..._) => f(a, ..._)
console.log(mult(1)(3));
// 3
const mult3 = mult(3);
console.log(mult3)
// (..._) => f(a, ..._)
console.log(mult3(3))
// 9
이전 구현하였던 map, filter, reduce 함수를 curry 함수로 감싸면
이 모든 함수들이 인자를 하나만 받으면 일단 이후 인자들을 받기로 기다리는 함수를 리턴하는 함수가 된다!
코드를 또 다른 값으로 표현할 수 있다.
const curry = f =>
(a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);
const filter = curry((f, iterable) => {
let res = [];
for (const a of iterable) {
if (f(a)) {
res.push(a);
}
}
return res;
});
const map = curry((f, iterable) => {
let res = [];
for (const a of iterable) {
res.push(f(a));
}
return res;
});
const reduce = curry((f, acc, iterable) => {
if (!iterable) {
iterable = acc[Symbol.iterator]();
acc = iterable.next().value;
}
for (const a of iterable) {
acc = f(acc, a);
}
return acc;
});
go(
products,
products => filter(a => a.price < 5000)(products),
products => map(a => a.price)(products),
prices => reduce(add)(prices),
console.log
);
편의상
a => b(c) 로 표현
products => filter(a => a.price < 5000)(products),
b라는 부분이 함수가 되고
실행을 하면서 c 라는 인자를 이후에 전달하도록 만들 수 있다.
사실 이것보다 더 간단하게 표현할 수 있다.
a 를 받아서 b 함수에 그대로 c (a, c는 같은 녀석)를 전달한다는 이야기는
a => b(c) 라는 녀석은 이 자리에 들어오는 함수가 products를 받는 다는 의미로
b 만을 작성해도 동작한다는 의미이다.
즉 아래와 같이 더 간결하게 표현 할 수 있다.
go(
products,
filter(a => a.price < 5000),
map(a => a.price),
reduce(add),
console.log
);
console.log(
reduce(
add,
map(a => a.price,
filter(a => a.price < 5000, products)
)
)
);
go(
products,
products => filter(a => a.price < 5000, products),
products => map(a => a.price, products),
prices => reduce(add, prices),
console.log
);
---------------------------------------------------------------
go(
products,
filter(a => a.price < 5000),
map(a => a.price),
reduce(add),
console.log
);
이처럼 함수를 중첩해서 사용하는 경우 go, pipe, curry 등을 통해 더 간결하게 코드를 문장으로 표현 할 수 있다.
보통 go + curry를 많이 사용하며
순서를 바꾸는 함수인 go 함수와
함수를 부분적으로 실행하는 curry 함수를 통해서 표현력이 높고 깔끔한 코드를 얻을 수 있다.
그럼 pipe는 구현만 해놓고 go 와 curry 예제만 있으니 뭔가 포스팅이 이상하다.
위의 3가지 함수들은 함수를 쪼개서 새로운 함수를 만들어 코드의 중복을 제거하는데 큰 역할을 한다고 한다.
바로 알아보자.
아래와 같이 코드는 거의 비슷하나 조건만 다르다.
얻고자 하는 정보가 다르지만 코드의 중복이 많으니 불편하다
이를 go, pipe, curry를 활용해서 함수를 분리하여 가독성 높은 코드를 만들어보자.
go, pipe , curry를 활용하긴 하지만
아래와 같이 코드의 중복을 제거하는 방법은 무궁무진하다는 것을 인지하고 넘어가자.
하나는 5000원 미만의 값이 필요하고
하나는 5000원 이상의 값이 필요하다
로직은 거의 비슷하지만 코드의 중복이 너무 많다.
go(
products,
filter(a => a.price < 5000),
map(a => a.price),
reduce(add),
console.log
);
go(
products,
filter(a => a.price >= 5000),
map(a => a.price),
reduce(add),
console.log
);
1. pipe를 이용해 중복되는 map과 reduce를 하나의 함수로 만들자!!
pipe 함수는 함수들이 나열되어 있는 합성된 함수를 만드는 함수이기 때문이다!
아래와 같이 잘 동작한다.
const total_price = pipe(
map(a => a.price),
reduce(add)
)
go(
products,
filter(a => a.price < 5000),
total_price,
console.log
);
go(
products,
filter(a => a.price >= 5000),
total_price,
console.log
);
2. pipe를 통해 total_price 라는 함수와 filter 함수를 하나의 함수로 다시 합치자.
이때 filter 함수에는 조건식이 들어가야하기에 이전 Map, filter, reduce 함수를 구현할 때와 같이
보조함수(predicate)에 이를 위임하자
const total_price = pipe(
map(a => a.price),
reduce(add)
)
const base_total_price = predicate => pipe(
filter(predicate),
total_price);
go(
products,
base_total_price(a => a.price < 5000),
console.log
);
go(
products,
base_total_price(a => a.price >= 5000),
console.log
);
SUMMARY
go 함수
함수를 위에서부터 아래로, 왼쪽에서부터 오른쪽으로부터 평가하면서
연속적으로 함수를 실행하고 이전에 실행된 함수의 결과를 다음 함수에 전달하는 함수
pipe 함수
pipe 함수는 함수들이 나열되어 있는 합성된 함수를 만드는 함수이다.
curry 함수
함수를 값으로 다루며 받아둔 함수를 내가 원하는 시점에 평가시키는 함수로
함수를 받아 함수를 리턴하고 인자를 받아서 원하는 갯수의 인자가 들어왔을때
받아온 함수를 나중에 평가시키는 함수이다!!
특히 curry 함수로 map, filter, reduce를 꾸며줌으로써 보다 더 코드를 값으로 다루는 문장을 간결하게 표현할 수 있다.