[es6for3] generator 대체 #2/2

es6for3 시리즈

ES6는 새로운 언어철학을 제시하고 편리한 기능을 제공합니다. 하지만 크롬(52버전까지)조차 별도로 플래그를 활성화해야 만 쓸 수 있는 상황입니다.
이에 업계는 바벨 등의 번역기를 이용하여 ES5로 번역시키는 수를 사용하고 있습니다. 결국 바벨이 번역한 코드는 바벨라이브러리를 사용하는 ES5코드로 번역됩니다. ES6에 대한 깊은 이해는 ES6의 코드 방식을 하위 ES3.1로 번역하는걸 가능하게 하죠. ES6 기능에 대한 정확한 이해와 나만의 바벨을 만들기 위한 첫 단계로 ES6의 각 기능을 ES3.1로 번역해보죠.

Yield함수를 이용한 블록킹 한계

이전 포스트에서 살펴본대로 running EC를 suspend하는건 불가능하지만 Yield함수 호출이 반응할지 안하게 할지는 결정할 수 있었습니다. 그 포스트 마지막에 등장한 window.Yield함수를 다시 살펴보죠.

window.Yield =  function(v, cnt){
	if(SELF.isYieldActive) return true;
	var id = SELF.seed++;
	if(SELF.ids[id] === undefined) SELF.ids[id] = cnt || 1;
	if(!SELF.ids[id]) return true;
	SELF.ids[id]--;
	SELF.isYieldActive = true;
	SELF.result = v;
	return false;
};

Yield함수가 반환값이 없는 것이 아니라 실행될때는 false, 이미 실행되어 스킵할 때는 true를 반환하도록 설계되어있습니다. 이렇게 설계한 이유는 제네레이터에 전달된 함수내에서 제어구조에 사용하기 위해서 입니다. 이해를 돕기 위해 ES6의 제네레이터 샘플을 보죠.

const generator = function*(v){
  let result;
  result = testA(v);
  if(result > 0) yield result;
  result = testB(v);
  if(result > 0) yield result;
  result = testC(v);
  if(result > 0) yield result;
};

위의 제네레이터는 지체없이 testA를 수행하고 조건에 들어오면 첫번째 yield를 일으킵니다. 이어서 testB, testC순서대로 최소 0번 최대 3번의 next에 대응하게 되죠. suspend를 이용하면 yield한 위치를 기억하므로 알아서 그 다음이 실행되어 편리하게 비동기적 코드를 동기적 코드로 기술할 수 있습니다.
이를 #1에서 구현한 Gene함수로 표현하면 다음과 같을 것입니다.

var generator = Gene(function(ec){
  ec.result = testA(ec.v);
  if(result > 0) Yield(ec.result);
  ec.result = testB(ec.v);
  if(result > 0) Yield(ec.result);
  ec.result = testC(ec.v);
  if(result > 0) Yield(ec.result);
});

이게 작동은 분명히 합니다. 하지만 진짜 제네레이터와는 굉장히 다른 이유로 작동되는 것처럼 보이는 것입니다.
무조건 전체코드가 실행되지만 Yield가 한 번만 작동하기 때문에 순차적으로 값이 반환되는 것이죠. 결국

  1. 최초 호출시에도 testA, testB, testC가 수행되었고
  2. 그렇게 세번 호출시마다 전부 호출되어 총 9개의 함수 호출이 발생 합니다.

이를 완화하여 처리하는 방법은 적절한 시점에 return을 해주는 것입니다.

var generator = Gene(function(ec){
  ec.result = testA(ec.v);
  if(result > 0) if(!Yield(ec.result)) return;
  ec.result = testB(ec.v);
  if(result > 0) if(!Yield(ec.result)) return;
  ec.result = testC(ec.v);
  if(result > 0) if(!Yield(ec.result)) return;
});

Yield의 반환값을 사용하면 물론 처음보다는 나아집니다만 그렇다고 해도 여전히

  1. 처음에는 testA가 수행되고,
  2. 두번째는 testA, testB가 수행되며
  3. 세번째는 testA, testB, testC가 수행되어

최초 9개의 호출보다 3개의 호출을 줄였을 뿐입니다. 이는 Yield의 반환값만으로 코루틴을 흉내낼 수 없다는 결론에 이르게 합니다.

새로운 Unused 함수를 통한 제어문 블록킹

이를 위해 새로운 window레벨의 Unused블록킹 함수를 도입합니다. 이를 도입하면 아래와 같은 식으로 완전히 블록킹할 수 있게 됩니다.

var generator = Gene(function(ec){
  if(Unused()){
    ec.result = testA(ec.v);
    if(result > 0) Yield(ec.result);
  }else if(Unused()){
    ec.result = testB(ec.v);
    if(result > 0) Yield(ec.result);
  }else if(Unused()){
    ec.result = testC(ec.v);
    if(result > 0) Yield(ec.result);
  }
});

