Ruby 1.9 にデフォルトで外部イテレータが入ったということで、早速遊んでみました。
まずはお約束。原因究明してないけど、多分 Fiber のせい。
callcc {|c| $c = c } def (o = Object.new).foo $c.call end o.to_enum(:foo).next #=> SEGV
常に 1 つ分だけ先読みするようです。考えてみれば当然 *1 ですが、ちょっと落とし穴かも。
def (o = Object.new).each yield 1; p 2 yield 3; p 4 yield 5; p 6 end g = o.to_enum p g.next #=> 1 p g.next #=> 3 p g.next #=> 5
$ ./ruby gen.rb 2 1 4 3 6 5
同じ理由で、以下の挙動も気持ち悪いですね。まあこんなコード書かないと思いますけど。
a = [1,2] g = a.each p g.next #=> 1 a.pop p g.next #=> 2 ("Enumerator#each reached at end (StopIteration)" ではない)
現状の Fiber の仕様で、1 つの外部イテレータに複数スレッドからアクセスできません (今のところ) 。タスクをリストにして各ワーカースレッドから取り合うような用途には使えなさそうです。
g = [1,2,3].each p g.next #=> 1 Thread.new do p g.next #=> fiber called across threads (FiberError) end.join
これも現状の Fiber の仕様 (というか問題?) で、外部イテレータ中で別の外部イテレータをまわすと変なことになります。どう変なことになっているかは ruby-dev:30990 あたりを参照のこと。
def (o = Object.new).each yield 0 p [1,2,3].to_enum.next end p o.to_enum.next
$ ./ruby double.rb 1 double.rb:6:in `<main>': unhandled exception
現状で考えられる普通の用途に対しては、非常に普通の挙動でした。なので特に感動もないですし、今後も大抵の場合は内部イテレータを使うと思います。
でも、出来合いの Enumerable なクラス (Array とか) は内部イテレータで使いやすいように設計されてます。なので、今の Ruby で内部イテレータの方がいいように見えるのは当然かも知れません。外部イテレータに慣れてくれば each 相当の関数を自作することが増えて、もっと感動的で便利な使い方が発見される、かも。
*1:next? のために、次の要素があるかどうかをあらかじめ見ておかないといけない。正確には「あらかじめ」である必要はなくて、next? が呼ばれたときだけキャッシュしてもいい。けどそれはそれで気持ち悪い。