javascript&typescript

Javascript - Iterator pattern

Fanso 2020. 10. 28. 11:38

Iterator pattern은 배열과 같은 순차적인 데이터를 순회하기 위한 방법으로, javascript ES6(ECMA2015) 버전부터 Symbol과 함께 추가된 개념입니다. ES6전에는 여러 가지 컬렉션 관련 라이브러리들이 각자의 방법으로 순회 방법들을 제공했었지만, ES6에서는 공식적으로 Iterator/Iterable Protocol을 적용하여 순차적인 데이터의 순회 방식을 통일시켰습니다.

Iterator pattern을 이용함으로써 기본적으로 순차적인 데이터 처리에서부터 지연 평가나 ES6에서 제공하는 for...of나 spread 및 rest 기능들을 사용할 수 있습니다.


# Iterator&Iterable Protocol

Iterator&Iterable Protocol은 아래와 같이 몇가지 규칙을 통하여 연속된 데이터를 순회할 수 있도록 하는 약속입니다.

  • Iterator는 {value: any, done: boolean} 모양의 객체를 리턴하는 next()라는 함수를 가진 값을 말합니다.
  • Iterable은 [Symbol.iterator]를 키로 하는 프로퍼티에 Iterator를 반환하는 함수가 들어있는 값을 말합니다.
  • Iterator&Iterable protocol을 준수하면 순차적으로 데이터를 순회할 수 있게 되어 for...ofspreadrest 기능들을 사용할 수 있게 됩니다.

Array, Map, Set과 같은 컬렉션형 객체들은 기본적으로 Iterable로써 Symbol.iterator를 키로 하여 Iterator를 반환하는 함수를 가지고 있으며, 이는 곧 Iterator&Iterable Protocol을 만족한다는 뜻입니다.

// 'Array --------------'
const arrIterable = [1, 2, 3];
for (const item of arrIterable) {
  console.log(item);	// 1  2  3
}

const arrIterator = arrIterable[Symbol.iterator]();

console.log(arrIterator.next());  // { value: 1, done: false }
console.log(arrIterator.next());  // { value: 2, done: false }
console.log(arrIterator.next());  // { value: 3, done: false }
console.log(arrIterator.next());  // { value: undefined, done: true }
// 'Map ----------------'
const mapIterable = new Map([[0, 1], [1, 2], [2, 3]])
const mapIterator = mapIterable[Symbol.iterator]();

mapIterator.next();              	// next was already called once, 

for (const item of mapIterator) {	// only rest of items called here
  console.log(item);			// [1,2]  [2,3]
}

console.log(mapIterator.next()); 	// {value: undefined, done: true}

 

이렇게 Iterator & Iterable Protocol을 만족하게 되면, ES6에서 제공하는 for...of 또는 spread기능을 사용할 수 있습니다.

// 'Iterable functions ----------------'
const arrIterable = [1, 2, 3];

// [for..of] functions
const arrIterator1 = arrIterable[Symbol.iterator]();
for (const item of arrIterator1) {
  console.log(item);			// 1  2  3
}

// [spread | rest] functions
const arrIterator2 = arrIterable[Symbol.iterator]();
const spreadArr = [...arrIterator2, 4, 5];	// 1, 2, 3, 4, 5

const arrIterator3 = arrIterable[Symbol.iterator]();
arrIterator3.next();
const restArr = [...arrIterator3, 4, 5];	// 2, 3, 4, 5

 

기본적으로 제공되는 Iterable외에도 직접 Iterable과 Iterator을 만들어서 사용할 수도 있습니다.

// 'Custom -------------'
const iterable = {
  [Symbol.iterator]() { // [Symbol.iterator] = () => return iterator
    let i = 0;
    const myItems = ['첫번째', 2, 3.0];
    
    return {
      next() {
        return i>=3 ? {done: true} : {value: myItems[i++], done: false};
      },
      [Symbol.iterator]() {
        return this;  // well made iterator
      }
    }
  }
}
for(const item of iterable){
  console.log(item);  // 첫번째  2  3.0
}

 


 

# Well made Iterator

Well made iterator란 Iterator이면서 Iterable인 값을 말하며(보통 자기 자신을 반환하는 Iterator), Iterator의 재사용성을 높이기 위한 방법입니다.

Well made iterator에 대해 설명하기 앞서 먼저 좀 더 명확하게 Iterable과 Iterator의 관계를 짚어볼 필요가 있습니다.

  • 순회는 'Iterable''Symbol.iterator'프로퍼티의 함수를 실행하여 'Iterator'를 만들고, 'Iterator''next'함수를 차례대로 호출하여 진행된다.
  • 엄밀히 따지면 'Iterable'을 순회하는 것이지 'Iterator'를 순회하는 것이 아닙니다.