내부적으로 Unused 호출건마다 고유한 id를 소유하는 컨셉은 Yield함수와 맥락이 같습니다. 호출된적 없으면 true, 한 번이라도 사용되었다면 false인 셈이죠.
이는 switch문으로 보다 간결하게 표현할 수 있습니다.

var generator = Gene(function(ec){
  switch(true){
  case Unused():
    ec.result = testA(ec.v);
    if(result > 0) Yield(ec.result);
    break;
  case Unused():
    ec.result = testB(ec.v);
    if(result > 0) Yield(ec.result);
    break;
  case Unused():
    ec.result = testC(ec.v);
    if(result > 0) Yield(ec.result);
  }
});

사실 처음부터 번역기를 만들 생각을 하고 호환성 레이어를 작성하고 있다보니 코드가 치환하기 좋은 형태로 작성할 수 있게 유도합니다. 실제 Unused 함수만 생성하여 SELF를 Yield와 공유할뿐 next에 반영해야하는 코드는 없습니다.

var SELF;
window.Unused = function(){
	//호출마다 고유한 id
	var id = 'U' + (SELF.seed++);

	//Generator의 ids를 공유. 설정한적 있으면 false
	if(SELF.ids[id]) return false;

	//최초 실행이라면 마킹하면서 true반환
	return SELF.ids[id] = true;
};

yield*의 처리

yield*는 다른 이터러블(과 이터레이터^^)가 제네레이터의 yield를 대신해 주는 기능입니다. 이 기능의 복잡한 점은 수행이 끝나면 다시 본체의 루틴으로 돌아온다는 것이죠. 사실 running EC관점에서는 해당 EC의 제어권을 넘겨준 정도로 굉장히 별일 아닌 일입니다만..이 수동 구현물에서는 큰일입니다.
우선 ES6기준의 코드를 살펴보고 대안을 모색해보죠.

const generator = function*(){
  yield 1;
  yield* [2,3,4];
  yield 5;
};

위의 제네레이터는 순회하면 1,2,3,4,5가 나오는데 이중 2,3,4는 두번째 yield* 에 전달된 배열의 이터레이터에서 위임되었다가 온 것입니다.
*는 식별자가 될 수 없으므로 Yield$() 라는 전역함수를 생각해보죠.

  1. 이 함수는 인자로 이터러블을 받을테고
  2. 이 함수가 이터러블을 받았다면
  3. Generator클래스는 next의 행동을 그 이터러블이 끝날 때까지 위임해야합니다.
  4. 그리고 다시 본체의 Yield검사로 돌아오는 거죠.

즉 next함수에서 현재 본인의 f를 호출할지 Yield$가 받아온 위임 이터러블부터 순회할지 결정해야하는 구조입니다.
받아온 이터러블이 연속해서 재귀를 호출할 수는 있지만 그렇다고 해도 Generator입장에서는 yield*** iter; 같은 문법은 없기 때문에 1단계까지의 중첩만 발생합니다. 이를 이용하면 손쉽게 구현할 수 있습니다. 우선 Yield$부터 간단히 구현해보죠.

window.Yield$ =  function(v, cnt){
	//window.Yield과 동일
	if(SELF.isYieldActive) return true;
	var id = SELF.seed++;
	if(SELF.ids[id] === undefined) SELF.ids[id] = cnt || 1;
	if(!SELF.ids[id]) return true;
	SELF.ids[id]--;
	SELF.isYieldActive = true;

	//이터러블이면 이터레이터로
	if(v[Symb.iterator]) v = v[Symb.iterator]();
	SELF.result$ = v;

	return false;
};

겨우 이 정도입니다. Yield함수와 중복되는 부분을 분리하여 check함수로 정리하죠. 마찬가지로 SELF를 바라보면 됩니다.

var SELF;
var check = function(cnt){
	var id;
	if(SELF.isYieldActive) return true;
	id = 'Y' + (SELF.seed++);
	if(SELF.ids[id] === undefined) SELF.ids[id] = cnt ? cnt < 0 ? 100000 : cnt : 1;
	if(!SELF.ids[id]) return true;
	SELF.ids[id]--;
	SELF.isYieldActive = true;
};
window.Yield = function(v, cnt){
	if(check(cnt)) return true;
	SELF.result = v;
	return false;
};
window.Yield$ = function(v, cnt){
	if(check(cnt)) return true;
	if(v[Symb.iterator]) v = v[Symb.iterator]();
	SELF.result$ = v;
	return false;
};

깨끗하게 정리되었습니다. 이제 위임된 결과가 있을 경우의 next구현을 할 차례입니다.

