ES6에서 도입된 제너레이터와 이터레이터 프로토콜은 지연평가(Lazy Evaluation)를 가능하게 하여 코드를 값으로 다루고, 안전하게 조합할 수 있는 기능을 제공합니다.
지연평가는 게으르다는 표현도 쓰지만, 영리한 평가라는 말도 씁니다. 그만큼 가장 필요할때까지 평가를 미루다가 정말 필요할때 값을 평가합니다
기존의 JavaScript에서는 지연평가를 위한 기본 프로토콜이 없어서 라이브러리나 함수를 사용하여 비슷한 동작을 구현해야 했습니다. 하지만 ES6에서 제너레이터와 이터레이터 프로토콜이 도입되면서 이런 기능들을 표준화하고 언어 자체에서 지원하게 되었습니다. 제너레이터 함수를 통해 이터레이터를 생성하고, 이터레이터를 통해 값을 지연적으로 평가할 수 있습니다.
또한, 이터레이터와 함께 사용되는 고차 함수인 map, filter, reduce, take 등을 이용하여 이터레이블 중심 프로그래밍, 리스트 중심 프로그래밍, 컬렉션 중심 프로그래밍 등의 기법을 구현할 수 있습니다. 이러한 기법은 데이터를 처리하는 과정을 추상화하고 합성 가능한 작은 단위로 분해하여 코드의 가독성과 유지보수성을 높여줍니다.
따라서, 제너레이터와 이터레이터를 활용한 지연평가는 ES6 이전에 비해 코드를 값으로 다루는 것을 훨씬 간편하게 만들어주고, 다양한 고차 함수를 통해 유연하고 안전한 조합성을 제공합니다.
const add = (a,b) => a+b;
const range = l => {
let i = -1;
let res = [];
while(++i < l) {
res.push(i);
}
return res;
}
var list = range(4);
log(list);
log(reduce(add,list));
const add = (a,b) => a+b;
const L = {};
L.range =function *(l) {
let i = -1;
while(++i < l) {
yield;
}
}
var list = L.range(4);
log(list);
log(reduce(add,list));
1번은 배열을 반환하고 2번은 이터레이터이다.
1번은 var list = range(4);
이때 이미 배열로 평가가 되지만, 2번은 reduce(add,list)
여기로 들어가 직접 순회하기 전까지 평가되지 않는다.
function test(name, time, f) {
console.time(name);
while(time--) f();
console.timeEnd(name);
}
test('range', 10, ()=>reduce(add,range(100000)));
test('L.range', 10, ()=>reduce(add,L.range(100000)));
const take = (l, iter) => {
let res = [];
for (const a of iter) {
res.push(a);
if (res.length == l) return res;
}
return res;
};
//일반함수로 만들어진 range(100) 이터레이터는 100을 전부 평가 후 2개를 자른다.
console.log(take(5, range(100));
//지연함수(제너레이터)로 만들어진 range(100) 이터레이터는 take 내부에서 `const a of iter` 될때 평가되기 때문에 2개만 만들어진다.
console.log(take(5, L.range(100));
//지연함수로 만들어진 이터레이터는 무한수열을 줘도 가능하다.
console.log(take(5, L.range(Infinity));
효율성도 지연함수가 좋음
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('');
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);
console.log([...it]);
L.map = function *(f, iter) {
for(const a of iter) if(f(a)) yield a;
}
var it = L.map(a=>a%2, [1,2,3,4]);
console.log(it.next);
console.log([...it]);
[[mapping, mapping], [filtering, filtering], [mapping, mapping]]
=
[[mapping, filtering, mapping],[mapping, filtering, mapping]]
이 결합 법칙은 함수형 프로그래밍에서 사용하는 데이터 변형 함수들이 어떤 순서로 결합되더라도 결과가 동일하다는 원리를 나타냅니다.
결합 법칙을 설명하기 위해 몇 가지 개념을 알아야 합니다. 먼저, "매핑(mapping)"은 데이터를 변형하는 작업을 의미하며, "필터링(filtering)"은 데이터를 걸러내는 작업을 의미합니다. 이러한 작업을 수행하는 함수들은 map과 filter 계열 함수들입니다.
결합 법칙은 다음과 같이 표현됩니다:
compose(map(f), map(g)) ≡ map(compose(f, g))
compose(filter(p), filter(q)) ≡ filter(compose(p, q))
여기서 compose
는 함수 조합을 나타내는 함수입니다. compose(f, g)
는 두 함수 f
와 g
를 조합하여 새로운 함수를 생성하는 역할을 합니다.
결합 법칙의 의미는 다음과 같습니다. 만약 데이터 변형 함수들을 일렬로 연결해서 사용한다면 (예: compose(map(f), map(g))
), 또는 이들 함수를 합성하여 하나의 함수로 만든다면 (예: map(compose(f, g))
), 그 결과는 동일하다는 것입니다. 즉, 함수들을 어떤 순서로 결합하더라도 최종적인 결과는 동일하다는 것을 보장합니다.
결합 법칙은 함수형 프로그래밍에서 코드의 가독성과 유지보수성을 높이는 데에 도움을 줍니다. 함수들을 결합하는 순서를 자유롭게 변경하거나 분해하여 재활용할 수 있기 때문입니다.