Javascript - DeepDive(9) : Iterator & Iterable & Generator

­이승환·2021년 7월 30일
0

Javascript DeepDive

목록 보기
9/13

기존과 달라진 ES6 에서 리스트 순회

리스트의 순회는 상당히 중요하다. ES6에서부터 새롭게 생긴 방식은 아래와 같다.

//AS-Was
const list = [1, 2, 3, 4]
for(let i = 0; i < list.length; i++){
	console.log(list[i]);
}

//To-BE
for(const a of list){
	console.log(a)
 }

위 두가지는 전혀 다른 방식임을 먼저 말하고 시작하겠다.
for _ of _ 의 경우 Symbol.iterator 라는 심볼을 통해서 접근하는 방식이다.
이터레이터의 경우 기본적으로 next 함수 하나만 있다고 생각하면 된다. 이해가 안 갈 것이다. 아래 예제 코드를 확인해보자

const array = [1, 2, 3, 4];

const iterator = (() => {
	let num = 1;

	return {
		next: () => {
			return num > 4 ? { done: true } : { done: false, value: num++ };
		},
	};
})();
/*
console.log(array); //[1, 2, 3, 4]
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
console.log(iterator.next().value); // 4
*/

next 함수를 통해서 done, value 두 항목을 전달하는 간단한 로직이다.
1부터 4까지 리스트에 담고 있는 방식이 아닌, num 이라는 메모리에 작은 한칸의 공간의 데이터값을 증가시키면서 가져오고 있다.

직관적으로도 굉장히 큰 리스트(피보나치수열같은) 대신 사용함으로써, 메모리를 아낄 수 있다.

예를 들어 이미지 분석을 위해 특정 디렉토리에 있는 이미지들을 불러와 미리 작성해둔 코드를 돌려가셔 처리해야 하는 작업(딥러닝에 있어서 전처리 과정) 에서 이터레이터를 통해 하나의 메모리만 올려놓고 접근해서 사용할 수도 있다.

Iterable

Iterable 은 반복 가능한 객체를 의미한다. Iterable한 객체는 for _ of _ Spread syntax Destructuring 을 사용할 수 있다고 한다. 이후에 더 자세히 알아보겠다.

Iterable 하기 위해선 2가지를 충족 시켜야 한다.
1. 객체 내에 Symbol.iterator 메서드를 가지낟.
2. [Symbol.iterator] 메서드는 Iterator 객체를 반환해야한다.

const iterable = {
  [Symbol.iterator]() {
    return someIteratorObject
  }
  ...
}

for(item of iterable) {
  console.log(item) // work
}

Iterator

Iterator 는 Iterable 객체에서 반복을 실행하는 반복기를 뜻한다. Iterable 객체가 반복하면서 어떠한 값을 반환 할지 결정하는 역할을 한다.

아래 4가지를 준수해야 한다.
1. 객체 내에 next() 가 필요
2. next 는 IteratorResult 를 반환해야한다
3. IteratorResult = {done : boolean, value : any} 의 형태를 띈다.
4. done 값이 true 를 반환했다면, 이후 호출에 대한 done 값도 true 여야 한다.

const iterable = {
  [Symbol.iterator]() {
    let i = 0
    // iterator 객체
    return {
      next() {
        while(i < 10) { // i가 10이 될 때까지 반복기 수행
          return { value: i++, done: false }
        }
        return { done: true } // i 가 10이 되면 반복 종료(value 값 생략 가능)
      }
    }
  }
}

for(let num of iterable) console.log(num) // 0, 1, ..., 9

well-formed iterable

Iterator 이면서 Iterable인 객체를 Well-formed iterable 이라고 한다. 아래의 폼을 맞춰주는 것을 의미한다.

const wellFormedIterable = { // Iterator 객체
  next() {
    return someIteratorResultObject
  }

  // Iterator 객체에 Symbol.iterator 메서드가 존재하며, 
  // 해당 메서드가 자기 자신(iterator)을 반환한다.
  [Symbol.iterator]() {
    return this
  }
  ...
}

well_formed 한 iterator 의 경우 자기 자신의 상태를 기억 할 수 있다는 것이 장점이다.

