앞서 만들어 뒀던 map, fiter, reduce를 모듈화 해놓고 사용해보자. 아래 코드는 정상적으로 잘 작동하지만 중첩되어 있어서 이해하기 어렵다.
const products = [
{name: '반팔티', price: 15000},
{name: '긴팔티', price: 20000},
{name: '핸드폰케이스', price: 15000},
{name: '후드티', price: 30000},
{name: '바지', price: 25000}
];
const add = (a, b) => a + b;
log(
reduce(
add,
map(p => p.price,
filter(p => p.price < 20000, products))));
해당 코드와 똑같은 기능을 하는 코드를 좀 더 사용 친화적인 코드로 변환해보자
const go = (...args) => reduce((a, f) => f(a), args);
go(
add(0, 1),
a => a + 10,
a => a + 100,
log);
// 111
// 함수를 리턴하는 함수
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));
log(
reduce(
add,
map(p => p.price,
filter(p => p.price < 20000, products))));
커리는 함수의 분해기법, 다수의 인자를 가지는 함수 대신 , 하나의 인자를 가지는 연속된 함수둘의 중첩이다. 클로저를 활용하여 외부환경의 값들을 기억했다가 사용한다.
const curry = f => (a, ..._) => _.length ? f(a,..._) : (..._) => f(a, ..._);
const mult = curry((a,b) => a*b)
console.log(mult(3)(2));
const mult3 = mult(3);
console.log(mult3(10));
console.log(mult3(5));
console.log(mult3(3));
위의 커리 함수를 이용해서 기존에 작성해뒀던 코드를 좀 더 깔끔하게 변경해보자
// 커리 사용 전
go(
products,
products => filter(p => p.price < 20000, products),
products => map(p => p.price, products),
prices => reduce(add, prices),
log);
// 커리로 기존 map, filter, reduce를 감싸준다
const map = curry((f, iter) => {
let res = [];
for (const a of iter) {
res.push(f(a));
}
return res;
});
const filter = curry((f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
});
const reduce = curry((f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
});
// 커리 사용 후
go(
products,
filter(p => p.price < 20000),
map(p => p.price),
reduce(add),
log);
const total_price = pipe(
map(p => p.price),
reduce(add));
const base_total_price = predi => pipe(
filter(predi),
total_price);
go(
products,
base_total_price(p => p.price < 20000),
log);
go(
products,
base_total_price(p => p.price >= 20000),
log);
제너레이터/이터레이터 프로토콜 기반으로 구현하여 값의 평가가 꼭 필요로할 떄만 하도록 하는 계산법
L.map = function* (f, iter) {
for (const a of iter) yield f(a);
}
var it = L.map(a => a+10, [1,2,3]);
console.log(it.next()) // {value: 11, done: false}
console.log(it.next()) // {value: 12, done: false}
console.log(it.next()) // {value: 13, done: true}
console.log([...it]) // [11,12,13]
L.filter = function* (f, iter) {
for (const a of iter) if (f(a)) yield a;
}
var it = L.filter(a => a % 10, [1,2,3,4]);
console.log(it.next()) // {value: 1, done: false}
console.log(it.next()) // {value: 3, done: false}
console.log(it.next()) // {value: undefined, done: true}
console.log([...it]) // [11,12,13]
const range = l => {
let i = -1;
let res = [];
while (++i < l) {
res.push(i);
}
return res;
};
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;
});
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;
});
const take = curry((l, iter) => {
let res = [];
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
res.push(a);
if (res.length == l) return res;
}
return res;
});
const reduce = curry((f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
} else {
iter = iter[Symbol.iterator]();
}
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
acc = f(acc, a);
}
return acc;
});
// console.time('');
// go(range(100000),
// map(n => n + 10),
// filter(n => n % 2),
// take(10),
// log);
// console.timeEnd('');
L.range = function* (l) {
let i = -1;
while (++i < l) {
yield i;
}
};
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.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;
}
}
});
// [0, 1, 2, 3, 4, 5, 6, 7, 8...]
// [10, 11, 12, ...]
// [11, 13, 15 ..]
// [11, 13]
//
// [0 [1
// 10 11
// false] true]
//
// console.time('L');
// go(L.range(Infinity),
// L.map(n => n + 10),
// L.filter(n => n % 2),
// take(10),
// log);
// console.timeEnd('L');
위에서 go 함수를 실행했을 때 결과의 실행순서를 보도록 하자
// 즉시 평가
// 1. range(10) 의 결과 값을 먼저 계산해서 map 함수로 넘겨준다
[0,1,2,3,4,5,6,7,8,9]
// 2. 해당 값을 가지고 map으로 계산되어 결과 값을 filter로 넘겨준다
[10,11,12,13,14,15,16,17,18,19]
// 3. filter 함수의 결과 값을 다 계산하여 take로 넘겨준다
[11,13,15,17,19]
// 4. take의 결과를 반환하여 평가한다.
[11,13]
// 지연 평가
// 1. 함수가 실행되면 L.range, L.map, L.filter의 평가를 미뤄두고 take함수부터 실행하게 된다.
// 2. take 함수의 !(cur = iter.next()).done 조건을 만나는 순간 평가를 미뤄뒀던 filter로 넘어 간다.
// 3. range까지 iter.next() 를 만나면 미뤄뒀던 평가를 하게 된다.
// 4. range 함수에서 yield를 만나면 평가된 값을 map으로 반환한다.
0
// 5. map에서 yield를 만나면 filter에게 평가를 맡긴다
10
// 6. filter의 결과를 take로 넘긴다.
false
// 7. 앞 값이 true일 시 take에서 값을 담는다. length가 2가 될 때까지 해당 작업들은 반복한다.
1
11
true
[11]
2
12
false
[11]
3
13
true
[11,13]
// 위와같이 실행된 다음 얻은 결과 값을 평가하게 된다.
즉시 평가와 지연 평가의 동작원리를 보면 계산의 효율성에서 큰 차이를 보인다는 것을 알 수 있다. 데이터의 크기가 커지면 커질수록 즉시평가보다 지연평가가 성능적으로 더 유리하다는 것을 알 수 있다.
정리를 하자면,