// 'Custom -------------'
const iterable = {
  [Symbol.iterator]() {
    let i = 3;
    return {
      next() {
        return i===0 ? {done: true} : {value: i--, done: false};
      }
    }
  }
}

// iterable 순회
for(const item of iterable){
  console.log(item);  // 3  2  1
}

// iterator 순회
const iterator = iterable[Symbol.iterator]();
for(const item of iterator){
  console.log(item);  // Error
}

위의 예제처럼 Iterable은 순회가 가능하지만 Iterator를 순회하려고 하면 오류가 발생하게 됩니다. 하지만 이게 뭐가 문제일까요? 사실 프로토콜 상으로는 아무런 문제가 없습니다. Iterable과 Iterator는 제 기능을 다하고 있습니다. 하지만 이러한 구조는 재사용성에 있어서 문제가 발생할 수 있습니다.

 

// 'Custom -------------'
const iterable = {
    [Symbol.iterator]() {
        let i = 3
        return {
            next() {
                return i === 0 ? { done: true } : { value: i--, done: false }
            },
        }
    },
}

// 첫번째 값은 출력하지 않고, 나머지 값의 합을 출력
// iterable은 단순 순회만 가능하므로, iterator로 처리
function removeFirst(itr) {
    return itr.next()
}

function sumAll(itr) {
    let item
    let sum = 0
    // iterator은 순회가 불가능해서 next반복 호출
    while (!(item = itr.next()).done) {
        sum += item.value
    }
    return sum
}

const iterator = iterable[Symbol.iterator]()
removeFirst(iterator) // { value: 3, done: false }
sumAll(iterator) // 2+1 = 3

위의 코드 예시처럼 단순 순회가 아니라 복잡한 로직의 경우에는 경우에는, Iterable의 순회기능을 사용하지 못하고 Iterator만으로 처리해야 합니다. 하지만 위 예시의 Iterator는 순회가 불가능해서 next 호출만으로 문제를  해결하고 있습니다. 만약 Iterator도 순회가 가능하다면  어떻게 달라질까요?

// 'Custom -------------'
const iterable = {
  [Symbol.iterator]() {
    let i = 3;
    return {
      next() {
        return i===0 ? {done: true} : {value: i--, done: false};
      },
      [Symbol.iterator]() {
        return this;	// well made iterator
      }
    }
  }
}

// 첫번째 값은 출력하지 않고, 나머지 값의 합을 출력
function removeFirst(itr) {
    return itr.next()
}

function sumAll(itr) {
    let sum = 0
    // Iterator도 순회기능들을 사용 가능
    for(const item of iterator2){
      sum += item;  // 2+1 = 3
    }
    return sum
}

const iterator = iterable[Symbol.iterator]()
removeFirst(iterator) // { value: 3, done: false }
sumAll(iterator) // 2+1 = 3

훨씬 코드가 깔끔하지 않나요?(javascript에서 제공하는 순회기능을 이용해서 쉽게 문제를 해결하고 있습니다!)

이처럼 Iterable & Iterator를 만들 때 잘 만든다면(Well made) 재사용성이 아주 높은 코드를 작성할 수 있습니다.


 

# Generator

  • Generator는 Well made Iterator를 만들어주는 함수를 말합니다.
  • 함수명 앞에 *을 붙여서 선언하며, arrow function에서는 사용할 수 없고 function명령어를 이용한 함수 선언에서만 사용할 수 있습니다.
  • yield 명령어를 이용하여 순회시킬 데이터를 결정합니다.
// 'Generator --------------'
function *gen() {
  yield 1;
  if(false) yield 2;
  yield 3;
}

const iter = gen();

console.log(iter[Symbol.iterator]() === iter);  // true, well made iterator

for(const a of gen()) {
  console.log(a); // 1  3
}
// '*Infinity --------------'
// 무한정 순회하는 iterator
function *infinity(i = 0) {
  while(true) yield i++;
}

// '*Limit -----------------'
// limit와 같아질 때 까지 순회하는 iterator
function *limit(l, iter) {
  for (const a of iter) {
    yield a;
    if (a === l) return;
  }
}

// '*Odds ------------------'
// l까지 홀수만 순회하는 iterator
function *odds(l) {
  for (const a of limit(l, infinity(1))) {
    if (a % 2) yield a;
  }
}

for(const a of odds(10)){
  console.log(a); // 1  3  5  7  9
}

console.log([...odds(10), ...odds(20)]);  // [1, 3, ... ,9, 1, 3, ..., 17, 19]

const [head, ...others] = odds(5);
console.log(head);    // 1
console.log(others);  // [3, 5]

const [a, b, ...rest] = odds(10);
console.log(a);       // 1
console.log(b);       // 3
console.log({rest});  // [5, 7, 9]