[ES6] Generator와 Lazy Evaluation

주형(Jureamer)·2022년 11월 23일
0

[ES6] Iterable, Iterator에 대하여

제너레이터(Generator)란?

일반적인 함수는 하나의 값을 반환하거나 반환하지 않는다.
제너레이터는 여러 개의 값을 필요에 따라 하나씩 반환(yield)할 수 있다.
제너레이터와 이터러블 객체를 함께 사용하면 데이터 스트림을 만들 수 있다.

제너레이터 함수는 일반 함수와 동작 방식이 다른데 제너레이터 함수를 호출하면 코드가 실행되지 않고, 대신 실행을 처리하는 특별 객체인 제너레이터 객체가 반환된다.

// 제너레이터 객체 반환 예시
function* exampleGenerator() {
  yield 1
  yield 2
  yield 3
}

let generator = exampleGenerator();
console.log(generator) // [object Generator]

yield, next

제너레이터는 yield라는 생소한 단어가 존재한다. 이는 제너레이터 함수의 실행을 일시적으로 정지시키며, 뒤에 오는 표현 식은 제너레이터의 caller에게 반환되는데 일반 함수의 return과 매우 유사하다고 볼 수 있다.

여기서 제너레이터 함수는 Callee이고, 이를 호출하는 함수가 Caller이며, Caller는 Callee의 yield 부분에서 다음 statement로 진행을 할 지 여부를 제어한다.

제너레이터는 next()를 통해 결과값을 받을 수 있는데 모든 yield를 처리하기 위해 그만큼의 next를 사용해야하는 가?에 대한 답은 그럴 수도 있고, 아닐 수도 있다이다.

next를 일일이 호출하지 않고 yield를 모두 처리하려면 다음과 같이 재귀 호출을 하면 된다.

// 홀수는 그대로 출력하고, 짝수에는 1을 더하여 출력하는 Code
function* generator() {
    console.log(yield 10)
    console.log(yield 15)
    console.log(yield 7)
}

function run(gen) {
    const it = gen();

    (function iterator({value, done}) {
        if (done) {
            return value
        } 

        if (value % 2 === 0) {
            return iterator(it.next(value + 1))
        } else {
            return iterator(it.next(value))
        }
    })(it.next())
}

// 11
// 15
// 7

제너레이터 종료

제너레이터에는 next외에도 throw, return 등의 메소드가 있는데 종료되는 방법의 차이가 있다.

  • next():
    이 메서드는 다음 값을 얻는 역할을 하며, iterator의 next와 달리, optional argument를 받는다(첫번째 호출에서는 무시된다.)
  • return(value):
    이 메서드는 매개변수로 온 값을 value로써 반환하고, 제너레이터를 종료시킨다.
  • throw(exception):
    이 메서드는 인자로 받은 에러를 발생시키고, 제너레이터를 종료시킨다. 제너레이터 함수 내부의 catch 구문을 통해 처리할 수도 있다.(내부에서 catch 구문으로 처리한 경우 제너레이터는 종료되지 않는다.)
  • 전개문자도 사용 가능하다

Symbol.iterator 대신 제너레이터 함수를 사용하면, 제너레이터 함수로 반복이 가능합니다.

let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() { // [Symbol.iterator]: function*()를 짧게 줄임
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

alert( [...range] ); // 1, 2, 3, 4, 5

지연 평가(Lazy evaluation)

제너레이터 특성상 값을 순회하거나 여러 개를 순차적으로 출력하게 되는 데, 이로 인해 계산의 결과 값이 필요할 때까지 계산을 늦출 수 있다. 이것을 지연평가라고 한다.

지연평가는 아래와 같은 3가지 이점을 가지고 있다.

1. 불필요한 계산을 하지 않으므로 빠른 계산이 가능하다.
2. 무한 자료 구조를 사용할 수 있다.
3. 복잡한 수식에서 오류 상태를 피할 수 있다.

이러한 장점으로 일반 함수와 제너레이터 함수 간의
배열을 만드는 속도 비교를 했을 때 아래와 같이 크게 차이 나는 경우가 생긴다.
이는 배열의 크기가 클 수록 더 벌어진다.

function makeArr(n) {
    let i = 1;
    const res = [];
    while (i < n) res.push(i++);
    return res;
}

function* makeArrGenerator(n) {
    let i = 1;
    while (i < n) yield i++;
}

function getDivideToFive(iter) {
    const res = [];
    for (const item of iter) {
        if (item % 5 == 0) res.push(item);
    }
    return;
}
console.time('1');
console.log(getDivideToFive(makeArr(10000)));
console.timeEnd('1');
// 7.69ms
console.time('2');
console.log(getDivideToFive(makeArrGenerator(10000)));
console.timeEnd('2');

// 3.857ms 

yield*의 2가지 기능

yield*을 사용했을 때 경우에 따라 아래 2가지 기능을 할 수 있다

  • 뒤에 이터러블 객체가 오면 이터러블을 순회할 수 있다.
// 1. 이터러블 순회 예시
function* iterableYield() {
  const a = 1;
  yield a;
  yield* [1, 2, 3].map(el => el * (10 ** a));

  const b = 2;
  yield b;
  yield* [1, 2, 3].map(el => el * (10 ** b));

  const c = 3;
  yield c;
  yield* [1, 2, 3].map(el => el * (10 ** c));
}

function run(gen) {
  const it = gen();

  (function iterate({ value, done }) {
    console.log({ value, done });
    if (done) {
      return value;
    }

    iterate(it.next(value));
  })(it.next());
}

run(iterableYield);

// { value: 1, done: false }
// { value: 10, done: false }
// { value: 20, done: false }
// { value: 30, done: false }
// { value: 2, done: false }
// { value: 100, done: false }
// { value: 200, done: false }
// { value: 300, done: false }
// { value: 3, done: false }
// { value: 1000, done: false }
// { value: 2000, done: false }
// { value: 3000, done: false }
// { value: undefined, done: true }
  • 뒤에 다른 제너레이터 함수가 오면 해당 함수를 실행할 수 있다.
// 2. 다른 제너레이터 함수 실행 예시
function* innerGenerator(arr) {
    yield* ['a', 'b', 'c', 'd'].map((each, idx) => {
        let obj = {}
        obj[each] = arr[idx]
        console.log(obj)
    });
}

function* generator() {
    arr = [1, 2, 3]
    yield* innerGenerator(arr);
} 

[...generator()];

// { a: 1 }
// { b: 2 }
// { c: 3 }
// { d: undefined }

끝으로

제너레이터와 이터레이블 개념 자체는 크게 어렵진 않았지만 실무에 적재적소에 활용하기엔 깊은 이해와 많은 연습이 필요할 것 같다!!

참고

profile
작게라도 꾸준히 성장하는게 목표입니다.

0개의 댓글