[JavaScript] 함수형 프로그래밍 (제너레이터, 이터레이터)

 

 

 

# 제너레이터 / 이터레이터

 - 제너레이터: 이터레이터이자 이터러블을 생성하는 함수

function* gen(){
  yield 1;
  if(false) yield 2;
  yield 3;
}

let iter = gen();
// console.log(iter[Symbol.iterator]() == iter); // true
console.log(iter.next()); // { value: 1, done: false } 
console.log(iter.next()); // { value: 3, done: false } 
console.log(iter.next()); // { value: undefined, done: true } 
console.log(iter.next()); // { value: undefined, done: true } 

for(const a of iter) console.log(a); // 1 3

제너레이터는 이터러블과 이터레이터의 조건을 만족한다. 따라서 제너레이터 함수의 객체를 이터러블을 만족하는 작업 없이도 for...of 문으로 순회 할 수 있다. 또한 제너레이터는 순회할 값을 문장(yield)으로 표현할 수 있다. 어떠한 값이든 이터러블이면 순회할 수 있고, 제너레이터는 문장을 값으로 만들수 있고 문장을 통해 순회할 수 있는 값을 만들 수 있기때문에 어떠한 상태나 어떠한 값이든 순회할 수 있도록 만들 수 있다.

 

이 점은 함수형 프로그래밍의 관점에서 중요하다. 자바스크립트는 다형성이 높다고 할 수있다. 제너레이터라는 문장을 통해 순회할 수 있는 값을 만들 수 있다는 것은 어떠한 값도 순회할 수 있는 형태로 동작할 수 있고 다양한 값을 순회하는 이터러블을 만들 수 있다.

 

 

# odds

function* infinity(i = 0){
  while(true) yield i++;
}

function* limit(l, iter){
  for (const a of iter){
    yield a;
    if(l == a) return;
  }
}

function* odds(l) {
  for (const a of limit(l, infinity(1))){
    if (a % 2) yield a;
  }
}

let iter = odds(10);
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 3, done: false }
iter.next(); // { value: 5, done: false }
iter.next(); // { value: 7, done: false }
iter.next(); // { value: 9, done: false }
iter.next(); // { value: undefined, done: true }
iter.next(); // { value: undefined, done: true }

for(const a of odds(40)) console.log(a); // 1, 3, 5 ... 39

제너레이터를 이용해서 홀수만을 출력하는 함수를 만들었다. infinity() 제너레이터함수의 경우는 무한하는 while 문에서 i의 값을 1씩 증가시킨다. 겉으로 보기엔 함수를 실행하면 브라우저가 다운되겠지만 제너레이터 함수의 yield를 통해서 하나씩 값을 실행하고 실행을 중지 시키기때문에 그렇게 되지는 않는다.

 

limit() 제너레이터함수는 파라미터로 숫자와 이터러블을 만족하는 값을 받아온다. 받아온 이터러블을 순회하면서 인자로 받은 숫자와 이터러블의 값이 같아질 때 리턴하여 done: true를 반환한다.

 

odds() 제너레이터 함수는 limit() 제너레이터함수와 infinity() 제너레이터함수를 사용하여 홀수 값을 가져온다. limit과 infinity는 이터러블 프로토콜을 만족하므로 for...of를 순회하는 이터러블 값으로 사용될 수 있다.

 

 

# for of, 전개 연산자, 구조 분해 할당, 나머지 연산자

console.log(...odds(10)); // 1 3 5 7 9
console.log([...odds(10), ...odds(20)]); // [1, 3, 5, 7, 9, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

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

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

자바스크립트에서는 이터러블 / 이터레이터 프로토콜을 가지고 활용할 수 있는 문법들과 기능들이 많다. 또한 많은 라이브러리나 함수들도 이터러블 프로토콜을 따르도록 구성이 되어있다면 제너레이터와 이터레이터를 활용하여 좀 더 조합성이 높은 프로그래밍을 할 수 있다.

[JavaScript] 함수형 프로그래밍 (이터러블, 이터레이터)

 

 

 

ES6부터 리스트를 순회하는 방법의 차이가 생겼다.

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

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

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

 

 

# 이터러블/이터레이터 프로토콜

 - 이터러블: 이터레이터를 리턴하는 [Symbol.iterator]() 를 가진 값

 - 이터레이터: { value, done } 객체를 리턴하는 next() 를 가진 값 (리절트 객체)

 - 이터러블 / 이터레이터 프로토콜: 이터러블을 for...of, 전개 연산자, 구조 분해 할당 등과 함께 동작하도록 만든 규약

arr[Symbol.iterator] // f values() { [native Code] }

let iterator = arr[Symbol.iterator]();
iterator.next() // { value: 1, done: false} 
iterator.next() // { value: 2, done: false} 
iterator.next() // { value: 3, done: false} 
iterator.next() // { value: undefined, done: true}

 

 

# 사용자 정의 이터러블을 통해 알아보기

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

let iterator = iterable[Symbol.iterator]();
iterator.next();
for(const a of iterable) console.log(a); // 2 1
for(const a of iterator) console.log(a); // TypeError: iterator is not iterable

이터러블 / 이터레이터 프로토콜을 사용하기위해 사용자가 정의할때는 위의 [Symbol.iterator]() 메서드를 정의하고 [Symbol.iterator]() 메서드는 next() 메서드를 반환해야하며 next() 메서드의 내용으로는 { value, done } 을 가지는 객체를 반환해야 한다.

 

하지만 위의 이터러블 / 이터레이터는 완벽하지 않다. Array나 Set, Map 등 다른 이터러블 프로토콜을 만족하는 것들의 내부를 보면 이터러블 프로토콜을 만족하면서 이터레이터 프로토콜을 만족한다. 그러한 이터러블 프로토콜을 well-formed 이터러블 프로토콜 이라고 한다.

 

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

let iterator = iterable[Symbol.iterator]();
iterator.next();
for(const a of iterable) console.log(a); // 2 1
for(const a of iterator) console.log(a); // 2 1

well-formed 이터러블 프로토콜은 이터러블을 for...of 문으로 순회를 해도 순회가되고 이터레이터로 만들어서 for...of 문으로 순회해도 순회가 된다. 또한 일정부분 next() 메서드를 통해 진행을 하고 순회를 해도 마찬가지로 순회가 된다.

 

console.log(document.querySelectorAll('*')) // NodeList(...) [...]
for(const a of document.querySelectorAll('*')) console.log(a); // <html>...</html> <head>...</head> ...
const all = document.querySelectorAll('*');
let iter = all[Symbol.iterator]();
iter.next(); // { value: html, done: false }
iter.next(); // { value: head, done: false }
iter.next(); // { value: script, done: false }

해당하는 이터러블 프로토콜은 오픈소스 라이브러리 뿐만 아니라 브라우저의 DOM 또한 이터러블 프로토콜을 만족한다.

 

 

# 전개연산자

const a = [1, 2];
// a[Symbol.iterator] = null; // null이라면 아래 코드 TypeError
console.log([...a, ...arr, ...set, ...map.keys()]); // [1, 2, 1, 2, 3, 1, 2, 3, "a", "b", "c"]

자바스크립트에서 ES6의 이터러블 / 이터레이터 프로토콜은 중요하다. 해당하는 프로토콜을 만족하면 기존에 ES5에서 사용하던 자료구조보다 더 다양하고 쉬운 방식으로 데이터들을 핸들링 할 수 있다.

+ Recent posts