이터레이터를 사용하는 이유

  1. More functionality doesn't mean better
  2. Iterator can save memory uses

Arra, Set, Map

//Arr 
const arr = [1,2,3]
for(const a of arr ) console.log(a)

//Set
const set = new Set([1,2,3])
for(const a of set ) console.log(a)

//Map
const map = new Map ([['a', 1], ['b' ,2], ['c',3]])
for(const a of map.keys() log(a) // a, b, c
for(const a of map.values()log(a) //1, 2, 3 
for(const a of map.entries() log(a)//['a', 1], ['b' ,2], ['c',3]

위와 같은 방식으로 다양한 자료구조에서 사용가능하다. 전부 Symbol.iterator 가 추가되서 가능하다고 한다.

사용자 정의 예제

const iterable = {
    [Symbol.iterator](){
        let i = 3
        return{
            next() {
                return i==0 ? {done: true }:{value:i--, done: false}
            },
        	[Symbol.iterator]() { return this } // well-formed iterable
        }
    }
}
let iterator = iterable[Symbol.iterator]()
for (const a of iterable) log(a)

전개 연산자

아래와 같은 경우에서도 Iterator/Iterable 한 방식이라고 한다.

const a = [1, 2]
console.log(...a) // 1 2
console.log([...a...[3, 4]] // 1, 2, 3, 4

참고로 ...input 인자를 통해서는 배열을 전달할 수 있고, 이것이 iterator/iterable 한 well-formed Iterable 객체가 된다는 뜻이다.

Generator

사실 well-formed-iterable 은 상태를 기억하고, 메모리를 아낄 수 있다는 장점이 있는데 구현하기가 여간 까다롭지 않을 수가 없다. 그래서 나온 방식이 Generator 이다. 한마디로 Iterable + Iterator 를 더욱 쉽게 만들어준다. 형식은 아래와 같다.

function* generatorFunction() {
  yield 42;
}

const generator = generatorFunction();

generator === generator[Symbol.iterator]();

// 이 말인 즉슨, 제너레이터의 이터러블은 다음과 같은 방식으로 구현되어 있을 거라는 것을 암시한다.
// generator[Symbol.iterator] = () => this;

Generator 와 Yield, Next

yield는 제너레이터 함수의 실행을 일시 정지시키고 잠시 결과값을 caller 에게 결과를 반환해준다. 여기서 상태를 기억하고 있으며 next 를 통해 그 다음 결과를 yield 해서 받아올 수 있다. 즉 상태를 기억하고 있다는 점이 동일하다.

function* increment() {
  console.log('[ENTERED]');
  let i = 0;

  try {
    while (true) {
      yield i++;
    }
  } catch (e) {
    console.log('[ERROR]', e);
  }
}

const withReturn = increment();
console.log(withReturn.next());
console.log(withReturn.next());
console.log(withReturn.next());
console.log(withReturn.next());
console.log(withReturn.return(42));

// [ENTERED]
// { value: 0, done: false }
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 42, done: true }

Generator 의 활용

function* testGenerator() {
yield 1;
const val = yield 2;
const final = yield 3;
return val; // customize this return value outside
}
const testGeneratorIt = testGenerator();
console.log(testGeneratorIt.next()); // { value: 1, done: false }
console.log(testGeneratorIt.next()); // { value: 2, done: false }
console.log(testGeneratorIt.next(100)); // { value: 3, done: false }
console.log(testGeneratorIt.next()); // { value: undefined -> 100, done: true }
function* testGenerator() {
yield 1;
const val = yield 2;
const final = yield 3;
return final; // customize this return value outside
}
const testGeneratorIt = testGenerator();
console.log(testGeneratorIt.next()); // { value: 1, done: false }
console.log(testGeneratorIt.next()); // { value: 2, done: false }
console.log(testGeneratorIt.next()); // { value: 3, done: false }
console.log(testGeneratorIt.next(100)); // { value: undefined -> 100, done: true }

yield 의 순서와 next(input) 의 순서를 잘 보면 감을 잡을 수 있다.

profile
Mechanical & Computer Science

0개의 댓글