M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

C#のyield return文に対応するものをJava 1.6でがんばってつくってみた

直近C#についてお勉強していて「これは、やばい・・・」と感じたのはなんといってもyield return文です。細かいことを述べるのはやめておきます。ようするに「for文を書いて遅延評価型の反復子をつくる」というとんでもない糖衣構文です。裏側ではC#コンパイラががんばっているようで、ソースコード上単なるfor文による正格評価であるものを、必要に応じて(必要になってから)都度値を取得する遅延評価につくりかえます。

IEnumarable<U> ReturnsLazyEnumerable<T,U>(IList<T> ts) {
    foreach (T t in ts)
    {
        // tを使用したヘビーな処理(結果値が本当に必要になるまでは処理をさせたくない)
        U u = DoHeavilyLoadedWork(t);
        // 値を返す(メソッドの戻り値と型が一致しないが問題ない)
        yield return u;
    }
}

Javaではバージョン8になってStream APIが導入されこれに相当する処理が実現できるようになりました(いずれにしてもあくまでもオブジェクトとして導入されたわけで、構文としての導入ではありません)。が、バージョン8以前の標準ライブラリでは当然これは使えません。

この記事などを見ると、yield return文をブロッキング・キューを用いて実装する例が示されています。たしかにコードの類似性を維持しつつ実現しようとするとこうなります。しかし、遅延評価のためにスレッド別立てまでするのはちょっとやり過ぎな気もします(好みの問題です)。

そういうわけで実装してみました。あくまでシングル・スレッドで処理させるため、コードの類似性は捨てざるを得ません。こちらのリポジトリにコードがコミットされています。使い方はこんなかんじ:

<T,U> Iterable<U> returnsLazyIterable(List<T> ts) {
	return LazyIterable.forEach(ts, new YieldCallable<T,U>(){
		public Yield<U> yield(T t, int index) {
			final U u = doHeavilyLoadedWork(t);
			return Yield.yieldReturn(u);
			// 値を捨てる場合はYield.yieldVoid()を
			// 反復を中断する場合はYield.yieldBreak()を返す
		}
	});
}

YieldCallable<T,U>#yield(T,int): Uメソッドが前掲のC#コードにおけるforeach文のブロック部分に対応します。データソースから1件ずつ値を取得し、yieldメソッドに渡すのはLazyIterable<U>が受け持ちます。Yield.<U>yieldReturn(U):Yield<U>は前掲のC#コードにおけるyield return文に対応します。

わざわざYieldCallableインターフェースの実装を行わないといけないことと、値を返すときに特殊なコンテナYieldをつかって制御情報をAPI側に伝える必要があることが、欠点でしょうか。少なくとも「がんばってる」感は出ていると思います。ちなみに前者についてはJava8ではラムダ式やメソッド参照で代替できますから、すこしはマシな状況になるでしょう:

<T,U> Iterable<U> returnsLazyIterable(List<T> ts) {
	return LazyIterable.forEach(ts, (t, i) -> 
	Yield.yieldReturn(doHeavilyLoadedWork(t)));
}

とか

<T,U> Iterable<U> returnsLazyIterable(List<T> ts) {
	return LazyIterable.forEach(ts, this::helperMethod);
}
<T,U> Yield<U> helperMethod(T t, int i) {
	return Yield.yieldReturn(doHeavilyLoadedWork(t));
}

最後にもう少し具体的な例を。この例では、生成されたIterable(が返すIterator)が返す文字列は、"100""102""104"となります:

final List<Integer> ints = Arrays.asList(100,101,102,103,104,105,106);
return LazyIterable.forEach(ints, new YieldCallable<Integer,String>(){
	public Yield<String> yield(Integer item, int index) {
		if (item > 104) {
			return Yield.yieldBreak();
		} else if (item % 2 == 1) {
			return Yield.yieldVoid();
		} else {
			return Yield.yieldReturn(item.toString());
		}
	}
});