Ruby で Generator の実装
Python に Generator というのがあります。Ruby では Enumerator でほぼ同じことができますが、引数の与え方がいまひとつよくわかりません。でというわけでもないのですが、勉強のために頑張って Fiber で実装してみました。Generator#args で引数を与えます。複数の引数を取れるのが考えたところです。
Genarator#next が終端に達すると、StopIteration 例外を発生させます。Kernel#loop は StopIteration を捕捉します。
Genarator#each を実装しているので、Enumerable モジュールを include しています。
generator.rb
class Generator class Yielder def <<(a) Fiber.yield(a) end end def initialize(&bk) @bk = bk refresh end def next @fib.resume end def each loop {yield(@fib.resume)} end def refresh @fib = Fiber.new do @bk.call(Yielder.new, *@args) raise StopIteration end end def args(*args) @args = args self end include Enumerable end
使い方。
g = Generator.new do |y, ar, s| ar.each do |i| y << s + i.to_s if i < 4 end end i = g.args([1, 10, 3, 8, 6, 2], "number: ") loop {puts i.next} #=>number: 1 #=>number: 3 #=>number: 2
g = Generator.new do |y| a, b = 0, 1 loop do a, b = b, a + b y << a end end g.each do |x| break if x > 100 puts x end g.refresh p g.take_while{|i| i < 100} #=>[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
step = 3 g = Generator.new do |y| v = 0 loop do v += step y << v end end p g.next #=>3 step = 5 p g.next #=>8
Enumerator::Lazy
Enumerator::Lazy を使えばこんな感じです。
ar = [1, 10, 3, 8, 6, 2] s = "number: " e = Enumerator::Lazy.new(ar) do |y, i| y << s + i.to_s if i < 4 end loop {p e.next} #=>number: 1 #=>number: 3 #=>number: 2
Enumerator::Lazy#new の取り得る引数は、each で値が取り出せるものに限られます。
別に Enumerator でこれでもいいか。なーんだ。
ar = [1, 10, 3, 8, 6, 2] s = "number: " e = Enumerator.new do |y| ar.each do |i| y << s + i.to_s if i < 4 end end loop {p e.next}
Enumerator も Enumerator::Lazy も遅延評価するというのは同じですね。