range(1000000)은 백만 개짜리 배열을 즉시 만든다. L.range(1000000)은 아무것도 만들지 않는다. 필요할 때 하나씩 꺼낼 수 있는 이터레이터만 반환한다. 이게 지연 평가다.
const L = {};
L.range = function* (l) {
let i = -1;
while (++i < l) yield i;
};
// 즉시 배열 생성
const list = range(4); // [0, 1, 2, 3]
// 이터레이터만 반환, 아직 값 없음
const lazyList = L.range(4); // GeneratorObject
reduce(add, L.range(1000000))은 reduce(add, range(1000000))과 결과가 같지만, 메모리 사용량이 다르다. L.range는 값을 하나씩 꺼내 즉시 합산하기 때문에 배열을 통째로 들고 있지 않는다.
필요한 개수만 꺼내는 함수. 지연 평가와 함께 쓸 때 효과가 두드러진다.
const take = curry((l, iter) => {
let res = [];
for (const a of iter) {
res.push(a);
if (res.length === l) return res;
}
return res;
});
take(5, L.range(Infinity)); // [0, 1, 2, 3, 4]
L.range(Infinity)는 무한히 값을 만들어낼 수 있지만, take(5)가 5개를 꺼내는 순간 순회를 멈춘다.
L.map = curry(function* (f, iter) {
for (const a of iter) yield f(a);
});
L.filter = curry(function* (f, iter) {
for (const a of iter) if (f(a)) yield a;
});
즉시 평가 버전과 다른 점은 yield로 값을 반환한다는 것이다. 다음 함수가 값을 요청할 때까지 실행을 멈춘다.
즉시 평가는 단계별로 전체를 처리한다.
map → [전체] → filter → [전체] → take
지연 평가는 값 하나가 파이프라인 끝까지 통과한 뒤 다음 값으로 넘어간다.
L.map(1) → L.filter(1) → take(1)
L.map(2) → L.filter(2) → take(2)
...
take가 충분한 개수를 채우면 L.map과 L.filter도 더 이상 실행되지 않는다. 이 때문에 지연 평가가 대용량 데이터에서 훨씬 효율적이다.