제너레이터 없이 제너레이터 구현해보기

Tei·2021년 11월 16일
5
post-thumbnail

더글라스 크락포트가 쓴 자바스크립트는 왜 그 모양일까? 를 보다가, 자바스크립트의 제너레이터는 구리다! 라는 필자의 주장이 있어 흥미를 가지고 읽어보니 또 나름 공감(?) 가는 부분이 있어서 코드를 작성해보았다.

거의 유사한 기능을 하는 코드를 제너레이터 없이 구현한 방식과 제너레이터를 사용하여 구현한 방식으로 두벌씩 만들어보았다.

  • 예시의 성격이 강해서 타입핑을 엄격하게 하지 않은 점 양해 부탁드립니다.

range 함수

  • range 함수는 주어진 인자 범위의 제너레이터를 생성하고, 해당 제너레이터를 부를때 마다 값이 평가되어 반환되는 함수이다.
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

함수의 구현부는 간단해지는데, 제너레이터를 부를때 이터러블 프로토콜로 불러주지 않으면 사용하기 귀찮은 구석이 있다.

collect 함수

  • 부를때마다 인자로 준 배열에 제너레이터의 값을 하나씩 집어넣는 함수.
  • 평가가 되지 않은 제너레이터의 값을 하나씩 배열로 옮겨줌.
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]

앞서 구현한 collectforOf를 활용해서 제너레이터를 모두 평가하는 함수를 구현해 보았다.
이를 제너레이터로 구현해본다면,

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 같은 좋은 라이브러리들이 많으니 클로저를 이렇게 사용할 수 있구나, 와 같은 마음으로 글을 재밌게 읽어 주셨길 바란다.

profile
Being a service developer

1개의 댓글

comment-user-thumbnail
2022년 1월 20일

안녕하세요! 글 정말 잘 보고 있습니다! 연락을 드려보고 싶은데 joon891030@gmail.com으로 메일 하나 보내주시겠어요?

답글 달기