rubyruby-1.9fiberenumerators

How do Enumerators work in Ruby 1.9.1?


This question is not about how to use Enumerators in Ruby 1.9.1 but rather I am curious how they work. Here is some code:

class Bunk
  def initialize
    @h = [*1..100]
  end

  def each
    if !block_given?
      enum_for(:each)
    else
      0.upto(@h.length) { |i|
        yield @h[i]
      }
    end
  end
end

In the above code I can use e = Bunk.new.each, and then e.next, e.next to get each successive element, but how exactly is it suspending execution and then resuming at the right spot?

I am aware that if the yield in the 0.upto is replaced with Fiber.yield then it's easy to understand, but that is not the case here. It is a plain old yield, so how does it work?

I looked at enumerator.c but it's neigh on incomprehensible for me. Maybe someone could provide an implementation in Ruby, using fibers, not 1.8.6 style continuation-based enumerators, that makes it all clear?


Solution

  • Here's a plain ruby enumerator that uses Fibers and should pretty much behave like the original:

    class MyEnumerator
      include Enumerable
    
      def initialize(obj, iterator_method)
        @f = Fiber.new do
          obj.send(iterator_method) do |*args|
            Fiber.yield(*args)
          end
          raise StopIteration
        end
      end
    
      def next
        @f.resume
      end
    
      def each
        loop do
          yield self.next
        end
      rescue StopIteration
        self
      end
    end
    

    And in case anyone is feeling uneasy about using exceptions for control flow: The real Enumerator raises StopIteration at the end, too, so I just emulated the original behaviour.

    Usage:

    >> enum = MyEnumerator.new([1,2,3,4], :each_with_index)
    => #<MyEnumerator:0x9d184f0 @f=#<Fiber:0x9d184dc>
    >> enum.next
    => [1, 0]
    >> enum.next
    => [2, 1]
    >> enum.to_a
    => [[3, 2], [4, 3]]