外部イテレータにケチをつける

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? が呼ばれたときだけキャッシュしてもいい。けどそれはそれで気持ち悪い。