##一.문제 재진술
이터레이터는 순회 작업을 단순화하는 데 사용되는 객체로, 가장 기본적인 value, next() 인터페이스에서 each(), take(), select(), takeWhile() 등의 고급 인터페이스까지 모두 순회 작업을 더욱 단순화한다
수동으로 이터레이터를 구현하는 것은 번거롭고, 내부 상태를 유지해야 하며, 구문도 충분히 우아하지 않고, 가독성은 말할 필요도 없다. 예를 들어:
function Iterator123(value) {
this._value = value;
this.value = this._value;
}
Iterator123.prototype.next = function() {
if (this._value >= 3) {
return false;
}
else {
return new Iterator123(this.value+1);
}
}
function oneToThree() {
return new Iterator123(1);
}
// use
for (var iter = oneToThree(); iter; iter = iter.next()) {
log(iter.value);
}
C# 은 yield 키��드를 제공하여 이터레이터를 빠르게 생성할 수 있으며, 이터레이터를 생성할 수 있는 것을 이터레이터의 제네레이터 (약칭 제네레이터) 라고 한다. 예를 들어 C# 구문:
public static IEnumerable<int> OneToThree()
{
yield return 1;
yield return 2;
yield return 3;
}
OneToThree() 를 호출하여 이터레이터를 생성한 후 MoveNext() 할 수 있다. 이터레이터를 생성하는 구문은 매우 우아하다 (yield return)
문제: js 로 유사한 yield 제네레이터를 구현하라
P.S. 문제는 趣味编程:在 JavaScript 中实现简单的 yield 功能(问题) 에서
##二.해법 1(from ivony)
코드 자체가 말한다:
log('from ivony');
function YieldHost(yieldFunction) {
this._yieldFunction = yieldFunction;
}
YieldHost.prototype.each = function(processor) {
var yield = function (result) {
processor(result);
};
this._yieldFunction(yield);
}
// use
function fun1(yield)
{
yield(1);
yield(2);
yield(3);
}
function fun2(yield) {
for (var i = 0; i < 3; i++)
yield(i);
}
// 传入 yieldFun,返回匿名函数,接受 processor
// 匿名函数内部实现了 yield 函数
var iter1 = new YieldHost(fun1);
var iter2 = new YieldHost(fun2);
// 传入 processor,用来处理 yield 返回结果
// 此处只是简单输出
iter1.each( function(item) {
log(item);
});
iter2.each( function(item) {
log(item);
});
(코드는 ivony 에서, 필자는 코드 구조를 수정하여 더 js 답게 만들었다)
思路: 먼저 전달된 processor 를 기반으로 yield 메서드를 조립한 후 yield 호출을 포함하는 yieldFun 을 실행
특징: 사용하기 쉬운 제네레이터를 구현, 구조는 범용이지만 순차 실행이며 멈출 수 없다
선배는 곧 멈출 수 있는 방법을 제시했다 (맞다, throw...catch 다). 코드는 다음과 같다:
log('from ivony');
function YieldHost(yieldFunction) {
this._yieldFunction = yieldFunction;
}
YieldHost.prototype.eachCanBreak = function(processor) {
var exception = Math.random();
var yield = function(result) {
processor(result);
}
try {
this._yieldFunction(function (result) {
if (processor(result))
throw exception;
});
}
catch (e) {
// 防止掩盖其它异常
if (e !== exception)
throw e;
}
};
YieldHost.prototype.take = function(size) {
var that = this;
return function (fun) {
that.eachCanBreak(function (item) {
if (size-- == 0)
return true;
else
return fun(item);
});
}
};
YieldHost.prototype.select = function(selector) {
var that = this;
return function (fun) {
that.eachCanBreak(function (item) {
return fun(selector(item));
});
}
};
function fun1(yield) {
for (var i = 0; i < 5; i++) {
yield(i);
}
}
function fun2(yield) {
var i = 0;
while (true)
yield(i++);
}
// 遍历有限集,条件中断
var iter1 = new YieldHost(fun1);
iter1.eachCanBreak(function(item) {
if (item > 1) {
return true;
}
log(item);
});
// 遍历无限集
var iter2 = new YieldHost(fun2);
iter2.take(3)(function(item) {
log(item);
});
// select
iter1.select(function(item) {
return item * 2;
})(function(item) {
log(item);
});
(코드는 ivony 에서, 필자는 코드 구조를 수정하여 더 js 답게 만들었다)
예외를 사용하여 중단을 구현, 멈출 수 있게 되었지만 next 는 구현할 수 없다 (처음부터 next 를 포기했기 때문에 each 만 구현, 실용적인 관점에서 문제 없음). 원문은 설계思路를 설명한다:
yieldHost 는 거꾸로 장착하는 작업을 완료했을 뿐이며, enumerator 가 받는 그 함수 (즉 window.alert( item ), 주: 열거 함수에 주입 (즉 fun)
처리 함수 processor 를 이터레이트 함수 fun 에 주입하고, 주입 형식으로 processor 를 yield 로 패키징
##三.해법 2(from Dexter.Yy)
말하는 코드는 다음과 같다:
log('from Dexter.Yy');
function generator(fn, args){
var queue, cache = [];
// 给参数列表末尾添上 yield
args.push(yield);
// 填充结果数组 cache
fn.apply(this, args);
// 逆置 cache,便于 pop 实现 next
init();
return {
next: next,
close: init,
each: function(fn, context){
var result, i = 0;
// 防止中途调用,导致不完全遍历
this.close();
while (result = this.next()) {
fn.call(context || window, result, i++);
}
}
};
// 装入数据
function yield(result){
cache.push(result);
}
// 取出数据
function next(){
return queue.pop();
}
function init(){
queue = cache.slice().reverse();
}
};
// 最后一个参数必须是 yield
function foo(a, b, yield){
for (var i = 0; i <= 10; i++)
yield(a + b + i);
}
var iter = generator(foo, [1, 2]);
log(iter.next());
log(iter.next());
log(iter.next());
iter.close();
log(iter.next());
iter.each(function(item, i){
log('i = ' + i + ', item = ' + item);
});
(코드는 Dexter.Yy 에서, 필자는 주석을 추가했다)
특징: 간단하고 사용하기 쉬운 제네레이터, 배열로 구현되어 무한 집합은 지원하지 않지만 매우 실용적
선배는 무한 집합을 지원하는 방법도 제시했다:
log('from Dexter.Yy');
// 避免一开始就对整个序列求值
function generator(fn, args){
var routine, self = this;
// 在参数列表末尾添上 yield
args.push(yield);
//
init();
return {
next: next,
close: init
};
function yield(result){
return result;
}
function next(){
// 不需要像原版这样写
// return routine.apply(self, arguments);
return routine();
}
function init(){
routine = fn.apply(self, args);
}
}
// 最后一个参数必须是 yield
function fibonacci(n, yield) {
var n1 = n2 = s = i = 1;
// 闭包保留 context,以实现 next
return function(){
for(; i<n; i++){
s = n1 + n2;
n1 = n2;
n2 = s;
return yield(n1);
}
};
}
var fibIter = generator(fibonacci, [1000]);
log(fibIter.next());
log(fibIter.next());
fibIter.close();
log(fibIter.next());
log(fibIter.next());
log(fibIter.next());
쉽게 발견할 수 있듯이, 실제로 yield 는 필요 없으며, 무한 집합 지원은 실제로 fib 구현에 달려 있고 제네레이터와는 관계없다. 제네레이터, 이터레이터 없이 더 간결하게, 예를 들어:
function fib(n) {
var n1 = n2 = s = i = 1;
// 闭包保留 context,以实现 next
return function(){
for(; i<n; i++){
s = n1 + n2;
n1 = n2;
n2 = s;
return n1;
}
};
}
var max = 1000;
var fun = fib(max);
log(fun());
log(fun());
log(fun());
log(fun());
##四.해법 3(from Jeffrey Zhao)
출제자가 준비한 정답:
log('from Jeffrey Zhao');
function oneToThree() {
return yield(1, function () {
return yield(2, function () {
return yield(3);
});
});
}
function numSeq(n) {
return yield(n, function () {
return yieldSeq(numSeq(n + 1));
});
}
function range(minInclusive, maxExclusive) {
if (minInclusive >= maxExclusive) return null;
return yield(minInclusive, function () {
return yieldSeq(range(minInclusive + 1, maxExclusive));
});
}
function fibSeq() {
function fibSeq$(a, b) {
var next = a + b;
return yield(next, function () {
return yieldSeq(fibSeq$(b, next));
});
}
return yield(0, function () {
return yield(1, function () {
return yieldSeq(fibSeq$(0, 1));
});
});
}
function yield(item, fn) {
return {
value: item,
next: function() {
if (typeof fn === 'function') {
return fn();
}
else {
return false;
}
}
};
}
function yieldSeq(iter) {
if (iter) {
return {
value: iter.value,
next: function() {
return iter.next();
}
};
}
else {
return false;
}
}
// use
for (var iter = oneToThree(); iter; iter = iter.next()) {
log(iter.value);
}
for (var iter = range(0, 3); iter; iter = iter.next()) {
log(iter.value);
}
for (var iter = numSeq(0); iter; iter = iter.next()) {
if (iter.value > 2) {
break;
}
log(iter.value);
}
for (var iter = fibSeq(); iter; iter = iter.next()) {
if (iter.value > 10) {
break;
}
log(iter.value);
}
(일부 코드는 Jeffrey Zhao 에서, 필자는 yield 와 yieldSeq 를 추가했다)
우아한 재귀方案이지만, 출제자 (Jeffrey Zhao) 는 ivony 의方案 (해법 1) 이 js 에 더 적합하다고 인정한다
##五.요약
-
yield구현 방식은 모두 함수 주입 방식을 채택, 즉 먼저 매개변수를 선언하고 최종 실행 context 에서 함수의 구체적 구현을 제공 -
클로저로 context 를 보유, 무한 집합을 처리할 때 매우 유용
-
재귀方案은 항상 간결하지만, 문제는,要么 생각나지 않거나,要么 이해하지 못하거나..
약간의 잡담: 노련한 개발자들은 내공이 깊어, 견고성 (e !== exception), 실용성 (배열方案) 은 물론 우아함 (다양한 yield) 도 나무랄 데가 없다. 温 선배가 말했듯이, 커뮤니티는 매우 좋은 학습 경로이며, 다른 사람의 코드를 보면 더 빠르게 자기 향상을 이룰 수 있다
테스트 파일:http://www.ayqy.net/temp/yield.html
###참고 자료
아직 댓글이 없습니다