range와 느긋한 L.range
range 함수
⇒ 숫자하나를 받으면 그 크기만큼 배열을 반환하는 함수
const range = l => {
let i = -1;
let res = [];
while (++i < l) {
res.push(i);
}
return res;
};
var list = range(4);
log(list); //[0,1,2,3]
log(reduce(add, list)); // 6
JavaScript
복사
느긋한 L.range
const L = {};
L.range = function *(l){
let i = -1;
while (++i < l) {
yield i;
}
};
var list = L.range(4);
log(list); //L.range {<suspended>}
log(reduce(add, list)); // 6
JavaScript
복사
•
느긋한 L.range에서는 배열이 바로 반환되는게 아닌 Iterator가 반환된 것을 확인할 수 있습니다.
차이점
일반 range함수는 함수 호출시점에 이미 배열로 평가가 되서 list에 대입되었지만, 느긋한 L.range는 함수 호출시점에는 실제 값이 대입되지 않습니다.
느긋한 L.range함수는 호출 시점에서는 내부의 어떤 로직도 동작하지 않습니다. 실제 호출되는시기는 이터레이터의 내부를 순회할 때마다 하나씩 값이 평가가됩니다. 즉, list.next()를 통해 순회를 할 때 결과가 꺼내집니다.
지연평가를 어째서 사용하는가?
기존의 일반 range를 호출할 경우 즉시 배열이 생성되어 호출됩니다. 하지만, 해당 배열을 사용하는 실제 로직이 실행되기전까진 해당 배열의 필요성및 중요도는 높지 않습니다. 그 반면 지연평가는 배열을 생성하는 함수를 호출한시점에서 실제 배열을 반환하지는 않습니다. 실제로 Iterator가 순회를하며 next값을 꺼낼때 값을 생성하여 반환합니다. 이처럼 값을 실제 사용할 때까지 계산을 늦춰서 얻을 수 있는 이점은 불필요한 계산을 하지 않으므로 빠른 계산이 가능하고 무한자료 구조를 사용할수도 있습니다.
range와 느긋한 L.range 테스트
⇒ 즉시평가 range와 느긋한 L.range의 성능 테스트 코드
function test(name, time, f) {
console.time(name);
while (time --)f();
console.timeEnd(name);
}
test('range', 10, () => reduce(add, range(1000000))); // 379.27294921875ms
test('L.range', 10, () => reduce(add, L.range(1000000))); // L.range: 268.756103515625ms
JavaScript
복사
take
개요
Iterable에서 원하는 갯수만큼의 값을 얻어오는 함수.
함수 - take
const take = (l, iter) => {
let res = [];
for (const a of iter) {
res.push(a);
if (res.length === l) return res;
}
return res;
};
log(take(5, range(1000000)));//[0,1,2,3,4]
log(take(5, L.range(1000000)));//[0,1,2,3,4]
log(take(5, range(Infinity)));//에러 발생
log(take(5, L.range(Infinity)));//[0,1,2,3,4] 무한수열을 적용해도 동작한다.
JavaScript
복사
•
iterable 을 인자로 받아서 Iterator 순회를 돌며 배열(res)에 값을 집어넣고, 원하는 length(l) 만큼 값을 다 꺼냈으면 결과값을 반환합니다.
만약 Iterable의 크기가 가져오고자 하는 범위보다 작을 경우에는 모든 값을 꺼내서 반환하게 됩니다.
Currying적용 take
const take = curry((l, iter) => {
let res = [];
for (const a of iter) {
res.push(a);
if (res.length === l) return res;
}
return res;
});
console.time('');
go(
range(10000),
take(5),
reduce(add),
log
);
console.timeEnd('');
console.time('');
go(
L.range(10000),
take(5),
reduce(add),
log
);
console.timeEnd('');
JavaScript
복사
•
결과
제너레이더/이터레이터 프로토콜로 구현하는 지연 평가
지연평가(Lazy Evaluation)은 영리한 평가라고도 불리고 제때 계산법이라고도 불립니다.
평가를 최대한 미루다가 정말 필요할 때 평가를 하는 기법으로, 위 예제를 보면 실제로 reduce같은 함수에서 a,b같은 인자값을 받으려하는 그 시기에 2개의 값만 생성해서 전달하는식입니다.
L.map
제너레이터/이터레이터기반의 지연성을 가진 L.map을 구현합니다.
함수
L.map = function* (f, iter) {
for (const a of iter) {
yield f(a);
}
};
var it = L.map(a=>1+10, [1, 2, 3]);
log(it.next()); //{value: 11, done: false}
log(it.next()); //{value: 12, done: false}
log(it.next()); //{value: 13, done: false}
log(it.next()); //{value: undefined, done: true}
var it = L.map(a=>1+10, [1, 2, 3]);
log([...it]) //[11,12,13]
JavaScript
복사
•
함수를 호출 한 당시에는 평가가 되지 않습니다.
•
호출방법은 예제와같이 next를 통해 호출할 수도 나머지연산자를 이용할수도 있습니다.
L.filter
함수
L.filter = function* (f, iter) {
for (const a of iter) {
if(f(a)) yield a;
}
};
let it = L.filter(a => a % 2, [1, 2, 3, 4]);
log(it.next()); //{value: 1, done: false}
log(it.next()); //{value: 1, done: false}
log(it.next()); //{value: undefined, done: true}
log(it.next()); //{value: undefined, done: true}
it = L.filter(a => a % 2, [1, 2, 3, 4]);
log([...it]); // [1,3]
JavaScript
복사
•
value가 undefined이거나 done이 true가 될 때까지 값을 생성하여 반환합니다.
range, map, filter, take, reduce 중첩 사용
코드
go(range(10),
map(n => n + 10),
filter(n => n % 2),
take(2),
log
); // [11,13]
JavaScript
복사
•
range에서 값 생성 ⇒ [0,1,2,3,4,5,6,7,8,9]
•
map에서 값 튜닝 ⇒ [10,11,12,13,14,15,16,17,18,19]
•
filter에서 값 필터링 ⇒ [11,13,15,17,19]
•
take에서 limit ⇒ [11,13]
•
log에서 출력
L.range, L.map, L.filter, take의 평가 순서
const L = {};
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;
}
});
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;
});
go(L.range(10),
L.map(n => n + 10),
L.filter(n => n % 2),
take(2),
log
); // [11,13]
JavaScript
복사
•
take에서 값 호출
•
L.filter에서 값 호출하여 condition compare 후 반환
•
L.map에서 filter로 부터 요청받은 값을 호출하기위해 다시 range에 요청해 받은 값에 function을 적용(n+10)해 반환
•
L.range는 L.map으로부터 요청을받으면 실제 값을 Generated해서 반환
•
실제 값들이 Generated 되는 순서는 기존 즉시평가와는 다르게 take→L.filter → L.map → L.range순서다.
◦
take에서 값을 꺼내려하면 L.filter에게 요청하게되고 L.filter는 map에게 요청해서 받은 값이 condition과 비교 후 true일때만 반환
•
Generator protocol 방식은 값의 흐름이 가로가 아닌 세로가 됩니다.
엄격한 계산과 느긋한 계산의 효율성 비교
map, filter계열 함수들이 가지는 결합 법칙
map, filter 계열 함수들은 특정한 방식으로 다르게 평가순서를 바꿔도 동일한 결과를 반환한다는 결합법칙을 가지고 있습니다.
사용하는 데이터가 무엇이든 사용하는 보조 함수가 순수 함수라면(ex: map(a⇒a+10) 에서 a⇒a+10이 보조함수) 결과가 동일하게 반환됩니다.