Javascript - Iterator pattern

2020. 10. 28. 11:38javascript&typescript

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]

'javascript&typescript' 카테고리의 다른 글

Javascript - module  (0) 2020.10.28