var done = {done:true}, noYield = {};
Generator.prototype.next = function(){
	var result, stack, prevSelf;
	if(this.done) return this;

	//1. 해소될 stack이 존재하면 이를 먼저 해결한다.
	if(this.stack){

		//스택측의 이터레이션을 처리한다.
		result = this.stack.next();

		//스택이 해소되면 제거한다.
		if(result.done) this.stack = null;
		//아니라면 스택값을 반환한다.
		else return result;
	}


	this.isYieldActive = false;
	this.seed = 1;
	this.result = noYield;

	//새롭게 result$도 초기화한다.
	this.result$ = noYield;
				
	prevSelf = SELF, SELF = this;
	this.f(this.ec);
	SELF = prevSelf;
				
	//2. Yield가 호출되면 즉시 해소.
	if(this.result !== noYield){
		//정상적인 값갱신
		this.value = this.result;
		return this;
	}
	//3. Yield$가 호출된 경우
	if(this.y$ !== noYield){

		//stack을 활성화하고 첫번째 값을 얻는다
		result = this.result$.next();

		//순회할 무언가가 있다면 반환한다.
		if(!result.done){
			this.stack = this.result$;
			return result;
		}
	}

	//위의 해당사항이 없으면 종결처리한다.
	return pool[pool.length] = this, done;
};

코드 상에서 1,3번의 역할이 추가되면서 위임이 가능한 구조로 변경되었습니다. 이제 다음과 같이 사용할 수 있게 되었습니다.

var generator = Gene(function(ec){
  Yield(1);
  Yield$([2,3,4]);
  Yield(5);
});

결론

여태 전개한 코드를 모두 모아보면 다음과 같습니다.

var Gene = (function(){
	var done = {done:true}, noYield = {}, SELF;
	var pool = [], Generator = function(){}, fn = Generator.prototype;
	fn.init = function(f, ec){
		this.f = f;
		this.ec = ec || {};
		this.ids = {};
		this.value = undefined, this.done = false;
	};
	fn.next = (function(){
		var result, stack, prevSelf;
		if(this.done) return this;
		if(this.stack){
			result = this.stack.next();
			if(result.done) this.stack = null;
			else return result;
		}
		this.isYieldActive = false, this.seed = 1;
		this.result$ = this.result = noYield;
		prevSelf = SELF, SELF = this;
		this.f(this.ec);
		SELF = prevSelf;
		if(this.result !== noYield){
			this.value = this.result;
			return this;
		}
		if(this.y$ !== noYield){
			result = this.result$.next();
			if(!result.done){
				this.stack = this.result$;
				return result;
			}
		}
		return pool[pool.length] = this, done;
	};
	var check = function(cnt){
		var id;
		if(SELF.isYieldActive) return true;
		id = 'Y' + (SELF.seed++);
		if(SELF.ids[id] === undefined) SELF.ids[id] = cnt ? cnt < 0 ? 100000 : cnt : 1;
		if(!SELF.ids[id]) return true;
		SELF.ids[id]--;
		SELF.isYieldActive = true;
	};
	window.Yield = function(v, cnt){
		if(check(cnt)) return true;
		SELF.result = v;
		return false;
	};
	window.Yield$ = function(v, cnt){
		if(check(cnt)) return true;
		if(v[Symb.iterator]) v = v[Symb.iterator]();
		SELF.result$ = v;
		return false;
	};
	window.Unused = function(){
		var id = 'U' + (SELF.seed++);
		if(SELF.ids[id]) return false;
		return SELF.ids[id] = true;
	};
	return function(f){
		return function(ec){
			var g = pool.length ? pool.pop() : new Generator();
			g.init(f, ec);
			return g;
		};
	};
})();

여기까지 고찰해본 Gene함수로부터 다음과 같은 복잡한 구조를 처리할 수 있게 됩니다.

var main = Gene(function(ec){
  switch(true){
  case Unused():
    if(ec.i < ec.j) Yield(ec.i++, ec.j);
    break;
  case Unused():
    Yield$(ec.sub);
    break;
  case Unused():
    Yield(6);
  }
});
var sub = Gene(function(ec){
  Yield$([3,4,5]);
});

var iterator = main({i:0, j:3, sub:sub()});

For.Of(iterator, function(v){
  console.log(v); //0,1,2,3,4,5,6 차례로 출력
});

(컨버터로 ES6로 변환하기에도 최적인 ^^;)

하지만 아직도 많은 주제가 남았습니다.

  1. 전역에 Yield, Yield$, Unused를 생성하는게 부담스럽다면 ec객체에 키로 잡아서 쓰는 방법이 있음.
  2. 클래스로부터 생성된 제네레이터는 this가 생성시점에 정적으로 바인딩되어야함.

1번이야 쉽게 패치할 수 있는 문제니 건너뜁니다. 패치후에는 전역의 함수는 제거되고 다음과 같은 코드가 되겠죠.

var generator = Gene(function(ec){
  ec.yield(1);
  ec.yield(2);
});

좀 더 안심되지만 기분이 다운됩니다..=.=;

2번은 이후 다루게 될 클래스 패치 편에서 부록으로 다루겠습니다.
#1에서와 마찬가지로 완성된 구현 예제는 여기에 있습니다.

%d 블로거가 이것을 좋아합니다: