본문 바로가기

Javascript

[Javascript] Generator (제너레이터)

 

개인적으로 공부하던 중  제너레이터로 굉장히 재밌는 일들이 가능한것을 발견하여, 공유해보자 글을 씁니다.(https://www.youtube.com/@jsconf_)

 

Generator?

Generator 객체는 generator function 으로부터 반환되며, 반복 가능한 프로토콜 반복자 프로토콜을 모두 준수합니다. - MDN

 

 

1. generator function

말 그대로, generator 객체를 반환하는 함수 입니다. 다음과 같은 방식으로 선언합니다.

function* myGeneratorFunc(){
    yield value1;
    yield value2;
    ...
    //until the end..
}

2. generator object

MDN정의에 맞게, iterable protocol 과 iterator protocol을 준수하는 객체입니다. (iterable protocol 과 iterator protocol은 아래 코드 참조)

고로 , iterator가 generator 객체의 초집합이라고 볼 수 있습니다.

generator 함수 내부 yield 키워드 우측에 정의되는 값은 generator 객체의 값으로써 반환됩니다.

function* myGenFunc(){
	console.log('generator function initialized!!');
    yield 'hello just started!';
    console.log('resumed')
    yield '2nd call'
    console.log('resumed');
    yield 'end of gen'
}
const a = myGenFunc();
console.log(a.next()) // 'generator function initialized!!' , 'hello just started!'
console.log(a.next()) // resumed , '2nd call
console.log(a.next()) // resumed , 'end of gen
console.log(a.next()) // undefined
// iterator protocol : function which returns object which has property func next( return {done : boolean , value :any }))
function iteratorProtocolFunction(start, end){
    let nextValue = start;
    return {
      next: () => {
        if(nextValue < end){
          let nextObj = { value: nextValue , done : false};
          return nextObj
        }
        return {
          value : nextValue,
          done : true
        }
      }
    }
  }
//
const iterableObject = {
	//iterable protocol @@iterator exist
	[Symbol.iterator] = iteratorProtocolFunction 
}

 

 

혹시 generator의 이 어마무시한 장점을 찾으셨나요?

바로..! 함수의 시행 시점을 기억하고 있을 뿐더러, 이를 통해 중단과 재시작과 같은 flow control이 가능합니다!!

flow control이 가능하단 점에서 co-routine도 충분히 가능할것 같습니다!!

 

바로 여러 예시를 보면서 한번 generator의 유용성을 확인해 보겠습니다.

 

In Practice

1. 커스텀 iterable

위에 서술했듯, generator 객체는 iterator protocol을 만족합니다. 따라서, 위에 작성한 방식보다 훨씬 간단하게 iterable protocol을 만족시킬 수 있습니다.

const myArray = [1,2,3,4,5];

myArray[Symbol.iterator] = function*(){
	let n = this.length - 1;
    while(n >= 0){
    	yield this[n];
        n -= 1;
    }
}

for(const elem of myArray){
	console.log(elem); // 5 , 4 , 3 , 2, 1
}

2. lazy evaluation (지연 평가)

generator 객체는 이터러블 하단 점을 이용하여, 원하는 시점에 지연 평가가 가능합니다.



function* take(n = Infinity, iterable){
    for(let item of iterable){
      if(n <= 0) return;
      if(item){
        n--;
        yield item;
      }
    }
  } 
  function* map(func , iter){
    for(const elem of iter){
      yield func(elem);
    }
  }
  function getOdd(elem){
    return elem % 2 ? elem : false;
  }

  const list = [1,2,4,5,6,7,10,12,14,16,18,20];

  const result = [...take(3 , map(getOdd,list))] 
  //vs => list.map(item => item % 2 === 1 ? item : false).filter(item => item > 0)

 

3. recursive (재귀)

 

yield * 키워드를 통해, 현재 iteration을 다른 iterator(generator object)에 인가함으로써, 이진트리를 구현할 수 있습니다.

function binaryTreeNode(value){
  let node = {value};
  node[Symbol.iterator] = function* depthFirst(){
    yield node.value;
    if(node.leftChild) yield* node.leftChild;
    if(node.rightChild) yield* node.rightChild;
  }
  return node;
}
const root = binaryTreeNode('root');
  root.leftChild = binaryTreeNode('1-l');
  root.rightChild = binaryTreeNode('1-r');
  root.leftChild.leftChild = binaryTreeNode('2-l')
  oot.leftChild.rightChild = binaryTreeNode('2-r')
  console.log(...root) 
  
  // root , 1-l , 2-l , 2-r , 1-r

 

4. Cursor based pagination 단순화 (feat, @@asyncIterator)

 

예시로는 그냥 전체 데이터를 fetching 하고 있어서 효용성은 못느끼지만, 실제 사용 시, generator의 yield 키워드를 통해 flow control하여, 원하는 시점에 데이터를 받아오면 될것 같습니다.

const getCursorBasedResult = (endpoint) => {
  return async function*(){
    let nextUrl = `https://myDomain/api/${endpoint}`;
    while(nextUrl){
      const response = await fetch(nextUrl);
      const data = await response.json();
      nextUrl = data.cursor
      yield* data.results;
    }
  }
}

// @@asyncIterator 정의
const dataCollection = ({
  data1: {
    [Symbol.asyncIterator]: getCursorBasedResult("data1")
  },
  data2: {
    [Symbol.asyncIterator]: getCursorBasedResult("data2")
  },
  data3: {
    [Symbol.asyncIterator]: getCursorBasedResult("data3")
  }
})

async function getAllResult(){
  let results = [];
  for await (const page of dataCollection.data2){
    results.push(page.name);
  }
}

 

5. State machine (상태 머신)

yield keyword 우측 값은, Generator obj value 표현값 , 좌측은 next 메서드의 인자로 할당 받는 값입니다

 

function* bankAccount(){
  let balance = 0;
  while(balance >= 0){
    let state = null;
    if(balance>50){
      state = 'enough'
    }else if(balance <= 50 && balance > 10){
      state = 'caution'
    }else if(balance <= 10){
      state = 'danger'
    }
    balance += yield [balance , state];
  }
  return 'bankrupt'
}

let acct = bankAccount();
console.log(acct.next()); // 0 , danger
console.log(acct.next(150)); // 150 , enough
console.log(acct.next(-120)); // 30 , caution
console.log(acct.next(-20)); // 10 , danger
console.log(acct.next(-200)); // bankrupt

 

6. event-driven

 

const chatRoomTable = {}
const messageQueue = [];

function sendMsg(chatRoomId, name , msg){
  console.log(msg);
  messageQueue.push([chatRoomId , name , msg]);
}

function* user(participate , key = null){
  const {send} = chatRoomTable[participate];
  send(key ==='Dovi' ? 'Justin' : 'Dovi' , 'there')
  let msg = yield ;
  send(key ==='Dovi' ? 'Justin' : 'Dovi' , 'hi there')
  if(msg === `${key} entered room.`) return;
  msg = yield
  send(key ==='Dovi' ? 'Justin' : 'Dovi' , 'bye there')
  msg = yield
  return;

}

function createChatRoom(user1 , user2){
  let chatRoomId = 'Room'+user1 + user2;
  chatRoomTable[chatRoomId] = {}
  chatRoomTable[chatRoomId][user1] = user.apply(this, [chatRoomId , user1])
  chatRoomTable[chatRoomId][user2] = user.apply(this, [chatRoomId, user2])
  chatRoomTable[chatRoomId].send = sendMsg.bind(this , chatRoomId);
  chatRoomTable[chatRoomId].send(user1 , `${user1} entered room.`);
  chatRoomTable[chatRoomId].send(user2 , `${user2} entered room.`);
}

function startChat(){
  while(messageQueue.length > 0){
    const [chatRoomId, name , msg] = messageQueue.shift();
    const targetRoom = chatRoomTable[chatRoomId];
    
    targetRoom[name].next(msg)
  }
}
createChatRoom('Justin' , 'Dovi')
startChat()