더글라스 크락포트가 쓴 자바스크립트는 왜 그 모양일까?
를 보다가, 자바스크립트의 제너레이터는 구리다! 라는 필자의 주장이 있어 흥미를 가지고 읽어보니 또 나름 공감(?) 가는 부분이 있어서 코드를 작성해보았다.
거의 유사한 기능을 하는 코드를 제너레이터 없이 구현한 방식과 제너레이터를 사용하여 구현한 방식으로 두벌씩 만들어보았다.
function range(from = 0, to = Number.MAX_SAFE_INTEGER, step = 1) {
return () => {
if (from < to) {
const result = from;
from += step;
return result;
}
return;
};
}
const rangeNoGen = range(0, 3);
console.log(rangeNoGen()); //0
console.log(rangeNoGen()); //1
console.log(rangeNoGen()); //2
0 , 1 , 2
function* rangeWithGen(from = 0, to = Number.MAX_SAFE_INTEGER, step = 1) {
for (let i = 0; i < to; i += step) {
yield i; //다음에 불렀을 떄 시작이 여기.
}
}
const rageGen = rangeWithGen(0, 3);
for (let elem of rageGen) {
console.log(elem);
}
//이터러블 프로토콜을 따라 불러러줘야함
0 , 1 , 2
함수의 구현부는 간단해지는데, 제너레이터를 부를때 이터러블 프로토콜로 불러주지 않으면 사용하기 귀찮은 구석이 있다.
function collect<T>(generator: Function, array: T[]) {
return (...args: any) => {
const value = generator(...args);
if (value !== undefined) {
array.push(value);
}
return value;
};
}
const arr = [];
const a = collect(range(0, 3, 1), arr);
console.log(a());
console.log(a());
console.log(a());
console.log(arr, "콜렉트 끝");
0
1
2
[ 0, 1, 2 ] '콜렉트 끝'
역시 제너레이터로 한번 구현해 보도록 하자.
function *collectWithGen<T>(generator: Generator<T>, array: T[]) {
for(let elem of generator){
yield array.push(elem)
}
}
const arr2 = [];
const a2 = collectWithGen(rangeWithGen(0, 3, 1), arr2);
for(let elem of a2){} //just runner
console.log(arr2, "콜렉트 끝")
[ 0, 1, 2 ] '콜렉트 끝'
출력해주기 위해서 빈 for 루프를 하나 집어넣었다.
for of 처럼 제너레이터만 넘겨주면 제너레이터의 끝까지 돌면서 실행해주는 간단한 함수를 구현해보자.
function forOf(gen: Function) {
if (gen() !== undefined) {
forOf(gen);
}
}
const arrayTemp: number[] = [];
forOf(
collect(range(0, 10, 1), arrayTemp)
);
console.log(arrayTemp); //제너레이터를 모두 평가
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
평가할 값이 없을 때 까지 다음 제너레이터를 부르는 간단한 함수이다.
function takeAll<T>(gen: Function) {
const array: T[] = [];
forOf(collect<T>(gen, array));
return array;
}
const result1 = takeAll(range(0, 3, 1));
console.log(result1);
[0,1,2]
앞서 구현한 collect
와 forOf
를 활용해서 제너레이터를 모두 평가하는 함수를 구현해 보았다.
이를 제너레이터로 구현해본다면,
function takeAllWithGen<T>(gen: Generator<T>) {
const array: T[] = [];
for(let elem of gen){
array.push(elem)
}
return array;
}
const result2 = takeAllWithGen(rangeWithGen(0, 3, 1));
console.log(result2);
[ 0, 1, 2 ]
와 같이 구현할 수 있다.
엄청 많이 사용하는 filter
함수를 한번 만들어보자.
function filter(gen: Function, filterFunc: (...args: any) => boolean) {
return function _filter(...args: any): any {
const value = gen(...args);
if (value !== undefined && !filterFunc(value)) {
return _filter(...args);
}
return value;
};
}
//제너레이터로 만들면
function *filterWithGen(gen: Generator, filterFunc: (...args: any) => boolean) {
for(let elem of gen){
if(filterFunc(elem)) yield elem
}
}
console.log(takeAll(filter(range(0, 10), (e) => e % 3 === 0)));
//[ 0, 3, 6, 9 ] 출력
console.log(takeAllWithGen(
filterWithGen(
rangeWithGen(0, 10), (e) => e % 3 === 0)
)
);
//[ 0, 3, 6, 9 ] 출력
같은 방식으로 map
, reduce
,every
같은 함수를 어렵지 않게 만들 수 있을 것이다.
자바스크립트의 제너레이터를 사용해도, 이렇게 클로저를 사용하여 제너레이터를 구현해도 지연평가의 이점을 누릴 수 있는것은 동일하다.
//[1,2,3,4,5].take(3) -> [1,2,3] 와 같이 제너레이터의 앞 n 개의 값만 평가하는 함수.
function take<T>(gen: Function,range:number) {
const array: T[] = [];
while( array.length !== range){
array.push(gen())
}
return array;
}
function takeWithGen<T>(gen: Generator<T>,range:number) {
const array: T[] = [];
for(let elem of gen){
array.push(elem)
if(array.length === range) break;
}
return array;
}
console.log(take(filter(range(0, 99999999), (e) => e % 3 === 0),2));
//0,1,2
console.log(takeWithGen(
filterWithGen(
rangeWithGen(0, 99999999), (e) => e % 3 === 0)
,2)
);
//0,1,2
위와 같은 상황에서, 우리가 만든 함수들은 실행 시점에 평가되기 때문에 99999999
와 같이 극단적으로 큰 숫자를 인자로 입력해도 실제 3 까지만 평가되어 아주 빠르게 동작한다.
하지만 이미 지연평가를 지원하는 lodash
같은 좋은 라이브러리들이 많으니 클로저를 이렇게 사용할 수 있구나, 와 같은 마음으로 글을 재밌게 읽어 주셨길 바란다.
안녕하세요! 글 정말 잘 보고 있습니다! 연락을 드려보고 싶은데 joon891030@gmail.com으로 메일 하나 보내주시겠어요?