読者です 読者をやめる 読者になる 読者になる

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 も遅延評価するというのは同じですね。