##一.問題の再述
イテレーターは走査操作を簡素化するためのオブジェクトで、最基本的な 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
###参考資料
コメントはまだありません