함수형 프로그래밍 2주차
섹션 5. 코드를 값으로 다루어 표현력 높이기
go
go 함수란?
인자를 받아 결과
를 바로 산출해내는 함수이다.
첫번째 인자는 시작되는 값을 받고, 나머지는 함수를 받아 인자가 함수들에 순차적으로 돌아가며 결과가 만들어지게 실행된다.
go 함수 내부 코드
const go = (...args) => reduce((a, f) => f(a), args);
// 예시
go(
add(0, 1), // 1
(a) => a + 10, // 11
(a) => a + 100, // 111
console.log // 111을 콘솔로 찍음
);
왜 쓰는거야?
아래와 같이 map, filter, reduce 가 중첩된 함수가 있다고 가정하자
reduce(
add,
map(
(p) => p.price,
filter((p) => p.price < 20000, products)
)
)
이해하기도 어렵고 보기도 힘들다.
그렇다면 go 함수로 나타내면 어떻게될까?
go(
products,
(products) => filter((p) => p.price < 20000, products), // 20000 미만인 상품들
(products) => map((p) => p.price, products), // 20000 미만인 상품들의 가격
(prices) => reduce(add, prices), // 20000 미만인 상품들의 가격 총합
log
);
코드가 길어지는 것 같지만 가독성이 훨씬 좋아졌다. ( 뒤에 코드를 줄이는 방법은 curry를 통해 설명한다. )
pipe
pipe 함수란?
pipe 함수는 인자를 받아 함수
를 리턴해주는 함수이다.
함수자체를 반환하여 최종적으로 인자값으로 받은 함수리스트를 합성해서 합성된 합수를 가지고 로직을 수행한다.
pipe 함수 내부코드
const pipe =
(f, ...fs) =>
(...as) =>
go(f(...as), ...fs);
// 예시
const f = pipe(
(a, b) => a + b,
(a) => a + 10,
(a) => a + 100
);
console.log(f(0, 1)); // 111
중복되는 코드 제거
pipe 함수를 통해 go 함수의 중복되는 코드를 제거할 수 있다.
go(
products,
(products) => filter((p) => p.price < 20000, products), // 20000 미만인 상품들
(products) => map((p) => p.price, products), // 20000 미만인 상품들의 가격
(prices) => reduce(add, prices), // 20000 미만인 상품들의 가격 총합
log
);
const total_price = pipe(
map((p) => p.price),
reduce(add)
);
// map, reduce를 total_price로 합침
go(
products,
(products) => filter((p) => p.price < 20000, products), // 20000 미만인 상품들
total_price,
log
);
curry
curry 함수란?
여러 개의 인자를 가진 함수를 호출 하는 경우, 파라미터의 수보다 적은 수의 파라미터를 인자로 받으면 누락된 파라미터를 인자로 받는 기법.
다시말해, 부분적으로 적용된 함수를 체인으로 계속 생성해서 결과적으로 값을 처리하도록 하는것이 본질이다.
const curry =
(f) =>
(a, ..._) =>
_.length ? f(a, ..._) : (..._) => f(a, ..._);
// 예시
const mult = curry((a, b) => a * b);
console.log(mult(2)); // (..._) => f(a, ..._) => 나머지 인자를 더 전달했을 때, 함수에게 받어두둔 인자와 함께 실행
console.log(mult(1)(2)); // 2
const mult3 = mult(3); // 첫 인자가 3으로 고정된 mult 함수
console.log(mult3(10)); // 30
console.log(mult3(5)); // 15
console.log(mult3(3)); // 9
위와 같이 curry를 통해 map, filter, reduce를 선언해준 뒤에 아래와 같이 작성할 수 있다.
go(
products,
(products) => filter((p) => p.price < 20000)(products),
(products) => map((p) => p.price)(products),
(products) => reduce(add)(products),
console.log
);
// 또 아래와 같이 다시 한번 줄일 수 있다.
go(
products,
filter((p) => p.price < 20000),
map((p) => p.price),
reduce(add),
console.log
);
섹션 7. 지연성 1
range 와 느긋한 L.range
range
받아온 l까지의 배열을 만들어주는 함수
const range = (l) => {
let i = -1;
let res = [];
while (++i < l) {
res.push(i);
}
return res;
};
L.range
generator로 생성된 함수로 순회를 시작하는 next()가 실행되어야 결과가 꺼내진다.
결과를 꺼내기 전까지는 console로 찍어도 배열의 형태가 나오지 않게 된다.
모든 배열을 미리 생성하지 않고 하나의 결과씩 꺼내온다.
const L = {};
L.range = function* (l) {
let i = -1;
while (++i < l) {
yield i;
}
};
효율성 테스트
test('range', 10, () => reduce(add, range(1000000)));
test('L.range', 10, () => reduce(add, L.range(1000000)));
take
특정 길이까지의 iter를 반환하는 함수
const take = curry((l, iter) => {
let res = [];
for (const a of iter) {
res.push(a);
if (res.length == l) return res;
}
return res;
});
map, range, filter
map
const map = curry((f, iter) => {
let res = [];
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
res.push(f(a));
}
return res;
});
range
const range = (l) => {
let i = -1;
let res = [];
while (++i < l) {
res.push(i);
}
return res;
};
filter
const filter = curry((f, iter) => {
let res = [];
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
if (f(a)) res.push(a);
}
return res;
L.map, L.range, L.filter
L.map
L.map = curry(function* (f, iter) {
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
yield f(a);
}
});
L.range
L.range = function* (l) {
let i = -1;
while (++i < l) {
yield i;
}
};
L.filter
L.filter = curry(function* (f, iter) {
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
if (f(a)) {
yield a;
}
}
});
중첩 사용 작동 순서
일반 함수
range를 실행하여 100000까지의 배열을 생성.
해당 배열을 map함수로 돌림.
filter가 반환 값을 받아 함수 실행 후 take로 배열 반환
take는 배열을 받아 10까지 실행.
console.time('');
go(range(100000),
map(n => n + 10),
filter(n => n % 2),
take(10),
console.log);
console.timeEnd('');
Lazy 함수
range를 실행하려 하였으나 next()문이 실행되지 않았기에 평가를 하지 않고 넘김
map, filter 또한 마찬가지로 평가를 하지 않고 take로 넘김
take에서 iter를 꺼내려 했으나 해당 iter는 filter에서 받은 인자이므로 filter 실행
filter를 실행 중 filter에 들어가는 iter는 map에서 받은 인자이므로 map 실행
map 또한 range에서 받은 인자이므로 range실행
range는 인자하나를 꺼내 map에 넘겨준다.
해당 인자는 map,filter, take 순으로 돌아가며 결과값을 받는다.
다음 take는 다음 인자가 필요하므로 위의 순서를 반복하게 된다.
console.time('L');
go(L.range(100000),
L.map(n => n + 10),
L.filter(n => n % 2),
take(10),
console.log);
console.timeEnd('L');